22 | * SPAlert, Copyright © 2021 Ivan Vorobei
23 | * SwiftGen, Copyright (c) 2020 SwiftGen
24 | * Optik, Copyright (c) 2017 Prolific Interactive.
25 | * [moduleRaid](https://github.com/pixeldesu/moduleRaid), Copyright (c) 2018 Andreas N.
26 |
27 | ```
28 | Permission is hereby granted, free of charge, to any person obtaining a copy
29 | of this software and associated documentation files (the "Software"), to deal
30 | in the Software without restriction, including without limitation the rights
31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32 | copies of the Software, and to permit persons to whom the Software is
33 | furnished to do so, subject to the following conditions:
34 |
35 | The above copyright notice and this permission notice shall be included in all
36 | copies or substantial portions of the Software.
37 |
38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44 | SOFTWARE.
45 | ```
46 |
47 | ## MPL-2.0 License
48 |
49 | * Giphy
50 |
51 | -----------
52 |
53 | ## Thanks!!!!
54 |
55 |
--------------------------------------------------------------------------------
/Marindeck/Extensions/WKWebView+loadFile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WKWebView+loadFile.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/01/25.
6 | //
7 |
8 | import Foundation
9 | import WebKit
10 |
11 | extension WKWebView {
12 | // CSSファイルをロード
13 | func loadCSSFile(forResource: String, ofType: String = "css") {
14 | guard let mtPath = Bundle.main.path(forResource: forResource, ofType: ofType) else {
15 | print("failed load style.css")
16 | return
17 | }
18 | let mtFile = FileHandle(forReadingAtPath: mtPath)!
19 | let mtContentData = mtFile.readDataToEndOfFile()
20 | let css = String(data: mtContentData, encoding: .utf8)!
21 | mtFile.closeFile()
22 | var deletecomment = css.replacingOccurrences(of: "[\\s\\t]*/\\*/?(\\n|[^/]|[^*]/)*\\*/", with: "")
23 | deletecomment = deletecomment.replacingOccurrences(of: "\"", with: "\\\"")
24 | deletecomment = deletecomment.replacingOccurrences(of: "\n", with: "\\\n")
25 | let script = """
26 | const h = document.documentElement;
27 | const s = document.createElement('style');
28 | s.insertAdjacentHTML('beforeend', "\(deletecomment)");
29 | h.insertAdjacentElement('beforeend', s)
30 | """
31 | self.evaluateJavaScript(script) { _, error in
32 | print("stylecss : ", error ?? "成功")
33 | }
34 | }
35 |
36 | // JSファイルをロード
37 | func loadJsFile(forResource: String, ofType: String = "js") {
38 | guard let mtPath = Bundle.main.path(forResource: forResource, ofType: ofType) else {
39 | print("ERROR")
40 | return
41 | }
42 | let mtFile = FileHandle(forReadingAtPath: mtPath)!
43 | let mtContentData = mtFile.readDataToEndOfFile()
44 | let mtContentString = String(data: mtContentData, encoding: .utf8)!
45 | mtFile.closeFile()
46 |
47 | let mtScript = mtContentString
48 | self.evaluateJavaScript(mtScript) { _, error in
49 | print("webViewLog : ", error ?? "成功")
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/docs/js/jazzy.js:
--------------------------------------------------------------------------------
1 | // Jazzy - https://github.com/realm/jazzy
2 | // Copyright Realm Inc.
3 | // SPDX-License-Identifier: MIT
4 |
5 | window.jazzy = {'docset': false}
6 | if (typeof window.dash != 'undefined') {
7 | document.documentElement.className += ' dash'
8 | window.jazzy.docset = true
9 | }
10 | if (navigator.userAgent.match(/xcode/i)) {
11 | document.documentElement.className += ' xcode'
12 | window.jazzy.docset = true
13 | }
14 |
15 | function toggleItem($link, $content) {
16 | var animationDuration = 300;
17 | $link.toggleClass('token-open');
18 | $content.slideToggle(animationDuration);
19 | }
20 |
21 | function itemLinkToContent($link) {
22 | return $link.parent().parent().next();
23 | }
24 |
25 | // On doc load + hash-change, open any targetted item
26 | function openCurrentItemIfClosed() {
27 | if (window.jazzy.docset) {
28 | return;
29 | }
30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token');
31 | $content = itemLinkToContent($link);
32 | if ($content.is(':hidden')) {
33 | toggleItem($link, $content);
34 | }
35 | }
36 |
37 | $(openCurrentItemIfClosed);
38 | $(window).on('hashchange', openCurrentItemIfClosed);
39 |
40 | // On item link ('token') click, toggle its discussion
41 | $('.token').on('click', function(event) {
42 | if (window.jazzy.docset) {
43 | return;
44 | }
45 | var $link = $(this);
46 | toggleItem($link, itemLinkToContent($link));
47 |
48 | // Keeps the document from jumping to the hash.
49 | var href = $link.attr('href');
50 | if (history.pushState) {
51 | history.pushState({}, '', href);
52 | } else {
53 | location.hash = href;
54 | }
55 | event.preventDefault();
56 | });
57 |
58 | // Clicks on links to the current, closed, item need to open the item
59 | $("a:not('.token')").on('click', function() {
60 | if (location == this.href) {
61 | openCurrentItemIfClosed();
62 | }
63 | });
64 |
65 | // KaTeX rendering
66 | if ("katex" in window) {
67 | $($('.math').each( (_, element) => {
68 | katex.render(element.textContent, element, {
69 | displayMode: $(element).hasClass('m-block'),
70 | throwOnError: false,
71 | trust: true
72 | });
73 | }))
74 | }
75 |
--------------------------------------------------------------------------------
/Marindeck/Extensions/UILabel+Padding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UILabel+Padding.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/11/11.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UILabel {
11 | private struct AssociatedKeys {
12 | static var padding = UIEdgeInsets()
13 | }
14 |
15 | public var padding: UIEdgeInsets? {
16 | get {
17 | return objc_getAssociatedObject(self, &AssociatedKeys.padding) as? UIEdgeInsets
18 | }
19 | set {
20 | if let newValue = newValue {
21 | objc_setAssociatedObject(self,
22 | &AssociatedKeys.padding,
23 | newValue as UIEdgeInsets?,
24 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
25 | }
26 | }
27 | }
28 |
29 | override open func draw(_ rect: CGRect) {
30 | if let insets = padding {
31 | self.drawText(in: rect.inset(by: insets))
32 | } else {
33 | self.drawText(in: rect)
34 | }
35 | }
36 |
37 | override open var intrinsicContentSize: CGSize {
38 | guard let text = self.text else {
39 | return super.intrinsicContentSize
40 | }
41 |
42 | var contentSize = super.intrinsicContentSize
43 | var textWidth: CGFloat = frame.size.width
44 | var insetsHeight: CGFloat = 0.0
45 | var insetsWidth: CGFloat = 0.0
46 |
47 | if let insets = padding {
48 | insetsWidth += insets.left + insets.right
49 | insetsHeight += insets.top + insets.bottom
50 | textWidth -= insetsWidth
51 | }
52 |
53 | let newSize = text.boundingRect(with: CGSize(width: textWidth, height: CGFloat.greatestFiniteMagnitude),
54 | options: NSStringDrawingOptions.usesLineFragmentOrigin,
55 | attributes: [NSAttributedString.Key.font: self.font!], context: nil)
56 |
57 | contentSize.height = ceil(newSize.size.height) + insetsHeight
58 | contentSize.width = ceil(newSize.size.width) + insetsWidth
59 |
60 | return contentSize
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/docs/docsets/Marindeck.docset/Contents/Resources/Documents/js/jazzy.js:
--------------------------------------------------------------------------------
1 | // Jazzy - https://github.com/realm/jazzy
2 | // Copyright Realm Inc.
3 | // SPDX-License-Identifier: MIT
4 |
5 | window.jazzy = {'docset': false}
6 | if (typeof window.dash != 'undefined') {
7 | document.documentElement.className += ' dash'
8 | window.jazzy.docset = true
9 | }
10 | if (navigator.userAgent.match(/xcode/i)) {
11 | document.documentElement.className += ' xcode'
12 | window.jazzy.docset = true
13 | }
14 |
15 | function toggleItem($link, $content) {
16 | var animationDuration = 300;
17 | $link.toggleClass('token-open');
18 | $content.slideToggle(animationDuration);
19 | }
20 |
21 | function itemLinkToContent($link) {
22 | return $link.parent().parent().next();
23 | }
24 |
25 | // On doc load + hash-change, open any targetted item
26 | function openCurrentItemIfClosed() {
27 | if (window.jazzy.docset) {
28 | return;
29 | }
30 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token');
31 | $content = itemLinkToContent($link);
32 | if ($content.is(':hidden')) {
33 | toggleItem($link, $content);
34 | }
35 | }
36 |
37 | $(openCurrentItemIfClosed);
38 | $(window).on('hashchange', openCurrentItemIfClosed);
39 |
40 | // On item link ('token') click, toggle its discussion
41 | $('.token').on('click', function(event) {
42 | if (window.jazzy.docset) {
43 | return;
44 | }
45 | var $link = $(this);
46 | toggleItem($link, itemLinkToContent($link));
47 |
48 | // Keeps the document from jumping to the hash.
49 | var href = $link.attr('href');
50 | if (history.pushState) {
51 | history.pushState({}, '', href);
52 | } else {
53 | location.hash = href;
54 | }
55 | event.preventDefault();
56 | });
57 |
58 | // Clicks on links to the current, closed, item need to open the item
59 | $("a:not('.token')").on('click', function() {
60 | if (location == this.href) {
61 | openCurrentItemIfClosed();
62 | }
63 | });
64 |
65 | // KaTeX rendering
66 | if ("katex" in window) {
67 | $($('.math').each( (_, element) => {
68 | katex.render(element.textContent, element, {
69 | displayMode: $(element).hasClass('m-block'),
70 | throwOnError: false,
71 | trust: true
72 | });
73 | }))
74 | }
75 |
--------------------------------------------------------------------------------
/README-ja.md:
--------------------------------------------------------------------------------
1 | # MarinDeck for iOS
2 |
3 | [](https://twitter.com/intent/follow?screen_name=vitomcharm)
4 | [](https://discord.gg/JKsqaxcnCW)
5 | [](https://hisubway.online/marindeck)
6 | 
7 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FRiniaOkyama%2FMarinDeck4iOS?ref=badge_shield)
8 | [](https://app.bitrise.io/app/731d5ad5a90fa3b9)
9 |
10 |
11 | 
12 |
13 | 
14 |
15 |
16 | [日本語 / [English](README.md)]
17 |
18 | ## 必要条件
19 |
20 | * iOS14.1+
21 | * Xcode14
22 |
23 | ## セットアップ
24 |
25 | ```
26 | $ brew install mint
27 | $ mint bootstrap
28 |
29 | $ mint run xcodegen generate
30 | $ xed .
31 | ```
32 |
33 |
34 |
35 | ## 貢献
36 |
37 | 貢献お待ちしております✨
38 | - [New issue](https://github.com/RiniaOkyama/MarinDeck4iOS/issues/new)
39 | - [New Pull Request](https://github.com/RiniaOkyama/MarinDeck4iOS/compare)
40 |
41 |
42 | ## Support
43 |
44 | iOSアプリについてサポートが必要な場合や質問がある場合は Discordチャンネル([#苹果](https://discord.gg/JKsqaxcnCW))でサポートを受けることができます。バグを発見した場合は、Githubに新しいissueを作成するか、Discordチャンネル([#苹果](https://discord.gg/JKsqaxcnCW))までお願いします。
45 |
46 | このリポジトリはiOSアプリのみを扱っています。Android版とは別なので注意してください。
47 |
48 | ## LICENSE
49 | いつか
50 |
51 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FRiniaOkyama%2FMarinDeck4iOS?ref=badge_large)
52 |
53 | ## Stats
54 |
55 | 
56 |
--------------------------------------------------------------------------------
/docs/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | // Jazzy - https://github.com/realm/jazzy
2 | // Copyright Realm Inc.
3 | // SPDX-License-Identifier: MIT
4 |
5 | $(function(){
6 | var $typeahead = $('[data-typeahead]');
7 | var $form = $typeahead.parents('form');
8 | var searchURL = $form.attr('action');
9 |
10 | function displayTemplate(result) {
11 | return result.name;
12 | }
13 |
14 | function suggestionTemplate(result) {
15 | var t = '';
16 | t += '' + result.name + '';
17 | if (result.parent_name) {
18 | t += '' + result.parent_name + '';
19 | }
20 | t += '
';
21 | return t;
22 | }
23 |
24 | $typeahead.one('focus', function() {
25 | $form.addClass('loading');
26 |
27 | $.getJSON(searchURL).then(function(searchData) {
28 | const searchIndex = lunr(function() {
29 | this.ref('url');
30 | this.field('name');
31 | this.field('abstract');
32 | for (const [url, doc] of Object.entries(searchData)) {
33 | this.add({url: url, name: doc.name, abstract: doc.abstract});
34 | }
35 | });
36 |
37 | $typeahead.typeahead(
38 | {
39 | highlight: true,
40 | minLength: 3,
41 | autoselect: true
42 | },
43 | {
44 | limit: 10,
45 | display: displayTemplate,
46 | templates: { suggestion: suggestionTemplate },
47 | source: function(query, sync) {
48 | const lcSearch = query.toLowerCase();
49 | const results = searchIndex.query(function(q) {
50 | q.term(lcSearch, { boost: 100 });
51 | q.term(lcSearch, {
52 | boost: 10,
53 | wildcard: lunr.Query.wildcard.TRAILING
54 | });
55 | }).map(function(result) {
56 | var doc = searchData[result.ref];
57 | doc.url = result.ref;
58 | return doc;
59 | });
60 | sync(results);
61 | }
62 | }
63 | );
64 | $form.removeClass('loading');
65 | $typeahead.trigger('focus');
66 | });
67 | });
68 |
69 | var baseURL = searchURL.slice(0, -"search.json".length);
70 |
71 | $typeahead.on('typeahead:select', function(e, result) {
72 | window.location = baseURL + result.url;
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/docs/docsets/Marindeck.docset/Contents/Resources/Documents/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | // Jazzy - https://github.com/realm/jazzy
2 | // Copyright Realm Inc.
3 | // SPDX-License-Identifier: MIT
4 |
5 | $(function(){
6 | var $typeahead = $('[data-typeahead]');
7 | var $form = $typeahead.parents('form');
8 | var searchURL = $form.attr('action');
9 |
10 | function displayTemplate(result) {
11 | return result.name;
12 | }
13 |
14 | function suggestionTemplate(result) {
15 | var t = '';
16 | t += '' + result.name + '';
17 | if (result.parent_name) {
18 | t += '' + result.parent_name + '';
19 | }
20 | t += '
';
21 | return t;
22 | }
23 |
24 | $typeahead.one('focus', function() {
25 | $form.addClass('loading');
26 |
27 | $.getJSON(searchURL).then(function(searchData) {
28 | const searchIndex = lunr(function() {
29 | this.ref('url');
30 | this.field('name');
31 | this.field('abstract');
32 | for (const [url, doc] of Object.entries(searchData)) {
33 | this.add({url: url, name: doc.name, abstract: doc.abstract});
34 | }
35 | });
36 |
37 | $typeahead.typeahead(
38 | {
39 | highlight: true,
40 | minLength: 3,
41 | autoselect: true
42 | },
43 | {
44 | limit: 10,
45 | display: displayTemplate,
46 | templates: { suggestion: suggestionTemplate },
47 | source: function(query, sync) {
48 | const lcSearch = query.toLowerCase();
49 | const results = searchIndex.query(function(q) {
50 | q.term(lcSearch, { boost: 100 });
51 | q.term(lcSearch, {
52 | boost: 10,
53 | wildcard: lunr.Query.wildcard.TRAILING
54 | });
55 | }).map(function(result) {
56 | var doc = searchData[result.ref];
57 | doc.url = result.ref;
58 | return doc;
59 | });
60 | sync(results);
61 | }
62 | }
63 | );
64 | $form.removeClass('loading');
65 | $typeahead.trigger('focus');
66 | });
67 | });
68 |
69 | var baseURL = searchURL.slice(0, -"search.json".length);
70 |
71 | $typeahead.on('typeahead:select', function(e, result) {
72 | window.location = baseURL + result.url;
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/Marindeck/Extensions/ViewController+biometrics.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by a on 2022/01/26.
3 | //
4 |
5 | import Foundation
6 | import UIKit
7 | import LocalAuthentication
8 |
9 | extension ViewController {
10 | // 生体認証
11 | func checkBiometrics() {
12 | let isUseBiometrics = UserDefaults.standard.bool(forKey: UserDefaultsKey.isUseBiometrics)
13 | if isUseBiometrics {
14 | if canUseBiometrics() {
15 | isMainDeckViewLock = true
16 | mainDeckView.isHidden = true
17 | let context = LAContext()
18 | let reason = "ロックを解除"
19 | let backBlackView = UIView(frame: view.bounds)
20 | backBlackView.backgroundColor = .black
21 | view.addSubview(backBlackView)
22 | context.evaluatePolicy(.deviceOwnerAuthentication,
23 | localizedReason: reason) { (success, evaluateError) in
24 | if success {
25 | DispatchQueue.main.async { [unowned self] in
26 | self.isMainDeckViewLock = false
27 | self.mainDeckView.isHidden = false
28 | self.tweetFloatingBtn.isHidden = false
29 | UIView.animate(withDuration: 0.3, animations: {
30 | backBlackView.alpha = 0
31 | }, completion: { _ in
32 | backBlackView.removeFromSuperview()
33 | })
34 | }
35 | } else {
36 | DispatchQueue.main.async {
37 | let errorLabel = UILabel(frame: backBlackView.bounds)
38 | errorLabel.textAlignment = .center
39 | errorLabel.text = "認証に失敗しました。"
40 | errorLabel.textColor = .white
41 | backBlackView.addSubview(errorLabel)
42 | }
43 | guard let error = evaluateError as NSError? else {
44 | print("Error")
45 | return
46 | }
47 | print("\(error.code): \(error.localizedDescription)")
48 | }
49 | }
50 | } else {
51 | // 生体認証をオンにしているが、許可されていない。
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Marindeck/View/Other/DraftViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DraftViewController.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/11/28.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DraftView: View {
11 | @Environment(\.presentationMode) var presentationMode
12 |
13 | typealias selectedCompletion = (_ index: Int) -> Void
14 | let selected: selectedCompletion?
15 | let drafts: [Draft]
16 |
17 | init(selected: selectedCompletion? = nil, drafts: [Draft]) {
18 | self.selected = selected
19 | self.drafts = drafts
20 | }
21 |
22 | var body: some View {
23 | VStack {
24 | if drafts.isEmpty {
25 | Text("下書きがありません。\n\nネイティブのツイート画面から\n下書き保存ができます。")
26 | .multilineTextAlignment(.center)
27 | } else {
28 | List {
29 | ForEach(0 ..< drafts.count) { index in
30 | if #available(iOS 15.0, *) {
31 | Text(drafts[index].text)
32 | // 開発環境がiOS14.5なのでデバッグできない。
33 | // .swipeActions(edge: .trailing) {
34 | // Button(role: .destructive) {
35 | // print("delete action.")
36 | // } label: {
37 | // Image(systemName: "trash.fill")
38 | // }
39 | // }
40 | .onTapGesture {
41 | presentationMode.wrappedValue.dismiss()
42 | selected?(index)
43 | }
44 | } else {
45 | Text(drafts[index].text)
46 | .onTapGesture {
47 | presentationMode.wrappedValue.dismiss()
48 | selected?(index)
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | struct DraftView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | DraftView(selected: nil, drafts: [])
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Marindeck/View/Core/ViewController+ImagePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController+ImagePicker.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/02/05.
6 | //
7 | import UIKit
8 | import Loaf
9 |
10 | extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
11 |
12 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
13 | print("\(info)")
14 | if let image = info[.originalImage] as? UIImage {
15 | guard let url = info[.imageURL] as? URL else { return }
16 | let pex = url.pathExtension
17 |
18 | var base64imgString = ""
19 | if pex == "png" {
20 | base64imgString = image.pngData()?.base64EncodedString(options: []) ?? ""
21 | } else if pex == "jpg" || pex == "jpeg" {
22 | base64imgString = image.jpegData(compressionQuality: 0.7)?.base64EncodedString(options: []) ?? ""
23 | }
24 |
25 | if base64imgString == "" {
26 | // TODO: L10n
27 | Loaf("画像が読み込めませんでした。", state: .error, location: .top, sender: self).show()
28 | return
29 | }
30 |
31 | print(pex)
32 | print(url.lastPathComponent)
33 | print(base64imgString.count)
34 | UIPasteboard.general.string = base64imgString
35 | webView.evaluateJavaScript("window.MarinDeckInputs.addTweetImage(\"data:image/\(pex);base64,\(base64imgString)\", \"image/\(pex)\", \"\(url.lastPathComponent)\")") { _, error in
36 | print("photoselected : ", error ?? "成功")
37 | }
38 | dismiss(animated: true, completion: nil)
39 | }
40 | }
41 |
42 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
43 |
44 | if let image = info[UIImagePickerController.InfoKey.originalImage.rawValue] as? UIImage {
45 | guard let base64img = image.pngData()?.base64EncodedString(options: []) else {
46 | return
47 | }
48 | webView.evaluateJavaScript("window.MarinDeckInputs.addTweetImage(\"data:image/png;base64,\(base64img)\", \"image/png\", \"test.png\")") { _, error in
49 | print("photoselected : ", error ?? "成功")
50 | }
51 | } else {
52 | print("Error")
53 | }
54 |
55 | dismiss(animated: true, completion: nil)
56 | }
57 |
58 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
59 | dismiss(animated: true, completion: nil)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Binding/src/inputs.ts:
--------------------------------------------------------------------------------
1 | import { jQuery } from "../MarinDeckObject/src/objects/jQuery"
2 |
3 |
4 | export interface MarinDeckInputsInterface {
5 | addTweetImage(base64: string, type: string, name: string): void
6 | touchPointTweetLike(x: number, y: number): void
7 | postTweet(text: string, reply_to_id, key): void
8 | updateSchedule(Y: number, M: number, D: number, h: number, m: number): void
9 | }
10 |
11 | /** tweet */
12 | // const getClient = (key = null) => window.TD.controller.clients.getClient(key || window.TD.storage.accountController.getDefault().privateState.key) || window.TD.controller.clients.getPreferredClient('twitter')
13 |
14 | // const getAllAccounts = () => {
15 | // const accounts = []
16 | // window.TD.storage.accountController.getAll().forEach((x) => {
17 | // if (x.managed) accounts.push({
18 | // key: x.privateState.key,
19 | // name: x.state.name,
20 | // userId: x.state.userId,
21 | // username: x.state.username,
22 | // profileImageURL: x.state.profileImageURL,
23 | // })
24 | // })
25 | // return accounts
26 | // }
27 |
28 | // window.TD.storage.accountController.getAll()
29 | // .filter(({managed}) => managed)
30 | // .map(({state: {name: fullname, username, userId, profileImageURL}}) => ({fullname, username, userId, profileImageURL}))
31 |
32 |
33 | export class MarinDeckInputs implements MarinDeckInputsInterface {
34 |
35 | addTweetImage(base64: string, type: string, name: string) {
36 | var bin = atob(base64.replace(/^.*,/, '')); // decode base64
37 | var buffer = new Uint8Array(bin.length); // to binary data
38 | for (var i = 0; i < bin.length; i++) {
39 | buffer[i] = bin.charCodeAt(i);
40 | }
41 | const imgFile = new File([buffer.buffer], name, {type: type});
42 |
43 | jQuery(document).trigger("uiFilesAdded", {files: [imgFile]});
44 | }
45 |
46 | touchPointTweetLike(x: number, y: number) {
47 | const element = document.elementFromPoint(x, y)?.closest(".tweet")?.getElementsByClassName("tweet-action")[2]
48 | if (element instanceof HTMLElement) {
49 | element.click()
50 | }
51 | }
52 |
53 | postTweet(text: string) {
54 | window.MD.TwitterAPI.update({
55 | status: text,
56 | from: window.TD.storage.accountController.getDefault().getUsername()
57 | })
58 | // getClient(key).update(text, reply_to_id, null, null, null, resolve, reject)
59 | }
60 |
61 | updateSchedule(Y: number, M: number, D: number, h: number, m: number): void {
62 | jQuery('.js-docked-compose').parent().trigger('uiComposeScheduleDate', {date: new Date(Y, M-1, D, h, m, 0, 0)});
63 | }
64 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MarinDeck for iOS
2 |
3 | [](https://twitter.com/intent/follow?screen_name=vitomcharm)
4 | [](https://discord.gg/JKsqaxcnCW)
5 | [](https://hisubway.online/marindeck)
6 | 
7 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FRiniaOkyama%2FMarinDeck4iOS?ref=badge_shield)
8 | [](https://app.bitrise.io/app/731d5ad5a90fa3b9)
9 |
10 |
11 | 
12 |
13 | 
14 |
15 | Check out [hisubway.online/marindeck](https://hisubway.online/marindeck/) and follow us on [twitter.com/HiSubway](https://twitter.com/HiSubway) and [twitter.com/vitomcharm](https://twitter.com/vitomcharm)
16 |
17 | [[日本語](README-ja.md) / English]
18 |
19 | ## Requirements
20 |
21 | * iOS14.1+
22 | * Xcode14
23 |
24 | ## Installation
25 |
26 | ```
27 | $ brew install mint
28 | $ mint bootstrap
29 |
30 | $ mint run xcodegen generate
31 | $ xed .
32 | ```
33 |
34 |
35 |
36 | ## Contribute
37 |
38 | If you want to contribute to MarinDeck4iOS, you are very welcome
39 | - [New issue](https://github.com/RiniaOkyama/MarinDeck4iOS/issues/new)
40 | - [New Pull Request](https://github.com/RiniaOkyama/MarinDeck4iOS/compare)
41 |
42 |
43 | ## Support
44 |
45 |
46 | If you need help or have questions about the iOS app, you can get support on our Discord channel ([#苹果](https://discord.gg/JKsqaxcnCW)). If you find a bug, please create a new issue on Github or send it to our Discord channel ([#苹果](https://discord.gg/JKsqaxcnCW)).
47 |
48 | Please note that this repository only deals with iOS apps; it is separate from the Android version.
49 |
50 | ## LICENSE
51 | someday...
52 |
53 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FRiniaOkyama%2FMarinDeck4iOS?ref=badge_large)
54 |
55 | ## Stats
56 |
57 | 
58 |
--------------------------------------------------------------------------------
/Marindeck/Application/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Marindecker
4 | //
5 | // Created by Rinia on 2021/01/12.
6 | //
7 |
8 | import UIKit
9 | import class SwiftUI.UIHostingController
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 | func scene(_ scene: UIScene,
16 | willConnectTo session: UISceneSession,
17 | options connectionOptions: UIScene.ConnectionOptions) {
18 |
19 | guard let scene = (scene as? UIWindowScene) else { return }
20 |
21 | window = UIWindow(windowScene: scene)
22 |
23 | if UserDefaults.standard.bool(forKey: .appDebugMode) {
24 | let view = AppDebugView(vc: nil)
25 | let vc = UIHostingController(rootView: view)
26 | window?.rootViewController = vc
27 | (window?.rootViewController as! UIHostingController).rootView.vc = window?.rootViewController
28 | } else {
29 | let view = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
30 | window?.rootViewController = view
31 | }
32 |
33 | window?.makeKeyAndVisible()
34 | }
35 |
36 | func sceneDidDisconnect(_ scene: UIScene) {
37 | // Called as the scene is being released by the system.
38 | // This occurs shortly after the scene enters the background, or when its session is discarded.
39 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
40 | // The scene may re-connect later, as its session was not necessarily discarded
41 | // (see `application:didDiscardSceneSessions` instead).
42 | }
43 |
44 | func sceneDidBecomeActive(_ scene: UIScene) {
45 | // Called when the scene has moved from an inactive state to an active state.
46 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
47 | }
48 |
49 | func sceneWillResignActive(_ scene: UIScene) {
50 | // Called when the scene will move from an active state to an inactive state.
51 | // This may occur due to temporary interruptions (ex. an incoming phone call).
52 | }
53 |
54 | func sceneWillEnterForeground(_ scene: UIScene) {
55 | // Called as the scene transitions from the background to the foreground.
56 | // Use this method to undo the changes made on entering the background.
57 | }
58 |
59 | func sceneDidEnterBackground(_ scene: UIScene) {
60 | // Called as the scene transitions from the foreground to the background.
61 | // Use this method to save data, release shared resources, and store enough scene-specific state information
62 | // to restore the scene back to its current state.
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Marindeck/View/Core/ViewController+WKNavigationDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController+WKNavigationDelegate.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/02/05.
6 | //
7 | import WebKit
8 | import SafariServices
9 |
10 | extension ViewController: WKNavigationDelegate {
11 | // MARK: - 読み込み設定(リクエスト前)
12 | func webView(_ webView: WKWebView,
13 | decidePolicyFor navigationAction: WKNavigationAction,
14 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
15 | let url = navigationAction.request.url
16 | guard let host = url?.host else {
17 | decisionHandler(.cancel)
18 | return
19 | }
20 |
21 | if ((url?.absoluteString.contains("twitter.com/i/cards")) ?? false) ||
22 | (url?.absoluteString.contains("youtube.com/embed") ?? false) {
23 | decisionHandler(.cancel)
24 | return
25 | }
26 |
27 | if host == "tweetdeck.twitter.com" {
28 | decisionHandler(.allow)
29 | // }else if host.hasPrefix("t.co") {
30 | // decisionHandler(.cancel)
31 | } else if host == "mobile.twitter.com" {
32 | let vc = LoginViewController()
33 | vc.delegate = self
34 | let nvc = UINavigationController(rootViewController: vc)
35 | present(nvc, animated: true, completion: nil)
36 | decisionHandler(.cancel)
37 | } else {
38 | let safariVC = SFSafariViewController(url: url!)
39 | present(safariVC, animated: true, completion: nil)
40 |
41 | decisionHandler(.cancel)
42 | }
43 | }
44 |
45 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
46 | webView.loadJsFile(forResource: "moduleraid")
47 | // loadJsFile(forResource: "marindeck-css")
48 | // webView.loadJsFile(forResource: "msecdeck.bundle")
49 | webView.loadJsFile(forResource: "marindeck")
50 | webView.loadCSSFile(forResource: "marindeck")
51 |
52 | let cjss = try! dbQueue.read { db in
53 | try CustomJS.fetchAll(db)
54 | }
55 | .filter { $0.isLoad }
56 | .sorted(by: { $0.loadIndex < $1.loadIndex })
57 | for item in cjss {
58 | webView.inject(js: item.js)
59 | }
60 |
61 | let csss = try! dbQueue.read { db in
62 | try CustomCSS.fetchAll(db)
63 | }
64 | .filter { $0.isLoad }
65 | .sorted(by: { $0.loadIndex < $1.loadIndex })
66 | for item in csss {
67 | webView.inject(css: item.css)
68 | }
69 |
70 | let theme = fetchTheme()
71 | webView.inject(js: theme.js)
72 |
73 | let rjs = RemoteJS.shared
74 | rjs.update { [weak self] () in
75 | self?.webView.inject(js: rjs.getJs(id: .navigationTab) ?? "")
76 | }
77 |
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Marindeck/View/Menu/MenuItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuItemView.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/05/06.
6 | //
7 |
8 | import UIKit
9 |
10 | @IBDesignable
11 | final class MenuItemView: UIView {
12 |
13 | @IBInspectable var iconImage: UIImage? {
14 | didSet {
15 | iconView.image = iconImage
16 | }
17 | // get { return iconView.image }
18 | // set { iconView.image = newValue }
19 | }
20 |
21 | @IBInspectable var title: String = "" {
22 | didSet {
23 | titleLabel.text = title
24 | }
25 | }
26 |
27 | public lazy var iconView: UIImageView = {
28 | let imgView = UIImageView()
29 | imgView.frame.size.height = self.frame.height
30 | imgView.frame.size.width = 22
31 | imgView.frame.origin.x = 12
32 | imgView.contentMode = .scaleAspectFit
33 | imgView.image = iconImage
34 | imgView.tintColor = .labelColor
35 | return imgView
36 | }()
37 |
38 | public lazy var titleLabel: UILabel = {
39 | let label = UILabel()
40 | label.frame.size.height = self.frame.height
41 | label.frame.size.width = self.frame.width - (36 + 12)
42 | label.frame.origin.x = 36 + 12
43 | label.textColor = .labelColor
44 |
45 | return label
46 | }()
47 |
48 | override init(frame: CGRect) {
49 | super.init(frame: frame)
50 | setupViews()
51 | }
52 |
53 | override func awakeFromNib() {
54 | super.awakeFromNib()
55 | setupViews()
56 | }
57 |
58 | override func prepareForInterfaceBuilder() {
59 | super.prepareForInterfaceBuilder()
60 | setupViews()
61 | // setNeedsDisplay()
62 | }
63 |
64 | required init?(coder aDecoder: NSCoder) {
65 | super.init(coder: aDecoder)
66 | }
67 |
68 | func setupViews() {
69 | self.addSubview(iconView)
70 | self.addSubview(titleLabel)
71 |
72 | self.layer.cornerRadius = 12
73 | self.backgroundColor = .clear
74 | }
75 |
76 | public func setTapEvent(action: Selector, target: Any) {
77 | let tapGestureRecognizer = UITapGestureRecognizer(target: target, action: action)
78 | self.addGestureRecognizer(tapGestureRecognizer)
79 | }
80 |
81 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
82 | self.backgroundColor = UIColor.black.withAlphaComponent(0.3)
83 | }
84 |
85 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
86 | UIView.animate(withDuration: 0.2, animations: {
87 | self.backgroundColor = .clear
88 | })
89 | }
90 |
91 | override func touchesEnded(_ touches: Set, with event: UIEvent?) {
92 | UIView.animate(withDuration: 0.2, animations: {
93 | self.backgroundColor = .clear
94 | })
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/Marindeck/View/Core/ViewController+MenuView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController+MenuView.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/02/11.
6 | //
7 |
8 | import Foundation
9 | import class UIKit.UIView
10 | import class UIKit.UIColor
11 |
12 | protocol MenuAction {
13 | func closeMenu()
14 | func openMenu()
15 | }
16 |
17 | extension ViewController: MenuDelegate {
18 | func reload() {
19 | webView.reload()
20 | closeMenu()
21 | }
22 |
23 | func openProfile() {
24 | closeMenu()
25 | webView.evaluateJavaScript("document.querySelector(\"body > div.application.js-app.is-condensed > header > div > div.js-account-summary > a > div\").click()") { _, error in
26 | print("openProfile : ", error ?? "成功")
27 | }
28 | }
29 |
30 | func openColumnAdd() {
31 | closeMenu()
32 | webView.evaluateJavaScript("document.querySelector(\".js-header-add-column\").click()") { _, error in
33 | print(#function, error ?? "成功")
34 | }
35 | }
36 |
37 | func openTdSettings() {
38 | closeMenu()
39 | td.actions.openTDSettings()
40 | }
41 | }
42 |
43 | extension ViewController: MenuAction {
44 | // Menuを閉じる
45 | func closeMenu() {
46 | isMenuOpen = false
47 | menuView.translatesAutoresizingMaskIntoConstraints = false
48 | mainDeckView.translatesAutoresizingMaskIntoConstraints = false
49 | mainDeckBlurView.isUserInteractionEnabled = false
50 | UIView.animate(withDuration: 0.2, animations: {
51 | self.mainDeckBlurView.backgroundColor = .none
52 |
53 | self.mainDeckBlurView.frame.origin.x = 0
54 | self.mainDeckView.frame.origin.x = 0
55 | self.bottomBackView.frame.origin.x = 0
56 | self.topBackView.frame.origin.x = 0
57 |
58 | self.menuView.frame.origin.x = -self.menuView.frame.width
59 | })
60 | }
61 |
62 | // Menuを開く
63 | func openMenu() {
64 | isMenuOpen = true
65 | menuView.translatesAutoresizingMaskIntoConstraints = true
66 | mainDeckView.translatesAutoresizingMaskIntoConstraints = true
67 | mainDeckBlurView.isUserInteractionEnabled = true
68 |
69 | td.account.getAccount { [weak self] account in
70 | self?.menuVC.setUserIcon(url: account.profileImageUrl ?? "")
71 | self?.menuVC.setUserNameID(name: account.name ?? "", id: account.userId ?? "")
72 | }
73 |
74 | UIView.animate(withDuration: 0.3, animations: {
75 | self.menuView.frame.origin.x = 0
76 | self.mainDeckBlurView.frame.origin.x = self.menuView.frame.width
77 | self.mainDeckView.frame.origin.x = self.menuView.frame.width
78 |
79 | UIView.animate(withDuration: 0.2, animations: {
80 | self.mainDeckBlurView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
81 | })
82 | })
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/project.yml:
--------------------------------------------------------------------------------
1 | name: Marindeck
2 |
3 | packages:
4 | GRDB:
5 | url: https://github.com/groue/GRDB.swift
6 | exactVersion: 5.12.0
7 | MarkdownView:
8 | url: https://github.com/hirossan4049/MarkdownView
9 | revision: 753be391e6d3640161ca21f49a57c306aaebaf3a
10 | GiphyUISDK:
11 | url: https://github.com/Giphy/giphy-ios-sdk
12 | exactVersion: 2.1.16
13 | Loaf:
14 | url: https://github.com/schmidyy/Loaf
15 | exactVersion: 0.6.0
16 | SwiftyStoreKit:
17 | url: https://github.com/bizz84/SwiftyStoreKit
18 | exactVersion: 0.16.4
19 |
20 |
21 |
22 | options:
23 | #bundleIdPrefix: marindeck
24 | postGenCommand: rbenv exec pod install
25 | developmentLanguage: en
26 |
27 | settingGroups:
28 | MarindeckBaseSettings:
29 | SWIFT_OBJC_BRIDGING_HEADER: Marindeck/Marindeck-Bridging-Header.h
30 |
31 | settings:
32 | base:
33 | DEVELOPMENT_TEAM: 726VR75V6L
34 | MARKETING_VERSION: 0.2.2
35 | CURRENT_PROJECT_VERSION: 0.2.2
36 | config:
37 | debug:
38 | DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym"
39 |
40 | # schemes:
41 | # Debug:
42 | # build:
43 | # targets:
44 | # MarinDeck: all
45 | # MarinDeckTests: [test]
46 | #
47 | # Release:
48 | # build:
49 | # targets:
50 | # MarinDeck: all
51 | # MarinDeckTests: [test]
52 | #
53 | # run:
54 | # config: Release
55 | # test:
56 | # config: Release
57 | # profile:
58 | # config: Release
59 | # analyze:
60 | # config: Release
61 | # archive:
62 | # config: Release
63 |
64 |
65 |
66 | targets:
67 | Marindeck:
68 | type: application
69 | platform: iOS
70 | deploymentTarget: "15"
71 | sources: Marindeck
72 | scheme: {}
73 | settings:
74 | base:
75 | DEVELOPMENT_TEAM: 726VR75V6L
76 | PRODUCT_BUNDLE_IDENTIFIER: marindeck
77 | DISPLAY_APPNAME: MarinDeck
78 | dependencies:
79 | - package: GRDB
80 | - package: MarkdownView
81 | - package: GiphyUISDK
82 | - package: Loaf
83 | - package: SwiftyStoreKit
84 | preBuildScripts:
85 | - path: ./scripts/swiftlint.sh
86 | name: Run SwiftLint
87 | - path: ./scripts/vite.sh
88 | name: Build Vite
89 |
90 | Marindeck-dev:
91 | type: application
92 | platform: iOS
93 | deploymentTarget: "15"
94 | sources: Marindeck
95 | scheme: {}
96 | settings:
97 | base:
98 | DEVELOPMENT_TEAM: 726VR75V6L
99 | PRODUCT_BUNDLE_IDENTIFIER: marindeck.dev
100 | DISPLAY_APPNAME: MD-dev
101 | dependencies:
102 | - package: GRDB
103 | - package: MarkdownView
104 | - package: GiphyUISDK
105 | - package: Loaf
106 | - package: SwiftyStoreKit
107 |
--------------------------------------------------------------------------------
/Marindeck/View/Settings/Icon/IconSettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IconSettings.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/12/31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct Icon: Hashable {
11 | let iconName: String
12 | let iconTitle: String
13 | let iconFlag: String?
14 | }
15 |
16 | struct IconSettingsListItem: View {
17 | let icon: Icon
18 | let isSelected: Bool
19 |
20 | var body: some View {
21 | HStack {
22 | Image(icon.iconName)
23 | .resizable()
24 | .frame(width: 48, height: 48)
25 | .cornerRadius(8)
26 | Text(icon.iconTitle)
27 | .frame(maxWidth: .infinity, alignment: .leading)
28 | .foregroundColor(Color(UIColor.labelColor))
29 | if isSelected {
30 | Image(systemName: "checkmark.circle.fill")
31 | .frame(width: 12.0, height: 12.0)
32 | .foregroundColor(Color(UIColor.labelColor))
33 | }
34 | }
35 | .frame(height: 54)
36 | }
37 | }
38 |
39 | struct IconSettingsView: View {
40 | @Environment(\.presentationMode) var presentationMode
41 |
42 | init() {
43 | UITableView.appearance().backgroundColor = .secondaryBackgroundColor
44 | }
45 |
46 | // @State private var icons = ["DefaultIcon": "白","BlackIcon": "黒","RainbowIcon": "ゲーミング"]
47 | let icons: [Icon] = [
48 | .init(iconName: "DefaultIcon", iconTitle: "白", iconFlag: nil),
49 | .init(iconName: "BlackIcon", iconTitle: "黒", iconFlag: "BlackIcon"),
50 | .init(iconName: "RainbowIcon", iconTitle: "ゲーミング", iconFlag: "RainbowIcon")
51 | ]
52 |
53 | @State private var alternateIconName = UIApplication.shared.alternateIconName
54 |
55 | var body: some View {
56 | ZStack {
57 | // TODO: NavigatonViewのタイトルを設定しても表示されない
58 | // NavigationView {
59 | List {
60 | ForEach(icons, id: \.self) { icon in
61 | IconSettingsListItem(icon: icon, isSelected: alternateIconName == icon.iconFlag )
62 | .contentShape(Rectangle())
63 | .onTapGesture {
64 | UIApplication.shared.setAlternateIconName(icon.iconFlag, completionHandler: nil)
65 | updateIconName()
66 | }
67 | .listRowBackground(Color(UIColor.secondaryBackgroundColor))
68 | }
69 | }
70 | .onAppear {
71 | updateIconName()
72 | }
73 | // }
74 | // .navigationBarTitle(Text("Users"))
75 | }
76 | }
77 |
78 | func updateIconName() {
79 | alternateIconName = UIApplication.shared.alternateIconName
80 | }
81 | }
82 |
83 | struct IconSettingsView_Previews: PreviewProvider {
84 | static var previews: some View {
85 | IconSettingsView()
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Marindeck/Application/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Marindecker
4 | //
5 | // Created by Rinia on 2021/01/12.
6 | //
7 |
8 | import UIKit
9 | import Keys
10 | import GiphyUISDK
11 | import SwiftyStoreKit
12 |
13 | @main
14 | class AppDelegate: UIResponder, UIApplicationDelegate {
15 |
16 | func application(_ application: UIApplication,
17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 |
20 | // let keys = MarindeckKeys()
21 | // DeployGateSDK
22 | // .sharedInstance()
23 | // .launchApplication(withAuthor: keys.deploygateUsername, key: keys.deploygateSdkApiKey)
24 | UserDefaults.standard.register(defaults: [.marginSafeArea: true,
25 | .appDebugMode: false])
26 |
27 | Database.shared.setup()
28 | Giphy.configure(apiKey: MarindeckKeys().giphyApiKey)
29 | UIApplication.shared.isIdleTimerDisabled = UserDefaults.standard.bool(forKey: .noSleep)
30 |
31 | #if DEBUG
32 | _ = Test()
33 | #endif
34 |
35 | SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
36 | for purchase in purchases {
37 | switch purchase.transaction.transactionState {
38 | case .purchased, .restored:
39 | if purchase.needsFinishTransaction {
40 | // Deliver content from server, then:
41 | SwiftyStoreKit.finishTransaction(purchase.transaction)
42 | }
43 | // Unlock content
44 | case .failed, .purchasing, .deferred:
45 | break // do nothing
46 | @unknown default:
47 | break
48 | }
49 | }
50 | }
51 |
52 | return true
53 | }
54 |
55 | // MARK: UISceneSession Lifecycle
56 |
57 | func application(_ application: UIApplication,
58 | configurationForConnecting connectingSceneSession: UISceneSession,
59 | options: UIScene.ConnectionOptions) -> UISceneConfiguration {
60 | // Called when a new scene session is being created.
61 | // Use this method to select a configuration to create the new scene with.
62 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
63 | }
64 |
65 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
66 | // Called when the user discards a scene session.
67 | // If any sessions were discarded while the application was not running,
68 | // this will be called shortly after application:didFinishLaunchingWithOptions.
69 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/Marindeck/View/Settings/Theme/ThemeDetailViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThemeDetailViewController.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/05/09.
6 | //
7 |
8 | import UIKit
9 |
10 | class ThemeDetailViewController: UIViewController {
11 | @IBOutlet weak var iconView: UIImageView!
12 | @IBOutlet weak var titleLabel: UILabel!
13 | @IBOutlet weak var userLabel: UILabel!
14 | @IBOutlet weak var descriptionTextView: UITextView!
15 |
16 | @IBOutlet weak var applyBtn: UIButton!
17 | @IBOutlet weak var previewBtn: UIButton!
18 |
19 | public var theme: Theme? {
20 | didSet {
21 | titleLabel?.text = theme?.title
22 | descriptionTextView?.text = theme?.description
23 | // iconView.image = theme?.icon
24 | userLabel?.text = "by \(theme?.user ?? "不明")"
25 | }
26 | }
27 |
28 | public var viewController: ViewController!
29 |
30 | private var isApplied = false
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | self.title = theme?.title
36 | applyBtn.layer.cornerRadius = 6
37 | previewBtn.layer.cornerRadius = 6
38 |
39 | iconView.clipsToBounds = true
40 | iconView.layer.cornerRadius = iconView.frame.width / 2
41 | iconView.image = UIImage(named: theme?.icon ?? "") ?? Asset.marindeckLogo.image
42 | titleLabel?.text = theme?.title
43 | descriptionTextView?.text = theme?.description
44 | // iconView.image = theme?.icon
45 | userLabel?.text = theme?.user
46 |
47 | reload()
48 |
49 | setSwipeBack()
50 | }
51 |
52 | func reload() {
53 | view.backgroundColor = .secondaryBackgroundColor
54 | navigationController?.navigationBar.tintColor = .labelColor
55 | navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.labelColor]
56 |
57 | titleLabel.textColor = .labelColor
58 | userLabel.textColor = .subLabelColor
59 | descriptionTextView.textColor = .subLabelColor
60 |
61 | if let themeID = UserDefaults.standard.string(forKey: UserDefaultsKey.themeID) {
62 | if themeID == theme?.id {
63 | isApplied = true
64 | }
65 | }
66 |
67 | if isApplied {
68 | applyBtn.backgroundColor = .backgroundColor
69 | applyBtn.setTitle("適用済", for: .normal)
70 | applyBtn.setTitleColor(.subLabelColor, for: .normal)
71 |
72 | } else {
73 | applyBtn.backgroundColor = .systemBlue
74 | applyBtn.setTitle("適用", for: .normal)
75 | }
76 | }
77 |
78 | @IBAction func preview() {
79 | // UserDefaults.standard.setValue("0", forKey: UserDefaultsKey.themeID)
80 | // reload()
81 | }
82 |
83 | @IBAction func apply() {
84 | if isApplied { return }
85 | UserDefaults.standard.set( theme?.id, forKey: .themeID)
86 | viewController.webView.reload()
87 | reload()
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/Marindeck/Strings/ja.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // MARK: Menu
2 | "menu.profile.title" = "プロフィール";
3 | "menu.reload.title" = "リロード";
4 | "menu.addColumn.title" = "カラムを追加";
5 |
6 | // MARK: ContextMenu(Image)
7 | "contextMenu.tweetImage.title" = "画像をツイート";
8 | "contextMenu.like.title" = "いいね";
9 | "contextMenu.saveImage.title" = "画像を保存";
10 | "contextMenu.saved.title" = "保存しました";
11 |
12 | // MARK: OnBoarding
13 | "onBoarding.startMarinDeck.title" = "MarinDeckをはじめる";
14 |
15 | // MARK: Settings
16 | "settings.navigation.title" = "設定";
17 | "settings.general.header.title" = "一般";
18 | "settings.customize.header.title" = "カスタマイズ";
19 | "settings.appinfo.header.title" = "アプリについて";
20 | "settings.donate.header.title" = "寄付";
21 | "settings.logout.header.title" = "ログアウト";
22 |
23 | "settings.nativePreview.cell.title" = "ネイティブのプレビューを使用";
24 | "settings.biometrics.cell.title" = "生体認証";
25 | "settings.tweetButtonBehavior.cell.title" = "ツイートボタンの動作";
26 | "settings.marginSafeArea.cell.title" = "SafeAreaを考慮する";
27 | "settings.noSleep.cell.title" = "スリープさせない";
28 |
29 | "settings.customJS.cell.title" = "カスタムJavaScript";
30 | "settings.customCSS.cell.title" = "カスタムCSS";
31 | "settings.theme.cell.title" = "テーマ";
32 | "settings.customActonButtons.cell.title" = "カスタムアクションボタン";
33 | "settings.icon.cell.title" = "アイコン";
34 |
35 | "settings.termsOfUse.cell.title" = "利用規約";
36 | "settings.license.cell.title" = "ライセンス";
37 | "settings.issueEnhancement.cell.title" = "ご意見・ご要望";
38 | "settings.developers.cell.title" = "開発者";
39 | "settings.donate.cell.title" = "寄付";
40 | "settings.version.cell.title" = "バージョン";
41 | "settings.checkUpdate.cell.title" = "更新を確認";
42 |
43 | "settings.importSettings.cell.title" = "設定をインポート";
44 | "settings.exportSettings.cell.title" = "設定をエクスポート";
45 |
46 | "settings.donate.cell.title" = "寄付";
47 |
48 | "settings.logout.cell.title" = "ログアウト";
49 |
50 | "settings.checkUpdate.checkingForUpdates.title" = "更新を確認";
51 | "settings.checkUpdate.existUpdate.title" = "更新があります";
52 | "settings.checkUpdate.latest.title" = "最新です";
53 |
54 | // MARK: ActionButtons
55 | "actionButton.debug.title" = "デバッグ";
56 | "actionButton.gif.title" = "GIF";
57 | "actionButton.tweet.title" = "ツイート";
58 | "actionButton.menu.title" = "メニュー";
59 | "actionButton.draft.title" = "ドラフト";
60 | "actionButton.settings.title" = "設定";
61 |
62 | "actionButton.debug.description" = "デバッグモーダルを開く";
63 | "actionButton.gif.description" = "GIFを選ぶ";
64 | "actionButton.tweet.description" = "ツイートモーダルを開く";
65 | "actionButton.menu.description" = "メニューを開く";
66 | "actionButton.draft.description" = "下書きを開く";
67 | "actionButton.settings.description" = "設定を開く";
68 | "actionButton.description.title" = "ツイートボタンを長押ししたときに出てくるアクションボタンを設定できます。";
69 |
70 |
71 |
72 | // MARK: Alert
73 | "alert.OK.title" = "OK";
74 | "alert.close.title" = "閉じる";
75 | "alert.open.title" = "開く";
76 | "alert.update.title" = "更新";
77 | "alert.cancel.title" = "キャンセル";
78 |
79 | "alert.openUrl.title" = "URLを開きますか?";
80 | "alert.logoutMessage.title" = "ログアウトしますか?";
81 |
82 | "alert.importedSettings.title" = "設定をインポートしました。";
83 | "alert.recommendRestartApp.title" = "アプリ再起動をおすすめします。";
84 |
--------------------------------------------------------------------------------
/Marindeck/View/ja.lproj/Main.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UILabel"; text = "ネイティブのプレビューを使用"; ObjectID = "25W-XT-4PI"; */
3 | "25W-XT-4PI.text" = "ネイティブのプレビューを使用";
4 |
5 | /* Class = "UITableViewSection"; headerTitle = "ログアウト"; ObjectID = "2qv-XT-C7t"; */
6 | "2qv-XT-C7t.headerTitle" = "ログアウト";
7 |
8 | /* Class = "UILabel"; text = "カスタムCSS"; ObjectID = "3jh-83-S3a"; */
9 | "3jh-83-S3a.text" = "カスタムCSS";
10 |
11 | /* Class = "UILabel"; text = "設定をインポート"; ObjectID = "9Of-JB-4oA"; */
12 | "9Of-JB-4oA.text" = "設定をインポート";
13 |
14 | /* Class = "UILabel"; text = "設定をエクスポート"; ObjectID = "9s8-52-qb7"; */
15 | "9s8-52-qb7.text" = "設定をエクスポート";
16 |
17 | /* Class = "UITextView"; text = "デスクトップ版Discordをモチーフにしたテーマです。明るすぎず暗すぎないDiscordのダークテーマが好きな方におすすめです"; ObjectID = "CEG-80-AEU"; */
18 | "CEG-80-AEU.text" = "デスクトップ版Discordをモチーフにしたテーマです。明るすぎず暗すぎないDiscordのダークテーマが好きな方におすすめです";
19 |
20 | /* Class = "UILabel"; text = "0.0.0 Alpha"; ObjectID = "DkX-M1-Evn"; */
21 | "DkX-M1-Evn.text" = "0.0.0 Alpha";
22 |
23 | /* Class = "UILabel"; text = "利用規約"; ObjectID = "G19-zP-4LD"; */
24 | "G19-zP-4LD.text" = "利用規約";
25 |
26 | /* Class = "UILabel"; text = "バージョン"; ObjectID = "GQ1-eM-49f"; */
27 | "GQ1-eM-49f.text" = "バージョン";
28 |
29 | /* Class = "UILabel"; text = "By TweetDeck"; ObjectID = "HHq-On-ZPg"; */
30 | "HHq-On-ZPg.text" = "By TweetDeck";
31 |
32 | /* Class = "UILabel"; text = "デフォルト"; ObjectID = "NDC-rS-v8P"; */
33 | "NDC-rS-v8P.text" = "デフォルト";
34 |
35 | /* Class = "UILabel"; text = "@twitter"; ObjectID = "QZQ-Zj-807"; */
36 | "QZQ-Zj-807.text" = "@twitter";
37 |
38 | /* Class = "UITableViewSection"; headerTitle = "アプリについて"; ObjectID = "QcA-JT-qUC"; */
39 | "QcA-JT-qUC.headerTitle" = "アプリについて";
40 |
41 | /* Class = "UILabel"; text = "サポーターの皆様"; ObjectID = "Yhd-Po-OYi"; */
42 | "Yhd-Po-OYi.text" = "サポーターの皆様";
43 |
44 | /* Class = "UILabel"; text = "着せ替え"; ObjectID = "YkZ-WD-38a"; */
45 | "YkZ-WD-38a.text" = "着せ替え";
46 |
47 | /* Class = "UILabel"; text = "ライセンス情報"; ObjectID = "Z1e-0c-usZ"; */
48 | "Z1e-0c-usZ.text" = "ライセンス情報";
49 |
50 | /* Class = "UILabel"; text = "ログアウト"; ObjectID = "ZZp-Ll-buN"; */
51 | "ZZp-Ll-buN.text" = "ログアウト";
52 |
53 | /* Class = "UIButton"; normalTitle = "プレビュー"; ObjectID = "cuw-34-Vjr"; */
54 | "cuw-34-Vjr.normalTitle" = "プレビュー";
55 |
56 | /* Class = "UITableViewSection"; headerTitle = "カスタマイズ"; ObjectID = "d0p-CE-9K3"; */
57 | "d0p-CE-9K3.headerTitle" = "カスタマイズ";
58 |
59 | /* Class = "UILabel"; text = "ご意見・ご要望"; ObjectID = "eRj-KM-XGr"; */
60 | "eRj-KM-XGr.text" = "ご意見・ご要望";
61 |
62 | /* Class = "UILabel"; text = "TestAccount"; ObjectID = "eai-3F-GbR"; */
63 | "eai-3F-GbR.text" = "TestAccount";
64 |
65 | /* Class = "UILabel"; text = "寄付"; ObjectID = "gSK-oI-7Qi"; */
66 | "gSK-oI-7Qi.text" = "寄付";
67 |
68 | /* Class = "UILabel"; text = "カスタムJavaScript"; ObjectID = "kGa-Gz-6XG"; */
69 | "kGa-Gz-6XG.text" = "カスタムJavaScript";
70 |
71 | /* Class = "UIButton"; normalTitle = "適用"; ObjectID = "nKz-ke-eMH"; */
72 | "nKz-ke-eMH.normalTitle" = "適用";
73 |
74 | /* Class = "UITableViewSection"; headerTitle = "一般"; ObjectID = "vTI-XN-UL0"; */
75 | "vTI-XN-UL0.headerTitle" = "一般";
76 |
77 | /* Class = "UILabel"; text = "利用規約"; ObjectID = "x3V-SQ-2Sn"; */
78 | "x3V-SQ-2Sn.text" = "利用規約";
79 |
--------------------------------------------------------------------------------
/Marindeck/Database/Database.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Database.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/10/30.
6 | //
7 |
8 | import Foundation
9 | import GRDB
10 |
11 | // FIXME: Modelに移動
12 | struct CustomJS: Codable, FetchableRecord, PersistableRecord {
13 | var id: Int64?
14 | var title: String
15 | var js: String
16 | var createAt: Date
17 | var updateAt: Date
18 | var loadIndex: Int32
19 | var isLoad: Bool
20 | }
21 |
22 | // FIXME: Modelに移動
23 | struct CustomCSS: Codable, FetchableRecord, PersistableRecord {
24 | var id: Int64?
25 | var title: String
26 | var css: String
27 | var createAt: Date
28 | var updateAt: Date
29 | var loadIndex: Int32
30 | var isLoad: Bool
31 | }
32 |
33 | struct Draft: Codable, FetchableRecord, PersistableRecord {
34 | var id: Int64?
35 | var text: String
36 | }
37 |
38 | class Database {
39 | static let shared = Database()
40 |
41 | public private(set) lazy var dbQueue: DatabaseQueue = {
42 | let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
43 | print("DATABASE DIR: \(dir.absoluteString + "database.sqlite")")
44 | return try! DatabaseQueue(path: dir.absoluteString + "database.sqlite")
45 | }()
46 |
47 | func setup() {
48 |
49 | try? dbQueue.write { db in
50 | // CustomJS
51 | try db.create(table: "customjs") { t in
52 | t.autoIncrementedPrimaryKey("id")
53 | t.column("title", .text).notNull()
54 | t.column("js", .text).notNull()
55 | t.column("createAt", .date).notNull()
56 | t.column("updateAt", .date).notNull()
57 | t.column("loadIndex", .integer).notNull()
58 | t.column("isLoad", .boolean).notNull()
59 | }
60 | }
61 | try? dbQueue.write { db in
62 | // CustomCSS
63 | try db.create(table: "customcss") { t in
64 | t.autoIncrementedPrimaryKey("id")
65 | t.column("title", .text).notNull()
66 | t.column("css", .text).notNull()
67 | t.column("createAt", .date).notNull()
68 | t.column("updateAt", .date).notNull()
69 | t.column("loadIndex", .integer).notNull()
70 | t.column("isLoad", .boolean).notNull()
71 | }
72 | }
73 | try? dbQueue.write { db in
74 | // Draft
75 | try db.create(table: "draft") { t in
76 | t.autoIncrementedPrimaryKey("id")
77 | t.column("text", .text).notNull()
78 | }
79 | }
80 |
81 | try? dbQueue.write { db in
82 | // RetemoJS
83 | try db.create(table: "remotejsdata") { t in
84 | t.autoIncrementedPrimaryKey("_id")
85 | t.column("id", .text).notNull()
86 | t.column("title", .text).notNull()
87 | t.column("version", .integer).notNull()
88 | t.column("jsUrl", .text).notNull()
89 | t.column("js", .text)
90 | }
91 | }
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/Marindeck/View/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Marindeck/View/Other/ModalBrowserViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModalBrowserViewController.swift
3 | // Marindeck
4 | //
5 | // Created by a on 12/13/22.
6 | //
7 |
8 | import UIKit
9 | import WebKit
10 |
11 | class ModalBrowserViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
12 | public var url: String = ""
13 |
14 | var webView: WKWebView!
15 | lazy var dismissButton: UIButton = {
16 | let btn = UIButton()
17 | let cf = UIImage.SymbolConfiguration(pointSize: 28, weight: .medium, scale: .default)
18 | btn.setImage(UIImage(systemName: "xmark.circle.fill", withConfiguration: cf), for: .normal)
19 | btn.tintColor = .label
20 | btn.translatesAutoresizingMaskIntoConstraints = false
21 | return btn
22 | }()
23 |
24 | override func loadView() {
25 | let webConfiguration = WKWebViewConfiguration()
26 | webView = WKWebView(frame: .zero, configuration: webConfiguration)
27 | webView.uiDelegate = self
28 | webView.navigationDelegate = self
29 | view = webView
30 | }
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | view.addSubview(dismissButton)
36 |
37 | NSLayoutConstraint.activate([
38 | dismissButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 12),
39 | dismissButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -12),
40 | dismissButton.widthAnchor.constraint(equalToConstant: 28),
41 | dismissButton.heightAnchor.constraint(equalToConstant: 28)
42 | ])
43 |
44 | dismissButton.addTarget(self, action: #selector(onDismiss), for: .touchUpInside)
45 |
46 | view.bringSubviewToFront(webView)
47 |
48 | let myURL = URL(string: url)
49 | let request = URLRequest(url: myURL!)
50 |
51 | let jsonString = """
52 | [{
53 | "trigger": {
54 | "url-filter": ".*"
55 | },
56 | "action": {
57 | "type": "css-display-none",
58 | "selector": "#layers"
59 | }
60 | }]
61 | """
62 |
63 | WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "ContentBlockingRules", encodedContentRuleList: jsonString) { rulesList, error in
64 | if let error = error {
65 | print(error)
66 | return
67 | }
68 | guard let rulesList = rulesList else {
69 | return
70 | }
71 | let config = self.webView.configuration
72 | config.userContentController.add(rulesList)
73 | self.webView.load(request)
74 | }
75 | }
76 |
77 | @objc
78 | func onDismiss() {
79 | dismiss(animated: true, completion: nil)
80 | }
81 |
82 | func webView(_ webView: WKWebView,
83 | decidePolicyFor navigationAction: WKNavigationAction,
84 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
85 | decisionHandler(.allow)
86 | }
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/Marindeck/Strings/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | // MARK: Menu
2 | "menu.profile.title" = "Profile";
3 | "menu.reload.title" = "Reload";
4 | "menu.addColumn.title" = "Add column";
5 |
6 | // MARK: ContextMenu(Image)
7 | "contextMenu.tweetImage.title" = "Tweet image";
8 | "contextMenu.like.title" = "Like";
9 | "contextMenu.saveImage.title" = "Save image";
10 | "contextMenu.saved.title" = "Saved";
11 |
12 | // MARK: OnBoarding
13 | "onBoarding.startMarinDeck.title" = "Start MarinDeck";
14 |
15 | // MARK: Settings
16 | "settings.navigation.title" = "Settings";
17 | "settings.general.header.title" = "General";
18 | "settings.customize.header.title" = "Customize";
19 | "settings.appinfo.header.title" = "App info";
20 | "settings.donate.header.title" = "Donate";
21 | "settings.logout.header.title" = "Logout";
22 |
23 | "settings.nativePreview.cell.title" = "Use native preview";
24 | "settings.biometrics.cell.title" = "Biometrics";
25 | "settings.tweetButtonBehavior.cell.title" = "Tweet button behavior";
26 | "settings.marginSafeArea.cell.title" = "Margin safeArea";
27 | "settings.noSleep.cell.title" = "No Sleep";
28 |
29 | "settings.customJS.cell.title" = "Custom JavaScript";
30 | "settings.customCSS.cell.title" = "Custom CSS";
31 | "settings.theme.cell.title" = "Themes";
32 | "settings.customActonButtons.cell.title" = "Custom action buttons";
33 | "settings.icon.cell.title" = "Icons";
34 |
35 | "settings.termsOfUse.cell.title" = "Team of use";
36 | "settings.license.cell.title" = "License";
37 | "settings.issueEnhancement.cell.title" = "issue, enhancement";
38 | "settings.developers.cell.title" = "Developers";
39 | "settings.donate.cell.title" = "Donate";
40 | "settings.version.cell.title" = "Version";
41 | "settings.checkUpdate.cell.title" = "Check update";
42 |
43 | "settings.importSettings.cell.title" = "Import settings";
44 | "settings.exportSettings.cell.title" = "Export settings";
45 |
46 | "settings.donate.cell.title" = "donate";
47 |
48 | "settings.logout.cell.title" = "logout";
49 |
50 | "settings.checkUpdate.checkingForUpdates.title" = "Checking for updates.";
51 | "settings.checkUpdate.existUpdate.title" = "There will be an update.";
52 | "settings.checkUpdate.latest.title" = "latest.";
53 |
54 |
55 | // MARK: ActionButtons
56 | "actionButton.debug.title" = "Debug";
57 | "actionButton.gif.title" = "GIF";
58 | "actionButton.tweet.title" = "Tweet";
59 | "actionButton.menu.title" = "Menu";
60 | "actionButton.draft.title" = "Draft";
61 | "actionButton.settings.title" = "Settings";
62 |
63 | "actionButton.debug.description" = "Open the debug modal.";
64 | "actionButton.gif.description" = "Select GIF";
65 | "actionButton.tweet.description" = "Open the tweet modal";
66 | "actionButton.menu.description" = "Open the menu";
67 | "actionButton.draft.description" = "Open a draft";
68 | "actionButton.settings.description" = "Open Settings";
69 | "actionButton.description.title" = "Set the action button to be displayed when you press and hold the tweet button.";
70 |
71 | // MARK: Alert
72 | "alert.OK.title" = "OK";
73 | "alert.close.title" = "Close";
74 | "alert.open.title" = "Open";
75 | "alert.update.title" = "Update";
76 | "alert.cancel.title" = "Cancel";
77 |
78 | "alert.openUrl.title" = "Open URL?";
79 | "alert.logoutMessage.title" = "";
80 |
81 | "alert.importedSettings.title" = "Imported the configuration.";
82 | "alert.recommendRestartApp.title" = "We recommend restarting the application.";
83 |
--------------------------------------------------------------------------------
/Marindeck/View/Common/AppDebugView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDebugView.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/03/11.
6 | //
7 |
8 | import SwiftUI
9 | import SwiftyStoreKit
10 |
11 | struct AppDebugView: View {
12 | @Environment(\.presentationMode) var presentationMode
13 | weak var vc: UIViewController?
14 |
15 | init(vc: UIViewController? = nil) {
16 | self.vc = vc
17 | }
18 |
19 | var body: some View {
20 | VStack(spacing: 32) {
21 | Button("AppDebugモードを終了", action: {
22 | UserDefaults.standard.set(false, forKey: .appDebugMode)
23 | presentationMode.wrappedValue.dismiss()
24 | })
25 | Button("Deckを表示", action: {
26 | let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
27 | self.vc?.present(vc, animated: false, completion: nil)
28 | })
29 | Button("設定を表示", action: {
30 | let vc = UIStoryboard(name: "Settings", bundle: nil).instantiateInitialViewController() as! SettingsTableViewController
31 | let nvc = UINavigationController(rootViewController: vc)
32 | self.vc?.present(nvc, animated: true, completion: nil)
33 | })
34 | Button("Twitter設定画面", action: {
35 | vc?.present(TwitterSettingsViewController(), animated: true, completion: nil)
36 | })
37 |
38 | Button("OnBoardingを表示", action: {
39 | let onBoardingVC = OnBoardingViewController()
40 | onBoardingVC.modalPresentationStyle = .currentContext
41 | vc?.present(onBoardingVC, animated: false, completion: nil)
42 | })
43 |
44 | Button("ネイティブTweetModal", action: {
45 | let vc = UIViewController()
46 | vc.modalPresentationStyle = .overFullScreen
47 | let view = TweetView()
48 | view.frame = vc.view.bounds
49 | view.normalTweetModalY = 40
50 | vc.view.addSubview(view)
51 | vc.view.backgroundColor = UIColor.green.withAlphaComponent(0.3)
52 | self.vc?.present(vc, animated: false, completion: nil)
53 | })
54 |
55 | Button("Test課金") {
56 | let productIds: Set = ["300yen"]
57 |
58 | SwiftyStoreKit.retrieveProductsInfo(productIds) { result in
59 | if result.retrievedProducts.first != nil {
60 | let products = result.retrievedProducts.sorted { (firstProduct, secondProduct) -> Bool in
61 | return firstProduct.price.doubleValue < secondProduct.price.doubleValue
62 | }
63 | for product in products {
64 | print(product)
65 | }
66 | } else if result.invalidProductIDs.first != nil {
67 | print("Invalid product identifier : \(result.invalidProductIDs)")
68 | } else {
69 | print("Error : \(result.error.debugDescription)")
70 | }
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | struct AppDebugView_Previews: PreviewProvider {
78 | static var previews: some View {
79 | AppDebugView()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Marindeck/View/Settings/TwitterSettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TwitterSettingsViewController.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/02/11.
6 | //
7 |
8 | import UIKit
9 | import WebKit
10 |
11 | class TwitterSettingsViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
12 | public var url = "https://mobile.twitter.com/settings"
13 |
14 | var webView: WKWebView!
15 | lazy var dismissButton: UIButton = {
16 | let btn = UIButton()
17 | let cf = UIImage.SymbolConfiguration(pointSize: 28, weight: .medium, scale: .default)
18 | btn.setImage(UIImage(systemName: "xmark.circle.fill", withConfiguration: cf), for: .normal)
19 | btn.tintColor = .label
20 | btn.translatesAutoresizingMaskIntoConstraints = false
21 | return btn
22 | }()
23 |
24 | override func loadView() {
25 | let webConfiguration = WKWebViewConfiguration()
26 | webView = WKWebView(frame: .zero, configuration: webConfiguration)
27 | webView.uiDelegate = self
28 | webView.navigationDelegate = self
29 | view = webView
30 | }
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | view.addSubview(dismissButton)
36 |
37 | NSLayoutConstraint.activate([
38 | dismissButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 12),
39 | dismissButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -12),
40 | dismissButton.widthAnchor.constraint(equalToConstant: 28),
41 | dismissButton.heightAnchor.constraint(equalToConstant: 28)
42 | ])
43 |
44 | dismissButton.addTarget(self, action: #selector(onDismiss), for: .touchUpInside)
45 |
46 | view.bringSubviewToFront(webView)
47 |
48 | let myURL = URL(string: url)
49 | let request = URLRequest(url: myURL!)
50 |
51 | let jsonString = """
52 | [{
53 | "trigger": {
54 | "url-filter": ".*"
55 | },
56 | "action": {
57 | "type": "css-display-none",
58 | "selector": "#layers"
59 | }
60 | }]
61 | """
62 |
63 | WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "ContentBlockingRules", encodedContentRuleList: jsonString) { rulesList, error in
64 | if let error = error {
65 | print(error)
66 | return
67 | }
68 | guard let rulesList = rulesList else {
69 | return
70 | }
71 | let config = self.webView.configuration
72 | config.userContentController.add(rulesList)
73 | self.webView.load(request)
74 | }
75 | }
76 |
77 | @objc
78 | func onDismiss() {
79 | dismiss(animated: true, completion: nil)
80 | }
81 |
82 | func webView(_ webView: WKWebView,
83 | decidePolicyFor navigationAction: WKNavigationAction,
84 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
85 | let url = navigationAction.request.url
86 |
87 | if !(url?.path.contains("/settings") ?? true) || !(url?.host == "mobile.twitter.com") {
88 | decisionHandler(.cancel)
89 | self.dismiss(animated: true, completion: nil)
90 | } else {
91 | decisionHandler(.allow)
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Marindeck/Util/RemoteJS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RemoteJS.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/02/02.
6 | //
7 |
8 | import Alamofire
9 | import Foundation
10 |
11 | class RemoteJS {
12 | static let shared = RemoteJS()
13 |
14 | private lazy var dbQueue = Database.shared.dbQueue
15 |
16 | public private(set) var remoteJSs: [RemoteJSData]?
17 | private var savedRemoteJSs: [RemoteJSData]?
18 |
19 | func update(completion: @escaping() -> Void) {
20 | fetchDb()
21 | fetch { [weak self] remoteJSs in
22 | guard let self = self else { return }
23 | self.remoteJSs = remoteJSs
24 |
25 | for remoteJS in remoteJSs ?? [] {
26 | if !self.isLatest(latest: remoteJS) {
27 | let result = self.fetchJS(remoteJS: remoteJS)
28 | self.saveDb(remoteJS: result)
29 | }
30 | }
31 | self.fetchDb()
32 | completion()
33 | }
34 | }
35 |
36 | func getJs(id: RemoteJSDataId) -> String? {
37 | savedRemoteJSs?.filter { $0.id == id.rawValue }[safe: 0]?.js
38 | }
39 |
40 | func isLatest(id: RemoteJSDataId) -> Bool {
41 | guard let latest = remoteJSs?.filter({ $0.id == id.rawValue })[safe: 0] else { return false }
42 | return isLatest(latest: latest)
43 | }
44 |
45 | func isLatest(latest: RemoteJSData) -> Bool {
46 | guard let currentVersion = savedRemoteJSs?
47 | .filter({ $0.id == latest.id })[safe: 0]?.version else { return false }
48 |
49 | return currentVersion >= latest.version
50 | }
51 |
52 | private func fetch(completion: @escaping([RemoteJSData]?) -> Void) {
53 | AF.request(DebugSettings.remoteJsUrl).response { response in
54 | guard let data = response.data else { return }
55 | let remoteJs = try? JSONDecoder().decode([RemoteJSData].self, from: data)
56 | completion(remoteJs)
57 | }
58 | }
59 |
60 | private func fetchDb() {
61 | savedRemoteJSs = try! dbQueue.read { db in
62 | try RemoteJSData.fetchAll(db)
63 | }
64 | }
65 |
66 | private func saveDb(remoteJS: RemoteJSData) {
67 | if let rjs = savedRemoteJSs?.filter({ $0.id == remoteJS.id })[safe: 0] {
68 | var saverjs = rjs
69 | saverjs.version = remoteJS.version
70 | saverjs.jsUrl = remoteJS.jsUrl
71 | saverjs.js = remoteJS.js
72 | try! dbQueue.write { db in
73 | try saverjs.update(db)
74 | }
75 | } else {
76 | try! dbQueue.write { db in
77 | try remoteJS.insert(db)
78 | }
79 | }
80 | }
81 |
82 | private func fetchJS(remoteJS: RemoteJSData) -> RemoteJSData {
83 | print(#function)
84 | let semaphore = DispatchSemaphore(value: 0)
85 | let queue = DispatchQueue.global(qos: .utility)
86 | var result = remoteJS
87 | AF.request(remoteJS.jsUrl).response(queue: queue) { response in
88 | print(response)
89 | guard let data = response.data else {
90 | semaphore.signal()
91 | return
92 | }
93 | var tmpRemoteJS = remoteJS
94 | tmpRemoteJS.js = String(data: data, encoding: .utf8)
95 | result = tmpRemoteJS
96 | semaphore.signal()
97 | }
98 | semaphore.wait()
99 | return result
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/Marindeck/Info-dev.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | ja_JP
7 | CFBundleDisplayName
8 | MarinDeck-dev
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIcons
12 |
13 | CFBundleAlternateIcons
14 |
15 | BlackIcon
16 |
17 | CFBundleIconFiles
18 |
19 | BlackIcon
20 |
21 |
22 | RainbowIcon
23 |
24 | CFBundleIconFiles
25 |
26 | RainbowIcon
27 |
28 |
29 |
30 |
31 | CFBundleIdentifier
32 | $(PRODUCT_BUNDLE_IDENTIFIER)
33 | CFBundleInfoDictionaryVersion
34 | 6.0
35 | CFBundleName
36 | $(PRODUCT_NAME)
37 | CFBundlePackageType
38 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
39 | CFBundleShortVersionString
40 | $(MARKETING_VERSION)
41 | CFBundleVersion
42 | $(CURRENT_PROJECT_VERSION)
43 | ITSAppUsesNonExemptEncryption
44 |
45 | LSApplicationCategoryType
46 | public.app-category.social-networking
47 | LSApplicationQueriesSchemes
48 |
49 | youtube
50 |
51 | LSRequiresIPhoneOS
52 |
53 | NSAppTransportSecurity
54 |
55 | NSAllowsArbitraryLoads
56 |
57 |
58 | NSCameraUsageDescription
59 | 画像を投稿するためにカメラを使用します。
60 | NSFaceIDUsageDescription
61 | アプリのロックに使用します。
62 | NSPhotoLibraryAddUsageDescription
63 | 投稿された画像を保存します。
64 | UIApplicationSceneManifest
65 |
66 | UIApplicationSupportsMultipleScenes
67 |
68 | UISceneConfigurations
69 |
70 | UIWindowSceneSessionRoleApplication
71 |
72 |
73 | UISceneConfigurationName
74 | Default Configuration
75 | UISceneDelegateClassName
76 | $(PRODUCT_MODULE_NAME).SceneDelegate
77 | UISceneStoryboardFile
78 | Main
79 |
80 |
81 |
82 |
83 | UIApplicationSupportsIndirectInputEvents
84 |
85 | UIFileSharingEnabled
86 |
87 | UILaunchStoryboardName
88 | LaunchScreen
89 | UIMainStoryboardFile
90 | Main
91 | UIRequiredDeviceCapabilities
92 |
93 | armv7
94 |
95 | UISupportedInterfaceOrientations
96 |
97 | UIInterfaceOrientationPortrait
98 | UIInterfaceOrientationLandscapeLeft
99 | UIInterfaceOrientationLandscapeRight
100 |
101 | UISupportedInterfaceOrientations~ipad
102 |
103 | UIInterfaceOrientationPortrait
104 | UIInterfaceOrientationPortraitUpsideDown
105 | UIInterfaceOrientationLandscapeLeft
106 | UIInterfaceOrientationLandscapeRight
107 |
108 | UIViewControllerBasedStatusBarAppearance
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/Marindeck/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | ja_JP
7 | CFBundleDisplayName
8 | $(DISPLAY_APPNAME)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIcons
12 |
13 | CFBundleAlternateIcons
14 |
15 | BlackIcon
16 |
17 | CFBundleIconFiles
18 |
19 | BlackIcon
20 |
21 |
22 | RainbowIcon
23 |
24 | CFBundleIconFiles
25 |
26 | RainbowIcon
27 |
28 |
29 |
30 |
31 | CFBundleIdentifier
32 | $(PRODUCT_BUNDLE_IDENTIFIER)
33 | CFBundleInfoDictionaryVersion
34 | 6.0
35 | CFBundleName
36 | $(PRODUCT_NAME)
37 | CFBundlePackageType
38 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
39 | CFBundleShortVersionString
40 | $(MARKETING_VERSION)
41 | CFBundleVersion
42 | $(CURRENT_PROJECT_VERSION)
43 | ITSAppUsesNonExemptEncryption
44 |
45 | LSApplicationCategoryType
46 | public.app-category.social-networking
47 | LSApplicationQueriesSchemes
48 |
49 | youtube
50 | twitter
51 |
52 | LSRequiresIPhoneOS
53 |
54 | NSAppTransportSecurity
55 |
56 | NSAllowsArbitraryLoads
57 |
58 |
59 | NSCameraUsageDescription
60 | 画像を投稿するためにカメラを使用します。
61 | NSFaceIDUsageDescription
62 | アプリのロックに使用します。
63 | NSPhotoLibraryAddUsageDescription
64 | 投稿された画像を保存します。
65 | UIApplicationSceneManifest
66 |
67 | UIApplicationSupportsMultipleScenes
68 |
69 | UISceneConfigurations
70 |
71 | UIWindowSceneSessionRoleApplication
72 |
73 |
74 | UISceneConfigurationName
75 | Default Configuration
76 | UISceneDelegateClassName
77 | $(PRODUCT_MODULE_NAME).SceneDelegate
78 | UISceneStoryboardFile
79 | Main
80 |
81 |
82 |
83 |
84 | UIApplicationSupportsIndirectInputEvents
85 |
86 | UIFileSharingEnabled
87 |
88 | UILaunchStoryboardName
89 | LaunchScreen
90 | UIMainStoryboardFile
91 | Main
92 | UIRequiredDeviceCapabilities
93 |
94 | armv7
95 |
96 | UISupportedInterfaceOrientations
97 |
98 | UIInterfaceOrientationPortrait
99 | UIInterfaceOrientationLandscapeLeft
100 | UIInterfaceOrientationLandscapeRight
101 |
102 | UISupportedInterfaceOrientations~ipad
103 |
104 | UIInterfaceOrientationPortrait
105 | UIInterfaceOrientationPortraitUpsideDown
106 | UIInterfaceOrientationLandscapeLeft
107 | UIInterfaceOrientationLandscapeRight
108 |
109 | UIViewControllerBasedStatusBarAppearance
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Marindeck/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "29.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "58.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "87.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "80.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "57.png",
47 | "idiom" : "iphone",
48 | "scale" : "1x",
49 | "size" : "57x57"
50 | },
51 | {
52 | "filename" : "114.png",
53 | "idiom" : "iphone",
54 | "scale" : "2x",
55 | "size" : "57x57"
56 | },
57 | {
58 | "filename" : "120.png",
59 | "idiom" : "iphone",
60 | "scale" : "2x",
61 | "size" : "60x60"
62 | },
63 | {
64 | "filename" : "180.png",
65 | "idiom" : "iphone",
66 | "scale" : "3x",
67 | "size" : "60x60"
68 | },
69 | {
70 | "filename" : "20.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "20x20"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "20x20"
80 | },
81 | {
82 | "filename" : "29.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "29x29"
86 | },
87 | {
88 | "filename" : "58.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "29x29"
92 | },
93 | {
94 | "filename" : "40.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "40x40"
98 | },
99 | {
100 | "filename" : "80.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "40x40"
104 | },
105 | {
106 | "filename" : "50.png",
107 | "idiom" : "ipad",
108 | "scale" : "1x",
109 | "size" : "50x50"
110 | },
111 | {
112 | "filename" : "100.png",
113 | "idiom" : "ipad",
114 | "scale" : "2x",
115 | "size" : "50x50"
116 | },
117 | {
118 | "filename" : "72.png",
119 | "idiom" : "ipad",
120 | "scale" : "1x",
121 | "size" : "72x72"
122 | },
123 | {
124 | "filename" : "144.png",
125 | "idiom" : "ipad",
126 | "scale" : "2x",
127 | "size" : "72x72"
128 | },
129 | {
130 | "filename" : "76.png",
131 | "idiom" : "ipad",
132 | "scale" : "1x",
133 | "size" : "76x76"
134 | },
135 | {
136 | "filename" : "152.png",
137 | "idiom" : "ipad",
138 | "scale" : "2x",
139 | "size" : "76x76"
140 | },
141 | {
142 | "filename" : "167.png",
143 | "idiom" : "ipad",
144 | "scale" : "2x",
145 | "size" : "83.5x83.5"
146 | },
147 | {
148 | "filename" : "1024.png",
149 | "idiom" : "ios-marketing",
150 | "scale" : "1x",
151 | "size" : "1024x1024"
152 | }
153 | ],
154 | "info" : {
155 | "author" : "xcode",
156 | "version" : 1
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Marindeck/Model/TD/TD+Actions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TD+Actions.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/01/25.
6 | //
7 | import Foundation
8 |
9 | extension TD.ActionsController {
10 | // ツイート画面のTextViewにフォーカスする
11 | func focusTweetTextArea() {
12 | webView?.evaluateJavaScript("document.querySelector(\".js-compose-text\").focus()") { _, error in
13 | print("focusTweetTextArea : ", error ?? "成功")
14 | }
15 | }
16 |
17 | // 座標の位置にあるツイートをいいね
18 | func positionTweetLike(x: Int, y: Int) {
19 | webView?.evaluateJavaScript("window.MarinDeckInputs.touchPointTweetLike(\(x), \(y))", completionHandler: { _, error in
20 | print("touchPointTweetLike : ", error ?? "成功")
21 | })
22 | }
23 |
24 | // カラムスクロールの制御(正常に動作しません。)
25 | func isColumnScroll(_ bool: Bool) {
26 | let isScroll = bool ? "on" : "off"
27 | webView?.evaluateJavaScript("columnScroll.\(isScroll)()") { _, error in
28 | print("webViewLog : ", error ?? "成功")
29 | }
30 | }
31 |
32 | // ツイート
33 | func tweet(text: String) {
34 | let replaceDict = ["\\": "\\\\", "\"": "\\\"", "\'": "\\\'"]
35 | let txt = replaceDict.reduce(text) { $0.replacingOccurrences(of: $1.key, with: $1.value) }
36 | webView?.evaluateJavaScript("window.MarinDeckInputs.postTweet('\(txt)')") { _, error in
37 | print("tweet : ", error ?? "成功")
38 | }
39 | }
40 |
41 | // FIXME: あとから出てきたheaderに適用されない。
42 | // top-margin
43 | func setStatusBarSpace(height: Int) {
44 | let headerHeight = height + 50
45 | webView?.evaluateJavaScript("""
46 | document.querySelectorAll(".column-header").forEach(function(item) {
47 | item.style.height = "\(headerHeight)px"
48 | item.style.maxHeight = "\(headerHeight)px"
49 | item.style.paddingTop = "\(height)px"
50 | })
51 | """) { _, error in
52 | print(#function, error ?? "成功")
53 | }
54 | webView?.evaluateJavaScript("""
55 | document.querySelectorAll(".js-detail-header").forEach(function(item) {
56 | item.style.height = "\(headerHeight)px"
57 | item.style.maxHeight = "\(headerHeight)px"
58 | item.style.paddingTop = "\(height)px"
59 | })
60 | """) { _, error in
61 | print(#function, error ?? "成功")
62 | }
63 | }
64 |
65 | // MARK: Blob
66 | func setBlob(url: String, base64: String, mimeType: String) {
67 | webView?.evaluateJavaScript("MD4iOS.Blob.set(\(url), \(base64), \(mimeType)")
68 | }
69 |
70 | func setSchedule(date: Date) {
71 | let calendar = Calendar.current
72 | let Y = calendar.component(.year, from: date)
73 | let M = calendar.component(.month, from: date)
74 | let D = calendar.component(.day, from: date)
75 | let h = calendar.component(.hour, from: date)
76 | let m = calendar.component(.minute, from: date)
77 |
78 | webView?.evaluateJavaScript("window.MarinDeckInputs.updateSchedule(\(Y), \(M), \(D), \(h), \(m))") { _, error in
79 | print(#function, error ?? "成功")
80 | }
81 | }
82 |
83 | func openTDSettings() {
84 | webView?.evaluateJavaScript("document.querySelector(\".js-app-settings\").click();document.querySelector(\"[data-action='globalSettings']\").click()")
85 | }
86 |
87 | func send(uuid: String, value: Any?) {
88 | webView?.evaluateJavaScript("window.MD.Native.send({uuid: '\(uuid)', value: \(value ?? "null")})")
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/docs/css/highlight.css:
--------------------------------------------------------------------------------
1 | /*! Jazzy - https://github.com/realm/jazzy
2 | * Copyright Realm Inc.
3 | * SPDX-License-Identifier: MIT
4 | */
5 | /* Credit to https://gist.github.com/wataru420/2048287 */
6 | .highlight .c {
7 | color: #999988;
8 | font-style: italic; }
9 |
10 | .highlight .err {
11 | color: #a61717;
12 | background-color: #e3d2d2; }
13 |
14 | .highlight .k {
15 | color: #000000;
16 | font-weight: bold; }
17 |
18 | .highlight .o {
19 | color: #000000;
20 | font-weight: bold; }
21 |
22 | .highlight .cm {
23 | color: #999988;
24 | font-style: italic; }
25 |
26 | .highlight .cp {
27 | color: #999999;
28 | font-weight: bold; }
29 |
30 | .highlight .c1 {
31 | color: #999988;
32 | font-style: italic; }
33 |
34 | .highlight .cs {
35 | color: #999999;
36 | font-weight: bold;
37 | font-style: italic; }
38 |
39 | .highlight .gd {
40 | color: #000000;
41 | background-color: #ffdddd; }
42 |
43 | .highlight .gd .x {
44 | color: #000000;
45 | background-color: #ffaaaa; }
46 |
47 | .highlight .ge {
48 | color: #000000;
49 | font-style: italic; }
50 |
51 | .highlight .gr {
52 | color: #aa0000; }
53 |
54 | .highlight .gh {
55 | color: #999999; }
56 |
57 | .highlight .gi {
58 | color: #000000;
59 | background-color: #ddffdd; }
60 |
61 | .highlight .gi .x {
62 | color: #000000;
63 | background-color: #aaffaa; }
64 |
65 | .highlight .go {
66 | color: #888888; }
67 |
68 | .highlight .gp {
69 | color: #555555; }
70 |
71 | .highlight .gs {
72 | font-weight: bold; }
73 |
74 | .highlight .gu {
75 | color: #aaaaaa; }
76 |
77 | .highlight .gt {
78 | color: #aa0000; }
79 |
80 | .highlight .kc {
81 | color: #000000;
82 | font-weight: bold; }
83 |
84 | .highlight .kd {
85 | color: #000000;
86 | font-weight: bold; }
87 |
88 | .highlight .kp {
89 | color: #000000;
90 | font-weight: bold; }
91 |
92 | .highlight .kr {
93 | color: #000000;
94 | font-weight: bold; }
95 |
96 | .highlight .kt {
97 | color: #445588; }
98 |
99 | .highlight .m {
100 | color: #009999; }
101 |
102 | .highlight .s {
103 | color: #d14; }
104 |
105 | .highlight .na {
106 | color: #008080; }
107 |
108 | .highlight .nb {
109 | color: #0086B3; }
110 |
111 | .highlight .nc {
112 | color: #445588;
113 | font-weight: bold; }
114 |
115 | .highlight .no {
116 | color: #008080; }
117 |
118 | .highlight .ni {
119 | color: #800080; }
120 |
121 | .highlight .ne {
122 | color: #990000;
123 | font-weight: bold; }
124 |
125 | .highlight .nf {
126 | color: #990000; }
127 |
128 | .highlight .nn {
129 | color: #555555; }
130 |
131 | .highlight .nt {
132 | color: #000080; }
133 |
134 | .highlight .nv {
135 | color: #008080; }
136 |
137 | .highlight .ow {
138 | color: #000000;
139 | font-weight: bold; }
140 |
141 | .highlight .w {
142 | color: #bbbbbb; }
143 |
144 | .highlight .mf {
145 | color: #009999; }
146 |
147 | .highlight .mh {
148 | color: #009999; }
149 |
150 | .highlight .mi {
151 | color: #009999; }
152 |
153 | .highlight .mo {
154 | color: #009999; }
155 |
156 | .highlight .sb {
157 | color: #d14; }
158 |
159 | .highlight .sc {
160 | color: #d14; }
161 |
162 | .highlight .sd {
163 | color: #d14; }
164 |
165 | .highlight .s2 {
166 | color: #d14; }
167 |
168 | .highlight .se {
169 | color: #d14; }
170 |
171 | .highlight .sh {
172 | color: #d14; }
173 |
174 | .highlight .si {
175 | color: #d14; }
176 |
177 | .highlight .sx {
178 | color: #d14; }
179 |
180 | .highlight .sr {
181 | color: #009926; }
182 |
183 | .highlight .s1 {
184 | color: #d14; }
185 |
186 | .highlight .ss {
187 | color: #990073; }
188 |
189 | .highlight .bp {
190 | color: #999999; }
191 |
192 | .highlight .vc {
193 | color: #008080; }
194 |
195 | .highlight .vg {
196 | color: #008080; }
197 |
198 | .highlight .vi {
199 | color: #008080; }
200 |
201 | .highlight .il {
202 | color: #009999; }
203 |
--------------------------------------------------------------------------------
/docs/docsets/Marindeck.docset/Contents/Resources/Documents/css/highlight.css:
--------------------------------------------------------------------------------
1 | /*! Jazzy - https://github.com/realm/jazzy
2 | * Copyright Realm Inc.
3 | * SPDX-License-Identifier: MIT
4 | */
5 | /* Credit to https://gist.github.com/wataru420/2048287 */
6 | .highlight .c {
7 | color: #999988;
8 | font-style: italic; }
9 |
10 | .highlight .err {
11 | color: #a61717;
12 | background-color: #e3d2d2; }
13 |
14 | .highlight .k {
15 | color: #000000;
16 | font-weight: bold; }
17 |
18 | .highlight .o {
19 | color: #000000;
20 | font-weight: bold; }
21 |
22 | .highlight .cm {
23 | color: #999988;
24 | font-style: italic; }
25 |
26 | .highlight .cp {
27 | color: #999999;
28 | font-weight: bold; }
29 |
30 | .highlight .c1 {
31 | color: #999988;
32 | font-style: italic; }
33 |
34 | .highlight .cs {
35 | color: #999999;
36 | font-weight: bold;
37 | font-style: italic; }
38 |
39 | .highlight .gd {
40 | color: #000000;
41 | background-color: #ffdddd; }
42 |
43 | .highlight .gd .x {
44 | color: #000000;
45 | background-color: #ffaaaa; }
46 |
47 | .highlight .ge {
48 | color: #000000;
49 | font-style: italic; }
50 |
51 | .highlight .gr {
52 | color: #aa0000; }
53 |
54 | .highlight .gh {
55 | color: #999999; }
56 |
57 | .highlight .gi {
58 | color: #000000;
59 | background-color: #ddffdd; }
60 |
61 | .highlight .gi .x {
62 | color: #000000;
63 | background-color: #aaffaa; }
64 |
65 | .highlight .go {
66 | color: #888888; }
67 |
68 | .highlight .gp {
69 | color: #555555; }
70 |
71 | .highlight .gs {
72 | font-weight: bold; }
73 |
74 | .highlight .gu {
75 | color: #aaaaaa; }
76 |
77 | .highlight .gt {
78 | color: #aa0000; }
79 |
80 | .highlight .kc {
81 | color: #000000;
82 | font-weight: bold; }
83 |
84 | .highlight .kd {
85 | color: #000000;
86 | font-weight: bold; }
87 |
88 | .highlight .kp {
89 | color: #000000;
90 | font-weight: bold; }
91 |
92 | .highlight .kr {
93 | color: #000000;
94 | font-weight: bold; }
95 |
96 | .highlight .kt {
97 | color: #445588; }
98 |
99 | .highlight .m {
100 | color: #009999; }
101 |
102 | .highlight .s {
103 | color: #d14; }
104 |
105 | .highlight .na {
106 | color: #008080; }
107 |
108 | .highlight .nb {
109 | color: #0086B3; }
110 |
111 | .highlight .nc {
112 | color: #445588;
113 | font-weight: bold; }
114 |
115 | .highlight .no {
116 | color: #008080; }
117 |
118 | .highlight .ni {
119 | color: #800080; }
120 |
121 | .highlight .ne {
122 | color: #990000;
123 | font-weight: bold; }
124 |
125 | .highlight .nf {
126 | color: #990000; }
127 |
128 | .highlight .nn {
129 | color: #555555; }
130 |
131 | .highlight .nt {
132 | color: #000080; }
133 |
134 | .highlight .nv {
135 | color: #008080; }
136 |
137 | .highlight .ow {
138 | color: #000000;
139 | font-weight: bold; }
140 |
141 | .highlight .w {
142 | color: #bbbbbb; }
143 |
144 | .highlight .mf {
145 | color: #009999; }
146 |
147 | .highlight .mh {
148 | color: #009999; }
149 |
150 | .highlight .mi {
151 | color: #009999; }
152 |
153 | .highlight .mo {
154 | color: #009999; }
155 |
156 | .highlight .sb {
157 | color: #d14; }
158 |
159 | .highlight .sc {
160 | color: #d14; }
161 |
162 | .highlight .sd {
163 | color: #d14; }
164 |
165 | .highlight .s2 {
166 | color: #d14; }
167 |
168 | .highlight .se {
169 | color: #d14; }
170 |
171 | .highlight .sh {
172 | color: #d14; }
173 |
174 | .highlight .si {
175 | color: #d14; }
176 |
177 | .highlight .sx {
178 | color: #d14; }
179 |
180 | .highlight .sr {
181 | color: #009926; }
182 |
183 | .highlight .s1 {
184 | color: #d14; }
185 |
186 | .highlight .ss {
187 | color: #990073; }
188 |
189 | .highlight .bp {
190 | color: #999999; }
191 |
192 | .highlight .vc {
193 | color: #008080; }
194 |
195 | .highlight .vg {
196 | color: #008080; }
197 |
198 | .highlight .vi {
199 | color: #008080; }
200 |
201 | .highlight .il {
202 | color: #009999; }
203 |
--------------------------------------------------------------------------------
/Marindeck/View/Settings/CustomCSS/EditCustomCSSViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Marindeck/Model/Handlers/General.swift:
--------------------------------------------------------------------------------
1 | //
2 | // General.swift
3 | // Marindeck
4 | //
5 | // Created by a on 2022/01/29.
6 | //
7 |
8 | import Foundation
9 |
10 | struct AnyCodingKeys: CodingKey {
11 | var stringValue: String
12 | var intValue: Int?
13 |
14 | init?(stringValue: String) { self.stringValue = stringValue }
15 |
16 | init?(intValue: Int) {
17 | self.stringValue = String(intValue)
18 | self.intValue = intValue
19 | }
20 | }
21 |
22 | extension KeyedDecodingContainer {
23 | func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] {
24 | var container = try self.nestedUnkeyedContainer(forKey: key)
25 | return try container.decode(type)
26 | }
27 |
28 | func decodeIfPresent(_ type: [Any].Type, forKey key: K) throws -> [Any]? {
29 | guard contains(key) else { return .none }
30 | return try decode(type, forKey: key)
31 | }
32 |
33 | func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] {
34 | let container = try nestedContainer(keyedBy: AnyCodingKeys.self, forKey: key)
35 | return try container.decode(type)
36 | }
37 |
38 | func decodeIfPresent(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any]? {
39 | guard contains(key) else { return .none }
40 | return try decode(type, forKey: key)
41 | }
42 |
43 | func decode(_ type: [String: Any].Type) throws -> [String: Any] {
44 | var dictionary = [String: Any]()
45 |
46 | allKeys.forEach { key in
47 | if let value = try? decode(Bool.self, forKey: key) {
48 | dictionary[key.stringValue] = value
49 | } else if let value = try? decode(String.self, forKey: key) {
50 | dictionary[key.stringValue] = value
51 | } else if let value = try? decode(Int64.self, forKey: key) {
52 | dictionary[key.stringValue] = value
53 | } else if let value = try? decode(Double.self, forKey: key) {
54 | dictionary[key.stringValue] = value
55 | } else if let value = try? decode([String: Any].self, forKey: key) {
56 | dictionary[key.stringValue] = value
57 | } else if let value = try? decode([Any].self, forKey: key) {
58 | dictionary[key.stringValue] = value
59 | }
60 | }
61 |
62 | return dictionary
63 | }
64 | }
65 |
66 | extension UnkeyedDecodingContainer {
67 | mutating func decode(_ type: [Any].Type) throws -> [Any] {
68 | var array = [Any]()
69 |
70 | while isAtEnd == false {
71 | if let value = try? decode(Bool.self) {
72 | array.append(value)
73 | } else if let value = try? decode(String.self) {
74 | array.append(value)
75 | } else if let value = try? decode(Int64.self) {
76 | array.append(value)
77 | } else if let value = try? decode(Double.self) {
78 | array.append(value)
79 | } else if let value = try? decode([String: Any].self) {
80 | array.append(value)
81 | } else if let value = try? decode([Any].self) {
82 | array.append(value)
83 | }
84 | }
85 |
86 | return array
87 | }
88 |
89 | mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] {
90 | let nestedContainer = try self.nestedContainer(keyedBy: AnyCodingKeys.self)
91 | return try nestedContainer.decode(type)
92 | }
93 | }
94 |
95 | struct General: Decodable {
96 | let type: JSCallbackFlag
97 | let body: Body
98 |
99 | struct Body: Decodable {
100 | let body: [String: Any]
101 |
102 | public init(from decoder: Decoder) throws {
103 | let container = try decoder.container(keyedBy: AnyCodingKeys.self)
104 | body = try container.decode([String: Any].self)
105 | }
106 | }
107 |
108 | // struct FetchImage: Decodable {
109 | // let url: String
110 | // }
111 | }
112 |
--------------------------------------------------------------------------------
/MarinDeckExtension/MarinDeckExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarinDeckExtension.swift
3 | // MarinDeckExtension
4 | //
5 | // Created by Rinia on 2021/04/15.
6 | //
7 |
8 | import WidgetKit
9 | import SwiftUI
10 | import Intents
11 |
12 | struct Provider: IntentTimelineProvider {
13 | func placeholder(in context: Context) -> SimpleEntry {
14 | SimpleEntry(date: Date(), configuration: ConfigurationIntent())
15 | }
16 |
17 | func getSnapshot(for configuration: ConfigurationIntent,
18 | in context: Context,
19 | completion: @escaping (SimpleEntry) -> Void) {
20 | let entry = SimpleEntry(date: Date(), configuration: configuration)
21 | completion(entry)
22 | }
23 |
24 | func getTimeline(for configuration: ConfigurationIntent,
25 | in context: Context,
26 | completion: @escaping (Timeline) -> Void) {
27 | var entries: [SimpleEntry] = []
28 |
29 | // Generate a timeline consisting of five entries an hour apart, starting from the current date.
30 | let currentDate = Date()
31 | for hourOffset in 0 ..< 5 {
32 | let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
33 | let entry = SimpleEntry(date: entryDate, configuration: configuration)
34 | entries.append(entry)
35 | }
36 |
37 | let timeline = Timeline(entries: entries, policy: .atEnd)
38 | completion(timeline)
39 | }
40 | }
41 |
42 | struct SimpleEntry: TimelineEntry {
43 | let date: Date
44 | let configuration: ConfigurationIntent
45 | }
46 |
47 | struct TrendView: View {
48 | @Binding var title: String
49 | @Binding var ranking: String
50 |
51 | var body: some View {
52 | HStack {
53 | // ZStack {
54 | // Circle()
55 | // .fill(Color.clear)
56 | // .frame(width: 30, height: 30)
57 | // Text(ranking)
58 | // .frame(width: 30, height: 30, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
59 | // }
60 |
61 | Text(title)
62 | .foregroundColor(.white)
63 | .fontWeight(.semibold)
64 | .font(/*@START_MENU_TOKEN@*/.title3/*@END_MENU_TOKEN@*/)
65 |
66 | }
67 | }
68 | }
69 |
70 | struct MarinDeckExtensionEntryView: View {
71 | var entry: Provider.Entry
72 |
73 | @State var title1st: String = "#ゲットしよう15種のお菓子"
74 | @State var title2nd: String = "#本当に人間ですか"
75 | @State var title3rd: String = "御園座初日"
76 |
77 | var body: some View {
78 | ZStack {
79 | Color(red: 0.08, green: 0.12, blue: 0.16)
80 | .edgesIgnoringSafeArea(.all)
81 |
82 | HStack {
83 | VStack(alignment: .leading, spacing: 15.0) {
84 | TrendView(title: $title1st, ranking: $title3rd)
85 | TrendView(title: $title2nd, ranking: $title3rd)
86 | TrendView(title: $title3rd, ranking: $title3rd)
87 |
88 | }
89 | .padding()
90 | }
91 | .padding(10)
92 | }
93 | }
94 |
95 | }
96 |
97 | @main
98 | struct MarinDeckExtension: Widget {
99 | let kind: String = "MarinDeckExtension"
100 |
101 | var body: some WidgetConfiguration {
102 | IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
103 | MarinDeckExtensionEntryView(entry: entry)
104 | }
105 | .configurationDisplayName("Twitter trend")
106 | .description("Twitterのトレンドが表示されます。")
107 | }
108 | }
109 |
110 | struct MarinDeckExtension_Previews: PreviewProvider {
111 | static var previews: some View {
112 | MarinDeckExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
113 | .previewContext(WidgetPreviewContext(family: .systemMedium))
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Marindeck/View/Settings/CustomJS/EditCustomJSViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/Marindeck/View/Login/LoginViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoginViewController.swift
3 | // Marindeck
4 | //
5 | // Created by Rinia on 2021/11/07.
6 | //
7 |
8 | import UIKit
9 | import WebKit
10 |
11 | protocol LoginViewControllerOutput {
12 | func logined()
13 | }
14 |
15 | class LoginViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
16 | public var delegate: LoginViewControllerOutput?
17 | public var url = "https://mobile.twitter.com/login?hide_message=true&redirect_after_login=https://tweetdeck.twitter.com"
18 |
19 | var webView: WKWebView!
20 |
21 | override func loadView() {
22 | let webConfiguration = WKWebViewConfiguration()
23 | webView = WKWebView(frame: .zero, configuration: webConfiguration)
24 | webView.uiDelegate = self
25 | webView.navigationDelegate = self
26 | // webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1"
27 | view = webView
28 | }
29 |
30 | override func viewDidLoad() {
31 | super.viewDidLoad()
32 |
33 | isModalInPresentation = true
34 |
35 | navigationController?.navigationBar.barTintColor = .label
36 | navigationController?.navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
37 | navigationController?.navigationBar.shadowImage = UIImage()
38 | navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark.circle.fill"),
39 | style: .plain,
40 | target: self,
41 | action: #selector(self.onDismiss))
42 | navigationController?.navigationBar.tintColor = .label
43 |
44 | let myURL = URL(string: url)
45 | let request = URLRequest(url: myURL!)
46 |
47 | let jsonString = """
48 | [{
49 | "trigger": {
50 | "url-filter": ".*"
51 | },
52 | "action": {
53 | "type": "css-display-none",
54 | "selector": "div[aria-label=閉じる]"
55 | },
56 | "action": {
57 | "type": "css-display-none",
58 | "selector": "div[role=progressbar]"
59 | }
60 | }]
61 | """
62 |
63 | WKContentRuleListStore.default()
64 | .compileContentRuleList(forIdentifier: "ContentBlockingRules",
65 | encodedContentRuleList: jsonString) { rulesList, error in
66 | if let error = error {
67 | print(error)
68 | return
69 | }
70 | guard let rulesList = rulesList else {
71 | return
72 | }
73 | let config = self.webView.configuration
74 | config.userContentController.add(rulesList)
75 | self.webView.load(request)
76 | }
77 | }
78 |
79 | @objc
80 | func onDismiss() {
81 | dismiss(animated: true, completion: nil)
82 | }
83 |
84 | func webView(_ webView: WKWebView,
85 | decidePolicyFor navigationAction: WKNavigationAction,
86 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
87 | let url = navigationAction.request.url
88 | guard let host = url?.host else {
89 | decisionHandler(.cancel)
90 | return
91 | }
92 |
93 | if host == "tweetdeck.twitter.com" || url?.lastPathComponent == "home" {
94 | decisionHandler(.cancel)
95 | dismiss(animated: true, completion: nil)
96 | delegate?.logined()
97 | return
98 | }
99 |
100 | decisionHandler(.allow)
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------