diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index 9b73dd4..a408626 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -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 = ""; }; 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = ""; }; + 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EE1DA952D3553C2002AEF73 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; /* 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 */, diff --git a/Sora/Localizable.xcstrings b/Sora/Localizable.xcstrings index 4edc2fe..188585c 100644 --- a/Sora/Localizable.xcstrings +++ b/Sora/Localizable.xcstrings @@ -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" : { diff --git a/Sora/Utils/Loaders/JSController.swift b/Sora/Utils/Loaders/JSController.swift index 6137c9e..37c9616 100644 --- a/Sora/Utils/Loaders/JSController.swift +++ b/Sora/Utils/Loaders/JSController.swift @@ -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() } diff --git a/Sora/Utils/Logger/Logger.swift b/Sora/Utils/Logger/Logger.swift index f2a9a23..2f34f81 100644 --- a/Sora/Utils/Logger/Logger.swift +++ b/Sora/Utils/Logger/Logger.swift @@ -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 { } } } -} \ No newline at end of file +} diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift index 413a872..cbf96ea 100644 --- a/Sora/Utils/Modules/Modules.swift +++ b/Sora/Utils/Modules/Modules.swift @@ -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) diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index 0f6a9bf..bd71a38 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -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") } } diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index f3acb8e..9e6e6fe 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -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 } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index bbafb22..fcd7fbe 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -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) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index ca823c3..640d34c 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -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 } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift index aeb83f2..4d7f433 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift @@ -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) } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift new file mode 100644 index 0000000..299948a --- /dev/null +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift @@ -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") + } +}