From c387454d9a9b98996c5ad3d23b0471b9c08bdfd8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 27 Oct 2021 23:11:38 +0200 Subject: [PATCH] Add settings for frontend instance URL --- Fixtures/Instance+Fixtures.swift | 2 +- Model/Accounts/Account.swift | 44 +++---------- Model/Accounts/AccountsBridge.swift | 37 +++++++++++ Model/Accounts/AccountsModel.swift | 2 +- Model/Accounts/Instance.swift | 54 ++++----------- Model/Accounts/InstancesBridge.swift | 38 +++++++++++ Model/Accounts/InstancesModel.swift | 11 +++- Model/Applications/InvidiousAPI.swift | 2 +- Model/Applications/PipedAPI.swift | 10 +-- Model/Applications/VideosAPI.swift | 1 + Model/Applications/VideosApp.swift | 4 ++ Pearvidious.xcodeproj/project.pbxproj | 16 +++++ Shared/Defaults.swift | 13 +++- Shared/Settings/AccountForm.swift | 4 +- Shared/Settings/AccountsSettings.swift | 91 +++++++++++++++++--------- Shared/Settings/InstanceForm.swift | 3 +- Shared/Settings/ServicesSettings.swift | 2 +- macOS/Settings/InstancesSettings.swift | 32 ++++++++- 18 files changed, 238 insertions(+), 128 deletions(-) create mode 100644 Model/Accounts/AccountsBridge.swift create mode 100644 Model/Accounts/InstancesBridge.swift diff --git a/Fixtures/Instance+Fixtures.swift b/Fixtures/Instance+Fixtures.swift index fd06a54c..eb088b2a 100644 --- a/Fixtures/Instance+Fixtures.swift +++ b/Fixtures/Instance+Fixtures.swift @@ -2,6 +2,6 @@ import Foundation extension Instance { static var fixture: Instance { - Instance(app: .invidious, name: "Home", url: "https://invidious.home.net") + Instance(app: .invidious, name: "Home", apiURL: "https://invidious.home.net") } } diff --git a/Model/Accounts/Account.swift b/Model/Accounts/Account.swift index d4ca6ed1..5bbf85c0 100644 --- a/Model/Accounts/Account.swift +++ b/Model/Accounts/Account.swift @@ -2,41 +2,6 @@ import Defaults import Foundation struct Account: Defaults.Serializable, Hashable, Identifiable { - struct AccountsBridge: Defaults.Bridge { - typealias Value = Account - typealias Serializable = [String: String] - - func serialize(_ value: Value?) -> Serializable? { - guard let value = value else { - return nil - } - - return [ - "id": value.id, - "instanceID": value.instanceID, - "name": value.name ?? "", - "url": value.url, - "sid": value.sid - ] - } - - func deserialize(_ object: Serializable?) -> Value? { - guard - let object = object, - let id = object["id"], - let instanceID = object["instanceID"], - let url = object["url"], - let sid = object["sid"] - else { - return nil - } - - let name = object["name"] ?? "" - - return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid) - } - } - static var bridge = AccountsBridge() let id: String @@ -46,7 +11,14 @@ struct Account: Defaults.Serializable, Hashable, Identifiable { let sid: String let anonymous: Bool - init(id: String? = nil, instanceID: String? = nil, name: String? = nil, url: String? = nil, sid: String? = nil, anonymous: Bool = false) { + init( + id: String? = nil, + instanceID: String? = nil, + name: String? = nil, + url: String? = nil, + sid: String? = nil, + anonymous: Bool = false + ) { self.anonymous = anonymous self.id = id ?? (anonymous ? "anonymous-\(instanceID!)" : UUID().uuidString) diff --git a/Model/Accounts/AccountsBridge.swift b/Model/Accounts/AccountsBridge.swift new file mode 100644 index 00000000..8bf87745 --- /dev/null +++ b/Model/Accounts/AccountsBridge.swift @@ -0,0 +1,37 @@ +import Defaults +import Foundation + +struct AccountsBridge: Defaults.Bridge { + typealias Value = Account + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return [ + "id": value.id, + "instanceID": value.instanceID, + "name": value.name ?? "", + "apiURL": value.url, + "sid": value.sid + ] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let id = object["id"], + let instanceID = object["instanceID"], + let url = object["apiURL"], + let sid = object["sid"] + else { + return nil + } + + let name = object["name"] ?? "" + + return Account(id: id, instanceID: instanceID, name: name, url: url, sid: sid) + } +} diff --git a/Model/Accounts/AccountsModel.swift b/Model/Accounts/AccountsModel.swift index 2296783e..71347829 100644 --- a/Model/Accounts/AccountsModel.swift +++ b/Model/Accounts/AccountsModel.swift @@ -75,7 +75,7 @@ final class AccountsModel: ObservableObject { } static func add(instance: Instance, name: String, sid: String) -> Account { - let account = Account(instanceID: instance.id, name: name, url: instance.url, sid: sid) + let account = Account(instanceID: instance.id, name: name, url: instance.apiURL, sid: sid) Defaults[.accounts].append(account) return account diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 91de308a..fe1f4e91 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -2,51 +2,20 @@ import Defaults import Foundation struct Instance: Defaults.Serializable, Hashable, Identifiable { - struct InstancesBridge: Defaults.Bridge { - typealias Value = Instance - typealias Serializable = [String: String] - - func serialize(_ value: Value?) -> Serializable? { - guard let value = value else { - return nil - } - - return [ - "app": value.app.rawValue, - "id": value.id, - "name": value.name, - "url": value.url - ] - } - - func deserialize(_ object: Serializable?) -> Value? { - guard - let object = object, - let app = VideosApp(rawValue: object["app"] ?? ""), - let id = object["id"], - let url = object["url"] - else { - return nil - } - - let name = object["name"] ?? "" - - return Instance(app: app, id: id, name: name, url: url) - } - } - static var bridge = InstancesBridge() let app: VideosApp let id: String let name: String - let url: String + let apiURL: String + var frontendURL: String? - init(app: VideosApp, id: String? = nil, name: String, url: String) { + init(app: VideosApp, id: String? = nil, name: String, apiURL: String, frontendURL: String? = nil) { self.app = app self.id = id ?? UUID().uuidString self.name = name - self.url = url + self.apiURL = apiURL + self.frontendURL = frontendURL } var anonymous: VideosAPI { @@ -63,27 +32,26 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { } var longDescription: String { - name.isEmpty ? "\(app.name) - \(url)" : "\(app.name) - \(name) (\(url))" + name.isEmpty ? "\(app.name) - \(apiURL)" : "\(app.name) - \(name) (\(apiURL))" } var shortDescription: String { - name.isEmpty ? url : name + name.isEmpty ? apiURL : name } var anonymousAccount: Account { - Account(instanceID: id, name: "Anonymous", url: url, anonymous: true) + Account(instanceID: id, name: "Anonymous", url: apiURL, anonymous: true) } var urlComponents: URLComponents { - URLComponents(string: url)! + URLComponents(string: apiURL)! } var frontendHost: String { - // TODO: piped frontend link - urlComponents.host!.replacingOccurrences(of: "api", with: "") + URLComponents(string: frontendURL!)!.host! } func hash(into hasher: inout Hasher) { - hasher.combine(url) + hasher.combine(apiURL) } } diff --git a/Model/Accounts/InstancesBridge.swift b/Model/Accounts/InstancesBridge.swift new file mode 100644 index 00000000..f9f84e7d --- /dev/null +++ b/Model/Accounts/InstancesBridge.swift @@ -0,0 +1,38 @@ + +import Defaults +import Foundation + +struct InstancesBridge: Defaults.Bridge { + typealias Value = Instance + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return [ + "app": value.app.rawValue, + "id": value.id, + "name": value.name, + "apiURL": value.apiURL, + "frontendURL": value.frontendURL ?? "" + ] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let app = VideosApp(rawValue: object["app"] ?? ""), + let id = object["id"], + let apiURL = object["apiURL"] + else { + return nil + } + + let name = object["name"] ?? "" + + let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"] + return Instance(app: app, id: id, name: name, apiURL: apiURL, frontendURL: frontendURL) + } +} diff --git a/Model/Accounts/InstancesModel.swift b/Model/Accounts/InstancesModel.swift index 836fd99b..08dc61d3 100644 --- a/Model/Accounts/InstancesModel.swift +++ b/Model/Accounts/InstancesModel.swift @@ -27,12 +27,21 @@ final class InstancesModel: ObservableObject { } static func add(app: VideosApp, name: String, url: String) -> Instance { - let instance = Instance(app: app, id: UUID().uuidString, name: name, url: url) + let instance = Instance(app: app, id: UUID().uuidString, name: name, apiURL: url) Defaults[.instances].append(instance) return instance } + static func setFrontendURL(_ instance: Instance, _ url: String) { + if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { + var instance = Defaults[.instances][index] + instance.frontendURL = url + + Defaults[.instances][index] = instance + } + } + static func remove(_ instance: Instance) { let accounts = InstancesModel.accounts(instance.id) if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index 12d34396..0b483b05 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -263,7 +263,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } static func proxiedAsset(instance: Instance, asset: AVURLAsset) -> AVURLAsset? { - guard let instanceURLComponents = URLComponents(string: instance.url), + guard let instanceURLComponents = URLComponents(string: instance.apiURL), var urlComponents = URLComponents(url: asset.url, resolvingAgainstBaseURL: false) else { return nil } urlComponents.scheme = instanceURLComponents.scheme diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 8300734f..9e7ac9db 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -7,7 +7,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { @Published var account: Account! var anonymousAccount: Account { - .init(instanceID: account.instance.id, name: "Anonymous", url: account.instance.url) + .init(instanceID: account.instance.id, name: "Anonymous", url: account.instance.apiURL) } init(account: Account? = nil) { @@ -65,23 +65,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { } func trending(country: Country, category _: TrendingCategory? = nil) -> Resource { - resource(baseURL: account.instance.url, path: "trending") + resource(baseURL: account.instance.apiURL, path: "trending") .withParam("region", country.rawValue) } func search(_ query: SearchQuery) -> Resource { - resource(baseURL: account.instance.url, path: "search") + resource(baseURL: account.instance.apiURL, path: "search") .withParam("q", query.query) .withParam("filter", "") } func searchSuggestions(query: String) -> Resource { - resource(baseURL: account.instance.url, path: "suggestions") + resource(baseURL: account.instance.apiURL, path: "suggestions") .withParam("query", query.lowercased()) } func video(_ id: Video.ID) -> Resource { - resource(baseURL: account.instance.url, path: "streams/\(id)") + resource(baseURL: account.instance.apiURL, path: "streams/\(id)") } var signedIn: Bool { false } diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index 25bed1d0..5632d8d1 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -51,6 +51,7 @@ extension VideosAPI { func shareURL(_ item: ContentItem) -> URL { var urlComponents = account.instance.urlComponents urlComponents.host = account.instance.frontendHost + switch item.contentType { case .video: urlComponents.path = "/watch" diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 0fc8432c..726c793d 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -30,4 +30,8 @@ enum VideosApp: String, CaseIterable { var supportsUserPlaylists: Bool { self == .invidious } + + var hasFrontendURL: Bool { + self == .piped + } } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index d1aca3f3..f3003c7b 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -68,6 +68,12 @@ 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; }; 37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; }; 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37152EE926EFEB95004FB96D /* LazyView.swift */; }; + 37169AA22729D98A0011DE61 /* InstancesBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA12729D98A0011DE61 /* InstancesBridge.swift */; }; + 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA12729D98A0011DE61 /* InstancesBridge.swift */; }; + 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA12729D98A0011DE61 /* InstancesBridge.swift */; }; + 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; + 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; + 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; @@ -502,6 +508,8 @@ 3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; 37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; + 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; }; @@ -849,9 +857,11 @@ isa = PBXGroup; children = ( 376A33E32720CB35000C1D6B /* Account.swift */, + 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */, 37001562271B1F250049C794 /* AccountsModel.swift */, 37484C3026FCB8F900287258 /* AccountValidator.swift */, 378E50FA26FE8B9F00F49626 /* Instance.swift */, + 37169AA12729D98A0011DE61 /* InstancesBridge.swift */, 375DFB5726F9DA010013F468 /* InstancesModel.swift */, ); path = Accounts; @@ -1625,6 +1635,7 @@ 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, + 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */, @@ -1672,6 +1683,7 @@ 37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */, 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */, 376578912685490700D4EA09 /* PlaylistsView.swift in Sources */, + 37169AA22729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */, 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, @@ -1769,6 +1781,7 @@ 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, + 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, 378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, @@ -1776,6 +1789,7 @@ 377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */, 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */, + 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */, @@ -1917,6 +1931,7 @@ 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, + 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularView.swift in Sources */, @@ -1963,6 +1978,7 @@ 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37D4B19926717E1500C925CA /* Video.swift in Sources */, 378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */, + 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index eea7ebca..99c8f595 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -7,8 +7,17 @@ extension Defaults.Keys { static let privateAccountID = "default-private-invidious-account" static let instances = Key<[Instance]>("instances", default: [ - .init(app: .piped, id: pipedInstanceID, name: "Public", url: "https://pipedapi.kavin.rocks"), - .init(app: .invidious, id: invidiousInstanceID, name: "Private", url: "https://invidious.home.arekf.net") + .init( + app: .piped, + id: pipedInstanceID, + name: "Public", + apiURL: "https://pipedapi.kavin.rocks", + frontendURL: "https://piped.kavin.rocks" + ), + .init(app: .invidious, + id: invidiousInstanceID, + name: "Private", + apiURL: "https://invidious.home.arekf.net") ]) static let accounts = Key<[Account]>("accounts", default: [ .init( diff --git a/Shared/Settings/AccountForm.swift b/Shared/Settings/AccountForm.swift index 44a8be40..4504818f 100644 --- a/Shared/Settings/AccountForm.swift +++ b/Shared/Settings/AccountForm.swift @@ -133,8 +133,8 @@ struct AccountForm: View { private var validator: AccountValidator { AccountValidator( app: .constant(instance.app), - url: instance.url, - account: Account(instanceID: instance.id, url: instance.url, sid: sid), + url: instance.apiURL, + account: Account(instanceID: instance.id, url: instance.apiURL, sid: sid), id: $sid, isValid: $isValid, isValidated: $isValidated, diff --git a/Shared/Settings/AccountsSettings.swift b/Shared/Settings/AccountsSettings.swift index ad54e6cb..998769f5 100644 --- a/Shared/Settings/AccountsSettings.swift +++ b/Shared/Settings/AccountsSettings.swift @@ -6,6 +6,8 @@ struct AccountsSettings: View { @State private var accountsChanged = false @State private var presentingAccountForm = false + @State private var frontendURL = "" + @EnvironmentObject private var model @EnvironmentObject private var instances @@ -14,56 +16,81 @@ struct AccountsSettings: View { } var body: some View { - VStack { - if instance.app.supportsAccounts { - accounts - } else { - Text("Accounts are not supported for the application of this instance") - .foregroundColor(.secondary) + List { + if instance.app.hasFrontendURL { + Section(header: Text("Frontend URL")) { + TextField( + "Frontend URL", + text: $frontendURL, + prompt: Text("To enable videos, channels and playlists sharing") + ) + .onAppear { + frontendURL = instance.frontendURL ?? "" + } + .onChange(of: frontendURL) { newValue in + InstancesModel.setFrontendURL(instance, newValue) + } + .labelsHidden() + .autocapitalization(.none) + .keyboardType(.URL) + } + } + + Section(header: Text("Accounts"), footer: sectionFooter) { + if instance.app.supportsAccounts { + accounts + } else { + Text("Accounts are not supported for the application of this instance") + .foregroundColor(.secondary) + } } } - .navigationTitle(instance.shortDescription) + #if os(tvOS) + .frame(maxWidth: 1000) + #endif + + .navigationTitle(instance.description) } var accounts: some View { - List { - Section(header: Text("Accounts"), footer: sectionFooter) { - ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in - #if os(tvOS) - Button(account.description) {} - .contextMenu { - Button("Remove", role: .destructive) { removeAccount(account) } - Button("Cancel", role: .cancel) {} - } - #else - Text(account.description) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button("Remove", role: .destructive) { removeAccount(account) } - } - #endif - } - .redrawOn(change: accountsChanged) + Group { + ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in + #if os(tvOS) + Button(account.description) {} + .contextMenu { + Button("Remove", role: .destructive) { removeAccount(account) } + Button("Cancel", role: .cancel) {} + } + #else + Text(account.description) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button("Remove", role: .destructive) { removeAccount(account) } + } + #endif + } + .redrawOn(change: accountsChanged) - Button("Add account...") { - presentingAccountForm = true - } + Button("Add account...") { + presentingAccountForm = true } } .sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) { AccountForm(instance: instance) } - #if os(iOS) + #if !os(tvOS) .listStyle(.insetGrouped) - #elseif os(tvOS) - .frame(maxWidth: 1000) #endif } private var sectionFooter: some View { + if !instance.app.supportsAccounts { + return Text("") + } + #if os(iOS) - Text("Swipe to remove account") + return Text("Swipe to remove account") #else - Text("Tap and hold to remove account") + return Text("Tap and hold to remove account") .foregroundColor(.secondary) #endif } diff --git a/Shared/Settings/InstanceForm.swift b/Shared/Settings/InstanceForm.swift index 2b793d3c..686fe169 100644 --- a/Shared/Settings/InstanceForm.swift +++ b/Shared/Settings/InstanceForm.swift @@ -82,7 +82,8 @@ struct InstanceForm: View { TextField("Name", text: $name, prompt: Text("Instance Name (optional)")) .focused($nameFieldFocused) - TextField("URL", text: $url, prompt: Text("https://invidious.home.net")) + TextField("API URL", text: $url, prompt: Text("https://invidious.home.net")) + #if !os(macOS) .autocapitalization(.none) .keyboardType(.URL) diff --git a/Shared/Settings/ServicesSettings.swift b/Shared/Settings/ServicesSettings.swift index f3bf070e..ec23d035 100644 --- a/Shared/Settings/ServicesSettings.swift +++ b/Shared/Settings/ServicesSettings.swift @@ -19,7 +19,7 @@ struct ServicesSettings: View { #endif } - Section(header: Text("SponsorBlock Categories to Skip")) { + Section(header: Text("Categories to Skip")) { #if os(macOS) List(SponsorBlockAPI.categories, id: \.self) { category in SponsorBlockCategorySelectionRow( diff --git a/macOS/Settings/InstancesSettings.swift b/macOS/Settings/InstancesSettings.swift index 86979466..f3a92440 100644 --- a/macOS/Settings/InstancesSettings.swift +++ b/macOS/Settings/InstancesSettings.swift @@ -12,6 +12,8 @@ struct InstancesSettings: View { @State private var presentingAccountRemovalConfirmation = false @State private var presentingInstanceRemovalConfirmation = false + @State private var frontendURL = "" + @EnvironmentObject private var accounts @EnvironmentObject private var model @@ -67,12 +69,38 @@ struct InstancesSettings: View { .listStyle(.inset(alternatesRowBackgrounds: true)) } + if selectedInstance != nil, selectedInstance.app.hasFrontendURL { + Text("Frontend URL") + + TextField("Frontend URL", text: $frontendURL, prompt: Text("Frontend URL")) + .onAppear { + frontendURL = selectedInstance.frontendURL ?? "" + } + .onChange(of: frontendURL) { newValue in + InstancesModel.setFrontendURL(selectedInstance, newValue) + } + .labelsHidden() + VStack(alignment: .leading, spacing: 0) { + Text("If provided, you can copy links from videos, channels and playlist using") + .padding(.trailing, 2) + + HStack(spacing: 0) { + Image(systemName: "command") + + Text("**+C**") + } + } + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + } + if selectedInstance != nil, !selectedInstance.app.supportsAccounts { + Spacer() Text("Accounts are not supported for the application of this instance") .font(.caption) .foregroundColor(.secondary) - - Spacer() } if selectedInstance != nil {