0794c725b6e07a51efed6b07c9992bc296c29135
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/Package.swift b/Package.swift
2index 3fca75f91544b9f4e33e72532867c0c1c6b4e8f7..b654e64a710da73cdfe4144ad1aac59ca55dafcc 100644
3--- a/Package.swift
4+++ b/Package.swift
5@@ -15,10 +15,20 @@ let package = Package(
6 name: "ytrssil",
7 targets: ["ytrssil"]
8 ),
9+ .library(
10+ name: "ytrssilShare",
11+ targets: ["ytrssilShare"]
12+ ),
13 ],
14 targets: [
15+ .target(name: "ytrssilShared"),
16+ .target(
17+ name: "ytrssil",
18+ dependencies: ["ytrssilShared"]
19+ ),
20 .target(
21- name: "ytrssil"
22+ name: "ytrssilShare",
23+ dependencies: ["ytrssilShared"]
24 ),
25 ]
26 )
27diff --git a/Resources/ytrssilShare-Info.plist b/Resources/ytrssilShare-Info.plist
28new file mode 100644
29index 0000000000000000000000000000000000000000..41069e06f88855a4c82782261a47413e7c21fe3f
30--- /dev/null
31+++ b/Resources/ytrssilShare-Info.plist
32@@ -0,0 +1,21 @@
33+<?xml version="1.0" encoding="UTF-8"?>
34+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
35+<plist version="1.0">
36+<dict>
37+ <key>NSExtension</key>
38+ <dict>
39+ <key>NSExtensionAttributes</key>
40+ <dict>
41+ <key>NSExtensionActivationRule</key>
42+ <dict>
43+ <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
44+ <integer>1</integer>
45+ </dict>
46+ </dict>
47+ <key>NSExtensionPointIdentifier</key>
48+ <string>com.apple.share-services</string>
49+ <key>NSExtensionPrincipalClass</key>
50+ <string>ytrssilShare.ShareViewController</string>
51+ </dict>
52+</dict>
53+</plist>
54diff --git a/Sources/ytrssil/APIClient.swift b/Sources/ytrssil/APIClient.swift
55index 67bbed7781c03942f4d76bd2cc259449ffd7ec26..ebb6c83f32042463c09b524dc9b330bb6bccf405 100644
56--- a/Sources/ytrssil/APIClient.swift
57+++ b/Sources/ytrssil/APIClient.swift
58@@ -1,4 +1,5 @@
59 import Foundation
60+import ytrssilShared
61
62 struct Video: Decodable, Identifiable, Sendable {
63 let videoID: String
64diff --git a/Sources/ytrssil/ServerConfiguration.swift b/Sources/ytrssil/ServerConfiguration.swift
65index caa36dda5bcb670c1c89dbfa3d65bed04e0a7068..9cb00cfaab6585ba4dd480f3a8ff424c29ee2ac0 100644
66--- a/Sources/ytrssil/ServerConfiguration.swift
67+++ b/Sources/ytrssil/ServerConfiguration.swift
68@@ -1,46 +1 @@
69-import Foundation
70-
71-struct ServerConfiguration: Sendable {
72- let baseURL: URL
73- let token: String
74-
75- init(serverURL: String, token: String) throws {
76- let trimmedServerURL = serverURL.trimmingCharacters(in: .whitespacesAndNewlines)
77-
78- guard !trimmedServerURL.isEmpty,
79- let url = URL(string: trimmedServerURL),
80- url.scheme?.lowercased() == "https",
81- url.host != nil,
82- url.user == nil,
83- url.password == nil,
84- url.query == nil,
85- url.fragment == nil else {
86- throw ConfigurationError.invalidServerURL
87- }
88-
89- guard !token.isEmpty else {
90- throw ConfigurationError.missingToken
91- }
92-
93- baseURL = url
94- self.token = token
95- }
96-
97- func endpoint(path: String) -> URL {
98- baseURL.appendingPathComponent(path)
99- }
100-}
101-
102-enum ConfigurationError: LocalizedError {
103- case invalidServerURL
104- case missingToken
105-
106- var errorDescription: String? {
107- switch self {
108- case .invalidServerURL:
109- "Enter a valid HTTPS server URL."
110- case .missingToken:
111- "Enter the API token."
112- }
113- }
114-}
115+import ytrssilShared
116diff --git a/Sources/ytrssil/SettingsStore.swift b/Sources/ytrssil/SettingsStore.swift
117index b744cfb39cf3876c4e38ad3284fa177bb5e88226..12729c3b6435a18aedd2e13ff722f48b0829b94d 100644
118--- a/Sources/ytrssil/SettingsStore.swift
119+++ b/Sources/ytrssil/SettingsStore.swift
120@@ -1,5 +1,6 @@
121 import Foundation
122 import Observation
123+import ytrssilShared
124
125 @MainActor
126 @Observable
127diff --git a/Sources/ytrssilShare/ShareViewController.swift b/Sources/ytrssilShare/ShareViewController.swift
128new file mode 100644
129index 0000000000000000000000000000000000000000..39a769eecac6da06276c2e8203b9f7d08d568b4e
130--- /dev/null
131+++ b/Sources/ytrssilShare/ShareViewController.swift
132@@ -0,0 +1,396 @@
133+import Foundation
134+import Security
135+import UIKit
136+import UniformTypeIdentifiers
137+import ytrssilShared
138+
139+final class ShareViewController: UIViewController, UITextFieldDelegate {
140+ private let activityIndicator = UIActivityIndicatorView(style: .large)
141+ private let messageLabel = UILabel()
142+ private let serverURLField = UITextField()
143+ private let tokenField = UITextField()
144+ private let saveButton = UIButton(type: .system)
145+ private var sharedItem: SharedItem?
146+
147+ override func viewDidLoad() {
148+ super.viewDidLoad()
149+ configureView()
150+ Task { await processShare() }
151+ }
152+
153+ private func configureView() {
154+ view.backgroundColor = .systemBackground
155+ preferredContentSize = CGSize(width: 320, height: 280)
156+
157+ messageLabel.font = .preferredFont(forTextStyle: .body)
158+ messageLabel.textAlignment = .center
159+ messageLabel.numberOfLines = 0
160+ messageLabel.adjustsFontForContentSizeCategory = true
161+ messageLabel.text = "Preparing link…"
162+
163+ serverURLField.placeholder = "Server URL"
164+ serverURLField.borderStyle = .roundedRect
165+ serverURLField.keyboardType = .URL
166+ serverURLField.autocapitalizationType = .none
167+ serverURLField.autocorrectionType = .no
168+ serverURLField.returnKeyType = .next
169+ serverURLField.delegate = self
170+ serverURLField.inputAccessoryView = keyboardToolbar()
171+
172+ tokenField.placeholder = "API token"
173+ tokenField.borderStyle = .roundedRect
174+ tokenField.isSecureTextEntry = true
175+ tokenField.autocapitalizationType = .none
176+ tokenField.autocorrectionType = .no
177+ tokenField.returnKeyType = .done
178+ tokenField.delegate = self
179+ tokenField.inputAccessoryView = keyboardToolbar()
180+
181+ saveButton.configuration = .filled()
182+ saveButton.configuration?.title = "Save and Continue"
183+ saveButton.addAction(UIAction { [weak self] _ in
184+ self?.saveConfiguration()
185+ }, for: .touchUpInside)
186+
187+ let stack = UIStackView(arrangedSubviews: [
188+ activityIndicator,
189+ messageLabel,
190+ serverURLField,
191+ tokenField,
192+ saveButton,
193+ ])
194+ stack.axis = .vertical
195+ stack.alignment = .fill
196+ stack.spacing = 16
197+ stack.translatesAutoresizingMaskIntoConstraints = false
198+ view.addSubview(stack)
199+
200+ NSLayoutConstraint.activate([
201+ stack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
202+ stack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
203+ stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
204+ ])
205+
206+ setConfigurationControlsHidden(true)
207+ activityIndicator.startAnimating()
208+ }
209+
210+ private func processShare() async {
211+ guard let url = await sharedURL() else {
212+ showFailure("Share a YouTube video or channel link.")
213+ return
214+ }
215+
216+ guard let sharedItem = SharedItem(url: url) else {
217+ showFailure("This is not a supported YouTube video or channel link.")
218+ return
219+ }
220+
221+ self.sharedItem = sharedItem
222+ guard let configuration = ShareConfigurationStore.configuration else {
223+ showConfiguration()
224+ return
225+ }
226+
227+ await send(sharedItem, using: configuration)
228+ }
229+
230+ private func saveConfiguration() {
231+ guard let sharedItem else {
232diff --git a/Sources/ytrssilShared/ServerConfiguration.swift b/Sources/ytrssilShared/ServerConfiguration.swift
233new file mode 100644
234index 0000000000000000000000000000000000000000..f6a003fd4ba07123a951bf88f064766b36fee44a
235--- /dev/null
236+++ b/Sources/ytrssilShared/ServerConfiguration.swift
237@@ -0,0 +1,46 @@
238+import Foundation
239+
240+public struct ServerConfiguration: Sendable {
241+ public let baseURL: URL
242+ public let token: String
243+
244+ public init(serverURL: String, token: String) throws {
245+ let trimmedServerURL = serverURL.trimmingCharacters(in: .whitespacesAndNewlines)
246+
247+ guard !trimmedServerURL.isEmpty,
248+ let url = URL(string: trimmedServerURL),
249+ url.scheme?.lowercased() == "https",
250+ url.host != nil,
251+ url.user == nil,
252+ url.password == nil,
253+ url.query == nil,
254+ url.fragment == nil else {
255+ throw ConfigurationError.invalidServerURL
256+ }
257+
258+ guard !token.isEmpty else {
259+ throw ConfigurationError.missingToken
260+ }
261+
262+ baseURL = url
263+ self.token = token
264+ }
265+
266+ public func endpoint(path: String) -> URL {
267+ baseURL.appendingPathComponent(path)
268+ }
269+}
270+
271+public enum ConfigurationError: LocalizedError {
272+ case invalidServerURL
273+ case missingToken
274+
275+ public var errorDescription: String? {
276+ switch self {
277+ case .invalidServerURL:
278+ "Enter a valid HTTPS server URL."
279+ case .missingToken:
280+ "Enter the API token."
281+ }
282+ }
283+}
284diff --git a/xtool.yml b/xtool.yml
285index c0c5522d302e260e547120d89574fbc1abc27355..d9dbdd45f045bb355492ad3c8e0df713e0e69942 100644
286--- a/xtool.yml
287+++ b/xtool.yml
288@@ -1,5 +1,9 @@
289 version: 1
290 bundleID: com.theedgeofrage.ytrssil
291+product: ytrssil
292+extensions:
293+- product: ytrssilShare
294+ infoPath: Resources/ytrssilShare-Info.plist
295 iconPath: Resources/AppIcon.png
296 infoPath: Resources/Info.plist
297 resources: