diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 107f550e..418adb0c 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -16794,6 +16794,17 @@ } } }, + "sources.field.frontendURL" : { + "comment" : "Field label for optional Piped web frontend URL", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frontend URL" + } + } + } + }, "sources.field.name" : { "comment" : "Field label for source name", "localizations" : { @@ -17025,6 +17036,17 @@ } } }, + "sources.footer.frontendURL" : { + "comment" : "Footer text explaining the optional Piped web frontend URL", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Optional. Piped often serves its web frontend from a different address than the API. When set, it is used for opening this instance in the browser and copying its link. Leave empty to use the API URL." + } + } + } + }, "sources.footer.proxiesVideos" : { "comment" : "Footer text explaining video proxy option", "localizations" : { diff --git a/Yattee/Models/Instance.swift b/Yattee/Models/Instance.swift index 1087b4df..ace1a84c 100644 --- a/Yattee/Models/Instance.swift +++ b/Yattee/Models/Instance.swift @@ -58,6 +58,11 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { /// The base URL of the instance. let url: URL + /// Optional URL of the web frontend, when it differs from the API URL + /// (Piped serves its API and frontend from separate hosts). Used for + /// browser-facing actions (open in browser, copy link) — never for API calls. + var frontendURL: URL? + /// Optional user-defined name for this instance. var name: String? @@ -102,6 +107,7 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { id: UUID = UUID(), type: InstanceType, url: URL, + frontendURL: URL? = nil, name: String? = nil, isEnabled: Bool = true, dateAdded: Date = Date(), @@ -113,6 +119,7 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { self.id = id self.type = type self.url = url + self.frontendURL = frontendURL self.name = name self.isEnabled = isEnabled self.dateAdded = dateAdded @@ -127,6 +134,7 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { id = try container.decode(UUID.self, forKey: .id) type = try container.decode(InstanceType.self, forKey: .type) url = try container.decode(URL.self, forKey: .url) + frontendURL = try container.decodeIfPresent(URL.self, forKey: .frontendURL) name = try container.decodeIfPresent(String.self, forKey: .name) isEnabled = try container.decode(Bool.self, forKey: .isEnabled) dateAdded = try container.decode(Date.self, forKey: .dateAdded) @@ -160,6 +168,12 @@ struct Instance: Identifiable, Codable, Hashable, Sendable { return components.url?.absoluteString ?? url.absoluteString } + /// The URL browser-facing actions should use: the web frontend when one is + /// configured, else the API URL. + var browseURL: URL { + frontendURL ?? url + } + var contentSource: ContentSource { type.contentSource(for: url) } diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index 2ae9c497..db8487c4 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -175,7 +175,7 @@ struct InstanceBrowseView: View { } Button { - openURL(instance.url) + openURL(instance.browseURL) } label: { Label(String(localized: "instance.browse.openInBrowser"), systemImage: "safari") } @@ -257,10 +257,10 @@ struct InstanceBrowseView: View { private func copyInstanceURL() { #if os(iOS) - UIPasteboard.general.string = instance.url.absoluteString + UIPasteboard.general.string = instance.browseURL.absoluteString #elseif os(macOS) NSPasteboard.general.clearContents() - NSPasteboard.general.setString(instance.url.absoluteString, forType: .string) + NSPasteboard.general.setString(instance.browseURL.absoluteString, forType: .string) #endif } diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index 0d9ab07d..c2f7bdc0 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -36,6 +36,10 @@ private struct EditRemoteServerContent: View { @State private var allowInvalidCertificates: Bool @State private var proxiesVideos: Bool + // Piped serves its API and web frontend from separate hosts; browser-facing + // actions (open in browser, copy link) use this URL when set. + @State private var frontendURLString: String + // HTTP Basic Auth credentials (for any instance type behind a reverse proxy; // required for Yattee Server, optional for Invidious/Piped/PeerTube). @State private var basicAuthUsername: String = "" @@ -66,6 +70,7 @@ private struct EditRemoteServerContent: View { _isEnabled = State(initialValue: instance.isEnabled) _allowInvalidCertificates = State(initialValue: instance.allowInvalidCertificates) _proxiesVideos = State(initialValue: instance.proxiesVideos) + _frontendURLString = State(initialValue: instance.frontendURL?.absoluteString ?? "") } var body: some View { @@ -121,6 +126,27 @@ private struct EditRemoteServerContent: View { #endif } + if instance.type == .piped { + Section { + #if os(tvOS) + TVSettingsTextField(title: String(localized: "sources.field.frontendURL"), text: $frontendURLString) + #elseif os(macOS) + LabeledContent(String(localized: "sources.field.frontendURL")) { + TextField("", text: $frontendURLString) + .autocorrectionDisabled() + } + #else + TextField(String(localized: "sources.field.frontendURL"), text: $frontendURLString) + .textContentType(.URL) + .keyboardType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + #endif + } footer: { + Text(String(localized: "sources.footer.frontendURL")) + } + } + if instance.type == .yatteeServer { // Server Info Section Section { @@ -465,6 +491,11 @@ private struct EditRemoteServerContent: View { updated.allowInvalidCertificates = allowInvalidCertificates updated.proxiesVideos = proxiesVideos + if instance.type == .piped { + let trimmed = frontendURLString.trimmingCharacters(in: .whitespacesAndNewlines) + updated.frontendURL = trimmed.isEmpty ? nil : Instance.normalizeSourceURL(trimmed) + } + // Save / clear HTTP Basic Auth credentials. Yattee Server never auto-clears // (credentials are required and the user re-enters to overwrite). Piped never // persists them — its session token reuses the same Authorization header.