diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sora/Assets.xcassets/AppIcon_Default.appiconset/Contents.json similarity index 100% rename from Sora/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Sora/Assets.xcassets/AppIcon_Default.appiconset/Contents.json diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png b/Sora/Assets.xcassets/AppIcon_Default.appiconset/darkmode.png similarity index 100% rename from Sora/Assets.xcassets/AppIcon.appiconset/darkmode.png rename to Sora/Assets.xcassets/AppIcon_Default.appiconset/darkmode.png diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png b/Sora/Assets.xcassets/AppIcon_Default.appiconset/lightmode.png similarity index 100% rename from Sora/Assets.xcassets/AppIcon.appiconset/lightmode.png rename to Sora/Assets.xcassets/AppIcon_Default.appiconset/lightmode.png diff --git a/Sora/Assets.xcassets/AppIcon.appiconset/tinting.png b/Sora/Assets.xcassets/AppIcon_Default.appiconset/tinting.png similarity index 100% rename from Sora/Assets.xcassets/AppIcon.appiconset/tinting.png rename to Sora/Assets.xcassets/AppIcon_Default.appiconset/tinting.png diff --git a/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/Contents.json b/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/Contents.json new file mode 100644 index 0000000..b85e601 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "preview.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/preview.png b/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/preview.png new file mode 100644 index 0000000..2f2be91 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/preview.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Original.appiconset/Contents.json b/Sora/Assets.xcassets/AppIcon_Original.appiconset/Contents.json new file mode 100644 index 0000000..adc0a20 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Original.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "original.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Original.appiconset/original.png b/Sora/Assets.xcassets/AppIcon_Original.appiconset/original.png new file mode 100644 index 0000000..c45e62a Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Original.appiconset/original.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/Contents.json b/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/Contents.json new file mode 100644 index 0000000..b85e601 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "preview.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/preview.png b/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/preview.png new file mode 100644 index 0000000..460e487 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/preview.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/Contents.json b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/Contents.json new file mode 100644 index 0000000..c7a15f9 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "lightmode.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "darkmode.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "tinting.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/darkmode.png b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/darkmode.png new file mode 100755 index 0000000..a84929a Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/darkmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/lightmode.png b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/lightmode.png new file mode 100755 index 0000000..7df252d Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/lightmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/tinting.png b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/tinting.png new file mode 100755 index 0000000..0adc458 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pixel.appiconset/tinting.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/Contents.json b/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/Contents.json new file mode 100644 index 0000000..b85e601 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "preview.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/preview.png b/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/preview.png new file mode 100644 index 0000000..1a5a0c8 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/preview.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pride.appiconset/Contents.json b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/Contents.json new file mode 100644 index 0000000..c7a15f9 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "lightmode.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "darkmode.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "tinting.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Pride.appiconset/darkmode.png b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/darkmode.png new file mode 100755 index 0000000..106c655 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/darkmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pride.appiconset/lightmode.png b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/lightmode.png new file mode 100755 index 0000000..451a2e8 Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/lightmode.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pride.appiconset/tinting.png b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/tinting.png new file mode 100755 index 0000000..d911fdb Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pride.appiconset/tinting.png differ diff --git a/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/Contents.json b/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/Contents.json new file mode 100644 index 0000000..b85e601 --- /dev/null +++ b/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "preview.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/preview.png b/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/preview.png new file mode 100644 index 0000000..7a6110b Binary files /dev/null and b/Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/preview.png differ diff --git a/Sora/Info.plist b/Sora/Info.plist index cf823a6..df1d517 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -2,8 +2,6 @@ - NSCameraUsageDescription - Sora may requires access to your device's camera. BGTaskSchedulerPermittedIdentifiers $(PRODUCT_BUNDLE_IDENTIFIER) @@ -38,7 +36,5 @@ audio processing - UISupportsDocumentBrowser - diff --git a/Sora/Sora.entitlements b/Sora/Sora.entitlements index d90dbc3..da83a05 100644 --- a/Sora/Sora.entitlements +++ b/Sora/Sora.entitlements @@ -4,7 +4,7 @@ com.apple.developer.icloud-container-identifiers - iCloud.me.cranci.sora.icloud + iCloud.de.devsforge.sulfur.fork com.apple.developer.icloud-services @@ -12,7 +12,7 @@ com.apple.developer.ubiquity-container-identifiers - iCloud.me.cranci.sora.icloud + iCloud.de.devsforge.sulfur.fork com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 241f442..4fc513d 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -11,11 +11,11 @@ import SwiftUI struct SoraApp: App { @StateObject private var settings = Settings() @StateObject private var moduleManager = ModuleManager() - @StateObject private var librarykManager = LibraryManager() - + @StateObject private var profileStore = ProfileStore() + @StateObject private var libraryManager = LibraryManager() + @StateObject private var continueWatchingManager = ContinueWatchingManager() + init() { - _ = iCloudSyncManager.shared - TraktToken.checkAuthenticationStatus { isAuthenticated in if isAuthenticated { Logger.shared.log("Trakt authentication is valid") @@ -27,12 +27,21 @@ struct SoraApp: App { var body: some Scene { WindowGroup { - ContentView() + RootView() .environmentObject(moduleManager) .environmentObject(settings) - .environmentObject(librarykManager) + .environmentObject(libraryManager) + .environmentObject(continueWatchingManager) + .environmentObject(profileStore) .accentColor(settings.accentColor) .onAppear { + // pass initial profile value to other manager + let suite = self.profileStore.getUserDefaultsSuite() + self.libraryManager.userDefaultsSuite = suite + self.continueWatchingManager.userDefaultsSuite = suite + + _ = iCloudSyncManager.shared + settings.updateAppearance() iCloudSyncManager.shared.syncModulesFromiCloud() Task { @@ -48,6 +57,12 @@ struct SoraApp: App { handleURL(url) } } + .onChange(of: profileStore.currentProfile) { _ in + // pass changed suite value to other manager + let suite = self.profileStore.getUserDefaultsSuite() + libraryManager.updateProfileSuite(suite) + continueWatchingManager.updateProfileSuite(suite) + } } } diff --git a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift index 99a7adb..4b66a3d 100644 --- a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift +++ b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift @@ -5,16 +5,22 @@ // Created by Francesco on 14/02/25. // -import Foundation +import SwiftUI -class ContinueWatchingManager { - static let shared = ContinueWatchingManager() +class ContinueWatchingManager: ObservableObject { + var userDefaultsSuite = UserDefaults.standard + @Published var items: [ContinueWatchingItem] = [] private let storageKey = "continueWatchingItems" - - private init() { + + init() { NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil) } - + + public func updateProfileSuite(_ newSuite: UserDefaults) { + userDefaultsSuite = newSuite + loadItems() + } + @objc private func handleiCloudSync() { NotificationCenter.default.post(name: .ContinueWatchingDidUpdate, object: nil) } @@ -24,31 +30,33 @@ class ContinueWatchingManager { remove(item: item) return } - - var items = fetchItems() - if let index = items.firstIndex(where: { $0.streamUrl == item.streamUrl && $0.episodeNumber == item.episodeNumber }) { + + if let index = items.firstIndex(where: { + $0.streamUrl == item.streamUrl && + $0.episodeNumber == item.episodeNumber }) { items[index] = item } else { items.append(item) } + if let data = try? JSONEncoder().encode(items) { - UserDefaults.standard.set(data, forKey: storageKey) + userDefaultsSuite.set(data, forKey: storageKey) } } - func fetchItems() -> [ContinueWatchingItem] { - if let data = UserDefaults.standard.data(forKey: storageKey), - let items = try? JSONDecoder().decode([ContinueWatchingItem].self, from: data) { - return items + func loadItems() { + if let data = userDefaultsSuite.data(forKey: storageKey), + let parsedItems = try? JSONDecoder().decode([ContinueWatchingItem].self, from: data) { + items = parsedItems + } else { + items = [] } - return [] } func remove(item: ContinueWatchingItem) { - var items = fetchItems() items.removeAll { $0.id == item.id } if let data = try? JSONEncoder().encode(items) { - UserDefaults.standard.set(data, forKey: storageKey) + userDefaultsSuite.set(data, forKey: storageKey) } } } diff --git a/Sora/Utils/Extensions/UIKit.swift b/Sora/Utils/Extensions/UIKit.swift new file mode 100644 index 0000000..e54169d --- /dev/null +++ b/Sora/Utils/Extensions/UIKit.swift @@ -0,0 +1,21 @@ +// +// UIApplication.swift +// Sulfur +// +// Created by Dominic on 21.04.25. +// + +import UIKit + +extension UIApplication { + + func dismissKeyboard(_ force: Bool) { + if #unavailable(iOS 15) { + windows.first?.endEditing(force) + } else { + guard let windowScene = connectedScenes.first as? UIWindowScene else { return } + windowScene.windows.first?.endEditing(force) + } + } + +} diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index efa4fcb..b19ee75 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -63,6 +63,7 @@ extension URLSession { configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent] return URLSession(configuration: configuration) }() + // return url session that redirects based on input static func fetchData(allowRedirects:Bool) -> URLSession { diff --git a/Sora/Utils/Extensions/View.swift b/Sora/Utils/Extensions/View.swift index 7c9d375..46db1cb 100644 --- a/Sora/Utils/Extensions/View.swift +++ b/Sora/Utils/Extensions/View.swift @@ -9,6 +9,6 @@ import SwiftUI extension View { func shimmering() -> some View { - self.modifier(Shimmer()) + self.modifier(ShimmeringEffect()) } } diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/Utils/MediaPlayer/VideoPlayer.swift index 122c466..ebd2e6c 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/Utils/MediaPlayer/VideoPlayer.swift @@ -10,7 +10,8 @@ import AVKit class VideoPlayerViewController: UIViewController { let module: ScrapingModule - + let continueWatchingManager: ContinueWatchingManager + var player: AVPlayer? var playerViewController: NormalPlayer? var timeObserverToken: Any? @@ -23,8 +24,9 @@ class VideoPlayerViewController: UIViewController { var episodeImageUrl: String = "" var mediaTitle: String = "" - init(module: ScrapingModule) { + init(module: ScrapingModule, continueWatchingManager: ContinueWatchingManager) { self.module = module + self.continueWatchingManager = continueWatchingManager super.init(nibName: nil, bundle: nil) } @@ -42,7 +44,7 @@ class VideoPlayerViewController: UIViewController { var request = URLRequest(url: url) request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer") request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin") - request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") + request.addValue(URLSession.randomUserAgent, forHTTPHeaderField: "User-Agent") let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]]) let playerItem = AVPlayerItem(asset: asset) @@ -129,7 +131,7 @@ class VideoPlayerViewController: UIViewController { aniListID: self.aniListID, module: self.module ) - ContinueWatchingManager.shared.save(item: item) + continueWatchingManager.save(item: item) } let remainingPercentage = (duration - currentTime) / duration diff --git a/Sora/Utils/ProfileStore/Profile.swift b/Sora/Utils/ProfileStore/Profile.swift new file mode 100644 index 0000000..4043b48 --- /dev/null +++ b/Sora/Utils/ProfileStore/Profile.swift @@ -0,0 +1,14 @@ +// +// Profile.swift +// Sulfur +// +// Created by Dominic on 21.04.25. +// + +import Foundation + +struct Profile: Identifiable, Equatable, Codable { + var id: UUID = UUID() + var name: String + var emoji: String +} diff --git a/Sora/Utils/ProfileStore/ProfileButtonStyle.swift b/Sora/Utils/ProfileStore/ProfileButtonStyle.swift new file mode 100644 index 0000000..3cfb216 --- /dev/null +++ b/Sora/Utils/ProfileStore/ProfileButtonStyle.swift @@ -0,0 +1,14 @@ +// +// ProfileButtonStyle.swift +// Sulfur +// +// Created by Dominic on 21.04.25. +// + +import SwiftUI + +struct ProfileButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} diff --git a/Sora/Utils/ProfileStore/ProfileStore.swift b/Sora/Utils/ProfileStore/ProfileStore.swift new file mode 100644 index 0000000..af19f63 --- /dev/null +++ b/Sora/Utils/ProfileStore/ProfileStore.swift @@ -0,0 +1,96 @@ +// +// ProfileStore.swift +// Sulfur +// +// Created by Dominic on 21.04.25. +// + +import SwiftUI + +class ProfileStore: ObservableObject { + @AppStorage("profilesData") private var profilesData: Data = Data() + @AppStorage("currentProfileID") private var currentProfileID: String = "" + + @Published public var profiles: [Profile] = [] + @Published public var currentProfile: Profile! + + public init() { + profiles = (try? JSONDecoder().decode([Profile].self, from: profilesData)) ?? [] + + if profiles.isEmpty { + + // load default value + let defaultProfile = Profile(name: "Default User", emoji: "๐Ÿ‘ค") + profiles = [defaultProfile] + + saveProfiles() + setCurrentProfile(defaultProfile) + } else { + + // load current profile + if let uuid = UUID(uuidString: currentProfileID), + let match = profiles.first(where: { $0.id == uuid }) { + currentProfile = match + } else if let firstProfile = profiles.first { + currentProfile = firstProfile + } else { + fatalError("profiles Array is not empty, but no profile was found") + } + } + } + + public func getUserDefaultsSuite() -> UserDefaults { + guard let suite = UserDefaults(suiteName: currentProfile.id.uuidString) else { + fatalError("This can only fail if suiteName == app bundle id ...") + } + + Logger.shared.log("loaded UserDefaults suite for \(currentProfile.name) (\(currentProfile.id.uuidString))", type: "Profile") + + return suite + } + + private func saveProfiles() { + profilesData = (try? JSONEncoder().encode(profiles)) ?? Data() + } + + public func setCurrentProfile(_ profile: Profile) { + currentProfile = profile + currentProfileID = profile.id.uuidString + } + + public func addProfile(name: String, emoji: String) { + let newProfile = Profile(name: name, emoji: emoji) + profiles.append(newProfile) + + saveProfiles() + setCurrentProfile(newProfile) + } + + public func editCurrentProfile(name: String, emoji: String) { + guard let index = profiles.firstIndex(where: { $0.id == currentProfile.id }) else { return } + profiles[index].name = name + profiles[index].emoji = emoji + + saveProfiles() + setCurrentProfile(profiles[index]) + } + + public func deleteCurrentProfile() { + if (profiles.count == 1) { return } + + if let suite = UserDefaults(suiteName: currentProfile.id.uuidString) { + for key in suite.dictionaryRepresentation().keys { + suite.removeObject(forKey: key) + } + } + + profiles.removeAll { $0.id == currentProfile.id } + + if let firstProfile = profiles.first { + saveProfiles() + setCurrentProfile(firstProfile) + } else { + fatalError("There should still be one Profile left") + } + } +} diff --git a/Sora/Utils/SkeletonCells/Shimmer.swift b/Sora/Utils/SkeletonCells/Shimmer.swift index e7a2984..7d506ab 100644 --- a/Sora/Utils/SkeletonCells/Shimmer.swift +++ b/Sora/Utils/SkeletonCells/Shimmer.swift @@ -7,9 +7,29 @@ import SwiftUI -struct Shimmer: ViewModifier { +enum ShimmerType: String, CaseIterable, Identifiable { + case shimmer, pulse, none + var id: String { self.rawValue } +} + +struct ShimmeringEffect: ViewModifier { + @EnvironmentObject var settings: Settings + + func body(content: Content) -> some View { + switch settings.shimmerType { + case .pulse: + return AnyView(content.modifier(ShimmerPulse())) + case .shimmer: + return AnyView(content.modifier(ShimmerDefault())) + default: + return AnyView(content.modifier(ShimmerNone())) + } + } +} + +struct ShimmerDefault: ViewModifier { @State private var phase: CGFloat = 0 - + func body(content: Content) -> some View { content .overlay( @@ -32,3 +52,41 @@ struct Shimmer: ViewModifier { } } } + +struct ShimmerPulse: ViewModifier { + @Environment(\.colorScheme) var colorScheme + @State private var opacity: Double = 0.3 + + func body(content: Content) -> some View { + content + .overlay( + (colorScheme == .light ? + Color.black.opacity(opacity) : + Color.white.opacity(opacity) + ) + .blendMode(.overlay) + ) + .mask(content) + .onAppear { + withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { + self.opacity = 0.8 + } + } + } +} + +struct ShimmerNone: ViewModifier { + @Environment(\.colorScheme) var colorScheme + + func body(content: Content) -> some View { + content + .overlay( + (colorScheme == .light ? + Color.black.opacity(0.3) : + Color.white.opacity(0.3) + ) + .blendMode(.overlay) + ) + .mask(content) + } +} diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index 5a7e768..21f08b5 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -7,6 +7,21 @@ import UIKit +// TODO: sync all profile user default suits +/* + profileStore.profiles.forEach { profile in + let suite = UserDefaults(suiteName: profile.id.uuidString) + ... + } + */ + +// TODO: add migration for legacy app users without profiles +/* + add all bookmarks and continue watching items to the first profile ?! + */ + +// TODO: update "clear data" feature +// TODO: tests class iCloudSyncManager { static let shared = iCloudSyncManager() @@ -30,8 +45,11 @@ class iCloudSyncManager { "analyticsEnabled", "refreshModulesOnLaunch", "fetchEpisodeMetadata", + "hideEmptySections", "multiThreads", - "metadataProviders" + "metadataProviders", + "profilesData", + "currentProfileID" ] private let modulesFileName = "modules.json" diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView/DownloadView.swift similarity index 100% rename from Sora/Views/DownloadView.swift rename to Sora/Views/DownloadView/DownloadView.swift diff --git a/Sora/Views/ExploreView/ExploreView.swift b/Sora/Views/ExploreView/ExploreView.swift new file mode 100644 index 0000000..147bdf6 --- /dev/null +++ b/Sora/Views/ExploreView/ExploreView.swift @@ -0,0 +1,196 @@ +// +// LibraryView.swift +// Sora +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher + +struct ExploreView: View { + @EnvironmentObject private var moduleManager: ModuleManager + @EnvironmentObject private var profileStore: ProfileStore + + @AppStorage("selectedModuleId") private var selectedModuleId: String? + @AppStorage("hideEmptySections") private var hideEmptySections: Bool? + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + + @Environment(\.verticalSizeClass) var verticalSizeClass + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape + @State private var showProfileSettings = false + + private var selectedModule: ScrapingModule? { + guard let id = selectedModuleId else { return nil } + return moduleManager.modules.first { $0.id.uuidString == id } + } + + private let columns = [ + GridItem(.adaptive(minimum: 150), spacing: 12) + ] + + private var columnsCount: Int { + if UIDevice.current.userInterfaceIdiom == .pad { + let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } + + private var cellWidth: CGFloat { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) } + .first + let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero + let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) + let availableWidth = safeWidth - totalSpacing + return availableWidth / CGFloat(columnsCount) + } + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + //TODO: add explore content views + } + .padding(.vertical, 20) + } + .navigationTitle("Explore") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Menu { + ForEach(profileStore.profiles) { profile in + Button { + profileStore.setCurrentProfile(profile) + } label: { + if profile == profileStore.currentProfile { + Label("\(profile.emoji) \(profile.name)", systemImage: "checkmark") + } else { + Text("\(profile.emoji) \(profile.name)") + } + } + } + + Divider() + + Button { + showProfileSettings = true + } label: { + Label("Edit Profiles", systemImage: "slider.horizontal.3") + } + + } label: { + Circle() + .fill(Color.secondary.opacity(0.3)) + .frame(width: 32, height: 32) + .overlay( + Text(profileStore.currentProfile.emoji) + .font(.system(size: 20)) + .foregroundStyle(.primary) + ) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Menu { + ForEach(getModuleLanguageGroups(), id: \.self) { language in + Menu(language) { + ForEach(getModulesForLanguage(language), id: \.id) { module in + Button { + selectedModuleId = module.id.uuidString + } label: { + HStack { + KFImage(URL(string: module.metadata.iconUrl)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + .cornerRadius(4) + Text(module.metadata.sourceName) + if module.id.uuidString == selectedModuleId { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + } + } + } label: { + HStack(spacing: 4) { + if let selectedModule = selectedModule { + Text(selectedModule.metadata.sourceName) + .font(.headline) + .foregroundColor(.secondary) + } else { + Text("Select Module") + .font(.headline) + .foregroundColor(.accentColor) + } + Image(systemName: "chevron.down") + .foregroundColor(.secondary) + } + } + .fixedSize() + } + } + + NavigationLink( + destination: SettingsViewProfile(), + isActive: $showProfileSettings, + label: { EmptyView() } + ) + .hidden() + } + .navigationViewStyle(StackNavigationViewStyle()) + .onAppear { + updateOrientation() + //TODO: fetch explore content + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } + } + + private func updateOrientation() { + DispatchQueue.main.async { + isLandscape = UIDevice.current.orientation.isLandscape + } + } + + private func cleanLanguageName(_ language: String?) -> String { + guard let language = language else { return "Unknown" } + + let cleaned = language.replacingOccurrences( + of: "\\s*\\([^\\)]*\\)", + with: "", + options: .regularExpression + ).trimmingCharacters(in: .whitespaces) + + return cleaned.isEmpty ? "Unknown" : cleaned + } + + private func getModulesByLanguage() -> [String: [ScrapingModule]] { + var result = [String: [ScrapingModule]]() + + for module in moduleManager.modules { + let language = cleanLanguageName(module.metadata.language) + if result[language] == nil { + result[language] = [module] + } else { + result[language]?.append(module) + } + } + + return result + } + + private func getModuleLanguageGroups() -> [String] { + return getModulesByLanguage().keys.sorted() + } + + private func getModulesForLanguage(_ language: String) -> [ScrapingModule] { + return getModulesByLanguage()[language] ?? [] + } +} diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index 6e8e3d3..6b37888 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -5,7 +5,7 @@ // Created by Francesco on 12/01/25. // -import Foundation +import SwiftUI struct LibraryItem: Codable, Identifiable { let id: UUID @@ -28,15 +28,19 @@ struct LibraryItem: Codable, Identifiable { } class LibraryManager: ObservableObject { + var userDefaultsSuite = UserDefaults.standard @Published var bookmarks: [LibraryItem] = [] private let bookmarksKey = "bookmarkedItems" init() { - loadBookmarks() - NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil) } - + + public func updateProfileSuite(_ newSuite: UserDefaults) { + userDefaultsSuite = newSuite + loadBookmarks() + } + @objc private func handleiCloudSync() { DispatchQueue.main.async { self.loadBookmarks() @@ -46,28 +50,31 @@ class LibraryManager: ObservableObject { func removeBookmark(item: LibraryItem) { if let index = bookmarks.firstIndex(where: { $0.id == item.id }) { bookmarks.remove(at: index) + Logger.shared.log("Removed series \(item.id) from bookmarks.",type: "Debug") saveBookmarks() } } private func loadBookmarks() { - guard let data = UserDefaults.standard.data(forKey: bookmarksKey) else { + guard let data = userDefaultsSuite.data(forKey: bookmarksKey) else { Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Debug") + bookmarks = [] return } - + do { bookmarks = try JSONDecoder().decode([LibraryItem].self, from: data) } catch { Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: "Error") + bookmarks = [] } } private func saveBookmarks() { do { let encoded = try JSONEncoder().encode(bookmarks) - UserDefaults.standard.set(encoded, forKey: bookmarksKey) + userDefaultsSuite.set(encoded, forKey: bookmarksKey) } catch { Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)", type: "Error") } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 99eb462..931ad35 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -10,16 +10,19 @@ import Kingfisher struct LibraryView: View { @EnvironmentObject private var libraryManager: LibraryManager + @EnvironmentObject private var continueWatchingManager: ContinueWatchingManager @EnvironmentObject private var moduleManager: ModuleManager - + @EnvironmentObject private var profileStore: ProfileStore + + @AppStorage("hideEmptySections") private var hideEmptySections: Bool? @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 @Environment(\.verticalSizeClass) var verticalSizeClass - - @State private var continueWatchingItems: [ContinueWatchingItem] = [] + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape - + @State private var showProfileSettings = false + private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) ] @@ -50,12 +53,15 @@ struct LibraryView: View { let columnsCount = determineColumns() VStack(alignment: .leading, spacing: 12) { - Text("Continue Watching") - .font(.title2) - .bold() - .padding(.horizontal, 20) - - if continueWatchingItems.isEmpty { + + if hideEmptySections != true || !continueWatchingManager.items.isEmpty { + Text("Continue Watching") + .font(.title2) + .bold() + .padding(.horizontal, 20) + } + + if !(hideEmptySections ?? false) && continueWatchingManager.items.isEmpty { VStack(spacing: 8) { Image(systemName: "play.circle") .font(.largeTitle) @@ -69,19 +75,21 @@ struct LibraryView: View { .padding() .frame(maxWidth: .infinity) } else { - ContinueWatchingSection(items: $continueWatchingItems, markAsWatched: { item in + ContinueWatchingSection(items: $continueWatchingManager.items, markAsWatched: { item in markContinueWatchingItemAsWatched(item: item) }, removeItem: { item in removeContinueWatchingItem(item: item) }) } - - Text("Bookmarks") - .font(.title2) - .bold() - .padding(.horizontal, 20) - - if libraryManager.bookmarks.isEmpty { + + if hideEmptySections != true || !libraryManager.bookmarks.isEmpty { + Text("Bookmarks") + .font(.title2) + .bold() + .padding(.horizontal, 20) + } + + if !(hideEmptySections ?? false) && libraryManager.bookmarks.isEmpty { VStack(spacing: 8) { Image(systemName: "magazine") .font(.largeTitle) @@ -141,26 +149,62 @@ struct LibraryView: View { } } .padding(.horizontal, 20) - .onAppear { - updateOrientation() - } - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - updateOrientation() - } } } .padding(.vertical, 20) + + NavigationLink( + destination: SettingsViewProfile(), + isActive: $showProfileSettings, + label: { EmptyView() } + ) + .hidden() } .navigationTitle("Library") - .onAppear { - fetchContinueWatching() + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Menu { + ForEach(profileStore.profiles) { profile in + Button { + profileStore.setCurrentProfile(profile) + } label: { + if profile == profileStore.currentProfile { + Label("\(profile.emoji) \(profile.name)", systemImage: "checkmark") + } else { + Text("\(profile.emoji) \(profile.name)") + } + } + } + + Divider() + + Button { + showProfileSettings = true + } label: { + Label("Edit Profiles", systemImage: "slider.horizontal.3") + } + + } label: { + Circle() + .fill(Color.secondary.opacity(0.3)) + .frame(width: 32, height: 32) + .overlay( + Text(profileStore.currentProfile.emoji) + .font(.system(size: 20)) + .foregroundStyle(.primary) + ) + } + } } } .navigationViewStyle(StackNavigationViewStyle()) - } - - private func fetchContinueWatching() { - continueWatchingItems = ContinueWatchingManager.shared.fetchItems() + .onAppear { + updateOrientation() + continueWatchingManager.loadItems() + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } } private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) { @@ -168,13 +212,11 @@ struct LibraryView: View { let totalKey = "totalTime_\(item.fullUrl)" UserDefaults.standard.set(99999999.0, forKey: key) UserDefaults.standard.set(99999999.0, forKey: totalKey) - ContinueWatchingManager.shared.remove(item: item) - continueWatchingItems.removeAll { $0.id == item.id } + continueWatchingManager.remove(item: item) } private func removeContinueWatchingItem(item: ContinueWatchingItem) { - ContinueWatchingManager.shared.remove(item: item) - continueWatchingItems.removeAll { $0.id == item.id } + continueWatchingManager.remove(item: item) } private func updateOrientation() { @@ -217,6 +259,8 @@ struct ContinueWatchingSection: View { } struct ContinueWatchingCell: View { + @EnvironmentObject private var continueWatchingManager: ContinueWatchingManager + let item: ContinueWatchingItem var markAsWatched: () -> Void var removeItem: () -> Void @@ -226,7 +270,7 @@ struct ContinueWatchingCell: View { var body: some View { Button(action: { if UserDefaults.standard.string(forKey: "externalPlayer") == "Default" { - let videoPlayerViewController = VideoPlayerViewController(module: item.module) + let videoPlayerViewController = VideoPlayerViewController(module: item.module, continueWatchingManager: continueWatchingManager) videoPlayerViewController.streamUrl = item.streamUrl videoPlayerViewController.fullUrl = item.fullUrl videoPlayerViewController.episodeImageUrl = item.imageUrl @@ -243,6 +287,7 @@ struct ContinueWatchingCell: View { } else { let customMediaPlayer = CustomMediaPlayerViewController( module: item.module, + continueWatchingManager: continueWatchingManager, urlString: item.streamUrl, fullUrl: item.fullUrl, title: item.mediaTitle, diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 902228e..76ef8b5 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -48,302 +48,230 @@ struct MediaInfoView: View { @StateObject private var jsController = JSController() @EnvironmentObject var moduleManager: ModuleManager @EnvironmentObject private var libraryManager: LibraryManager - + @EnvironmentObject private var continueWatchingManager: ContinueWatchingManager + @State private var selectedRange: Range = 0..<100 @State private var showSettingsMenu = false @State private var customAniListID: Int? - @State private var showStreamLoadingView: Bool = false - @State private var currentStreamTitle: String = "" - - @State private var activeFetchID: UUID? = nil - @Environment(\.dismiss) private var dismiss private var isGroupedBySeasons: Bool { return groupedEpisodes().count > 1 } var body: some View { - ZStack { - Group { - if isLoading { - ProgressView() - .padding() - } else { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 10) { - KFImage(URL(string: imageUrl)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) - .shimmering() + Group { + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: imageUrl)) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 150, height: 225) + .shimmering() + } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 225) + .clipped() + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 17)) + .fontWeight(.bold) + .onLongPressGesture { + UIPasteboard.general.string = title + DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 150, height: 225) - .clipped() - .cornerRadius(10) - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.system(size: 17)) - .fontWeight(.bold) - .onLongPressGesture { - UIPasteboard.general.string = title - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) - } - - if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { - Text(aliases) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - Spacer() - - if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { - HStack(alignment: .center, spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "calendar") - .resizable() - .frame(width: 15, height: 15) - .foregroundColor(.secondary) - - Text(airdate) - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(4) - } - } - + if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + + Spacer() + + if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { HStack(alignment: .center, spacing: 12) { - Button(action: { - openSafariViewController(with: href) - }) { - HStack(spacing: 4) { - Text(module.metadata.sourceName) - .font(.system(size: 13)) - .foregroundColor(.primary) - - Image(systemName: "safari") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.primary) - } - .padding(4) - .background(Capsule().fill(Color.accentColor.opacity(0.4))) + HStack(spacing: 4) { + Image(systemName: "calendar") + .resizable() + .frame(width: 15, height: 15) + .foregroundColor(.secondary) + + Text(airdate) + .font(.system(size: 12)) + .foregroundColor(.secondary) } - - Menu { - Button(action: { - showCustomIDAlert() - }) { - Label("Set Custom AniList ID", systemImage: "number") - } + .padding(4) + } + } + + HStack(alignment: .center, spacing: 12) { + Button(action: { + openSafariViewController(with: href) + }) { + HStack(spacing: 4) { + Text(module.metadata.sourceName) + .font(.system(size: 13)) + .foregroundColor(.primary) - if let customID = customAniListID { - Button(action: { - customAniListID = nil - itemID = nil - fetchItemID(byTitle: cleanTitle(title)) { result in - switch result { - case .success(let id): - itemID = id - case .failure(let error): - Logger.shared.log("Failed to fetch AniList ID: \(error)") - } - } - }) { - Label("Reset AniList ID", systemImage: "arrow.clockwise") - } - } - - if let id = itemID ?? customAniListID { - Button(action: { - if let url = URL(string: "https://anilist.co/anime/\(id)") { - openSafariViewController(with: url.absoluteString) - } - }) { - Label("Open in AniList", systemImage: "link") - } - } - - Divider() - - Button(action: { - Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") - DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) - }) { - Label("Log Debug Info", systemImage: "terminal") - } - } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "safari") .resizable() .frame(width: 20, height: 20) .foregroundColor(.primary) } - } - } - } - - if !synopsis.isEmpty { - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center) { - Text("Synopsis") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - Button(action: { - showFullSynopsis.toggle() - }) { - Text(showFullSynopsis ? "Less" : "More") - .font(.system(size: 14)) - } + .padding(4) + .background(Capsule().fill(Color.accentColor.opacity(0.4))) } - Text(synopsis) - .lineLimit(showFullSynopsis ? nil : 4) - .font(.system(size: 14)) - } - } - - HStack { - Button(action: { - playFirstUnwatchedEpisode() - }) { - HStack { - Image(systemName: "play.fill") - .foregroundColor(.primary) - Text(startWatchingText) - .font(.headline) + Menu { + Button(action: { + showCustomIDAlert() + }) { + Label("Set Custom AniList ID", systemImage: "number") + } + + if let _ = customAniListID { + Button(action: { + customAniListID = nil + itemID = nil + fetchItemID(byTitle: cleanTitle(title)) { result in + switch result { + case .success(let id): + itemID = id + case .failure(let error): + Logger.shared.log("Failed to fetch AniList ID: \(error)") + } + } + }) { + Label("Reset AniList ID", systemImage: "arrow.clockwise") + } + } + + if let id = itemID ?? customAniListID { + Button(action: { + if let url = URL(string: "https://anilist.co/anime/\(id)") { + openSafariViewController(with: url.absoluteString) + } + }) { + Label("Open in AniList", systemImage: "link") + } + } + + Divider() + + Button(action: { + Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") + DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) + }) { + Label("Log Debug Info", systemImage: "terminal") + } + } label: { + Image(systemName: "ellipsis.circle") + .resizable() + .frame(width: 20, height: 20) .foregroundColor(.primary) } - .padding() - .frame(maxWidth: .infinity) - .background(Color.accentColor) - .cornerRadius(10) - } - .disabled(isFetchingEpisode) - .id(buttonRefreshTrigger) - - Button(action: { - libraryManager.toggleBookmark( - title: title, - imageUrl: imageUrl, - href: href, - moduleId: module.id.uuidString, - moduleName: module.metadata.sourceName - ) - }) { - Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark") - .resizable() - .frame(width: 20, height: 27) - .foregroundColor(Color.accentColor) } } - - if !episodeLinks.isEmpty { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("Episodes") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { + } + + if !synopsis.isEmpty { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center) { + Text("Synopsis") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + Button(action: { + showFullSynopsis.toggle() + }) { + Text(showFullSynopsis ? "Less" : "More") + .font(.system(size: 14)) + } + } + + Text(synopsis) + .lineLimit(showFullSynopsis ? nil : 4) + .font(.system(size: 14)) + } + } + + Button(action: { + playFirstUnwatchedEpisode() + }) { + HStack { + Image(systemName: "play.fill") + .foregroundColor(.primary) + Text(startWatchingText) + .font(.headline) + .foregroundColor(.primary) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.accentColor) + .cornerRadius(10) + } + .disabled(isFetchingEpisode) + .id(buttonRefreshTrigger) + + if !episodeLinks.isEmpty { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Episodes") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { + Menu { + ForEach(generateRanges(), id: \.self) { range in + Button(action: { selectedRange = range }) { + Text("\(range.lowerBound + 1)-\(range.upperBound)") + } + } + } label: { + Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)") + .font(.system(size: 14)) + .foregroundColor(.accentColor) + } + } else if isGroupedBySeasons { + let seasons = groupedEpisodes() + if seasons.count > 1 { Menu { - ForEach(generateRanges(), id: \.self) { range in - Button(action: { selectedRange = range }) { - Text("\(range.lowerBound + 1)-\(range.upperBound)") + ForEach(0.. 1 { - Menu { - ForEach(0.. 0 ? lastPlayedTime / totalTime : 0 - - EpisodeCell( - episodeIndex: selectedSeason, - episode: ep.href, - episodeID: ep.number - 1, - progress: progress, - itemID: itemID ?? 0, - onTap: { imageUrl in - if !isFetchingEpisode { - selectedEpisodeNumber = ep.number - selectedEpisodeImage = imageUrl - fetchStream(href: ep.href) - AnalyticsManager.shared.sendEvent( - event: "watch", - additionalData: ["title": title, "episode": ep.number] - ) - } - }, - onMarkAllPrevious: { - let userDefaults = UserDefaults.standard - var updates = [String: Double]() - - for ep2 in seasons[selectedSeason] where ep2.number < ep.number { - let href = ep2.href - updates["lastPlayedTime_\(href)"] = 99999999.0 - updates["totalTime_\(href)"] = 99999999.0 - } - - for (key, value) in updates { - userDefaults.set(value, forKey: key) - } - - userDefaults.synchronize() - - refreshTrigger.toggle() - Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: "General") - } - ) - .id(refreshTrigger) - .disabled(isFetchingEpisode) - } - } else { - Text("No episodes available") - } - } else { - ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in - let ep = episodeLinks[i] + } + if isGroupedBySeasons { + let seasons = groupedEpisodes() + if !seasons.isEmpty, selectedSeason < seasons.count { + ForEach(seasons[selectedSeason]) { ep in let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 EpisodeCell( - episodeIndex: i, + episodeIndex: selectedSeason, episode: ep.href, episodeID: ep.number - 1, progress: progress, @@ -363,148 +291,159 @@ struct MediaInfoView: View { let userDefaults = UserDefaults.standard var updates = [String: Double]() - for idx in 0.. 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell( + episodeIndex: i, + episode: ep.href, + episodeID: ep.number - 1, + progress: progress, + itemID: itemID ?? 0, + onTap: { imageUrl in + if !isFetchingEpisode { + selectedEpisodeNumber = ep.number + selectedEpisodeImage = imageUrl + fetchStream(href: ep.href) + AnalyticsManager.shared.sendEvent( + event: "watch", + additionalData: ["title": title, "episode": ep.number] + ) + } + }, + onMarkAllPrevious: { + let userDefaults = UserDefaults.standard + var updates = [String: Double]() + + for idx in 0.. 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -666,8 +600,6 @@ struct MediaInfoView: View { } } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, softsub: true, module: module) { result in - guard self.activeFetchID == fetchID else { return } - if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -683,8 +615,6 @@ struct MediaInfoView: View { } } else { jsController.fetchStreamUrl(episodeUrl: href, softsub: true, module: module) { result in - guard self.activeFetchID == fetchID else { return } - if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -702,8 +632,6 @@ struct MediaInfoView: View { } else { if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: href, module: module) { result in - guard self.activeFetchID == fetchID else { return } - if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -719,8 +647,6 @@ struct MediaInfoView: View { } } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, module: module) { result in - guard self.activeFetchID == fetchID else { return } - if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -736,8 +662,6 @@ struct MediaInfoView: View { } } else { jsController.fetchStreamUrl(episodeUrl: href, module: module) { result in - guard self.activeFetchID == fetchID else { return } - if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -764,8 +688,6 @@ struct MediaInfoView: View { } func handleStreamFailure(error: Error? = nil) { - self.isFetchingEpisode = false - self.showStreamLoadingView = false if let error = error { Logger.shared.log("Error loading module: \(error)", type: "Error") AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"]) @@ -777,8 +699,6 @@ struct MediaInfoView: View { } func showStreamSelectionAlert(streams: [String], fullURL: String, subtitles: String? = nil) { - self.isFetchingEpisode = false - self.showStreamLoadingView = false DispatchQueue.main.async { let alert = UIAlertController(title: "Select Server", message: "Choose a server to play from", preferredStyle: .actionSheet) @@ -841,8 +761,6 @@ struct MediaInfoView: View { } func playStream(url: String, fullURL: String, subtitles: String? = nil) { - self.isFetchingEpisode = false - self.showStreamLoadingView = false DispatchQueue.main.async { let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String? @@ -857,7 +775,7 @@ struct MediaInfoView: View { case "nPlayer": scheme = "nplayer-\(url)" case "Default": - let videoPlayerViewController = VideoPlayerViewController(module: module) + let videoPlayerViewController = VideoPlayerViewController(module: module, continueWatchingManager: continueWatchingManager) videoPlayerViewController.streamUrl = url videoPlayerViewController.fullUrl = fullURL videoPlayerViewController.episodeNumber = selectedEpisodeNumber @@ -888,6 +806,7 @@ struct MediaInfoView: View { let customMediaPlayer = CustomMediaPlayerViewController( module: module, + continueWatchingManager: continueWatchingManager, urlString: url.absoluteString, fullUrl: fullURL, title: title, diff --git a/Sora/ContentView.swift b/Sora/Views/RootView/RootView.swift similarity index 78% rename from Sora/ContentView.swift rename to Sora/Views/RootView/RootView.swift index afbd4ea..1570fa6 100644 --- a/Sora/ContentView.swift +++ b/Sora/Views/RootView/RootView.swift @@ -6,11 +6,14 @@ // import SwiftUI -import Kingfisher -struct ContentView: View { +struct RootView: View { var body: some View { TabView { + ExploreView() + .tabItem { + Label("Explore", systemImage: "star") + } LibraryView() .tabItem { Label("Library", systemImage: "books.vertical") diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView/SearchView.swift similarity index 88% rename from Sora/Views/SearchView.swift rename to Sora/Views/SearchView/SearchView.swift index 856e2a4..be75e6d 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView/SearchView.swift @@ -17,12 +17,14 @@ struct SearchItem: Identifiable { struct SearchView: View { + @AppStorage("hideEmptySections") private var hideEmptySections: Bool? @AppStorage("selectedModuleId") private var selectedModuleId: String? @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 @StateObject private var jsController = JSController() - @EnvironmentObject var moduleManager: ModuleManager + @EnvironmentObject private var moduleManager: ModuleManager + @EnvironmentObject private var profileStore: ProfileStore @Environment(\.verticalSizeClass) var verticalSizeClass @State private var searchItems: [SearchItem] = [] @@ -32,7 +34,8 @@ struct SearchView: View { @State private var hasNoResults = false @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape @State private var isModuleSelectorPresented = false - + @State private var showProfileSettings = false + private var selectedModule: ScrapingModule? { guard let id = selectedModuleId else { return nil } return moduleManager.modules.first { $0.id.uuidString == id } @@ -88,7 +91,7 @@ struct SearchView: View { } } - if selectedModule == nil { + if !(hideEmptySections ?? false) && selectedModule == nil { VStack(spacing: 8) { Image(systemName: "questionmark.app") .font(.largeTitle) @@ -102,7 +105,6 @@ struct SearchView: View { .padding() .frame(maxWidth: .infinity) .background(Color(.systemBackground)) - .shadow(color: Color.black.opacity(0.1), radius: 2, y: 1) } if !searchText.isEmpty { @@ -160,10 +162,50 @@ struct SearchView: View { } } } + + NavigationLink( + destination: SettingsViewProfile(), + isActive: $showProfileSettings, + label: { EmptyView() } + ) + .hidden() } .navigationTitle("Search") .navigationBarTitleDisplayMode(.large) .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Menu { + ForEach(profileStore.profiles) { profile in + Button { + profileStore.setCurrentProfile(profile) + } label: { + if profile == profileStore.currentProfile { + Label("\(profile.emoji) \(profile.name)", systemImage: "checkmark") + } else { + Text("\(profile.emoji) \(profile.name)") + } + } + } + + Divider() + + Button { + showProfileSettings = true + } label: { + Label("Edit Profiles", systemImage: "slider.horizontal.3") + } + + } label: { + Circle() + .fill(Color.secondary.opacity(0.3)) + .frame(width: 32, height: 32) + .overlay( + Text(profileStore.currentProfile.emoji) + .font(.system(size: 20)) + .foregroundStyle(.primary) + ) + } + } ToolbarItem(placement: .navigationBarTrailing) { Menu { ForEach(getModuleLanguageGroups(), id: \.self) { language in diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift new file mode 100644 index 0000000..2b329ca --- /dev/null +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift @@ -0,0 +1,69 @@ +// +// SettingsViewAlternateAppIconPicker.swift +// Sulfur +// +// Created by Dominic on 20.04.25. +// + +import SwiftUI + +struct SettingsViewAlternateAppIconPicker: View { + @Binding var isPresented: Bool + @AppStorage("currentAppIcon") private var currentAppIcon: String = "Default" + + let icons: [(name: String, icon: String)] = [ + ("Default", "Default"), + ("Original", "Original"), + ("Pixel", "Pixel"), + ("Pride", "Pride") + ] + + var body: some View { + VStack { + Text("Select an App Icon") + .font(.headline) + .padding() + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 20) { + ForEach(icons, id: \.name) { icon in + VStack { + Image("AppIcon_\(icon.icon)_Preview", bundle: .main) + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .cornerRadius(10) + .padding() + .background( + currentAppIcon == icon.name ? Color.accentColor.opacity(0.3) : Color.clear + ) + .cornerRadius(10) + + Text(icon.name) + .font(.caption) + .foregroundColor(currentAppIcon == icon.name ? .accentColor : .primary) + } + .onTapGesture { + currentAppIcon = icon.name + setAppIcon(named: icon.icon) + } + } + } + .padding() + } + + Spacer() + } + } + + private func setAppIcon(named iconName: String) { + if UIApplication.shared.supportsAlternateIcons { + UIApplication.shared.setAlternateIconName(iconName == "Default" ? nil : "AppIcon_\(iconName)", completionHandler: { error in + isPresented = false + if let error = error { + print("Failed to set alternate icon: \(error.localizedDescription)") + } + }) + } + } +} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ed21a08..50ac9a8 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -16,23 +16,60 @@ struct SettingsViewGeneral: View { @AppStorage("metadataProviders") private var metadataProviders: String = "AniList" @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 - + @AppStorage("hideEmptySections") private var hideEmptySections: Bool = false + @AppStorage("currentAppIcon") private var currentAppIcon: String = "Default" + private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings - + @State var showAppIconPicker: Bool = false + var body: some View { Form { Section(header: Text("Interface")) { ColorPicker("Accent Color", selection: $settings.accentColor) HStack { Text("Appearance") - Picker("Appearance", selection: $settings.selectedAppearance) { - Text("System").tag(Appearance.system) - Text("Light").tag(Appearance.light) - Text("Dark").tag(Appearance.dark) + Spacer() + Menu { + ForEach(Appearance.allCases) { appearance in + Button { + settings.selectedAppearance = appearance + } label: { + Label(appearance.rawValue.capitalized, systemImage: settings.selectedAppearance == appearance ? "checkmark" : "") + } + } + } label: { + Text(settings.selectedAppearance.rawValue.capitalized) } - .pickerStyle(SegmentedPickerStyle()) } + HStack { + Text("App Icon") + Spacer() + Button(action: { + showAppIconPicker.toggle() + }) { + Text(currentAppIcon.isEmpty ? "Default" : currentAppIcon) + .font(.body) + .foregroundColor(.accentColor) + } + } + HStack { + Text("Loading Animation") + Spacer() + Menu { + ForEach(ShimmerType.allCases) { shimmerType in + Button { + settings.shimmerType = shimmerType + } label: { + Label(shimmerType.rawValue.capitalized, systemImage: settings.shimmerType == shimmerType ? "checkmark" : "") + } + } + } label: { + Text(settings.shimmerType.rawValue.capitalized) + } + } + Toggle("Hide Empty Sections", isOn: $hideEmptySections) + .tint(.accentColor) } Section(header: Text("Media View"), footer: Text("The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1-25, 26-50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata it is refering to the episode thumbnail and title, since sometimes it can contain spoilers.")) { @@ -103,5 +140,13 @@ struct SettingsViewGeneral: View { } } .navigationTitle("General") + .sheet(isPresented: $showAppIconPicker) { + if #available(iOS 16.0, *) { + SettingsViewAlternateAppIconPicker(isPresented: $showAppIconPicker) + .presentationDetents([.height(200)]) + } else { + SettingsViewAlternateAppIconPicker(isPresented: $showAppIconPicker) + } + } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index 1f52970..dfb1bc0 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -9,9 +9,12 @@ import SwiftUI import Kingfisher struct SettingsViewModule: View { - @AppStorage("selectedModuleId") private var selectedModuleId: String? @EnvironmentObject var moduleManager: ModuleManager - + @EnvironmentObject var settings: Settings + + @AppStorage("selectedModuleId") private var selectedModuleId: String? + @AppStorage("hideEmptySections") private var hideEmptySections: Bool? + @State private var errorMessage: String? @State private var isLoading = false @State private var isRefreshing = false @@ -21,7 +24,7 @@ struct SettingsViewModule: View { var body: some View { VStack { Form { - if moduleManager.modules.isEmpty { + if !(hideEmptySections ?? false) && moduleManager.modules.isEmpty { VStack(spacing: 8) { Image(systemName: "plus.app") .font(.largeTitle) @@ -65,7 +68,7 @@ struct SettingsViewModule: View { Spacer() if module.id.uuidString == selectedModuleId { - Image(systemName: "checkmark.circle.fill") + Image(systemName: "checkmark") .foregroundColor(.accentColor) .frame(width: 25, height: 25) } @@ -150,7 +153,7 @@ struct SettingsViewModule: View { message: "We found some text in your clipboard. Would you like to use it as the module URL?", preferredStyle: .alert ) - + clipboardAlert.addAction(UIAlertAction(title: "Use Clipboard", style: .default, handler: { _ in self.displayModuleView(url: pasteboardString) })) @@ -161,6 +164,7 @@ struct SettingsViewModule: View { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController { + windowScene.windows.first?.tintColor = UIColor(settings.accentColor) rootViewController.present(clipboardAlert, animated: true, completion: nil) } @@ -189,6 +193,7 @@ struct SettingsViewModule: View { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = windowScene.windows.first?.rootViewController { + windowScene.windows.first?.tintColor = UIColor(settings.accentColor) rootViewController.present(alert, animated: true, completion: nil) } } @@ -201,6 +206,7 @@ struct SettingsViewModule: View { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first { + window.tintColor = UIColor(settings.accentColor) window.rootViewController?.present(hostingController, animated: true, completion: nil) } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift new file mode 100644 index 0000000..8947bb3 --- /dev/null +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift @@ -0,0 +1,137 @@ +// +// ProfileView.swift +// Sulfur +// +// Created by Dominic on 20.04.25. +// + +import SwiftUI + +struct ProfileCell: View { + let profile: Profile + var isSelected: Bool = false + + var body: some View { + HStack(spacing: 10) { + Circle() + .fill(Color.secondary.opacity(0.3)) + .frame(width: 50, height: 50) + .overlay( + Text(profile.emoji) + .font(.system(size: 28)) + .foregroundStyle(.primary) + ) + + Text(profile.name) + .font(.headline) + .foregroundColor(.primary) + + Spacer() + + if isSelected { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + .padding(.vertical, 4) + } +} + +struct SettingsViewProfile: View { + @EnvironmentObject var profileStore: ProfileStore + + @State private var showDeleteAlert = false + + var body: some View { + Form { + Section(header: Text("Select Profile")) { + ForEach(profileStore.profiles) { profile in + Button { + profileStore.setCurrentProfile(profile) + } label: { + ProfileCell(profile: profile, + isSelected: profile.id == profileStore.currentProfile.id + ) + } + } + } + + Section(header: Text("Edit Selected Profile")) { + HStack { + Text("Avatar") + TextField("Avatar", text: Binding( + get: { profileStore.currentProfile.emoji }, + set: { newValue in + + // handle multi unicode emojis like "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ" or "๐Ÿง™โ€โ™‚๏ธ" + let emoji = String(newValue + .trimmingCharacters(in: .whitespacesAndNewlines) + .prefix(2) + ) + + profileStore.editCurrentProfile(name: profileStore.currentProfile.name, emoji: emoji) + } + )) + .lineLimit(1) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } + + HStack { + Text("Name") + TextField("Name", text: Binding( + get: { profileStore.currentProfile.name }, + set: { newValue in + profileStore.editCurrentProfile(name: newValue, emoji: profileStore.currentProfile.emoji) + } + )) + .lineLimit(1) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + } + + if profileStore.profiles.count > 1 { + Button(action: { + showDeleteAlert = true + }) { + Text("Delete Selected Profile") + .fontWeight(.semibold) + .foregroundColor(.red) + } + } + } + } + .navigationTitle("Profiles") + .alert(isPresented: $showDeleteAlert) { + Alert( + title: Text("Delete Profile"), + message: Text("Are you sure you want to delete this profile? This action cannot be undone."), + primaryButton: .destructive(Text("Delete")) { + profileStore.deleteCurrentProfile() + }, + secondaryButton: .cancel() + ) + } + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button { + UIApplication.shared.dismissKeyboard(true) + } label: { + Text("Done") + .foregroundColor(.accentColor) + .fontWeight(.semibold) + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + profileStore.addProfile(name: "New Profile", emoji: "๐Ÿง™โ€โ™‚๏ธ") + } label: { + Image(systemName: "plus") + .foregroundColor(.accentColor) + } + } + } + } +} diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index e156260..0c97689 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -8,9 +8,17 @@ import SwiftUI struct SettingsView: View { + @EnvironmentObject var profileStore: ProfileStore + var body: some View { NavigationView { Form { + Section { + NavigationLink(destination: SettingsViewProfile()) { + ProfileCell(profile: profileStore.currentProfile) + } + } + Section(header: Text("Main")) { NavigationLink(destination: SettingsViewGeneral()) { Text("General Preferences") @@ -26,7 +34,7 @@ struct SettingsView: View { } } - Section(header: Text("Info")) { + Section(header: Text("Diagnostics & Storage")) { NavigationLink(destination: SettingsViewData()) { Text("Data") } @@ -88,6 +96,19 @@ struct SettingsView: View { .foregroundColor(.secondary) } } + Button(action: { + if let url = URL(string: "https://github.com/cranci1/Sora/graphs/contributors") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Contributors") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } } Section(footer: Text("Running Sora 0.2.2 - cranci1")) {} } @@ -104,11 +125,17 @@ enum Appearance: String, CaseIterable, Identifiable { } class Settings: ObservableObject { + @Published var shimmerType: ShimmerType { + didSet { + UserDefaults.standard.set(shimmerType.rawValue, forKey: "shimmerType") + } + } @Published var accentColor: Color { didSet { saveAccentColor(accentColor) } } + @Published var selectedAppearance: Appearance { didSet { UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance") @@ -117,21 +144,38 @@ class Settings: ObservableObject { } init() { + if let shimmerRawValue = UserDefaults.standard.string(forKey: "shimmerType"), + let shimmer = ShimmerType(rawValue: shimmerRawValue) { + self.shimmerType = shimmer + } else { + self.shimmerType = .shimmer + } + if let colorData = UserDefaults.standard.data(forKey: "accentColor"), let uiColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData) { self.accentColor = Color(uiColor) } else { self.accentColor = .accentColor } + + if let appearanceRawValue = UserDefaults.standard.string(forKey: "selectedAppearance"), let appearance = Appearance(rawValue: appearanceRawValue) { self.selectedAppearance = appearance } else { self.selectedAppearance = .system } + + applyColorToUIKit(accentColor) updateAppearance() } - + + private func applyColorToUIKit(_ color: Color) { + let tempStepper = UIStepper() + UIStepper.appearance().setDecrementImage(tempStepper.decrementImage(for: .normal), for: .normal) + UIStepper.appearance().setIncrementImage(tempStepper.incrementImage(for: .normal), for: .normal) + } + private func saveAccentColor(_ color: Color) { let uiColor = UIColor(color) do { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 8e19b85..3cbaf52 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -3,10 +3,17 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ + 12D1E2692DB45CBE00EFCDAB /* SettingsViewAlternateAppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E2682DB45CB500EFCDAB /* SettingsViewAlternateAppIconPicker.swift */; }; + 12D1E26B2DB4BA9E00EFCDAB /* SettingsViewProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E26A2DB4BA9D00EFCDAB /* SettingsViewProfile.swift */; }; + 12D1E26E2DB66F9800EFCDAB /* ProfileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E26D2DB66F9600EFCDAB /* ProfileStore.swift */; }; + 12D1E2702DB6702700EFCDAB /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E26F2DB6702500EFCDAB /* Profile.swift */; }; + 12D1E2722DB6771100EFCDAB /* ProfileButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E2712DB6770D00EFCDAB /* ProfileButtonStyle.swift */; }; + 12D1E2742DB67EB200EFCDAB /* UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E2732DB67EA900EFCDAB /* UIKit.swift */; }; + 12D1E27A2DB69D7F00EFCDAB /* ExploreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D1E2792DB69D7F00EFCDAB /* ExploreView.swift */; }; 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; }; 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; }; 13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; }; @@ -21,9 +28,8 @@ 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */; }; 132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; - 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; + 133D7C702D2BE2500075467E /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* RootView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; - 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C742D2BE2520075467E /* Preview Assets.xcassets */; }; 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7C2D2BE2630075467E /* SearchView.swift */; }; 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7E2D2BE2630075467E /* LibraryView.swift */; }; 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C802D2BE2630075467E /* MediaInfoView.swift */; }; @@ -71,6 +77,14 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 120764652DB6F6E0003621E9 /* SulfurTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SulfurTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 12D1E2682DB45CB500EFCDAB /* SettingsViewAlternateAppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewAlternateAppIconPicker.swift; sourceTree = ""; }; + 12D1E26A2DB4BA9D00EFCDAB /* SettingsViewProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewProfile.swift; sourceTree = ""; }; + 12D1E26D2DB66F9600EFCDAB /* ProfileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStore.swift; sourceTree = ""; }; + 12D1E26F2DB6702500EFCDAB /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; + 12D1E2712DB6770D00EFCDAB /* ProfileButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileButtonStyle.swift; sourceTree = ""; }; + 12D1E2732DB67EA900EFCDAB /* UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKit.swift; sourceTree = ""; }; + 12D1E2792DB69D7F00EFCDAB /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = ""; }; 130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; }; @@ -84,9 +98,8 @@ 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; - 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 133D7C6F2D2BE2500075467E /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 133D7C712D2BE2520075467E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 133D7C742D2BE2520075467E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 133D7C7C2D2BE2630075467E /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 133D7C7E2D2BE2630075467E /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 133D7C802D2BE2630075467E /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = ""; }; @@ -119,7 +132,7 @@ 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; - 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 13E62FC12DABC5830007E259 /* Trakt-Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Login.swift"; sourceTree = ""; }; 13E62FC32DABC58C0007E259 /* Trakt-Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Token.swift"; sourceTree = ""; }; @@ -133,7 +146,18 @@ 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 120764662DB6F6E0003621E9 /* SulfurTV */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SulfurTV; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ + 120764622DB6F6E0003621E9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 133D7C672D2BE2500075467E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -148,6 +172,48 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1207649E2DB6FA7F003621E9 /* SearchView */ = { + isa = PBXGroup; + children = ( + 133D7C7C2D2BE2630075467E /* SearchView.swift */, + ); + path = SearchView; + sourceTree = ""; + }; + 1207649F2DB6FA88003621E9 /* DownloadView */ = { + isa = PBXGroup; + children = ( + 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, + ); + path = DownloadView; + sourceTree = ""; + }; + 120764A02DB6FA9D003621E9 /* RootView */ = { + isa = PBXGroup; + children = ( + 133D7C6F2D2BE2500075467E /* RootView.swift */, + ); + path = RootView; + sourceTree = ""; + }; + 12D1E26C2DB66F8B00EFCDAB /* ProfileStore */ = { + isa = PBXGroup; + children = ( + 12D1E2712DB6770D00EFCDAB /* ProfileButtonStyle.swift */, + 12D1E26F2DB6702500EFCDAB /* Profile.swift */, + 12D1E26D2DB66F9600EFCDAB /* ProfileStore.swift */, + ); + path = ProfileStore; + sourceTree = ""; + }; + 12D1E2782DB69D7900EFCDAB /* ExploreView */ = { + isa = PBXGroup; + children = ( + 12D1E2792DB69D7F00EFCDAB /* ExploreView.swift */, + ); + path = ExploreView; + sourceTree = ""; + }; 13103E802D589D6C000F0673 /* Tracking Services */ = { isa = PBXGroup; children = ( @@ -187,6 +253,7 @@ isa = PBXGroup; children = ( 133D7C6C2D2BE2500075467E /* Sora */, + 120764662DB6F6E0003621E9 /* SulfurTV */, 133D7C6B2D2BE2500075467E /* Products */, ); sourceTree = ""; @@ -195,6 +262,7 @@ isa = PBXGroup; children = ( 133D7C6A2D2BE2500075467E /* Sulfur.app */, + 120764652DB6F6E0003621E9 /* SulfurTV.app */, ); name = Products; sourceTree = ""; @@ -202,35 +270,27 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, + 133D7C712D2BE2520075467E /* Assets.xcassets */, + 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, + 133D7C6D2D2BE2500075467E /* SoraApp.swift */, 13103E802D589D6C000F0673 /* Tracking Services */, 133D7C852D2BE2640075467E /* Utils */, 133D7C7B2D2BE2630075467E /* Views */, - 133D7C6D2D2BE2500075467E /* SoraApp.swift */, - 133D7C6F2D2BE2500075467E /* ContentView.swift */, - 133D7C712D2BE2520075467E /* Assets.xcassets */, - 133D7C732D2BE2520075467E /* Preview Content */, ); path = Sora; sourceTree = ""; }; - 133D7C732D2BE2520075467E /* Preview Content */ = { - isa = PBXGroup; - children = ( - 133D7C742D2BE2520075467E /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 133D7C7B2D2BE2630075467E /* Views */ = { isa = PBXGroup; children = ( + 120764A02DB6FA9D003621E9 /* RootView */, + 1207649F2DB6FA88003621E9 /* DownloadView */, + 1207649E2DB6FA7F003621E9 /* SearchView */, + 12D1E2782DB69D7900EFCDAB /* ExploreView */, 133D7C7F2D2BE2630075467E /* MediaInfoView */, 1399FAD22D3AB34F00E97C31 /* SettingsView */, 133F55B92D33B53E00E08EEA /* LibraryView */, - 133D7C7C2D2BE2630075467E /* SearchView.swift */, - 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, ); path = Views; sourceTree = ""; @@ -247,6 +307,7 @@ 133D7C832D2BE2630075467E /* SettingsSubViews */ = { isa = PBXGroup; children = ( + 12D1E2682DB45CB500EFCDAB /* SettingsViewAlternateAppIconPicker.swift */, 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */, 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */, 133D7C842D2BE2630075467E /* SettingsViewModule.swift */, @@ -254,6 +315,7 @@ 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */, 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */, 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */, + 12D1E26A2DB4BA9D00EFCDAB /* SettingsViewProfile.swift */, ); path = SettingsSubViews; sourceTree = ""; @@ -261,6 +323,7 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( + 12D1E26C2DB66F8B00EFCDAB /* ProfileStore */, 136BBE7C2DB102BE00906B5E /* iCloudSyncManager */, 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, @@ -279,6 +342,7 @@ 133D7C862D2BE2640075467E /* Extensions */ = { isa = PBXGroup; children = ( + 12D1E2732DB67EA900EFCDAB /* UIKit.swift */, 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */, 136BBE7F2DB1038000906B5E /* Notification+Name.swift */, 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */, @@ -465,6 +529,28 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 120764642DB6F6E0003621E9 /* SulfurTV */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1207646F2DB6F6E1003621E9 /* Build configuration list for PBXNativeTarget "SulfurTV" */; + buildPhases = ( + 120764612DB6F6E0003621E9 /* Sources */, + 120764622DB6F6E0003621E9 /* Frameworks */, + 120764632DB6F6E0003621E9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 120764662DB6F6E0003621E9 /* SulfurTV */, + ); + name = SulfurTV; + packageProductDependencies = ( + ); + productName = SulfurTV; + productReference = 120764652DB6F6E0003621E9 /* SulfurTV.app */; + productType = "com.apple.product-type.application"; + }; 133D7C692D2BE2500075467E /* Sulfur */ = { isa = PBXNativeTarget; buildConfigurationList = 133D7C782D2BE2520075467E /* Build configuration list for PBXNativeTarget "Sulfur" */; @@ -495,9 +581,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1320; - LastUpgradeCheck = 1320; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; TargetAttributes = { + 120764642DB6F6E0003621E9 = { + CreatedOnToolsVersion = 16.3; + }; 133D7C692D2BE2500075467E = { CreatedOnToolsVersion = 13.2.1; }; @@ -522,16 +611,23 @@ projectRoot = ""; targets = ( 133D7C692D2BE2500075467E /* Sulfur */, + 120764642DB6F6E0003621E9 /* SulfurTV */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 120764632DB6F6E0003621E9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 133D7C682D2BE2500075467E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */, 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -539,6 +635,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 120764612DB6F6E0003621E9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 133D7C662D2BE2500075467E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -550,6 +653,7 @@ 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */, 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */, 139935662D468C450065CEFF /* ModuleManager.swift in Sources */, + 12D1E2692DB45CBE00EFCDAB /* SettingsViewAlternateAppIconPicker.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, @@ -558,11 +662,13 @@ 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, + 12D1E26B2DB4BA9E00EFCDAB /* SettingsViewProfile.swift in Sources */, 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */, 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, + 12D1E2742DB67EB200EFCDAB /* UIKit.swift in Sources */, 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, - 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, + 133D7C702D2BE2500075467E /* RootView.swift in Sources */, 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */, @@ -572,6 +678,7 @@ 132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */, 131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */, 13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */, + 12D1E26E2DB66F9800EFCDAB /* ProfileStore.swift in Sources */, 13D842552D45267500EBBFA6 /* DropManager.swift in Sources */, 13103E8B2D58E028000F0673 /* View.swift in Sources */, 1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */, @@ -585,10 +692,13 @@ 13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */, 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, + 12D1E2722DB6771100EFCDAB /* ProfileButtonStyle.swift in Sources */, 13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, 13E62FC72DABFE900007E259 /* TraktPushUpdates.swift in Sources */, + 12D1E27A2DB69D7F00EFCDAB /* ExploreView.swift in Sources */, 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, + 12D1E2702DB6702700EFCDAB /* Profile.swift in Sources */, 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, @@ -604,10 +714,72 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ + 1207646D2DB6F6E1003621E9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = AXLC3PQNC8; + ENABLE_PREVIEWS = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.devsforge.tvos.SulfurTV; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 18.4; + }; + name = Debug; + }; + 1207646E2DB6F6E1003621E9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = AXLC3PQNC8; + ENABLE_PREVIEWS = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.devsforge.tvos.SulfurTV; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 18.4; + }; + name = Release; + }; 133D7C762D2BE2520075467E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -639,8 +811,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = AXLC3PQNC8; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -670,6 +844,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -701,8 +876,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = AXLC3PQNC8; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -725,15 +902,15 @@ 133D7C792D2BE2520075467E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon_Pride AppIcon_Original AppIcon_Pixel"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Default; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -754,7 +931,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.2; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = de.devsforge.sulfur.fork; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; @@ -768,15 +945,15 @@ 133D7C7A2D2BE2520075467E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon_Pride AppIcon_Original AppIcon_Pixel"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon_Default; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Sora/Sora.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -797,7 +974,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.2; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = de.devsforge.sulfur.fork; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; @@ -811,6 +988,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 1207646F2DB6F6E1003621E9 /* Build configuration list for PBXNativeTarget "SulfurTV" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1207646D2DB6F6E1003621E9 /* Debug */, + 1207646E2DB6F6E1003621E9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 133D7C652D2BE2500075467E /* Build configuration list for PBXProject "Sulfur" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SulfurTV/Assets.xcassets/AccentColor.colorset/Contents.json b/SulfurTV/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..bb57667 --- /dev/null +++ b/SulfurTV/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemMintColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..cec685a --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "large.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/large.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/large.png new file mode 100644 index 0000000..f1f3206 Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/large.png differ diff --git a/Sora/Preview Content/Preview Assets.xcassets/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json similarity index 100% rename from Sora/Preview Content/Preview Assets.xcassets/Contents.json rename to SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..cec685a --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "large.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/large.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/large.png new file mode 100644 index 0000000..95b562a Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/large.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..2e00335 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..91050f5 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "small.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "large.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/large.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/large.png new file mode 100644 index 0000000..c050a91 Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/large.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/small.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/small.png new file mode 100644 index 0000000..017666f Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/small.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..91050f5 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "small.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "large.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/large.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/large.png new file mode 100644 index 0000000..fcf3e9e Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/large.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/small.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/small.png new file mode 100644 index 0000000..02f486f Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/small.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 0000000..f47ba43 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "filename" : "App Icon - App Store.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "1280x768" + }, + { + "filename" : "App Icon.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "400x240" + }, + { + "filename" : "Top Shelf Image Wide.imageset", + "idiom" : "tv", + "role" : "top-shelf-image-wide", + "size" : "2320x720" + }, + { + "filename" : "Top Shelf Image.imageset", + "idiom" : "tv", + "role" : "top-shelf-image", + "size" : "1920x720" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 0000000..91050f5 --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "small.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "large.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/large.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/large.png new file mode 100644 index 0000000..754c8b1 Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/large.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/small.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/small.png new file mode 100644 index 0000000..368683e Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/small.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000..684d25c --- /dev/null +++ b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "small_2.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "large_2.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/large_2.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/large_2.png new file mode 100644 index 0000000..ebcd8fa Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/large_2.png differ diff --git a/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/small_2.png b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/small_2.png new file mode 100644 index 0000000..357a4c9 Binary files /dev/null and b/SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/small_2.png differ diff --git a/SulfurTV/Assets.xcassets/Contents.json b/SulfurTV/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/SulfurTV/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SulfurTV/SulfurTVApp.swift b/SulfurTV/SulfurTVApp.swift new file mode 100644 index 0000000..0083ccf --- /dev/null +++ b/SulfurTV/SulfurTVApp.swift @@ -0,0 +1,17 @@ +// +// SulfurTVApp.swift +// SulfurTV +// +// Created by Dominic on 21.04.25. +// + +import SwiftUI + +@main +struct SulfurTVApp: App { + var body: some Scene { + WindowGroup { + RootView() + } + } +} diff --git a/SulfurTV/Views/ExploreView/ExploreView.swift b/SulfurTV/Views/ExploreView/ExploreView.swift new file mode 100644 index 0000000..05f760c --- /dev/null +++ b/SulfurTV/Views/ExploreView/ExploreView.swift @@ -0,0 +1,14 @@ +// +// ExploreView.swift +// Sulfur +// +// Created by Dominic on 22.04.25. +// + +import SwiftUI + +struct ExploreView: View { + var body: some View { + Text("Explore View") + } +} diff --git a/SulfurTV/Views/LibraryView/LibraryView.swift b/SulfurTV/Views/LibraryView/LibraryView.swift new file mode 100644 index 0000000..df1d84e --- /dev/null +++ b/SulfurTV/Views/LibraryView/LibraryView.swift @@ -0,0 +1,14 @@ +// +// ExploreView.swift +// Sulfur +// +// Created by Dominic on 22.04.25. +// + +import SwiftUI + +struct LibraryView: View { + var body: some View { + Text("Library View") + } +} diff --git a/SulfurTV/Views/RootView/RootView.swift b/SulfurTV/Views/RootView/RootView.swift new file mode 100644 index 0000000..734daf4 --- /dev/null +++ b/SulfurTV/Views/RootView/RootView.swift @@ -0,0 +1,31 @@ +// +// ContentView.swift +// SulfurTV +// +// Created by Dominic on 21.04.25. +// + +import SwiftUI + +struct RootView: View { + var body: some View { + TabView { + ExploreView() + .tabItem { + Label("Explore", systemImage: "star.fill") + } + LibraryView() + .tabItem { + Label("Library", systemImage: "books.vertical.fill") + } + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") + } + } + } +} diff --git a/SulfurTV/Views/SearchView/SearchView.swift b/SulfurTV/Views/SearchView/SearchView.swift new file mode 100644 index 0000000..9b5b0a1 --- /dev/null +++ b/SulfurTV/Views/SearchView/SearchView.swift @@ -0,0 +1,14 @@ +// +// ExploreView.swift +// Sulfur +// +// Created by Dominic on 22.04.25. +// + +import SwiftUI + +struct SearchView: View { + var body: some View { + Text("Search View") + } +} diff --git a/SulfurTV/Views/SettingsView/SettingsView.swift b/SulfurTV/Views/SettingsView/SettingsView.swift new file mode 100644 index 0000000..82cd915 --- /dev/null +++ b/SulfurTV/Views/SettingsView/SettingsView.swift @@ -0,0 +1,42 @@ +// +// ExploreView.swift +// Sulfur +// +// Created by Dominic on 22.04.25. +// + +import SwiftUI + +struct SettingsView: View { + @FocusState private var focusedSetting: Int? + private let screenWidth = UIScreen.main.bounds.width + + var body: some View { + HStack(spacing: 0) { + VStack { + Group { + RoundedRectangle(cornerRadius: 90, style: .circular) + .fill(.gray.opacity(0.3)) + .frame(width: UIScreen.main.bounds.width * 0.3, height: UIScreen.main.bounds.width * 0.3) + .shadow(radius: 12) + } + } + .frame(width: screenWidth / 2.0) + + VStack { + ForEach(1..<7) { index in + Button(action: { + print("Selected Index: \(index)") + }) { + Text("Random Setting \(index)") + .frame(maxWidth: screenWidth / 2.5) + .scaleEffect(focusedSetting == index ? 1.0 : 0.85) + .animation(.easeInOut(duration: 0.2), value: focusedSetting == index) + } + .focused($focusedSetting, equals: index) + } + } + .frame(width: screenWidth / 2.0) + } + } +}