0794c725b6e07a51efed6b07c9992bc296c29135

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add YouTube share extension

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: