Huge things frfrfrf
|
|
@ -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 */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"platform" : "universal",
|
||||
"reference" : "systemMintColor"
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/120-1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/120.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/152.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/167.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/180.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/20.png
Normal file
|
After Width: | Height: | Size: 384 B |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/29.png
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/40-1.png
Normal file
|
After Width: | Height: | Size: 832 B |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/40-2.png
Normal file
|
After Width: | Height: | Size: 832 B |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/40.png
Normal file
|
After Width: | Height: | Size: 832 B |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/58-1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/58.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/60.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/76.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/80-1.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/80.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Sora-JS/Assets.xcassets/AppIcon.appiconset/87.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
Sora-JS/Views/HomeView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
16
Sora-JS/Views/LibraryView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
174
Sora-JS/Views/SearchView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
139
Sora-JS/Views/SettingsView/SettingsView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Sora-JS/Views/SettingsView/SubViews/SettingsViewModule.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||