diff --git a/Model/Accounts/AccountValidator.swift b/Model/Accounts/AccountValidator.swift index 4a428537..2429a051 100644 --- a/Model/Accounts/AccountValidator.swift +++ b/Model/Accounts/AccountValidator.swift @@ -55,6 +55,9 @@ final class AccountValidator: Service { case .piped: return resource("/streams/dQw4w9WgXcQ") + + case .demoApp: + return resource("/") } } diff --git a/Model/Accounts/AccountsModel.swift b/Model/Accounts/AccountsModel.swift index 39a4c5d4..afd473aa 100644 --- a/Model/Accounts/AccountsModel.swift +++ b/Model/Accounts/AccountsModel.swift @@ -7,6 +7,7 @@ final class AccountsModel: ObservableObject { @Published private var invidious = InvidiousAPI() @Published private var piped = PipedAPI() + @Published private var demo = DemoAppAPI() @Published var publicAccount: Account? @@ -33,7 +34,14 @@ final class AccountsModel: ObservableObject { } var api: VideosAPI { - app == .piped ? piped : invidious + switch app { + case .piped: + return piped + case .invidious: + return invidious + case .demoApp: + return demo + } } var isEmpty: Bool { @@ -44,6 +52,10 @@ final class AccountsModel: ObservableObject { !isEmpty && !current.anonymous && api.signedIn } + var isDemo: Bool { + current?.app == .demoApp + } + init() { cancellables.append( invidious.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } @@ -79,6 +91,8 @@ final class AccountsModel: ObservableObject { invidious.setAccount(account) case .piped: piped.setAccount(account) + case .demoApp: + break } Defaults[.lastAccountIsPublic] = account.isPublic diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 4275f134..2b5be30d 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -26,6 +26,8 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { return InvidiousAPI(account: anonymousAccount) case .piped: return PipedAPI(account: anonymousAccount) + case .demoApp: + return DemoAppAPI() } } @@ -34,7 +36,9 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { } var longDescription: String { - name.isEmpty ? "\(app.name) - \(apiURL)" : "\(app.name) - \(name) (\(apiURL))" + guard app != .demoApp else { return "Demo" } + + return name.isEmpty ? "\(app.name) - \(apiURL)" : "\(app.name) - \(name) (\(apiURL))" } var shortDescription: String { diff --git a/Model/Applications/DemoAppAPI.swift b/Model/Applications/DemoAppAPI.swift new file mode 100644 index 00000000..b6547b52 --- /dev/null +++ b/Model/Applications/DemoAppAPI.swift @@ -0,0 +1,395 @@ +import AVFoundation +import Foundation +import Siesta +import SwiftyJSON + +final class DemoAppAPI: Service, ObservableObject, VideosAPI { + static var url = "https://r.yattee.stream/demo" + + var account: Account! { + .init( + id: UUID().uuidString, + app: .demoApp, + name: "Demo", + url: Self.url, + anonymous: true + ) + } + + var signedIn: Bool { + true + } + + init() { + super.init() + + configure() + } + + func configure() { + configure { + $0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) + } + + configureTransformer(pathPattern("channels/*"), requestMethods: [.get]) { (content: Entity) -> Channel? in + self.extractChannel(from: content.json) + } + + configureTransformer(pathPattern("search*"), requestMethods: [.get]) { (content: Entity) -> SearchPage in + let nextPage = content.json.dictionaryValue["nextpage"]?.string + return SearchPage( + results: self.extractContentItems(from: content.json.dictionaryValue["items"]!), + nextPage: nextPage, + last: nextPage == "null" + ) + } + + configureTransformer(pathPattern("suggestions*")) { (content: Entity) -> [String] in + content.json.arrayValue.map(String.init) + } + + configureTransformer(pathPattern("videos/*")) { (content: Entity) -> Video? in + self.extractVideo(from: content.json) + } + + configureTransformer(pathPattern("trending*")) { (content: Entity) -> [Video] in + self.extractVideos(from: content.json) + } + } + + func channel(_ channel: String) -> Resource { + resource(baseURL: Self.url, path: "/channels/\(channel).json") + } + + func channelByName(_: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func channelByUsername(_: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func channelVideos(_ id: String) -> Resource { + resource(baseURL: Self.url, path: "/channels/\(id).json") + } + + func trending(country _: Country, category _: TrendingCategory?) -> Resource { + resource(baseURL: Self.url, path: "/trending.json") + } + + func search(_ query: SearchQuery, page: String?) -> Resource { + resource(baseURL: Self.url, path: "/search.json") + .withParam("q", query.query) + .withParam("p", page) + } + + func searchSuggestions(query _: String) -> Resource { + resource(baseURL: Self.url, path: "/suggestions.json") + } + + func video(_ id: Video.ID) -> Resource { + resource(baseURL: Self.url, path: "/videos/\(id).json") + } + + var subscriptions: Resource? + + var feed: Resource? + + var home: Resource? + + var popular: Resource? + + var playlists: Resource? + + func subscribe(_: String, onCompletion _: @escaping () -> Void) {} + + func unsubscribe(_: String, onCompletion _: @escaping () -> Void) {} + + func playlist(_: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func playlistVideo(_: String, _: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func playlistVideos(_: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func addVideoToPlaylist(_: String, _: String, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} + + func removeVideoFromPlaylist(_: String, _: String, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} + + func playlistForm(_: String, _: String, playlist _: Playlist?, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping (Playlist?) -> Void) {} + + func deletePlaylist(_: Playlist, onFailure _: @escaping (RequestError) -> Void, onSuccess _: @escaping () -> Void) {} + + func channelPlaylist(_: String) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + func comments(_: Video.ID, page _: String?) -> Resource? { + resource(baseURL: Self.url, path: "") + } + + private func pathPattern(_ path: String) -> String { + "**\(Self.url)/\(path)" + } + + private func extractChannel(from content: JSON) -> Channel? { + let attributes = content.dictionaryValue + guard let id = attributes["id"]?.string ?? + (attributes["url"] ?? attributes["uploaderUrl"])?.string?.components(separatedBy: "/").last + else { + return nil + } + + let subscriptionsCount = attributes["subscriberCount"]?.int ?? attributes["subscribers"]?.int + + var videos = [Video]() + if let relatedStreams = attributes["relatedStreams"] { + videos = extractVideos(from: relatedStreams) + } + + let name = attributes["name"]?.string ?? + attributes["uploaderName"]?.string ?? + attributes["uploader"]?.string ?? "" + + let thumbnailURL = attributes["avatarUrl"]?.url ?? + attributes["uploaderAvatar"]?.url ?? + attributes["avatar"]?.url ?? + attributes["thumbnail"]?.url + + return Channel( + id: id, + name: name, + thumbnailURL: thumbnailURL, + subscriptionsCount: subscriptionsCount, + videos: videos + ) + } + + private func extractVideos(from content: JSON) -> [Video] { + content.arrayValue.compactMap(extractVideo(from:)) + } + + private func extractVideo(from content: JSON) -> Video? { + let details = content.dictionaryValue + + if let url = details["url"]?.string { + guard url.contains("/watch") else { + return nil + } + } + + let channelId = details["uploaderUrl"]?.string?.components(separatedBy: "/").last ?? "unknown" + + let thumbnails: [Thumbnail] = Thumbnail.Quality.allCases.compactMap { + if let url = buildThumbnailURL(from: content, quality: $0) { + return Thumbnail(url: url, quality: $0) + } + + return nil + } + + let author = details["uploaderName"]?.string ?? details["uploader"]?.string ?? "" + let authorThumbnailURL = details["avatarUrl"]?.url ?? details["uploaderAvatar"]?.url ?? details["avatar"]?.url + let subscriptionsCount = details["uploaderSubscriberCount"]?.int + + let uploaded = details["uploaded"]?.double + var published = (uploaded.isNil || uploaded == -1) ? nil : (uploaded! / 1000).formattedAsRelativeTime() + if published.isNil { + published = (details["uploadedDate"] ?? details["uploadDate"])?.string ?? "" + } + + let live = details["livestream"]?.bool ?? (details["duration"]?.int == -1) + + let description = extractDescription(from: content) ?? "" + + return Video( + videoID: extractID(from: content), + title: details["title"]?.string ?? "", + author: author, + length: details["duration"]?.double ?? 0, + published: published ?? "", + views: details["views"]?.int ?? 0, + description: description, + channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount), + thumbnails: thumbnails, + live: live, + likes: details["likes"]?.int, + dislikes: details["dislikes"]?.int, + streams: extractStreams(from: content), + related: extractRelated(from: content) + ) + } + + private func buildThumbnailURL(from content: JSON, quality: Thumbnail.Quality) -> URL? { + guard let thumbnailURL = extractThumbnailURL(from: content) else { + return nil + } + + return URL(string: thumbnailURL + .absoluteString + .replacingOccurrences(of: "hqdefault", with: quality.filename) + .replacingOccurrences(of: "maxresdefault", with: quality.filename) + ) + } + + private func extractID(from content: JSON) -> Video.ID { + content.dictionaryValue["url"]?.string?.components(separatedBy: "?v=").last ?? + extractThumbnailURL(from: content)?.relativeString.components(separatedBy: "/")[5].replacingFirstOccurrence(of: ".png", with: "") ?? "" + } + + private func extractDescription(from content: JSON) -> String? { + guard var description = content.dictionaryValue["description"]?.string else { + return nil + } + + description = description.replacingOccurrences( + of: "
|
|
", + with: "\n", + options: .regularExpression, + range: nil + ) + + let linkRegex = #"(]*?\s+)?href=\"[^"]*\">[^<]*<\/a>)"# + let hrefRegex = #"href=\"([^"]*)\">"# + guard let hrefRegex = try? NSRegularExpression(pattern: hrefRegex) else { return description } + + description = description.replacingMatches(regex: linkRegex) { matchingGroup in + let results = hrefRegex.matches(in: matchingGroup, range: NSRange(matchingGroup.startIndex..., in: matchingGroup)) + + if let result = results.first { + if let swiftRange = Range(result.range(at: 1), in: matchingGroup) { + return String(matchingGroup[swiftRange]) + } + } + + return matchingGroup + } + + description = description.replacingOccurrences(of: "&", with: "&") + + description = description.replacingOccurrences( + of: "<[^>]+>", + with: "", + options: .regularExpression, + range: nil + ) + + return description + } + + private func extractStreams(from content: JSON) -> [Stream] { + var streams = [Stream]() + + if let hlsURL = content.dictionaryValue["hls"]?.url { + streams.append(Stream(instance: account.instance, hlsURL: hlsURL)) + } + + let audioStreams = content + .dictionaryValue["audioStreams"]? + .arrayValue + .filter { $0.dictionaryValue["format"]?.string == "M4A" } + .sorted { + $0.dictionaryValue["bitrate"]?.int ?? 0 > + $1.dictionaryValue["bitrate"]?.int ?? 0 + } ?? [] + + guard let audioStream = audioStreams.first else { + return streams + } + + let videoStreams = content.dictionaryValue["videoStreams"]?.arrayValue ?? [] + + videoStreams.forEach { videoStream in + let videoCodec = videoStream.dictionaryValue["codec"]?.string ?? "" + + guard let audioAssetUrl = audioStream.dictionaryValue["url"]?.url, + let videoAssetUrl = videoStream.dictionaryValue["url"]?.url + else { + return + } + + let audioAsset = AVURLAsset(url: audioAssetUrl) + let videoAsset = AVURLAsset(url: videoAssetUrl) + + let videoOnly = videoStream.dictionaryValue["videoOnly"]?.bool ?? true + let quality = videoStream.dictionaryValue["quality"]?.string ?? "unknown" + let qualityComponents = quality.components(separatedBy: "p") + let fps = qualityComponents.count > 1 ? Int(qualityComponents[1]) : 30 + let resolution = Stream.Resolution.from(resolution: quality, fps: fps) + let videoFormat = videoStream.dictionaryValue["format"]?.string + + if videoOnly { + streams.append( + Stream( + instance: account.instance, + audioAsset: audioAsset, + videoAsset: videoAsset, + resolution: resolution, + kind: .adaptive, + videoFormat: videoFormat + ) + ) + } else { + streams.append( + SingleAssetStream( + instance: account.instance, + avAsset: videoAsset, + resolution: resolution, + kind: .stream + ) + ) + } + } + + return streams + } + + private func extractRelated(from content: JSON) -> [Video] { + content + .dictionaryValue["relatedStreams"]? + .arrayValue + .compactMap(extractVideo(from:)) ?? [] + } + + private func extractThumbnailURL(from content: JSON) -> URL? { + content.dictionaryValue["thumbnail"]?.url ?? content.dictionaryValue["thumbnailUrl"]?.url + } + + private func extractContentItem(from content: JSON) -> ContentItem? { + let details = content.dictionaryValue + + let contentType: ContentItem.ContentType + + if let url = details["url"]?.string { + if url.contains("/playlist") { + contentType = .playlist + } else if url.contains("/channel") { + contentType = .channel + } else { + contentType = .video + } + } else { + contentType = .video + } + + switch contentType { + case .video: + if let video = extractVideo(from: content) { + return ContentItem(video: video) + } + default: + return nil + } + + return nil + } + + private func extractContentItems(from content: JSON) -> [ContentItem] { + content.arrayValue.compactMap { extractContentItem(from: $0) } + } +} diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 47e5ce53..0c6d7b75 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -2,6 +2,7 @@ import Foundation enum VideosApp: String, CaseIterable { case invidious, piped + case demoApp var name: String { rawValue.capitalized diff --git a/Model/InstancesManifest.swift b/Model/InstancesManifest.swift index 1776e99c..45f0bd85 100644 --- a/Model/InstancesManifest.swift +++ b/Model/InstancesManifest.swift @@ -4,7 +4,6 @@ import Siesta import SwiftyJSON final class InstancesManifest: Service, ObservableObject { - static let builtinManifestUrl = "https://r.yattee.stream/manifest.json" static let shared = InstancesManifest() @Published var instances = [ManifestedInstance]() @@ -12,18 +11,26 @@ final class InstancesManifest: Service, ObservableObject { init() { super.init() + configure() + } + + func configure() { + invalidateConfiguration() + configure { $0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) } - configureTransformer( - manifestURL, - requestMethods: [.get] - ) { (content: Entity - ) -> [ManifestedInstance] in - guard let instances = content.json.dictionaryValue["instances"] else { return [] } + if let manifestURL { + configureTransformer( + manifestURL, + requestMethods: [.get] + ) { (content: Entity + ) -> [ManifestedInstance] in + guard let instances = content.json.dictionaryValue["instances"] else { return [] } - return instances.arrayValue.compactMap(self.extractInstance) + return instances.arrayValue.compactMap(self.extractInstance) + } } } @@ -36,7 +43,7 @@ final class InstancesManifest: Service, ObservableObject { return } - instancesList.load().onSuccess { response in + instancesList?.load().onSuccess { response in if let instances: [ManifestedInstance] = response.typedContent() { guard let instance = instances.filter { $0.country == country }.randomElement() else { return } let account = instance.anonymousAccount @@ -49,7 +56,7 @@ final class InstancesManifest: Service, ObservableObject { } func changePublicAccount(_ accounts: AccountsModel, settings: SettingsModel) { - instancesList.load().onSuccess { response in + instancesList?.load().onSuccess { response in if let instances: [ManifestedInstance] = response.typedContent() { var countryInstances = instances.filter { $0.country == Defaults[.countryOfPublicInstances] } let region = countryInstances.first?.region ?? "Europe" @@ -97,17 +104,12 @@ final class InstancesManifest: Service, ObservableObject { ) } - var manifestURL: String { - var url = Defaults[.instancesManifest] - - if url.isEmpty { - url = Self.builtinManifestUrl - } - - return url + var manifestURL: String? { + Defaults[.instancesManifest].isEmpty ? nil : Defaults[.instancesManifest] } - var instancesList: Resource { - resource(absoluteURL: manifestURL) + var instancesList: Resource? { + guard let manifestURL else { return nil } + return resource(absoluteURL: manifestURL) } } diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index db612ce5..8f7baa56 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -146,7 +146,7 @@ final class PlayerModel: ObservableObject { var playerError: Error? { didSet { if let error = playerError { - navigation.presentAlert(title: "Failed loading video", message: error.localizedDescription) + navigation.presentAlert(title: "Failed loading video".localized(), message: error.localizedDescription) } }} diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 5de153de..b72cd63c 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -9,7 +9,7 @@ extension Defaults.Keys { static let instancesManifest = Key("instancesManifest", default: "") static let countryOfPublicInstances = Key("countryOfPublicInstances") - static let instances = Key<[Instance]>("instances", default: []) + static let instances = Key<[Instance]>("instances", default: [.init(app: .demoApp, name: "Demo", apiURL: "")]) static let accounts = Key<[Account]>("accounts", default: []) static let lastAccountID = Key("lastAccountID") static let lastInstanceID = Key("lastInstanceID") @@ -21,9 +21,7 @@ extension Defaults.Keys { static let enableReturnYouTubeDislike = Key("enableReturnYouTubeDislike", default: false) - static let favorites = Key<[FavoriteItem]>("favorites", default: [ - .init(section: .channel("UCE_M8A5yxnLfW0KghEeajjw", "Apple")) - ]) + static let favorites = Key<[FavoriteItem]>("favorites", default: []) #if !os(tvOS) #if os(macOS) diff --git a/Shared/Favorites/FavoriteItemView.swift b/Shared/Favorites/FavoriteItemView.swift index 4846f444..5ed686a6 100644 --- a/Shared/Favorites/FavoriteItemView.swift +++ b/Shared/Favorites/FavoriteItemView.swift @@ -54,7 +54,7 @@ struct FavoriteItemView: View { #endif .onAppear { resource?.addObserver(store) - resource?.load() + resource?.loadIfNeeded() } #if !os(tvOS) .onDrag { diff --git a/Shared/Settings/AccountsNavigationLink.swift b/Shared/Settings/AccountsNavigationLink.swift index 9290f284..bc3a68ad 100644 --- a/Shared/Settings/AccountsNavigationLink.swift +++ b/Shared/Settings/AccountsNavigationLink.swift @@ -8,6 +8,7 @@ struct AccountsNavigationLink: View { NavigationLink(instance.longDescription) { InstanceSettings(instance: instance) } + .disabled(instance.app == .demoApp) .buttonStyle(.plain) .contextMenu { removeInstanceButton(instance) diff --git a/Shared/Settings/LocationsSettings.swift b/Shared/Settings/LocationsSettings.swift index c7633f13..3e71abc0 100644 --- a/Shared/Settings/LocationsSettings.swift +++ b/Shared/Settings/LocationsSettings.swift @@ -41,28 +41,30 @@ struct LocationsSettings: View { } @ViewBuilder var settings: some View { - Section(header: SettingsHeader(text: "Public Locations".localized()), footer: countryFooter) { - Picker("Country", selection: $countryOfPublicInstances) { - Text("Don't use public locations").tag(String?.none) - ForEach(countries, id: \.self) { country in - Text(country).tag(Optional(country)) + if !InstancesManifest.shared.manifestURL.isNil { + Section(header: SettingsHeader(text: "Public Locations".localized()), footer: countryFooter) { + Picker("Country", selection: $countryOfPublicInstances) { + Text("Don't use public locations").tag(String?.none) + ForEach(countries, id: \.self) { country in + Text(country).tag(Optional(country)) + } } - } - #if os(tvOS) - .pickerStyle(.inline) - #endif - .disabled(countries.isEmpty) + #if os(tvOS) + .pickerStyle(.inline) + #endif + .disabled(countries.isEmpty) - Button { - InstancesManifest.shared.changePublicAccount(accounts, settings: model) - } label: { - if let account = accounts.current, account.isPublic { - Text("Switch to other public location") - } else { - Text("Switch to public locations") + Button { + InstancesManifest.shared.changePublicAccount(accounts, settings: model) + } label: { + if let account = accounts.current, account.isPublic { + Text("Switch to other public location") + } else { + Text("Switch to public locations") + } } + .disabled(countryOfPublicInstances.isNil) } - .disabled(countryOfPublicInstances.isNil) } Section(header: SettingsHeader(text: "Custom Locations".localized())) { @@ -92,7 +94,8 @@ struct LocationsSettings: View { } func loadCountries() { - InstancesManifest.shared.instancesList.load() + InstancesManifest.shared.configure() + InstancesManifest.shared.instancesList?.load() .onSuccess { response in if let instances: [ManifestedInstance] = response.typedContent() { self.countries = instances.map(\.country).unique().sorted() diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 563159ac..1d0c0ea0 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -31,6 +31,7 @@ struct PlayerSettings: View { @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike @Default(.systemControlsCommands) private var systemControlsCommands + @EnvironmentObject private var accounts @EnvironmentObject private var player #if os(iOS) @@ -99,7 +100,10 @@ struct PlayerSettings: View { keywordsToggle showHistoryToggle - returnYouTubeDislikeToggle + + if !accounts.isDemo { + returnYouTubeDislikeToggle + } } #if os(iOS) diff --git a/Shared/Views/ShareButton.swift b/Shared/Views/ShareButton.swift index 9f0884a6..64001549 100644 --- a/Shared/Views/ShareButton.swift +++ b/Shared/Views/ShareButton.swift @@ -15,7 +15,9 @@ struct ShareButton: View { Menu { instanceActions Divider() - youtubeActions + if !accounts.isDemo { + youtubeActions + } } label: { Label("Share...", systemImage: "square.and.arrow.up") } diff --git a/Shared/Views/WelcomeScreen.swift b/Shared/Views/WelcomeScreen.swift index 4109f454..df6adadd 100644 --- a/Shared/Views/WelcomeScreen.swift +++ b/Shared/Views/WelcomeScreen.swift @@ -67,7 +67,7 @@ struct WelcomeScreen: View { .foregroundColor(.secondary) } .onAppear { - resource.load().onSuccess { response in + resource?.load().onSuccess { response in if let instances: [ManifestedInstance] = response.typedContent() { store = instances } @@ -85,7 +85,7 @@ struct WelcomeScreen: View { store.first { $0.country == country }?.flag } - var resource: Resource { + var resource: Resource? { InstancesManifest.shared.instancesList } } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 8233fac1..abda00ee 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -174,13 +174,8 @@ struct YatteeApp: App { accounts.configureAccount() } - let countryOfPublicInstances = Defaults[.countryOfPublicInstances] - if accounts.current.isNil, countryOfPublicInstances.isNil { - navigation.presentingWelcomeScreen = true - } - - if !countryOfPublicInstances.isNil { - InstancesManifest.shared.setPublicAccount(countryOfPublicInstances!, accounts: accounts, asCurrent: accounts.current.isNil) + if let countryOfPublicInstances = Defaults[.countryOfPublicInstances] { + InstancesManifest.shared.setPublicAccount(countryOfPublicInstances, accounts: accounts, asCurrent: accounts.current.isNil) } playlists.accounts = accounts diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 87ecd12d..8f22c66d 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -434,6 +434,9 @@ 376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376E331228AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */; }; + 3771429829087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; + 3771429929087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; + 3771429A29087DFC00306CEA /* DemoAppAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771429729087DFC00306CEA /* DemoAppAPI.swift */; }; 3772003827E8EEB100CB2475 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003227E8EEA100CB2475 /* AudioToolbox.framework */; }; 3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003427E8EEA100CB2475 /* AVFoundation.framework */; }; 3772003A27E8EEBE00CB2475 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3772003527E8EEA100CB2475 /* CoreMedia.framework */; }; @@ -1151,6 +1154,7 @@ 376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollDismissesKeyboard+Backport.swift"; sourceTree = ""; }; 3771429529087BE100306CEA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 3771429629087BF000306CEA /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; + 3771429729087DFC00306CEA /* DemoAppAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppAPI.swift; sourceTree = ""; }; 3772002527E8ED2600CB2475 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; 3772003127E8EEA100CB2475 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 3772003227E8EEA100CB2475 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.4.sdk/System/Library/Frameworks/AudioToolbox.framework; sourceTree = DEVELOPER_DIR; }; @@ -1739,6 +1743,7 @@ 3700155A271B0D4D0049C794 /* PipedAPI.swift */, 37D526DD2720AC4400ED2F5E /* VideosAPI.swift */, 376A33DF2720CAD6000C1D6B /* VideosApp.swift */, + 3771429729087DFC00306CEA /* DemoAppAPI.swift */, ); path = Applications; sourceTree = ""; @@ -2884,6 +2889,7 @@ 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, + 3771429829087DFC00306CEA /* DemoAppAPI.swift in Sources */, 37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */, @@ -3274,6 +3280,7 @@ 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 370015AA28BBAE7F000149FD /* ProgressBar.swift in Sources */, + 3771429929087DFC00306CEA /* DemoAppAPI.swift in Sources */, 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */, 3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, @@ -3418,6 +3425,7 @@ 3752069B285E8DD300CA655F /* Chapter.swift in Sources */, 37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */, 3743B86A27216D3600261544 /* ChannelCell.swift in Sources */, + 3771429A29087DFC00306CEA /* DemoAppAPI.swift in Sources */, 37E80F47287B7B9400561799 /* VideoDetailsOverlay.swift in Sources */, 37E80F44287B7AB400561799 /* VideoDetails.swift in Sources */, 3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,