Move Development from Fork into main Repository (#100)
* add contributor link, add hide empty sections toggle, cleanup warnings, tests * fix darkmode label color * use primary and secondary colors ( for consistency with rest of codebase ) * add basic profile views / ui * add current profile view to important places ( navigationbar leading ) * reorder contributors row, update url * merge upstream into fork * add new icons, cleanup, tests. * close app icon sheet automatically on completion * add profilestore ( persistence, enviromentobject ), finalize profile settings view, cleanup, tests * add profilestore ( persistence, enviromentobject ), finalize profile settings view, cleanup, tests * add dismiss keyboard extension, dismiss keyboard on tap outside ( profile settings view ) * fix icon transparency issue, add profile data to icloud sync * remove weird empty view ( search ) shadow, fix dismiss keyboard, align system appearance to other rows ( style ), cleanup, tests * fancy profile switch manu ( navigationbar ) * add explore view ( basic library and search view copy ) * fix uikit alerts not using the correct accentColor * apply custom accentColor to stepper components * style consistency ( icons, colors ), change duplicate section title ( "Info" ), hide more empty sections conditionally, cleanup * fix missing section headers * fix copy paste error ^^' * add empty explore view placeholder, add new shimmer effect ( configurable via settings ), cleanup * convert ContinueWatchingManager() singleton to dependency injected enviroment object to match similar manager structures * fix spelling, inject profile into library and continueWatching Managers, fix iCloudSync premature execution, remove profile from explore view ( wont be needed ) * add update profile function to library and continuewatching managers ( to reload the media items ), change media fetching style of continuewatchingmanager to better match librarymanager, update libraryview to use the new continuewatchingmanager fetch style * fix state desync on insertion / removal / profile change * switched from filtering by profile ids to seperated data storage via user default suites with different ids. * update todo markers * fix bookmarks not getting overwritten on empty userdefaults load * add the profile button back to the explore view ( you might wanna change profile quickly ), add todos * moved some views into folders, renamed contentview to rootview, moved bookmark button into navigationbar, used randomUseragents everywhere, add tvos target, add tvos images and basic tabview, started work on settings view design * add new shimmer type, swap two settings rows, add detailed instructions to some todos * Squashed commit of the following: commit5d076e0cf7Author: cranci <100066266+cranci1@users.noreply.github.com> Date: Tue Apr 22 15:03:25 2025 +0200 Aniskip logic and basic buttons (#96) (#97) * Aniskip logic and basic buttons * good fuckin enough for now * im callin good enough * bug fix * its something * hallelujah * Update SearchView.swift * made subs go up the progress bar if it is showing --------- Co-authored-by: ibro <54913038+xibrox@users.noreply.github.com> Co-authored-by: Seiike <122684677+Seeike@users.noreply.github.com> commit0ad4659d2cAuthor: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sun Apr 20 19:50:15 2025 +0200 hello 👋 (#95) * bug fix dimming * improved the fetchEpisodeMetadata logic commit83cf7b0e9fMerge:d28a55a68e8196Author: cranci <100066266+cranci1@users.noreply.github.com> Date: Sun Apr 20 08:53:08 2025 +0200 Implementation of loading modal and dim mode (#93) --------- Co-authored-by: Dominic Drees <dominic.drees@atino.de> Co-authored-by: Francesco <100066266+cranci1@users.noreply.github.com>
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
12
Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "preview.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Default_Preview.imageset/preview.png
vendored
Normal file
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "original.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Original.appiconset/original.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
12
Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "preview.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Original_Preview.imageset/preview.png
vendored
Normal file
|
After Width: | Height: | Size: 248 KiB |
38
Sora/Assets.xcassets/AppIcon_Pixel.appiconset/Contents.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Pixel.appiconset/darkmode.png
Executable file
|
After Width: | Height: | Size: 141 KiB |
BIN
Sora/Assets.xcassets/AppIcon_Pixel.appiconset/lightmode.png
Executable file
|
After Width: | Height: | Size: 141 KiB |
BIN
Sora/Assets.xcassets/AppIcon_Pixel.appiconset/tinting.png
Executable file
|
After Width: | Height: | Size: 96 KiB |
12
Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "preview.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Pixel_Preview.imageset/preview.png
vendored
Normal file
|
After Width: | Height: | Size: 143 KiB |
38
Sora/Assets.xcassets/AppIcon_Pride.appiconset/Contents.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Pride.appiconset/darkmode.png
Executable file
|
After Width: | Height: | Size: 167 KiB |
BIN
Sora/Assets.xcassets/AppIcon_Pride.appiconset/lightmode.png
Executable file
|
After Width: | Height: | Size: 179 KiB |
BIN
Sora/Assets.xcassets/AppIcon_Pride.appiconset/tinting.png
Executable file
|
After Width: | Height: | Size: 127 KiB |
12
Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "preview.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/AppIcon_Pride_Preview.imageset/preview.png
vendored
Normal file
|
After Width: | Height: | Size: 169 KiB |
|
|
@ -2,8 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sora may requires access to your device's camera.</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
|
|
@ -38,7 +36,5 @@
|
|||
<string>audio</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<dict>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.me.cranci.sora.icloud</string>
|
||||
<string>iCloud.de.devsforge.sulfur.fork</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.me.cranci.sora.icloud</string>
|
||||
<string>iCloud.de.devsforge.sulfur.fork</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
Sora/Utils/Extensions/UIKit.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ import SwiftUI
|
|||
|
||||
extension View {
|
||||
func shimmering() -> some View {
|
||||
self.modifier(Shimmer())
|
||||
self.modifier(ShimmeringEffect())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
14
Sora/Utils/ProfileStore/Profile.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
14
Sora/Utils/ProfileStore/ProfileButtonStyle.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
96
Sora/Utils/ProfileStore/ProfileStore.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
196
Sora/Views/ExploreView/ExploreView.swift
Normal file
|
|
@ -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] ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Int> = 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..<seasons.count, id: \.self) { index in
|
||||
Button(action: { selectedSeason = index }) {
|
||||
Text("Season \(index + 1)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)")
|
||||
Text("Season \(selectedSeason + 1)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
} else if isGroupedBySeasons {
|
||||
let seasons = groupedEpisodes()
|
||||
if seasons.count > 1 {
|
||||
Menu {
|
||||
ForEach(0..<seasons.count, id: \.self) { index in
|
||||
Button(action: { selectedSeason = index }) {
|
||||
Text("Season \(index + 1)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Season \(selectedSeason + 1)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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: 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..<i {
|
||||
if idx < episodeLinks.count {
|
||||
let href = episodeLinks[idx].href
|
||||
updates["lastPlayedTime_\(href)"] = 1000.0
|
||||
updates["totalTime_\(href)"] = 1000.0
|
||||
}
|
||||
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 \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General")
|
||||
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]
|
||||
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,
|
||||
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..<i {
|
||||
if idx < episodeLinks.count {
|
||||
let href = episodeLinks[idx].href
|
||||
updates["lastPlayedTime_\(href)"] = 1000.0
|
||||
updates["totalTime_\(href)"] = 1000.0
|
||||
}
|
||||
}
|
||||
|
||||
for (key, value) in updates {
|
||||
userDefaults.set(value, forKey: key)
|
||||
}
|
||||
|
||||
refreshTrigger.toggle()
|
||||
Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General")
|
||||
}
|
||||
)
|
||||
.id(refreshTrigger)
|
||||
.disabled(isFetchingEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Episodes")
|
||||
.font(.system(size: 18))
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
if isRefetching {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Episodes")
|
||||
.font(.system(size: 18))
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
if isRefetching {
|
||||
ProgressView()
|
||||
.padding()
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 2) {
|
||||
Text("No episodes Found:")
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 2) {
|
||||
Text("No episodes Found:")
|
||||
.foregroundColor(.secondary)
|
||||
Button(action: {
|
||||
isRefetching = true
|
||||
fetchDetails()
|
||||
}) {
|
||||
Text("Retry")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
Button(action: {
|
||||
isRefetching = true
|
||||
fetchDetails()
|
||||
}) {
|
||||
Text("Retry")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding()
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarTitle("")
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
buttonRefreshTrigger.toggle()
|
||||
|
||||
if !hasFetched {
|
||||
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||
fetchDetails()
|
||||
|
||||
if let savedID = UserDefaults.standard.object(forKey: "custom_anilist_id_\(href)") as? Int {
|
||||
customAniListID = savedID
|
||||
itemID = savedID
|
||||
Logger.shared.log("Using custom AniList ID: \(savedID)", type: "Debug")
|
||||
} else {
|
||||
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)")
|
||||
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch AniList ID"])
|
||||
.padding()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
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()
|
||||
.foregroundColor(Color.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasFetched = true
|
||||
AnalyticsManager.shared.sendEvent(event: "search", additionalData: ["title": title])
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarTitle("")
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
selectedRange = 0..<episodeChunkSize
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
buttonRefreshTrigger.toggle()
|
||||
|
||||
if showStreamLoadingView {
|
||||
VStack(spacing: 16) {
|
||||
Text("Loading \(currentStreamTitle)…")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Button("Cancel") {
|
||||
withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) {
|
||||
activeFetchID = nil
|
||||
isFetchingEpisode = false
|
||||
showStreamLoadingView = false
|
||||
if !hasFetched {
|
||||
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||
fetchDetails()
|
||||
|
||||
if let savedID = UserDefaults.standard.object(forKey: "custom_anilist_id_\(href)") as? Int {
|
||||
customAniListID = savedID
|
||||
itemID = savedID
|
||||
Logger.shared.log("Using custom AniList ID: \(savedID)", type: "Debug")
|
||||
} else {
|
||||
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)")
|
||||
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch AniList ID"])
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 24)
|
||||
.background(
|
||||
// Hex #FF705E
|
||||
Color(red: 1.0, green: 112/255.0, blue: 94/255.0)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 40)
|
||||
.background(.ultraThinMaterial)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: Color.black.opacity(0.3), radius: 12, x: 0, y: 8)
|
||||
.frame(maxWidth: 300)
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
.animation(.spring(response: 0.4, dampingFraction: 0.6), value: showStreamLoadingView)
|
||||
|
||||
hasFetched = true
|
||||
AnalyticsManager.shared.sendEvent(event: "search", additionalData: ["title": title])
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(action: {
|
||||
activeFetchID = nil
|
||||
isFetchingEpisode = false
|
||||
showStreamLoadingView = false
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Search")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
activeFetchID = nil
|
||||
isFetchingEpisode = false
|
||||
showStreamLoadingView = false
|
||||
selectedRange = 0..<episodeChunkSize
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -635,10 +574,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
|
||||
func fetchStream(href: String) {
|
||||
let fetchID = UUID()
|
||||
activeFetchID = fetchID
|
||||
currentStreamTitle = "Episode \(selectedEpisodeNumber)"
|
||||
showStreamLoadingView = true
|
||||
DropManager.shared.showDrop(title: "Fetching Stream", subtitle: "", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||
isFetchingEpisode = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
Task {
|
||||
|
|
@ -649,8 +585,6 @@ struct MediaInfoView: View {
|
|||
if module.metadata.softsub == true {
|
||||
if module.metadata.asyncJS == true {
|
||||
jsController.fetchStreamUrlJS(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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
12D1E26A2DB4BA9D00EFCDAB /* SettingsViewProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewProfile.swift; sourceTree = "<group>"; };
|
||||
12D1E26D2DB66F9600EFCDAB /* ProfileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStore.swift; sourceTree = "<group>"; };
|
||||
12D1E26F2DB6702500EFCDAB /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
||||
12D1E2712DB6770D00EFCDAB /* ProfileButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileButtonStyle.swift; sourceTree = "<group>"; };
|
||||
12D1E2732DB67EA900EFCDAB /* UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKit.swift; sourceTree = "<group>"; };
|
||||
12D1E2792DB69D7F00EFCDAB /* ExploreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreView.swift; sourceTree = "<group>"; };
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = "<group>"; };
|
||||
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -84,9 +98,8 @@
|
|||
132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
133D7C6F2D2BE2500075467E /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||
133D7C712D2BE2520075467E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
133D7C742D2BE2520075467E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
133D7C7C2D2BE2630075467E /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
133D7C7E2D2BE2630075467E /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
133D7C802D2BE2630075467E /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -119,7 +132,7 @@
|
|||
13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = "<group>"; };
|
||||
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
|
||||
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
13E62FC12DABC5830007E259 /* Trakt-Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Login.swift"; sourceTree = "<group>"; };
|
||||
13E62FC32DABC58C0007E259 /* Trakt-Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Token.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -133,7 +146,18 @@
|
|||
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
120764662DB6F6E0003621E9 /* SulfurTV */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SulfurTV; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
1207649F2DB6FA88003621E9 /* DownloadView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
|
||||
);
|
||||
path = DownloadView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
120764A02DB6FA9D003621E9 /* RootView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
133D7C6F2D2BE2500075467E /* RootView.swift */,
|
||||
);
|
||||
path = RootView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
12D1E26C2DB66F8B00EFCDAB /* ProfileStore */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
12D1E2712DB6770D00EFCDAB /* ProfileButtonStyle.swift */,
|
||||
12D1E26F2DB6702500EFCDAB /* Profile.swift */,
|
||||
12D1E26D2DB66F9600EFCDAB /* ProfileStore.swift */,
|
||||
);
|
||||
path = ProfileStore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
12D1E2782DB69D7900EFCDAB /* ExploreView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
12D1E2792DB69D7F00EFCDAB /* ExploreView.swift */,
|
||||
);
|
||||
path = ExploreView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13103E802D589D6C000F0673 /* Tracking Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -187,6 +253,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
133D7C6C2D2BE2500075467E /* Sora */,
|
||||
120764662DB6F6E0003621E9 /* SulfurTV */,
|
||||
133D7C6B2D2BE2500075467E /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -195,6 +262,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
133D7C6A2D2BE2500075467E /* Sulfur.app */,
|
||||
120764652DB6F6E0003621E9 /* SulfurTV.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
};
|
||||
133D7C732D2BE2520075467E /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
133D7C742D2BE2520075467E /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 = (
|
||||
|
|
|
|||
15
SulfurTV/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemMintColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "large.png",
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 790 KiB |
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "large.png",
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 152 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "small.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "large.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "small.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "large.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "small.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "large.png",
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 795 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/large_2.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
SulfurTV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/small_2.png
vendored
Normal file
|
After Width: | Height: | Size: 651 KiB |
6
SulfurTV/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
17
SulfurTV/SulfurTVApp.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
14
SulfurTV/Views/ExploreView/ExploreView.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
14
SulfurTV/Views/LibraryView/LibraryView.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
31
SulfurTV/Views/RootView/RootView.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
SulfurTV/Views/SearchView/SearchView.swift
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
42
SulfurTV/Views/SettingsView/SettingsView.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||