From 30785549bdb93063c2e6cbee4c90514ec838efd0 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:34:30 +0200 Subject: [PATCH] Added Module Settings support --- .../Utlis & Misc/Modules/ModuleSettings.swift | 126 +++++++++ .../Modules/ModuleSettingsView.swift | 243 ++++++++++++++++++ .../SettingsSubViews/SettingsViewModule.swift | 29 +++ Sulfur.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/swiftpm/Package.resolved | 3 +- 5 files changed, 413 insertions(+), 8 deletions(-) create mode 100644 Sora/Utlis & Misc/Modules/ModuleSettings.swift create mode 100644 Sora/Utlis & Misc/Modules/ModuleSettingsView.swift diff --git a/Sora/Utlis & Misc/Modules/ModuleSettings.swift b/Sora/Utlis & Misc/Modules/ModuleSettings.swift new file mode 100644 index 0000000..f384d30 --- /dev/null +++ b/Sora/Utlis & Misc/Modules/ModuleSettings.swift @@ -0,0 +1,126 @@ +// +// ModuleSettings.swift +// Sulfur +// +// Created by Francesco on 14/07/26. +// + +import Foundation + +struct ModuleSetting: Identifiable, Hashable { + enum SettingType: String, Codable { + case string, bool, int, float + + init(rawType: String) { + switch rawType.lowercased() { + case "bool", "boolean": + self = .bool + case "int", "integer": + self = .int + case "float", "double", "number": + self = .float + default: + self = .string + } + } + } + + var id: String { key } + let key: String + var value: String + let type: SettingType + let comment: String? + let options: [String]? + + init(key: String, value: String, type: SettingType, comment: String? = nil, options: [String]? = nil) { + self.key = key + self.value = value + self.type = type + self.comment = comment + self.options = options + } +} + +private struct ModuleSettingSchemaEntry: Codable { + let key: String + let type: String + let comment: String? + let defaultValue: String? + let options: [String]? + + enum CodingKeys: String, CodingKey { + case key, type, comment, options + case defaultValue = "default" + } +} + +extension ModuleManager { + func hasSettings(_ module: ScrapingModule) -> Bool { + guard let content = try? getModuleContent(module) else { return false } + return !Self.parseSettingsSchema(from: content).isEmpty + } + + func getModuleSettings(_ module: ScrapingModule) -> [ModuleSetting] { + guard let content = try? getModuleContent(module) else { return [] } + let schema = Self.parseSettingsSchema(from: content) + let overrides = loadSettingOverrides(for: module) + + return schema.map { entry in + let type = ModuleSetting.SettingType(rawValue: entry.type.lowercased()) ?? .string + let storedValue = overrides[entry.key] ?? entry.defaultValue ?? "" + return ModuleSetting( + key: entry.key, + value: storedValue, + type: type, + comment: entry.comment, + options: entry.options + ) + } + } + + @discardableResult + func updateModuleSettings(_ module: ScrapingModule, settings: [ModuleSetting]) -> Bool { + var overrides: [String: String] = [:] + for setting in settings { + overrides[setting.key] = setting.value + } + + guard let data = try? JSONEncoder().encode(overrides) else { + Logger.shared.log("Failed to encode settings for module: \(module.metadata.sourceName)", type: "Error") + return false + } + + UserDefaults.standard.set(data, forKey: settingsStorageKey(for: module)) + Logger.shared.log("Updated settings for module: \(module.metadata.sourceName)") + return true + } + + private func settingsStorageKey(for module: ScrapingModule) -> String { + "moduleSettings_\(module.id.uuidString)" + } + + private func loadSettingOverrides(for module: ScrapingModule) -> [String: String] { + guard + let data = UserDefaults.standard.data(forKey: settingsStorageKey(for: module)), + let dict = try? JSONDecoder().decode([String: String].self, from: data) + else { return [:] } + return dict + } + + fileprivate static func parseSettingsSchema(from script: String) -> [ModuleSettingSchemaEntry] { + guard + let markerRange = script.range(of: "SETTINGS_SCHEMA:"), + let lineEnd = script[markerRange.upperBound...].firstIndex(of: "\n") + else { return [] } + + let jsonString = String(script[markerRange.upperBound.. Void let onSelect: () -> Void + let onEditSettings: () -> Void var body: some View { VStack(spacing: 0) { @@ -148,6 +150,16 @@ fileprivate struct ModuleListItemView: View { Spacer() + if hasSettings { + Button(action: onEditSettings) { + Image(systemName: "pencil.circle") + .foregroundStyle(.gray) + .frame(width: 20, height: 20) + } + .buttonStyle(PlainButtonStyle()) + .padding(.trailing, module.id.uuidString == selectedModuleId ? 12 : 0) + } + if module.id.uuidString == selectedModuleId { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Color.accentColor) @@ -237,12 +249,16 @@ struct SettingsViewModule: View { ModuleListItemView( module: module, selectedModuleId: selectedModuleId, + hasSettings: moduleManager.hasSettings(module), onDelete: { moduleManager.deleteModule(module) DropManager.shared.showDrop(title: NSLocalizedString("Module Removed", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "trash")) }, onSelect: { selectedModuleId = module.id.uuidString + }, + onEditSettings: { + showModuleSettings(module) } ) @@ -394,4 +410,17 @@ struct SettingsViewModule: View { } } } + + func showModuleSettings(_ module: Module) { + DispatchQueue.main.async { + let settingsView = ModuleSettingsView(module: module) + .environmentObject(self.moduleManager) + let hostingController = UIHostingController(rootView: settingsView) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + window.rootViewController?.present(hostingController, animated: true, completion: nil) + } + } + } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 8dec489..f4e5799 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1202D99951700A0140B /* JSController-Streams.swift */; }; 132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; }; 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; + 132C590F30067E590049A742 /* ModuleSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132C590E30067E590049A742 /* ModuleSettings.swift */; }; + 132C591130067EAF0049A742 /* ModuleSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132C591030067EAF0049A742 /* ModuleSettingsView.swift */; }; 13367ECC2DF70698009CB33F /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECB2DF70698009CB33F /* Nuke */; }; 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECD2DF70698009CB33F /* NukeUI */; }; 133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */; }; @@ -195,6 +197,8 @@ 132AF1202D99951700A0140B /* JSController-Streams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Streams.swift"; sourceTree = ""; }; 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; + 132C590E30067E590049A742 /* ModuleSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleSettings.swift; sourceTree = ""; }; + 132C591030067EAF0049A742 /* ModuleSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleSettingsView.swift; sourceTree = ""; }; 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWatchingActivity.swift; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; @@ -688,6 +692,8 @@ 139935652D468C450065CEFF /* ModuleManager.swift */, 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */, 133D7C892D2BE2640075467E /* Modules.swift */, + 132C590E30067E590049A742 /* ModuleSettings.swift */, + 132C591030067EAF0049A742 /* ModuleSettingsView.swift */, ); path = Modules; sourceTree = ""; @@ -1140,6 +1146,7 @@ 0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */, 0414ECFE2E32D6EF00A7E76A /* Bundle+Language.swift in Sources */, 0414ECFF2E32D6EF00A7E76A /* LocalizationManager.swift in Sources */, + 132C590F30067E590049A742 /* ModuleSettings.swift in Sources */, 7273F0402E26B19700DF083D /* DownloadPersistence.swift in Sources */, 0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */, 72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */, @@ -1153,6 +1160,7 @@ 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, 7260B66D2E32A8CB00365CDA /* OrphanedDownloadsView.swift in Sources */, + 132C591130067EAF0049A742 /* ModuleSettingsView.swift in Sources */, 146A48DE2F02F1980017D145 /* IntroDB-FetchIntro.swift in Sources */, 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */, 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */, @@ -1465,7 +1473,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = USF65A4WGS; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1482,7 +1490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1507,7 +1515,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = USF65A4WGS; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1524,7 +1532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1574,8 +1582,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/cranci1/SoraCore"; requirement = { - branch = main; - kind = branch; + kind = revision; + revision = 50ddc160fd606e9708adefc1d9fc7003b574446b; }; }; 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = { diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 36de013..1ec82ec 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cranci1/SoraCore", "state" : { - "branch" : "main", - "revision" : "543fe1c8c1d421201aeb10e7d2438a91c90c8ac5" + "revision" : "50ddc160fd606e9708adefc1d9fc7003b574446b" } } ],