mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-18 07:02:45 +00:00
Merge pull request #20 from Seeike/main
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
This commit is contained in:
commit
ce29144951
11 changed files with 210 additions and 102 deletions
|
|
@ -32,6 +32,7 @@
|
|||
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */; };
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
|
||||
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */; };
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
|
||||
1EE1DA962D3553C2002AEF73 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1EE1DA952D3553C2002AEF73 /* Localizable.xcstrings */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -62,6 +63,7 @@
|
|||
13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = "<group>"; };
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||
1EE1DA952D3553C2002AEF73 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -141,6 +143,7 @@
|
|||
133D7C832D2BE2630075467E /* SettingsSubViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
|
||||
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
|
||||
133D7C842D2BE2630075467E /* SettingsViewModule.swift */,
|
||||
);
|
||||
|
|
@ -340,6 +343,7 @@
|
|||
1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */,
|
||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */,
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@
|
|||
"value" : "Autor: %@"
|
||||
}
|
||||
},
|
||||
"sk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Autor: %@"
|
||||
}
|
||||
},
|
||||
"sq" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
|
@ -67,6 +73,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Clear Logs" : {
|
||||
|
||||
},
|
||||
"Copy to Clipboard" : {
|
||||
|
||||
},
|
||||
"Copy URL" : {
|
||||
"localizations" : {
|
||||
|
|
@ -99,6 +111,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Debug" : {
|
||||
|
||||
},
|
||||
"Delete" : {
|
||||
"localizations" : {
|
||||
|
|
@ -307,6 +322,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Logs" : {
|
||||
|
||||
},
|
||||
"Mark as Watched" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -17,31 +17,31 @@ class JSController: ObservableObject {
|
|||
|
||||
private func setupContext() {
|
||||
let logFunction: @convention(block) (String) -> Void = { message in
|
||||
Logger.shared.log("JavaScript log: \(message)")
|
||||
Logger.shared.log("JavaScript log: \(message)", type: "Debug")
|
||||
}
|
||||
context.setObject(logFunction, forKeyedSubscript: "log" as NSString)
|
||||
|
||||
let fetchNativeFunction: @convention(block) (String, JSValue, JSValue) -> Void = { urlString, resolve, reject in
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Invalid URL")
|
||||
Logger.shared.log("Invalid URL",type: "Error")
|
||||
reject.call(withArguments: ["Invalid URL"])
|
||||
return
|
||||
}
|
||||
let task = URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)")
|
||||
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)",type: "Error")
|
||||
reject.call(withArguments: [error.localizedDescription])
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
Logger.shared.log("No data in response")
|
||||
Logger.shared.log("No data in response",type: "Error")
|
||||
reject.call(withArguments: ["No data"])
|
||||
return
|
||||
}
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
resolve.call(withArguments: [text])
|
||||
} else {
|
||||
Logger.shared.log("Unable to decode data to text")
|
||||
Logger.shared.log("Unable to decode data to text",type: "Error")
|
||||
reject.call(withArguments: ["Unable to decode data"])
|
||||
}
|
||||
}
|
||||
|
|
@ -77,18 +77,18 @@ class JSController: ObservableObject {
|
|||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)")
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML")
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log(html)
|
||||
Logger.shared.log(html,type: "Debug")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
let resultItems = results.map { item in
|
||||
|
|
@ -102,7 +102,7 @@ class JSController: ObservableObject {
|
|||
completion(resultItems)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results")
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
}
|
||||
}.resume()
|
||||
|
|
@ -118,13 +118,13 @@ class JSController: ObservableObject {
|
|||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)")
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML")
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ class JSController: ObservableObject {
|
|||
var resultItems: [MediaItem] = []
|
||||
var episodeLinks: [EpisodeLink] = []
|
||||
|
||||
Logger.shared.log(html)
|
||||
Logger.shared.log(html,type: "Debug")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
|
||||
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
|
||||
resultItems = results.map { item in
|
||||
|
|
@ -143,7 +143,7 @@ class JSController: ObservableObject {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse results")
|
||||
Logger.shared.log("Failed to parse results",type: "Error")
|
||||
}
|
||||
|
||||
if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"),
|
||||
|
|
@ -171,25 +171,26 @@ class JSController: ObservableObject {
|
|||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
Logger.shared.log("Network error: \(error)")
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML")
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shared.log(html)
|
||||
Logger.shared.log(html,type: "Debug")
|
||||
if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"),
|
||||
let streamUrl = parseFunction.call(withArguments: [html]).toString() {
|
||||
Logger.shared.log("Staring stream from: \(streamUrl)", type: "Stream")
|
||||
DispatchQueue.main.async {
|
||||
completion(streamUrl)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to extract stream URL")
|
||||
Logger.shared.log("Failed to extract stream URL",type: "Error")
|
||||
DispatchQueue.main.async { completion(nil) }
|
||||
}
|
||||
}.resume()
|
||||
|
|
@ -197,27 +198,27 @@ class JSController: ObservableObject {
|
|||
|
||||
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)")
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
|
||||
Logger.shared.log("No JavaScript function searchResults found")
|
||||
Logger.shared.log("No JavaScript function searchResults found",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
|
||||
guard let promise = promiseValue else {
|
||||
Logger.shared.log("searchResults did not return a Promise")
|
||||
Logger.shared.log("searchResults did not return a Promise",type: "Error")
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
|
||||
Logger.shared.log(result.toString())
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonString = result.toString(),
|
||||
let data = jsonString.data(using: .utf8) {
|
||||
do {
|
||||
|
|
@ -234,19 +235,19 @@ class JSController: ObservableObject {
|
|||
}
|
||||
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON")
|
||||
Logger.shared.log("Failed to parse JSON",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error: \(error)")
|
||||
Logger.shared.log("JSON parsing error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string")
|
||||
Logger.shared.log("Result is not a string",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
|
|
@ -254,7 +255,7 @@ class JSController: ObservableObject {
|
|||
}
|
||||
|
||||
let catchBlock: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))")
|
||||
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))",type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
completion([])
|
||||
}
|
||||
|
|
@ -274,19 +275,19 @@ class JSController: ObservableObject {
|
|||
}
|
||||
|
||||
if let exception = context.exception {
|
||||
Logger.shared.log("JavaScript exception: \(exception)")
|
||||
Logger.shared.log("JavaScript exception: \(exception)",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else {
|
||||
Logger.shared.log("No JavaScript function extractDetails found")
|
||||
Logger.shared.log("No JavaScript function extractDetails found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else {
|
||||
Logger.shared.log("No JavaScript function extractEpisodes found")
|
||||
Logger.shared.log("No JavaScript function extractEpisodes found",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
|
@ -299,13 +300,13 @@ class JSController: ObservableObject {
|
|||
dispatchGroup.enter()
|
||||
let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseDetails = promiseValueDetails else {
|
||||
Logger.shared.log("extractDetails did not return a Promise")
|
||||
Logger.shared.log("extractDetails did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString())
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfDetails = result.toString(),
|
||||
let dataDetails = jsonOfDetails.data(using: .utf8) {
|
||||
do {
|
||||
|
|
@ -318,19 +319,19 @@ class JSController: ObservableObject {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractDetails")
|
||||
Logger.shared.log("Failed to parse JSON of extractDetails",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extract details: \(error)")
|
||||
Logger.shared.log("JSON parsing error of extract details: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractDetails")
|
||||
Logger.shared.log("Result is not a string of extractDetails",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))")
|
||||
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
|
|
@ -343,13 +344,13 @@ class JSController: ObservableObject {
|
|||
dispatchGroup.enter()
|
||||
let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString])
|
||||
guard let promiseEpisodes = promiseValueEpisodes else {
|
||||
Logger.shared.log("extractEpisodes did not return a Promise")
|
||||
Logger.shared.log("extractEpisodes did not return a Promise",type: "Error")
|
||||
completion([], [])
|
||||
return
|
||||
}
|
||||
|
||||
let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in
|
||||
Logger.shared.log(result.toString())
|
||||
Logger.shared.log(result.toString(),type: "Debug")
|
||||
if let jsonOfEpisodes = result.toString(),
|
||||
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
|
||||
do {
|
||||
|
|
@ -361,19 +362,19 @@ class JSController: ObservableObject {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON of extractEpisodes")
|
||||
Logger.shared.log("Failed to parse JSON of extractEpisodes",type: "Error")
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)")
|
||||
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)",type: "Error")
|
||||
}
|
||||
} else {
|
||||
Logger.shared.log("Result is not a string of extractEpisodes")
|
||||
Logger.shared.log("Result is not a string of extractEpisodes",type: "Error")
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in
|
||||
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))")
|
||||
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))",type: "Error")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,32 +10,34 @@ import Foundation
|
|||
class Logger {
|
||||
static let shared = Logger()
|
||||
|
||||
enum LogLevel: String {
|
||||
case info = "INFO"
|
||||
case warning = "WARNING"
|
||||
case error = "ERROR"
|
||||
struct LogEntry {
|
||||
let message: String
|
||||
let type: String
|
||||
let timestamp: Date
|
||||
}
|
||||
|
||||
private var logs: [(level: LogLevel, message: String, timestamp: Date)] = []
|
||||
private var logs: [LogEntry] = []
|
||||
private let logFileURL: URL
|
||||
private let logFilterViewModel = LogFilterViewModel.shared
|
||||
|
||||
private init() {
|
||||
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
logFileURL = documentDirectory.appendingPathComponent("logs.txt")
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
func log(_ message: String, level: LogLevel = .info) {
|
||||
let entry = (level: level, message: message, timestamp: Date())
|
||||
func log(_ message: String, type: String = "General") {
|
||||
guard logFilterViewModel.isFilterEnabled(for: type) else { return }
|
||||
|
||||
let entry = LogEntry(message: message, type: type, timestamp: Date())
|
||||
logs.append(entry)
|
||||
saveLogToFile(entry)
|
||||
}
|
||||
|
||||
func getLogs() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] [\($0.level.rawValue)] \($0.message)" }
|
||||
.joined(separator: "\n---\n")
|
||||
dateFormatter.dateFormat = "dd-MM-yyyy HH:mm:ss"
|
||||
return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] [\($0.type)] \($0.message)" }
|
||||
.joined(separator: "\n----\n")
|
||||
}
|
||||
|
||||
func clearLogs() {
|
||||
|
|
@ -43,32 +45,11 @@ class Logger {
|
|||
try? FileManager.default.removeItem(at: logFileURL)
|
||||
}
|
||||
|
||||
private func loadLogs() {
|
||||
guard let data = try? Data(contentsOf: logFileURL),
|
||||
let content = String(data: data, encoding: .utf8) else { return }
|
||||
|
||||
private func saveLogToFile(_ log: LogEntry) {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dateFormatter.dateFormat = "dd-MM-yyyy HH:mm:ss"
|
||||
|
||||
content.components(separatedBy: "\n---\n").forEach { line in
|
||||
let components = line.components(separatedBy: "] [")
|
||||
guard components.count == 3,
|
||||
let timestampString = components.first?.dropFirst().trimmingCharacters(in: .whitespaces),
|
||||
let timestamp = dateFormatter.date(from: timestampString),
|
||||
let message = components.last?.dropLast() else { return }
|
||||
|
||||
let levelRaw = components[1].trimmingCharacters(in: .whitespaces)
|
||||
guard let level = LogLevel(rawValue: levelRaw) else { return }
|
||||
|
||||
logs.append((level: level, message: String(message), timestamp: timestamp))
|
||||
}
|
||||
}
|
||||
|
||||
private func saveLogToFile(_ log: (level: LogLevel, message: String, timestamp: Date)) {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
let logString = "[\(dateFormatter.string(from: log.timestamp))] [\(log.level.rawValue)] \(log.message)\n---\n"
|
||||
let logString = "[\(dateFormatter.string(from: log.timestamp))] [\(log.type)] \(log.message)\n---\n"
|
||||
|
||||
if let data = logString.data(using: .utf8) {
|
||||
if FileManager.default.fileExists(atPath: logFileURL.path) {
|
||||
|
|
@ -82,4 +63,4 @@ class Logger {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,19 +103,21 @@ class ModuleManager: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.modules.append(module)
|
||||
self.saveModules()
|
||||
Logger.shared.log("Added module: \(module.metadata.sourceName)")
|
||||
}
|
||||
|
||||
return module
|
||||
}
|
||||
|
||||
|
||||
func deleteModule(_ module: ScrapingModule) {
|
||||
let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath)
|
||||
try? fileManager.removeItem(at: localUrl)
|
||||
|
||||
modules.removeAll { $0.id == module.id }
|
||||
saveModules()
|
||||
Logger.shared.log("Deleted module: \(module.metadata.sourceName)")
|
||||
}
|
||||
|
||||
|
||||
func getModuleContent(_ module: ScrapingModule) throws -> String {
|
||||
let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath)
|
||||
return try String(contentsOf: localUrl, encoding: .utf8)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class LibraryManager: ObservableObject {
|
|||
do {
|
||||
bookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ class LibraryManager: ObservableObject {
|
|||
let encoded = try JSONEncoder().encode(bookmarks)
|
||||
UserDefaults.standard.set(encoded, forKey: bookmarksKey)
|
||||
} catch {
|
||||
Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)")
|
||||
Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ struct EpisodeCell: View {
|
|||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
if let error = error {
|
||||
Logger.shared.log("Failed to fetch episode details: \(error)")
|
||||
Logger.shared.log("Failed to fetch episode details: \(error)", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ struct EpisodeCell: View {
|
|||
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
|
||||
let title = episodeDetails["title"] as? [String: String],
|
||||
let image = episodeDetails["image"] as? String else {
|
||||
Logger.shared.log("Invalid response format")
|
||||
Logger.shared.log("Invalid response format", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ struct EpisodeCell: View {
|
|||
self.isLoading = false
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Failed to parse JSON: \(error)")
|
||||
Logger.shared.log("Failed to parse JSON: \(error)", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error loading module: \(error)")
|
||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error loading module: \(error)")
|
||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ struct MediaInfoView: View {
|
|||
|
||||
private func openSafariViewController(with urlString: String) {
|
||||
guard let url = URL(string: urlString) else {
|
||||
Logger.shared.log("Unable to open the webpage")
|
||||
Logger.shared.log("Unable to open the webpage", type: "Error")
|
||||
return
|
||||
}
|
||||
let safariViewController = SFSafariViewController(url: url)
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ struct SearchView: View {
|
|||
}
|
||||
|
||||
private func performSearch() {
|
||||
Logger.shared.log("Searching for: \(searchText)", type: "General")
|
||||
guard !searchText.isEmpty, let module = selectedModule else {
|
||||
searchItems = []
|
||||
hasNoResults = false
|
||||
|
|
@ -168,7 +169,7 @@ struct SearchView: View {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error loading module: \(error)")
|
||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||
isSearching = false
|
||||
hasNoResults = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import SwiftUI
|
|||
|
||||
struct SettingsViewLogger: View {
|
||||
@State private var logs: String = ""
|
||||
|
||||
@StateObject private var filterViewModel = LogFilterViewModel.shared
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView {
|
||||
|
|
@ -27,22 +28,28 @@ struct SettingsViewLogger: View {
|
|||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = logs
|
||||
}) {
|
||||
Label("Copy to Clipboard", systemImage: "doc.on.doc")
|
||||
HStack {
|
||||
NavigationLink(destination: SettingsViewLoggerFilter(viewModel: filterViewModel)) {
|
||||
Image(systemName: "gearshape")
|
||||
|
||||
Menu {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = logs
|
||||
}) {
|
||||
Label("Copy to Clipboard", systemImage: "doc.on.doc")
|
||||
}
|
||||
Button(role: .destructive, action: {
|
||||
Logger.shared.clearLogs()
|
||||
logs = Logger.shared.getLogs()
|
||||
}) {
|
||||
Label("Clear Logs", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
Button(role: .destructive, action: {
|
||||
Logger.shared.clearLogs()
|
||||
logs = Logger.shared.getLogs()
|
||||
}) {
|
||||
Label("Clear Logs", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
//
|
||||
// SettingsViewLoggerFilter.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by seiike on 21/01/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LogFilter: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let type: String
|
||||
var isEnabled: Bool
|
||||
let description: String
|
||||
}
|
||||
|
||||
|
||||
class LogFilterViewModel: ObservableObject {
|
||||
static let shared = LogFilterViewModel() // Singleton instance
|
||||
|
||||
@Published var filters: [LogFilter] = [] {
|
||||
didSet {
|
||||
saveFiltersToUserDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
private let userDefaultsKey = "LogFilterStates"
|
||||
private let hardcodedFilters: [(type: String, description: String, defaultState: Bool)] = [
|
||||
("General", "Logs for general events and activities.", true),
|
||||
("Stream", "Logs for streaming and video playback.", true),
|
||||
("Error", "Logs for errors and critical issues.", true),
|
||||
("Debug", "Logs for debugging and troubleshooting.", false)
|
||||
]
|
||||
|
||||
private init() {
|
||||
loadFilters()
|
||||
}
|
||||
|
||||
func loadFilters() {
|
||||
if let savedStates = UserDefaults.standard.dictionary(forKey: userDefaultsKey) as? [String: Bool] {
|
||||
filters = hardcodedFilters.map {
|
||||
LogFilter(
|
||||
type: $0.type,
|
||||
isEnabled: savedStates[$0.type] ?? $0.defaultState,
|
||||
description: $0.description
|
||||
)
|
||||
}
|
||||
} else {
|
||||
filters = hardcodedFilters.map {
|
||||
LogFilter(type: $0.type, isEnabled: $0.defaultState, description: $0.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFilter(for type: String) {
|
||||
if let index = filters.firstIndex(where: { $0.type == type }) {
|
||||
filters[index].isEnabled.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
func isFilterEnabled(for type: String) -> Bool {
|
||||
return filters.first(where: { $0.type == type })?.isEnabled ?? true
|
||||
}
|
||||
|
||||
private func saveFiltersToUserDefaults() {
|
||||
let states = filters.reduce(into: [String: Bool]()) { result, filter in
|
||||
result[filter.type] = filter.isEnabled
|
||||
}
|
||||
UserDefaults.standard.set(states, forKey: userDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsViewLoggerFilter: View {
|
||||
@ObservedObject var viewModel = LogFilterViewModel.shared
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach($viewModel.filters) { $filter in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Toggle(filter.type, isOn: $filter.isEnabled)
|
||||
.font(.headline)
|
||||
|
||||
Text(filter.description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Log Filters")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue