Huge things frfrfrf

This commit is contained in:
cranci1 2025-01-05 14:26:46 +01:00
parent 4f9d80c4c2
commit e8f1a7d896
31 changed files with 625 additions and 301 deletions

View file

@ -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 = "<group>"; };
1329D5DA2D29821B008AEDA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
13AEE6182D2A75110096D953 /* Modules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modules.swift; sourceTree = "<group>"; };
13AEE61A2D2A78050096D953 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
13AEE61A2D2A78050096D953 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
13AEE61C2D2A78160096D953 /* JSController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
13AEE6242D2AB1730096D953 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
13AEE6262D2AB1990096D953 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
13AEE6282D2AB2070096D953 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
13AEE62C2D2ABCD30096D953 /* SettingsViewModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModule.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -78,6 +88,50 @@
path = "Preview Content";
sourceTree = "<group>";
};
13AEE61E2D2AAD1E0096D953 /* Views */ = {
isa = PBXGroup;
children = (
13AEE62A2D2ABCB40096D953 /* SettingsView */,
13AEE6242D2AB1730096D953 /* HomeView.swift */,
13AEE6262D2AB1990096D953 /* LibraryView.swift */,
13AEE6282D2AB2070096D953 /* SearchView.swift */,
);
path = Views;
sourceTree = "<group>";
};
13AEE61F2D2AAD2D0096D953 /* Loaders */ = {
isa = PBXGroup;
children = (
13AEE61C2D2A78160096D953 /* JSController.swift */,
);
path = Loaders;
sourceTree = "<group>";
};
13AEE6202D2AAD390096D953 /* Modules */ = {
isa = PBXGroup;
children = (
13AEE6182D2A75110096D953 /* Modules.swift */,
);
path = Modules;
sourceTree = "<group>";
};
13AEE62A2D2ABCB40096D953 /* SettingsView */ = {
isa = PBXGroup;
children = (
13AEE62B2D2ABCC10096D953 /* SubViews */,
13AEE61A2D2A78050096D953 /* SettingsView.swift */,
);
path = SettingsView;
sourceTree = "<group>";
};
13AEE62B2D2ABCC10096D953 /* SubViews */ = {
isa = PBXGroup;
children = (
13AEE62C2D2ABCD30096D953 /* SettingsViewModule.swift */,
);
path = SubViews;
sourceTree = "<group>";
};
/* 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 */;
}

View file

@ -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
}

View file

@ -1,6 +1,10 @@
{
"colors" : [
{
"color" : {
"platform" : "universal",
"reference" : "systemMintColor"
},
"idiom" : "universal"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -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"

View file

@ -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")
}
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}
)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}
}
}