diff --git a/Sora-JS.xcodeproj/project.pbxproj b/Sora-JS.xcodeproj/project.pbxproj index 766ee9e..ff8f7b7 100644 --- a/Sora-JS.xcodeproj/project.pbxproj +++ b/Sora-JS.xcodeproj/project.pbxproj @@ -12,8 +12,13 @@ 1329D5CB2D298199008AEDA2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1329D5CA2D298199008AEDA2 /* Assets.xcassets */; }; 1329D5CE2D298199008AEDA2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1329D5CD2D298199008AEDA2 /* Preview Assets.xcassets */; }; 13AEE6192D2A75110096D953 /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE6182D2A75110096D953 /* Modules.swift */; }; - 13AEE61B2D2A78050096D953 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE61A2D2A78050096D953 /* Settings.swift */; }; + 13AEE61B2D2A78050096D953 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE61A2D2A78050096D953 /* SettingsView.swift */; }; 13AEE61D2D2A78160096D953 /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE61C2D2A78160096D953 /* JSController.swift */; }; + 13AEE6232D2AAF160096D953 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13AEE6222D2AAF160096D953 /* Kingfisher */; }; + 13AEE6252D2AB1730096D953 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE6242D2AB1730096D953 /* HomeView.swift */; }; + 13AEE6272D2AB1990096D953 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE6262D2AB1990096D953 /* LibraryView.swift */; }; + 13AEE6292D2AB2070096D953 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE6282D2AB2070096D953 /* SearchView.swift */; }; + 13AEE62D2D2ABCD30096D953 /* SettingsViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13AEE62C2D2ABCD30096D953 /* SettingsViewModule.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -24,8 +29,12 @@ 1329D5CD2D298199008AEDA2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 1329D5DA2D29821B008AEDA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 13AEE6182D2A75110096D953 /* Modules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = ""; }; - 13AEE61A2D2A78050096D953 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + 13AEE61A2D2A78050096D953 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 13AEE61C2D2A78160096D953 /* JSController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = ""; }; + 13AEE6242D2AB1730096D953 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 13AEE6262D2AB1990096D953 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; + 13AEE6282D2AB2070096D953 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + 13AEE62C2D2ABCD30096D953 /* SettingsViewModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModule.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -33,6 +42,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 13AEE6232D2AAF160096D953 /* Kingfisher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -58,14 +68,14 @@ 1329D5C52D298198008AEDA2 /* Sora-JS */ = { isa = PBXGroup; children = ( + 13AEE6202D2AAD390096D953 /* Modules */, + 13AEE61F2D2AAD2D0096D953 /* Loaders */, + 13AEE61E2D2AAD1E0096D953 /* Views */, 1329D5DA2D29821B008AEDA2 /* Info.plist */, 1329D5C62D298198008AEDA2 /* Sora_JSApp.swift */, 1329D5C82D298198008AEDA2 /* ContentView.swift */, - 13AEE6182D2A75110096D953 /* Modules.swift */, 1329D5CA2D298199008AEDA2 /* Assets.xcassets */, 1329D5CC2D298199008AEDA2 /* Preview Content */, - 13AEE61A2D2A78050096D953 /* Settings.swift */, - 13AEE61C2D2A78160096D953 /* JSController.swift */, ); path = "Sora-JS"; sourceTree = ""; @@ -78,6 +88,50 @@ path = "Preview Content"; sourceTree = ""; }; + 13AEE61E2D2AAD1E0096D953 /* Views */ = { + isa = PBXGroup; + children = ( + 13AEE62A2D2ABCB40096D953 /* SettingsView */, + 13AEE6242D2AB1730096D953 /* HomeView.swift */, + 13AEE6262D2AB1990096D953 /* LibraryView.swift */, + 13AEE6282D2AB2070096D953 /* SearchView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 13AEE61F2D2AAD2D0096D953 /* Loaders */ = { + isa = PBXGroup; + children = ( + 13AEE61C2D2A78160096D953 /* JSController.swift */, + ); + path = Loaders; + sourceTree = ""; + }; + 13AEE6202D2AAD390096D953 /* Modules */ = { + isa = PBXGroup; + children = ( + 13AEE6182D2A75110096D953 /* Modules.swift */, + ); + path = Modules; + sourceTree = ""; + }; + 13AEE62A2D2ABCB40096D953 /* SettingsView */ = { + isa = PBXGroup; + children = ( + 13AEE62B2D2ABCC10096D953 /* SubViews */, + 13AEE61A2D2A78050096D953 /* SettingsView.swift */, + ); + path = SettingsView; + sourceTree = ""; + }; + 13AEE62B2D2ABCC10096D953 /* SubViews */ = { + isa = PBXGroup; + children = ( + 13AEE62C2D2ABCD30096D953 /* SettingsViewModule.swift */, + ); + path = SubViews; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -94,6 +148,9 @@ dependencies = ( ); name = "Sora-JS"; + packageProductDependencies = ( + 13AEE6222D2AAF160096D953 /* Kingfisher */, + ); productName = "Sora-JS"; productReference = 1329D5C32D298198008AEDA2 /* Sora-JS.app */; productType = "com.apple.product-type.application"; @@ -122,6 +179,9 @@ Base, ); mainGroup = 1329D5BA2D298198008AEDA2; + packageReferences = ( + 13AEE6212D2AAF160096D953 /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); productRefGroup = 1329D5C42D298198008AEDA2 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -148,10 +208,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 13AEE6272D2AB1990096D953 /* LibraryView.swift in Sources */, 1329D5C92D298198008AEDA2 /* ContentView.swift in Sources */, + 13AEE62D2D2ABCD30096D953 /* SettingsViewModule.swift in Sources */, 13AEE61D2D2A78160096D953 /* JSController.swift in Sources */, 13AEE6192D2A75110096D953 /* Modules.swift in Sources */, - 13AEE61B2D2A78050096D953 /* Settings.swift in Sources */, + 13AEE6292D2AB2070096D953 /* SearchView.swift in Sources */, + 13AEE6252D2AB1730096D953 /* HomeView.swift in Sources */, + 13AEE61B2D2A78050096D953 /* SettingsView.swift in Sources */, 1329D5C72D298198008AEDA2 /* Sora_JSApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -210,7 +274,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -265,7 +329,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -292,6 +356,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -322,6 +387,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -357,6 +423,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 13AEE6212D2AAF160096D953 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = exactVersion; + version = 7.9.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 13AEE6222D2AAF160096D953 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 13AEE6212D2AAF160096D953 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 1329D5BB2D298198008AEDA2 /* Project object */; } diff --git a/Sora-JS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sora-JS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..bdcfc53 --- /dev/null +++ b/Sora-JS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", + "version": "7.9.1" + } + } + ] + }, + "version": 1 +} diff --git a/Sora-JS/Assets.xcassets/AccentColor.colorset/Contents.json b/Sora-JS/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..bb57667 100644 --- a/Sora-JS/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Sora-JS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,10 @@ { "colors" : [ { + "color" : { + "platform" : "universal", + "reference" : "systemMintColor" + }, "idiom" : "universal" } ], diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/120-1.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/120-1.png new file mode 100644 index 0000000..1cbc052 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/120-1.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/120.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..1cbc052 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/152.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..953db5a Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/167.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..6e41ff7 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/180.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..394bb7b Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/20.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..3dbb016 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/29.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..55dd67d Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-1.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-1.png new file mode 100644 index 0000000..1e9c473 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-1.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-2.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-2.png new file mode 100644 index 0000000..1e9c473 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40-2.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/40.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..1e9c473 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/58-1.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/58-1.png new file mode 100644 index 0000000..25a3dec Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/58-1.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/58.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..25a3dec Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/60.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..d007118 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/76.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..6648094 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/80-1.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/80-1.png new file mode 100644 index 0000000..b048379 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/80-1.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/80.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..b048379 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/87.png b/Sora-JS/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..0659285 Binary files /dev/null and b/Sora-JS/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Sora-JS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sora-JS/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9b..1fa06d6 100644 --- a/Sora-JS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Sora-JS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,91 +1,109 @@ { "images" : [ { + "filename" : "40-2.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "58.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "80.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "120-1.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "40-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "58-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "80-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "152.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "167.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "1024.jpg", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/Sora-JS/ContentView.swift b/Sora-JS/ContentView.swift index ea663bd..c92d876 100644 --- a/Sora-JS/ContentView.swift +++ b/Sora-JS/ContentView.swift @@ -6,173 +6,30 @@ // import SwiftUI - -struct AnimeItem: Identifiable { - let id = UUID() - let title: String - let imageUrl: String -} +import Kingfisher struct ContentView: View { - @StateObject private var jsController = JSController() - @EnvironmentObject var moduleManager: ModuleManager - @State private var searchText = "" - @State private var animeItems: [AnimeItem] = [] - @State private var isSearching = false - @AppStorage("selectedModuleId") private var selectedModuleId: String? - - private var selectedModule: ScrapingModule? { - guard let id = selectedModuleId else { return nil } - return moduleManager.modules.first { $0.id.uuidString == id } - } - var body: some View { - NavigationView { - VStack(spacing: 0) { - if let selectedModule = selectedModule { - HStack { - AsyncImage(url: URL(string: selectedModule.metadata.iconUrl)) { image in - image - .resizable() - .aspectRatio(contentMode: .fit) - } placeholder: { - Color.gray - } - .frame(width: 30, height: 30) - .cornerRadius(6) - - VStack(alignment: .leading) { - Text(selectedModule.metadata.mediaType) - .font(.headline) - Text(selectedModule.metadata.language) - .font(.caption) - .foregroundColor(.gray) - } - - Spacer() - - Menu { - ForEach(moduleManager.modules) { module in - Button { - selectedModuleId = module.id.uuidString - } label: { - HStack { - AsyncImage(url: URL(string: module.metadata.iconUrl)) { image in - image - .resizable() - .aspectRatio(contentMode: .fit) - } placeholder: { - Color.gray - } - .frame(width: 20, height: 20) - .cornerRadius(4) - - Text(module.metadata.mediaType) - Spacer() - if module.id.uuidString == selectedModuleId { - Image(systemName: "checkmark") - } - } - } - } - } label: { - Image(systemName: "chevron.up.chevron.down") - .foregroundColor(.gray) - } - } - .padding() - .background(Color(.systemBackground)) - .shadow(color: Color.black.opacity(0.1), radius: 2, y: 1) - } else { - VStack(spacing: 8) { - Text("No Module Selected") - .font(.headline) - Text("Please select a module from settings") - .font(.caption) - .foregroundColor(.gray) - } - .padding() - .frame(maxWidth: .infinity) - .background(Color(.systemBackground)) - .shadow(color: Color.black.opacity(0.1), radius: 2, y: 1) + TabView { + HomeView() + .tabItem { + Label("Home", systemImage: "house") } - - TextField("Search...", text: $searchText) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding() - .onChange(of: searchText) { newValue in - guard !newValue.isEmpty, let module = selectedModule else { - animeItems = [] - return - } - - isSearching = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - Task { - do { - let jsContent = try moduleManager.getModuleContent(module) - jsController.loadScript(jsContent) - jsController.scrapeAnime(keyword: newValue, module: module) { items in - animeItems = items - isSearching = false - } - } catch { - print("Error loading module: \(error)") - isSearching = false - } - } - } - } - - if isSearching { - ProgressView() - .padding() + + LibraryView() + .tabItem { + Label("Library", systemImage: "books.vertical") } - - ScrollView { - LazyVGrid(columns: [ - GridItem(.flexible(), spacing: 16), - GridItem(.flexible(), spacing: 16) - ], spacing: 16) { - ForEach(animeItems) { item in - VStack(alignment: .leading) { - if let url = URL(string: item.imageUrl) { - AsyncImage(url: url) { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - } placeholder: { - Rectangle() - .fill(Color.gray.opacity(0.2)) - .overlay( - ProgressView() - ) - } - .frame(height: 200) - .clipped() - .cornerRadius(8) - } - - Text(item.title) - .font(.headline) - .lineLimit(2) - .padding(.vertical, 4) - } - .frame(maxWidth: .infinity) - .background(Color(.secondarySystemBackground)) - .cornerRadius(12) - } - } - .padding() + + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") } - } - .navigationTitle("Search") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - NavigationLink(destination: SettingsView()) { - Image(systemName: "gear") + + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") } - } } } } diff --git a/Sora-JS/JSController.swift b/Sora-JS/Loaders/JSController.swift similarity index 100% rename from Sora-JS/JSController.swift rename to Sora-JS/Loaders/JSController.swift diff --git a/Sora-JS/Modules.swift b/Sora-JS/Modules/Modules.swift similarity index 100% rename from Sora-JS/Modules.swift rename to Sora-JS/Modules/Modules.swift diff --git a/Sora-JS/Settings.swift b/Sora-JS/Settings.swift deleted file mode 100644 index 4e4022c..0000000 --- a/Sora-JS/Settings.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Settings.swift -// Sora-JS -// -// Created by Francesco on 05/01/25. -// - -import SwiftUI - -struct SettingsView: View { - @EnvironmentObject var moduleManager: ModuleManager - @AppStorage("selectedModuleId") private var selectedModuleId: String? - @State private var showingAddModule = false - @State private var newModuleUrl = "" - @State private var isLoading = false - @State private var errorMessage: String? - - var body: some View { - List { - ForEach(moduleManager.modules) { module in - ModuleRow(module: module, isSelected: module.id.uuidString == selectedModuleId) - .contentShape(Rectangle()) - .onTapGesture { - selectedModuleId = module.id.uuidString - } - .swipeActions { - Button(role: .destructive) { - if selectedModuleId == module.id.uuidString { - selectedModuleId = nil - } - moduleManager.deleteModule(module) - } label: { - Label("Delete", systemImage: "trash") - } - } - } - } - .navigationTitle("Scraping Modules") - .toolbar { - Button { - showingAddModule = true - } label: { - Label("Add Module", systemImage: "plus") - } - } - .sheet(isPresented: $showingAddModule) { - NavigationView { - Form { - Section(header: Text("New Module")) { - TextField("Module JSON URL", text: $newModuleUrl) - .autocapitalization(.none) - .keyboardType(.URL) - } - - if let error = errorMessage { - Section { - Text(error) - .foregroundColor(.red) - } - } - } - .navigationTitle("Add Module") - .navigationBarItems( - leading: Button("Cancel") { - showingAddModule = false - }, - trailing: Button("Add") { - addModule() - } - .disabled(newModuleUrl.isEmpty || isLoading) - ) - } - } - } - - private func addModule() { - isLoading = true - errorMessage = nil - - Task { - do { - _ = try await moduleManager.addModule(metadataUrl: newModuleUrl) - DispatchQueue.main.async { - isLoading = false - showingAddModule = false - newModuleUrl = "" - } - } catch { - DispatchQueue.main.async { - isLoading = false - errorMessage = error.localizedDescription - } - } - } - } -} - -struct ModuleRow: View { - let module: ScrapingModule - let isSelected: Bool - - var body: some View { - HStack { - AsyncImage(url: URL(string: module.metadata.iconUrl)) { image in - image - .resizable() - .aspectRatio(contentMode: .fit) - } placeholder: { - Color.gray - } - .frame(width: 40, height: 40) - .cornerRadius(8) - - VStack(alignment: .leading) { - Text(module.metadata.mediaType) - .font(.headline) - Text("by \(module.metadata.author)") - .font(.caption) - .foregroundColor(.gray) - Text("\(module.metadata.language) • v\(module.metadata.version)") - .font(.caption2) - .foregroundColor(.gray) - } - - Spacer() - - if isSelected { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.blue) - } - } - } -} diff --git a/Sora-JS/Sora_JSApp.swift b/Sora-JS/Sora_JSApp.swift index 9d33576..70245bd 100644 --- a/Sora-JS/Sora_JSApp.swift +++ b/Sora-JS/Sora_JSApp.swift @@ -9,12 +9,18 @@ import SwiftUI @main struct Sora_JSApp: App { + @StateObject private var settings = Settings() @StateObject private var moduleManager = ModuleManager() var body: some Scene { WindowGroup { ContentView() .environmentObject(moduleManager) + .environmentObject(settings) + .accentColor(settings.accentColor) + .onAppear { + settings.updateAppearance() + } } } } diff --git a/Sora-JS/Views/HomeView.swift b/Sora-JS/Views/HomeView.swift new file mode 100644 index 0000000..db6742f --- /dev/null +++ b/Sora-JS/Views/HomeView.swift @@ -0,0 +1,16 @@ +// +// HomeView.swift +// Sora-JS +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI + +struct HomeView: View { + var body: some View { + Text("Home View") + .font(.largeTitle) + .padding() + } +} diff --git a/Sora-JS/Views/LibraryView.swift b/Sora-JS/Views/LibraryView.swift new file mode 100644 index 0000000..1e6408d --- /dev/null +++ b/Sora-JS/Views/LibraryView.swift @@ -0,0 +1,16 @@ +// +// LibraryView.swift +// Sora-JS +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI + +struct LibraryView: View { + var body: some View { + Text("Library View") + .font(.largeTitle) + .padding() + } +} diff --git a/Sora-JS/Views/SearchView.swift b/Sora-JS/Views/SearchView.swift new file mode 100644 index 0000000..cd144e7 --- /dev/null +++ b/Sora-JS/Views/SearchView.swift @@ -0,0 +1,174 @@ +// +// SearchView.swift +// Sora-JS +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher + +struct AnimeItem: Identifiable { + let id = UUID() + let title: String + let imageUrl: String +} + +struct SearchView: View { + @StateObject private var jsController = JSController() + @EnvironmentObject var moduleManager: ModuleManager + @State private var searchText = "" + @State private var animeItems: [AnimeItem] = [] + @State private var isSearching = false + @AppStorage("selectedModuleId") private var selectedModuleId: String? + + private var selectedModule: ScrapingModule? { + guard let id = selectedModuleId else { return nil } + return moduleManager.modules.first { $0.id.uuidString == id } + } + + var body: some View { + NavigationView { + ScrollView { + VStack(spacing: 0) { + if selectedModule == nil { + VStack(spacing: 8) { + Text("No Module Selected") + .font(.headline) + Text("Please select a module from settings") + .font(.caption) + .foregroundColor(.gray) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color(.systemBackground)) + .shadow(color: Color.black.opacity(0.1), radius: 2, y: 1) + } + + SearchBar(text: $searchText, onSearchButtonClicked: performSearch) + .padding() + .disabled(selectedModule == nil) + + if isSearching { + ProgressView() + .padding() + } + + LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) { + ForEach(animeItems) { item in + VStack { + KFImage(URL(string: item.imageUrl)) + .resizable() + .aspectRatio(2/3, contentMode: .fill) + .cornerRadius(10) + .frame(width: 150, height: 225) + + Text(item.title) + .font(.subheadline) + .foregroundColor(Color.primary) + .padding([.leading, .bottom], 8) + .lineLimit(1) + } + } + } + .padding() + } + } + .navigationTitle("Search") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + HStack { + if let selectedModule = selectedModule { + Text(selectedModule.metadata.mediaType) + .font(.headline) + .foregroundColor(.gray) + } + Menu { + ForEach(moduleManager.modules) { 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.mediaType) + Spacer() + if module.id.uuidString == selectedModuleId { + Image(systemName: "checkmark") + } + } + } + } + } label: { + Image(systemName: "chevron.up.chevron.down") + .foregroundColor(.gray) + } + } + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } + + private func performSearch() { + guard !searchText.isEmpty, let module = selectedModule else { + animeItems = [] + return + } + + isSearching = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + Task { + do { + let jsContent = try moduleManager.getModuleContent(module) + jsController.loadScript(jsContent) + jsController.scrapeAnime(keyword: searchText, module: module) { items in + animeItems = items + isSearching = false + } + } catch { + print("Error loading module: \(error)") + isSearching = false + } + } + } + } +} + +struct SearchBar: View { + @Binding var text: String + var onSearchButtonClicked: () -> Void + + var body: some View { + HStack { + TextField("Search...", text: $text, onCommit: onSearchButtonClicked) + .padding(7) + .padding(.horizontal, 25) + .background(Color(.systemGray6)) + .cornerRadius(8) + .overlay( + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .padding(.leading, 8) + + if !text.isEmpty { + Button(action: { + self.text = "" + }) { + Image(systemName: "multiply.circle.fill") + .foregroundColor(.gray) + .padding(.trailing, 8) + } + } + } + ) + } + } +} diff --git a/Sora-JS/Views/SettingsView/SettingsView.swift b/Sora-JS/Views/SettingsView/SettingsView.swift new file mode 100644 index 0000000..f882c4f --- /dev/null +++ b/Sora-JS/Views/SettingsView/SettingsView.swift @@ -0,0 +1,139 @@ +// +// SettingsView.swift +// Sora-JS +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var settings: Settings + + var body: some View { + NavigationView { + 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) + } + .pickerStyle(SegmentedPickerStyle()) + } + } + + Section(header: Text("External Features")) { + NavigationLink(destination: SettingsViewModule()) { + Text("Modules") + } + } + + Section(header: Text("Info")) { + Button(action: { + if let url = URL(string: "https://github.com/cranci1/Sora") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Sora github repo") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + Button(action: { + if let url = URL(string: "https://github.com/cranci1/Sora/issues") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Report an issue") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + Button(action: { + if let url = URL(string: "https://discord.gg/x7hppDWFDZ") { + UIApplication.shared.open(url) + } + }) { + HStack { + Text("Join the Discord") + .foregroundColor(.primary) + Spacer() + Image(systemName: "safari") + .foregroundColor(.secondary) + } + } + } + } + .navigationTitle("Settings") + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} + +enum Appearance: String, CaseIterable, Identifiable { + case system, light, dark + + var id: String { self.rawValue } +} + +class Settings: ObservableObject { + @Published var accentColor: Color { + didSet { + saveAccentColor(accentColor) + } + } + @Published var selectedAppearance: Appearance { + didSet { + UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance") + updateAppearance() + } + } + + init() { + if let colorData = UserDefaults.standard.data(forKey: "accentColor"), + let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(colorData) as? UIColor { + 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 + } + updateAppearance() + } + + private func saveAccentColor(_ color: Color) { + let uiColor = UIColor(color) + do { + let colorData = try NSKeyedArchiver.archivedData(withRootObject: uiColor, requiringSecureCoding: false) + UserDefaults.standard.set(colorData, forKey: "accentColor") + } catch { + print("Failed to save accent color: \(error.localizedDescription)") + } + } + + func updateAppearance() { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return } + switch selectedAppearance { + case .system: + windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified + case .light: + windowScene.windows.first?.overrideUserInterfaceStyle = .light + case .dark: + windowScene.windows.first?.overrideUserInterfaceStyle = .dark + } + } +} diff --git a/Sora-JS/Views/SettingsView/SubViews/SettingsViewModule.swift b/Sora-JS/Views/SettingsView/SubViews/SettingsViewModule.swift new file mode 100644 index 0000000..6a21f60 --- /dev/null +++ b/Sora-JS/Views/SettingsView/SubViews/SettingsViewModule.swift @@ -0,0 +1,126 @@ +// +// SettingsViewModule.swift +// Sora-JS +// +// Created by Francesco on 05/01/25. +// + +import SwiftUI +import Kingfisher + +struct SettingsViewModule: View { + @EnvironmentObject var moduleManager: ModuleManager + @AppStorage("selectedModuleId") private var selectedModuleId: String? + @State private var isLoading = false + @State private var errorMessage: String? + + var body: some View { + VStack { + Form { + ForEach(moduleManager.modules) { module in + HStack { + KFImage(URL(string: module.metadata.iconUrl)) + .resizable() + .frame(width: 50, height: 50) + .clipShape(Circle()) + .padding(.trailing, 10) + + VStack(alignment: .leading) { + Text(module.metadata.mediaType) + .font(.system(size: 16)) + .foregroundColor(.primary) + Text("Author: \(module.metadata.author)") + .font(.system(size: 14)) + .foregroundColor(.secondary) + Text("\(module.metadata.language) • v\(module.metadata.version)") + .font(.system(size: 14)) + .foregroundColor(.secondary) + } + + Spacer() + + if module.id.uuidString == selectedModuleId { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + .frame(width: 25, height: 25) + } + } + .contentShape(Rectangle()) + .onTapGesture { + selectedModuleId = module.id.uuidString + } + .contextMenu { + Button(action: { + UIPasteboard.general.string = module.metadata.iconUrl + }) { + Label("Copy URL", systemImage: "doc.on.doc") + } + Button(role: .destructive) { + if selectedModuleId == module.id.uuidString { + selectedModuleId = nil + } + moduleManager.deleteModule(module) + } label: { + Label("Delete", systemImage: "trash") + } + } + .swipeActions { + Button(role: .destructive) { + if selectedModuleId == module.id.uuidString { + selectedModuleId = nil + } + moduleManager.deleteModule(module) + } label: { + Label("Delete", systemImage: "trash") + } + } + } + } + .navigationTitle("Modules") + .navigationBarItems(trailing: Button(action: { + showAddModuleAlert() + }) { + Image(systemName: "plus") + .resizable() + .padding(5) + }) + } + } + + func showAddModuleAlert() { + let alert = UIAlertController(title: "Add Module", message: "Enter the URL of the module file", preferredStyle: .alert) + alert.addTextField { textField in + textField.placeholder = "https://real.url/module.json" + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in + if let url = alert.textFields?.first?.text { + addModule(from: url) + } + })) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(alert, animated: true, completion: nil) + } + } + + private func addModule(from url: String) { + isLoading = true + errorMessage = nil + + Task { + do { + _ = try await moduleManager.addModule(metadataUrl: url) + DispatchQueue.main.async { + isLoading = false + } + } catch { + DispatchQueue.main.async { + isLoading = false + errorMessage = error.localizedDescription + } + } + } + } +}