diff --git a/Shared/Logging/Logger.swift b/Shared/Logging/Logger.swift index 0cf9a5d..bb1c35d 100644 --- a/Shared/Logging/Logger.swift +++ b/Shared/Logging/Logger.swift @@ -35,32 +35,70 @@ final class Debug { static let shared = Debug() private let subsystem = Bundle.main.bundleIdentifier! + private var logFilePath: URL { + return getDocumentsDirectory().appendingPathComponent("logs.txt") + } + + private func appendLogToFile(_ message: String) { + do { + if FileManager.default.fileExists(atPath: logFilePath.path) { + let fileHandle = try FileHandle(forUpdating: logFilePath) + fileHandle.seekToEndOfFile() + if let data = message.data(using: .utf8) { + fileHandle.write(data) + } + fileHandle.closeFile() + } + } catch { + Debug.shared.log(message: "Error writing to logs.txt: \(error)") + } + } + func log(message: String, type: LogType? = nil, function: String = #function, file: String = #file, line: Int = #line) { - lazy var logger = Logger(subsystem: subsystem, category: file+"->"+function) + lazy var logger = Logger(subsystem: subsystem, category: file + "->" + function) + + // Prepare the emoji based on the log type + var emoji: String switch type { case .success: + emoji = "✅" logger.info("\(message)") showSuccessAlert(with: String.localized("ALERT_SUCCESS"), subtitle: message) case .info: + emoji = "ℹī¸" logger.info("\(message)") case .debug: + emoji = "🐛" logger.debug("\(message)") case .trace: + emoji = "🔍" logger.trace("\(message)") showErrorUIAlert(with: String.localized("ALERT_TRACE"), subtitle: message) case .warning: + emoji = "⚠ī¸" logger.warning("\(message)") showErrorAlert(with: String.localized("ALERT_ERROR"), subtitle: message) case .error: + emoji = "❌" logger.error("\(message)") showErrorAlert(with: String.localized("ALERT_ERROR"), subtitle: message) case .critical: + emoji = "đŸ”Ĩ" logger.critical("\(message)") showErrorUIAlert(with: String.localized("ALERT_CRITICAL"), subtitle: message) default: + emoji = "📝" logger.log("\(message)") } + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss" + let timeString = dateFormatter.string(from: Date()) + + let logMessage = "[\(timeString)] \(emoji) \(message)\n" + appendLogToFile(logMessage) } + func showSuccessAlert(with title: String, subtitle: String) { DispatchQueue.main.async { diff --git a/Shared/Server/Server.swift b/Shared/Server/Server.swift index d7e1a36..6f530ca 100644 --- a/Shared/Server/Server.swift +++ b/Shared/Server/Server.swift @@ -98,7 +98,6 @@ class Installer: Identifiable, ObservableObject { return Response(status: .ok, headers: headers, body: .init(string: html)) } - try app.server.start() needsShutdown = true Debug.shared.log(message: "Server started: Port \(port) for \(Self.sni)") @@ -135,7 +134,7 @@ extension Installer { app.http.server.configuration.tlsConfiguration = try Self.setupTLS() } app.http.server.configuration.hostname = Self.sni - print(self.sni) + Debug.shared.log(message: self.sni) app.http.server.configuration.tcpNoDelay = true app.http.server.configuration.address = .hostname("0.0.0.0", port: port) diff --git a/Shared/Signing/AppSigner.swift b/Shared/Signing/AppSigner.swift index 1e01d02..1f2d758 100644 --- a/Shared/Signing/AppSigner.swift +++ b/Shared/Signing/AppSigner.swift @@ -46,6 +46,9 @@ func signInitialApp(options: AppSigningOptions, appPath: URL, completion: @escap var iconURL = "" do { + Debug.shared.log(message: "============================================") + Debug.shared.log(message: "\(options)") + Debug.shared.log(message: "============================================") try fileManager.createDirectory(at: tmpDir, withIntermediateDirectories: true) try fileManager.copyItem(at: appPath, to: tmpDirApp) @@ -77,8 +80,12 @@ func signInitialApp(options: AppSigningOptions, appPath: URL, completion: @escap let provisionPath = certPath.appendingPathComponent("\(options.certificate?.provisionPath ?? "")").path let p12Path = certPath.appendingPathComponent("\(options.certificate?.p12Path ?? "")").path + Debug.shared.log(message: "đŸĻ‹ Start Signing đŸĻ‹") + try signAppWithZSign(tmpDirApp: tmpDirApp, certPaths: (provisionPath, p12Path), password: options.certificate?.password ?? "", options: options) + Debug.shared.log(message: "đŸĻ‹ End Signing đŸĻ‹") + let signedUUID = UUID().uuidString try fileManager.createDirectory(at: getDocumentsDirectory().appendingPathComponent("Apps/Signed"), withIntermediateDirectories: true) let path = getDocumentsDirectory().appendingPathComponent("Apps/Signed").appendingPathComponent(signedUUID) @@ -101,6 +108,7 @@ func signInitialApp(options: AppSigningOptions, appPath: URL, completion: @escap } Debug.shared.log(message: String.localized("SUCCESS_SIGNED", arguments: "\(options.name ?? String.localized("UNKNOWN"))"), type: .success) + Debug.shared.log(message: "============================================") UIApplication.shared.isIdleTimerDisabled = false completion(true) @@ -123,11 +131,17 @@ func resignApp(certificate: Certificate, appPath: URL, completion: @escaping (Bo let provisionPath = certPath.appendingPathComponent("\(certificate.provisionPath ?? "")").path let p12Path = certPath.appendingPathComponent("\(certificate.p12Path ?? "")").path + Debug.shared.log(message: "============================================") + Debug.shared.log(message: "đŸĻ‹ Start Resigning đŸĻ‹") + try signAppWithZSign(tmpDirApp: appPath, certPaths: (provisionPath, p12Path), password: certificate.password ?? "", options: nil) + + Debug.shared.log(message: "đŸĻ‹ End Resigning đŸĻ‹") DispatchQueue.main.async { UIApplication.shared.isIdleTimerDisabled = false Debug.shared.log(message: String.localized("SUCCESS_RESIGN"), type: .success) } + Debug.shared.log(message: "============================================") completion(true) } catch { Debug.shared.log(message: "\(error)", type: .warning) @@ -166,13 +180,12 @@ func updateMobileProvision(app: URL) throws { if FileManager.default.fileExists(atPath: provisioningFilePath.path) { do { try FileManager.default.removeItem(at: provisioningFilePath) - Debug.shared.log(message: "embedded.mobileprovision file removed successfully.") + Debug.shared.log(message: "Embedded.mobileprovision file removed successfully!") } catch { - Debug.shared.log(message: "Failed to remove embedded.mobileprovision file: \(error)") throw error } } else { - Debug.shared.log(message: "No embedded.mobileprovision file found.") + Debug.shared.log(message: "Could not find any mobileprovision to remove. ") } } @@ -185,7 +198,7 @@ func listDylibs(filePath: String) -> [String]? { let dylibPaths = dylibPathsArray as! [String] return dylibPaths } else { - print("Failed to list dylibs.") + Debug.shared.log(message: "Failed to list dylibs.") return nil } } @@ -202,9 +215,12 @@ func updatePlugIns(options: AppSigningOptions, app: URL) throws { if filemanager.fileExists(atPath: path.path) { do { try filemanager.removeItem(at: path) + Debug.shared.log(message: "Removed PlugIns!") } catch { throw error } + } else { + Debug.shared.log(message: "Could not find any PlugIns to remove.") } } } @@ -216,9 +232,12 @@ func removeDumbAssPlaceHolderExtension(options: AppSigningOptions, app: URL) thr if filemanager.fileExists(atPath: path.path) { do { try filemanager.removeItem(at: path) + Debug.shared.log(message: "Removed placeholder watch app!") } catch { throw error } + } else { + Debug.shared.log(message: "Placeholder watch app not found.") } } } @@ -263,7 +282,7 @@ func updateInfoPlist(infoDict: NSMutableDictionary, options: AppSigningOptions, infoDict["CFBundleIcons~ipad"] = cfBundleIconsIpad } else { - Debug.shared.log(message: "updateInfoPlist.updateicon: Does not include an icon! Will not this.") + Debug.shared.log(message: "updateInfoPlist.updateicon: Does not include an icon! Will not do this.") } if options.forceFileSharing! { infoDict.setObject(true, forKey: "UISupportsDocumentBrowser" as NSCopying) } diff --git a/Shared/Signing/zsign/Utils.hpp b/Shared/Signing/zsign/Utils.hpp new file mode 100644 index 0000000..146feeb --- /dev/null +++ b/Shared/Signing/zsign/Utils.hpp @@ -0,0 +1,25 @@ +// +// Utils.hpp +// feather +// +// Created by samara on 30.09.2024. +// + +#ifndef Utils_hpp +#define Utils_hpp + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +const char* getDocumentsDirectory(); + + +#ifdef __cplusplus +} +#endif + +#endif /* zsign_hpp */ diff --git a/Shared/Signing/zsign/Utils.mm b/Shared/Signing/zsign/Utils.mm new file mode 100644 index 0000000..8c0c451 --- /dev/null +++ b/Shared/Signing/zsign/Utils.mm @@ -0,0 +1,19 @@ +// +// Utils.cpp +// feather +// +// Created by samara on 30.09.2024. +// + +#include "Utils.hpp" +#import + +extern "C" { + +const char* getDocumentsDirectory() { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths firstObject]; + const char *documentsPath = [documentsDirectory UTF8String]; + return documentsPath; +} +} diff --git a/Shared/Signing/zsign/common/common.cpp b/Shared/Signing/zsign/common/common.cpp index 3033529..fd94860 100644 --- a/Shared/Signing/zsign/common/common.cpp +++ b/Shared/Signing/zsign/common/common.cpp @@ -4,6 +4,8 @@ #include #include #include +#include "Utils.hpp" +#include #define PARSEVALIST(szFormatArgs, szArgs) \ ZBuffer buffer; \ @@ -701,20 +703,33 @@ void ZLog::SetLogLever(int nLogLevel) g_nLogLevel = nLogLevel; } +void ZLog::writeToLogFile(const std::string& message) { + const char* documentsPath = getDocumentsDirectory(); + std::string logFilePath = std::string(documentsPath) + "/logs.txt"; + + std::ofstream logFile(logFilePath, std::ios_base::app); + if (logFile.is_open()) { + logFile << message; + logFile.close(); + } else { + std::cerr << "Failed to open log file: " << logFilePath << std::endl; + } +} + void ZLog::Print(int nLevel, const char *szLog) { if (g_nLogLevel >= nLevel) { write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } -void ZLog::PrintV(int nLevel, const char *szFormatArgs, ...) -{ - if (g_nLogLevel >= nLevel) - { +void ZLog::PrintV(int nLevel, const char *szFormatArgs, ...) { + if (g_nLogLevel >= nLevel) { PARSEVALIST(szFormatArgs, szLog) write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } @@ -723,6 +738,7 @@ bool ZLog::Error(const char *szLog) write(STDOUT_FILENO, "\033[31m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return false; } @@ -732,6 +748,7 @@ bool ZLog::ErrorV(const char *szFormatArgs, ...) write(STDOUT_FILENO, "\033[31m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return false; } @@ -740,6 +757,7 @@ bool ZLog::Success(const char *szLog) write(STDOUT_FILENO, "\033[32m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return true; } @@ -749,6 +767,7 @@ bool ZLog::SuccessV(const char *szFormatArgs, ...) write(STDOUT_FILENO, "\033[32m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return true; } @@ -768,6 +787,7 @@ bool ZLog::Warn(const char *szLog) write(STDOUT_FILENO, "\033[33m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return false; } @@ -777,6 +797,7 @@ bool ZLog::WarnV(const char *szFormatArgs, ...) write(STDOUT_FILENO, "\033[33m", 5); write(STDOUT_FILENO, szLog, strlen(szLog)); write(STDOUT_FILENO, "\033[0m", 4); + writeToLogFile(szLog); return false; } @@ -785,6 +806,7 @@ void ZLog::Print(const char *szLog) if (g_nLogLevel >= E_INFO) { write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } @@ -794,6 +816,7 @@ void ZLog::PrintV(const char *szFormatArgs, ...) { PARSEVALIST(szFormatArgs, szLog) write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } @@ -802,6 +825,7 @@ void ZLog::Debug(const char *szLog) if (g_nLogLevel >= E_DEBUG) { write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } @@ -811,6 +835,7 @@ void ZLog::DebugV(const char *szFormatArgs, ...) { PARSEVALIST(szFormatArgs, szLog) write(STDOUT_FILENO, szLog, strlen(szLog)); + writeToLogFile(szLog); } } diff --git a/Shared/Signing/zsign/common/common.h b/Shared/Signing/zsign/common/common.h index 0e1592b..14045d6 100644 --- a/Shared/Signing/zsign/common/common.h +++ b/Shared/Signing/zsign/common/common.h @@ -157,4 +157,6 @@ class ZLog private: static int g_nLogLevel; -}; \ No newline at end of file + static void writeToLogFile(const std::string& message); + +}; diff --git a/Shared/Signing/zsign/zsign.mm b/Shared/Signing/zsign/zsign.mm index 983c715..04814e2 100644 --- a/Shared/Signing/zsign/zsign.mm +++ b/Shared/Signing/zsign/zsign.mm @@ -14,7 +14,6 @@ return [[[paths objectAtIndex:0] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"tmp"]; } - extern "C" { bool InjectDyLib(NSString *filePath, NSString *dylibPath, bool weakInject, bool bCreate) { diff --git a/feather.xcodeproj/project.pbxproj b/feather.xcodeproj/project.pbxproj index 5ea3a57..9dc6894 100644 --- a/feather.xcodeproj/project.pbxproj +++ b/feather.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 33570F612C32B80E008CB560 /* AppsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33570F602C32B80E008CB560 /* AppsTableViewCell.swift */; }; 33570F672C34CCD5008CB560 /* AppsInformationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33570F662C34CCD5008CB560 /* AppsInformationViewController.swift */; }; 33570F692C34E5B2008CB560 /* SectionHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33570F682C34E5B2008CB560 /* SectionHeaders.swift */; }; + 335EB8602CAB889C00208442 /* Utils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 335EB85E2CAB889C00208442 /* Utils.mm */; }; 335F0C2C2C5DC3AE00A4F0AE /* CoreDataManager+SignedApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335F0C2B2C5DC3AE00A4F0AE /* CoreDataManager+SignedApps.swift */; }; 335F0C2E2C5E0FFF00A4F0AE /* CoreDataManager+Certificates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335F0C2D2C5E0FFF00A4F0AE /* CoreDataManager+Certificates.swift */; }; 335F0C302C5E175B00A4F0AE /* CertData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335F0C2F2C5E175B00A4F0AE /* CertData.swift */; }; @@ -152,6 +153,8 @@ 33570F602C32B80E008CB560 /* AppsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsTableViewCell.swift; sourceTree = ""; }; 33570F662C34CCD5008CB560 /* AppsInformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsInformationViewController.swift; sourceTree = ""; }; 33570F682C34E5B2008CB560 /* SectionHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaders.swift; sourceTree = ""; }; + 335EB85E2CAB889C00208442 /* Utils.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Utils.mm; sourceTree = ""; }; + 335EB85F2CAB889C00208442 /* Utils.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Utils.hpp; sourceTree = ""; }; 335F0C2B2C5DC3AE00A4F0AE /* CoreDataManager+SignedApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreDataManager+SignedApps.swift"; sourceTree = ""; }; 335F0C2D2C5E0FFF00A4F0AE /* CoreDataManager+Certificates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreDataManager+Certificates.swift"; sourceTree = ""; }; 335F0C2F2C5E175B00A4F0AE /* CertData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertData.swift; sourceTree = ""; }; @@ -715,6 +718,8 @@ D0651C2B2BFEEA4B00D40829 /* signing.h */, D0651C0F2BFEE93C00D40829 /* zsign.mm */, D0651C102BFEE93C00D40829 /* zsign.hpp */, + 335EB85E2CAB889C00208442 /* Utils.mm */, + 335EB85F2CAB889C00208442 /* Utils.hpp */, ); path = zsign; sourceTree = ""; @@ -873,6 +878,7 @@ D0651C372BFEEA4B00D40829 /* openssl.cpp in Sources */, D0651C1A2BFEE97F00D40829 /* base64.cpp in Sources */, 33BE875F2C6AF91B0044D245 /* IconImageViewCell.swift in Sources */, + 335EB8602CAB889C00208442 /* Utils.mm in Sources */, AF4D46EF2C432CCA003FA335 /* TweakLibraryViewCell.swift in Sources */, 33E4D8322C64CB39006A1C26 /* SearchResultsTableViewController.swift in Sources */, 335F0C2C2C5DC3AE00A4F0AE /* CoreDataManager+SignedApps.swift in Sources */, diff --git a/iOS/Delegates/AppDelegate.swift b/iOS/Delegates/AppDelegate.swift index 57e24c1..26c91a3 100644 --- a/iOS/Delegates/AppDelegate.swift +++ b/iOS/Delegates/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import Nuke import CoreData import UIOnboarding +import Foundation var downloadTaskManager = DownloadTaskManager.shared class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControllerDelegate { @@ -19,10 +20,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle var loaderAlert = presentLoader() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - UserDefaults.standard.set(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, forKey: "currentVersion") + addDefaultRepos() imagePipline() - + setupLogFile() + cleanTmp() + createSourcesDirectory() + window = UIWindow(frame: UIScreen.main.bounds) if Preferences.isOnboardingActive { @@ -40,22 +44,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle } window?.makeKeyAndVisible() - createSourcesDirectory() - - let fileManager = FileManager.default - let tmpDirectory = NSHomeDirectory() + "/tmp" - - if let files = try? fileManager.contentsOfDirectory(atPath: tmpDirectory) { - for file in files { - try? fileManager.removeItem(atPath: tmpDirectory + "/" + file) - } - } - + let generatedString = AppDelegate.generateRandomString() if Preferences.pPQCheckString.isEmpty { Preferences.pPQCheckString = generatedString } - + UserDefaults.standard.set(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, forKey: "currentVersion") + + Debug.shared.log(message: "Device: \(UIDevice.current.model)") + Debug.shared.log(message: "Version: \(UIDevice.current.systemVersion)") + Debug.shared.log(message: "Name: \(UIDevice.current.name)") + Debug.shared.log(message: "Model: \(UIDevice.current.model)") + Debug.shared.log(message: "Feather Version: \(logAppVersionInfo())\n") + return true } @@ -196,6 +197,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle } ImagePipeline.shared = pipeline } + + func setupLogFile() { + let logFilePath = getDocumentsDirectory().appendingPathComponent("logs.txt") + if FileManager.default.fileExists(atPath: logFilePath.path) { + do { + try FileManager.default.removeItem(at: logFilePath) + } catch { + print("Error removing existing logs.txt: \(error)") + } + } + + do { + try "".write(to: logFilePath, atomically: true, encoding: .utf8) + } catch { + print("Error removing existing logs.txt: \(error)") + } + } + + func cleanTmp() { + let fileManager = FileManager.default + let tmpDirectory = NSHomeDirectory() + "/tmp" + + if let files = try? fileManager.contentsOfDirectory(atPath: tmpDirectory) { + for file in files { + try? fileManager.removeItem(atPath: tmpDirectory + "/" + file) + } + } + } + + func logAppVersionInfo() -> String { + if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + return "App Version: \(version), Build Number: \(build)" + } + return "" + } } extension UIOnboardingViewConfiguration { diff --git a/iOS/Views/Apps/LibraryViewController+Import.swift b/iOS/Views/Apps/LibraryViewController+Import.swift index e74334e..7b66708 100644 --- a/iOS/Views/Apps/LibraryViewController+Import.swift +++ b/iOS/Views/Apps/LibraryViewController+Import.swift @@ -177,7 +177,7 @@ extension LibraryViewController { DispatchQueue.main.async { self.loaderAlert?.dismiss(animated: true) } - + Debug.shared.log(message: "Failed to Import: \(error)", type: .error) } else if let uuid = uuid, let filePath = filePath { LibraryViewController.appDownload?.extractCompressedBundle(packageURL: filePath) { (targetBundle, error) in @@ -185,12 +185,14 @@ extension LibraryViewController { DispatchQueue.main.async { self.loaderAlert?.dismiss(animated: true) } + Debug.shared.log(message: "Failed to Import: \(error)", type: .error) } else if let targetBundle = targetBundle { LibraryViewController.appDownload?.addToApps(bundlePath: targetBundle, uuid: uuid, sourceLocation: sourceLocation) { error in if let error = error { DispatchQueue.main.async { self.loaderAlert?.dismiss(animated: true) } + Debug.shared.log(message: "Failed to Import: \(error)", type: .error) } else { DispatchQueue.main.async { self.loaderAlert?.dismiss(animated: true) diff --git a/iOS/Views/Debug/DebugViewController.swift b/iOS/Views/Debug/DebugViewController.swift index b9dff24..1cb0c8a 100644 --- a/iOS/Views/Debug/DebugViewController.swift +++ b/iOS/Views/Debug/DebugViewController.swift @@ -24,16 +24,214 @@ class DebugHostingController: UIHostingController { navigationItem.largeTitleDisplayMode = .always } } - +#warning +(""" +This view should never be translated, its there for debug purposes and not for the average user to see. +""") struct DebugViewController: View { + @State private var isOnboardingActive: Bool = Preferences.isOnboardingActive + + @State private var successCount = 0 + @State private var infoCount = 0 + @State private var debugCount = 0 + @State private var traceCount = 0 + @State private var warningCount = 0 + @State private var criticalCount = 0 + @State private var errorCount = 0 + @State private var basicCount = 0 + + @State private var logContents: String = "" + private let timer = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect() + + @State private var showShareSheet = false + var body: some View { List { + Section(footer: Text("You will need to restart the app after toggling.")) { + Toggle(isOn: $isOnboardingActive) { Text("Show Onboarding") } + .onChange(of: isOnboardingActive) { newValue in + Preferences.isOnboardingActive = newValue + } + } + + Section { + Button(action: { openDirectory(named: "Signed") }) { + Text("Open Signed Folder") + } + + Button(action: { openDirectory(named: "Unsigned") }) { + Text("Open Unsigned Folder") + } + } + + Section { + Text("\(countToEmoji(count: successCount)) Success message(s)") + .foregroundColor(.white) + .listRowBackground(Color.green.opacity(0.2)) + + Text("\(countToEmoji(count: infoCount)) Info message(s)") + .foregroundColor(.white) + .listRowBackground(Color.accentColor.opacity(0.2)) + + Text("\(countToEmoji(count: debugCount)) Debug message(s)") + .foregroundColor(.white) + .listRowBackground(Color.blue.opacity(0.2)) + + Text("\(countToEmoji(count: traceCount)) Trace(s)") + .foregroundColor(.white) + .listRowBackground(Color.indigo.opacity(0.2)) + + Text("\(countToEmoji(count: warningCount)) Warning(s)") + .foregroundColor(.white) + .listRowBackground(Color.yellow.opacity(0.2)) + Text("\(countToEmoji(count: criticalCount)) Critical error(s)") + .foregroundColor(.white) + .listRowBackground(Color.red.opacity(0.2)) + + Text("\(countToEmoji(count: errorCount)) Error(s)") + .foregroundColor(.white) + .listRowBackground(Color.orange.opacity(0.2)) + + Text("\(countToEmoji(count: basicCount)) Messages(s)") + .foregroundColor(.white) + } + + Section { + ScrollView { + Text(logContents) + .font(.system(.footnote, design: .monospaced)) + } + .frame(height: 400) + .onAppear(perform: loadLogContents) + .onReceive(timer) { _ in + loadLogContents() + parseLogFile() + } + + Button(action: { showShareSheet = true }) { + HStack(spacing: .maximum(18, 18)) { + sfGradient(systemName: "square.and.arrow.up", gradientColors: [.teal, .blue]) + CellText(text: "Share Log File") + } + } + } + + } + .sheet(isPresented: $showShareSheet) { + let logFilePath = getDocumentsDirectory().appendingPathComponent("logs.txt") + ActivityViewController(activityItems: [logFilePath]) } .onAppear { - + isOnboardingActive = Preferences.isOnboardingActive + } + } + + + private func openDirectory(named directoryName: String) { + let directoryURL = getDocumentsDirectory().appendingPathComponent("Apps").appendingPathComponent(directoryName) + let path = directoryURL.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://") + + UIApplication.shared.open(URL(string: path)!, options: [:]) { success in + if success { + Debug.shared.log(message: "File opened successfully.") + } else { + Debug.shared.log(message: "Failed to open file.") + } } } + + private func loadLogContents() { + let logFilePath = getDocumentsDirectory().appendingPathComponent("logs.txt") + + do { + logContents = try String(contentsOf: logFilePath, encoding: .utf8) + } catch { + logContents = "Failed to load logs" + } + } + + func parseLogFile() { + let logFilePath = getDocumentsDirectory().appendingPathComponent("logs.txt") + do { + let logContents = try String(contentsOf: logFilePath) + + + successCount = 0 + infoCount = 0 + debugCount = 0 + traceCount = 0 + warningCount = 0 + criticalCount = 0 + errorCount = 0 + basicCount = 0 + + let logEntries = logContents.components(separatedBy: .newlines) + + for entry in logEntries { + if entry.contains("✅") { + successCount += 1 + } else if entry.contains("ℹī¸") { + infoCount += 1 + } else if entry.contains("🐛") { + debugCount += 1 + } else if entry.contains("🔍") { + traceCount += 1 + } else if entry.contains("⚠ī¸") { + warningCount += 1 + } else if entry.contains("❌") { + errorCount += 1 + } else if entry.contains("đŸ”Ĩ") { + criticalCount += 1 + } else if entry.contains("📝") { + basicCount += 1 + } + } + + } catch { + print("Error reading log file: \(error)") + } + } + + + func countToEmoji(count: Int) -> String { + return "\(count)" + } + } +extension View { + @ViewBuilder + func ifAvailable(iOS17Modifier: (Self) -> T) -> some View { + if #available(iOS 17, *) { + iOS17Modifier(self) + } else { + self + } + } +} + +func sfGradient(systemName: String, gradientColors: [Color]) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .overlay( + LinearGradient( + gradient: Gradient(colors: gradientColors), + startPoint: .trailing, + endPoint: .leading + ) + ) + .mask( + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + ) +} + +func CellText(text: String) -> some View { + Text(text) + .foregroundColor(Color(UIColor.label)) +} diff --git a/iOS/Views/Sources/RepoViewController.swift b/iOS/Views/Sources/RepoViewController.swift index bdaf17d..c8795e5 100644 --- a/iOS/Views/Sources/RepoViewController.swift +++ b/iOS/Views/Sources/RepoViewController.swift @@ -59,7 +59,7 @@ struct RepoViewController: View { Button(action: { let pasteboard = UIPasteboard.general if let clipboardText = pasteboard.string { - Debug.shared.log(message: "meow2") + Debug.shared.log(message: "Pasted from clipboard") self.decodeRepositories(text: clipboardText) } }) { @@ -165,7 +165,7 @@ extension RepoViewController { isSyncing = true let isBase64 = isValidBase64String(text) let repoLinks: [String] - Debug.shared.log(message: "meow") + Debug.shared.log(message: "Trying to add repositories...") if isBase64 { guard let decodedString = decodeBase64String(text) else { Debug.shared.log(message: "Failed to decode base64 string", type: .error) @@ -193,7 +193,7 @@ extension RepoViewController { } } DispatchQueue.main.async { - Debug.shared.showSuccessAlert(with: "Successfully imported \(success) repos", subtitle: "") + Debug.shared.log(message: "Successfully imported \(success) repos", type: .success) presentationMode.wrappedValue.dismiss() NotificationCenter.default.post(name: Notification.Name("sfetch"), object: nil) }