Add an optional frontend URL for Piped instances used by browser actions

This commit is contained in:
Arkadiusz Fal
2026-09-18 23:36:11 +02:00
parent 045e0ba574
commit 172d712f25
4 changed files with 70 additions and 3 deletions

View File

@@ -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" : {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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.