From d2d7d7364fff92cf5df07fc7057bda4e92c69fe2 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 10 Mar 2023 15:01:00 -0500 Subject: [PATCH] Logging: Add exportability to logs Logs can be exported into their own files. I'm still debating on writing a continuous stream to a logfile that persists on app crashes. Also make the show error toasts toggle only apply to generic errors and not customized error toasts. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 8 ++-- Ferrite/Extensions/DateFormatter.swift | 1 + Ferrite/ViewModels/LoggingManager.swift | 48 ++++++++++++++++++- .../Settings/SettingsLogView.swift | 38 ++++++++++++++- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index dded14c..ff6e047 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -993,7 +993,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportsDocumentBrowser = NO; + INFOPLIST_KEY_UISupportsDocumentBrowser = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1028,7 +1028,7 @@ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportsDocumentBrowser = NO; + INFOPLIST_KEY_UISupportsDocumentBrowser = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1072,8 +1072,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftUIX/SwiftUIX"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.3; + branch = master; + kind = branch; }; }; 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { diff --git a/Ferrite/Extensions/DateFormatter.swift b/Ferrite/Extensions/DateFormatter.swift index 4177912..1e83ada 100644 --- a/Ferrite/Extensions/DateFormatter.swift +++ b/Ferrite/Extensions/DateFormatter.swift @@ -7,6 +7,7 @@ import Foundation +// A static DateFormatter is better than initializing new ones extension DateFormatter { static let historyDateFormatter: DateFormatter = { let df = DateFormatter() diff --git a/Ferrite/ViewModels/LoggingManager.swift b/Ferrite/ViewModels/LoggingManager.swift index 8db85ef..f128b63 100644 --- a/Ferrite/ViewModels/LoggingManager.swift +++ b/Ferrite/ViewModels/LoggingManager.swift @@ -9,10 +9,13 @@ import SwiftUI @MainActor class LoggingManager: ObservableObject { + let logFormatter = DateFormatter() + struct Log: Hashable { let level: LogLevel let message: String let timeStamp: Date = .init() + var isExpanded: Bool = false func toMessage() -> String { "[\(level.rawValue)]: \(message)" @@ -30,6 +33,7 @@ class LoggingManager: ObservableObject { } @Published var messageArray: [Log] = [] + @Published var showLogExportedAlert = false // Toast variables @Published var toastDescription: String? = nil { @@ -57,7 +61,13 @@ class LoggingManager: ObservableObject { @Published var indeterminateCancelAction: (() -> Void)? = nil @Published var showIndeterminateToast: Bool = false + init() { + logFormatter.dateStyle = .short + logFormatter.timeStyle = .long + } + // MARK: - Logging functions + // TODO: Maybe append to a constant logfile? public func info(_ message: String, description: String? = nil) @@ -105,8 +115,13 @@ class LoggingManager: ObservableObject { ) // If a task is run in parallel, don't show a toast on error - if showToast && showErrorToasts { - toastDescription = description.map { $0 } ?? "An error was logged" + // Only gate generic error toasts behind the settings option + if showToast { + if let description { + toastDescription = description + } else if showErrorToasts { + toastDescription = "An error was logged" + } } messageArray.append(log) @@ -133,4 +148,33 @@ class LoggingManager: ObservableObject { indeterminateToastDescription = "" indeterminateCancelAction = nil } + + public func exportLogs() { + logFormatter.dateFormat = "yyyy-MM-dd-HHmmss" + let logFileName = "ferrite_session_\(logFormatter.string(from: Date())).txt" + let logFolderPath = FileManager.default.appDirectory.appendingPathComponent("Logs") + let logPath = logFolderPath.appendingPathComponent(logFileName) + + logFormatter.dateStyle = .short + logFormatter.timeStyle = .long + let joinedMessages = messageArray.map { "\(logFormatter.string(from: $0.timeStamp)): \($0.toMessage())" }.joined(separator: "\n") + + do { + if FileManager.default.fileExists(atPath: logPath.path) { + try FileManager.default.removeItem(at: logPath) + } else if !FileManager.default.fileExists(atPath: logFolderPath.path) { + try FileManager.default.createDirectory(atPath: logFolderPath.path, withIntermediateDirectories: true, attributes: nil) + } + + try joinedMessages.write(to: logPath, atomically: true, encoding: .utf8) + + self.info("Log \(logFileName) was written to path \(logPath.description)") + showLogExportedAlert.toggle() + } catch { + self.error( + "Log export for file \(logFileName): \(error)", + description: "Exporting your log file failed. Please check the logs page." + ) + } + } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsLogView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsLogView.swift index b18d24d..1ac865d 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsLogView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsLogView.swift @@ -13,15 +13,51 @@ struct SettingsLogView: View { var body: some View { NavView { List { - ForEach(logManager.messageArray, id: \.self) { log in + ForEach($logManager.messageArray, id: \.self) { $log in Text(log.toMessage()) .font(.caption) .foregroundColor(.secondary) + .lineLimit(log.isExpanded ? nil : 5) + .onTapGesture { + log.isExpanded.toggle() + } } } .listStyle(.plain) + .backport.alert( + isPresented: $logManager.showLogExportedAlert, + title: "Success", + message: "Log successfully exported in Ferrite's logs folder" + ) .navigationTitle("Logs") .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu { + Button { + logManager.exportLogs() + } label: { + Label("Export", systemImage: "square.and.arrow.up") + } + + if #available(iOS 15, *) { + Button(role: .destructive) { + logManager.messageArray = [] + } label: { + Label("Clear session logs", systemImage: "trash") + } + } else { + Button { + logManager.messageArray = [] + } label: { + Label("Clear session logs", systemImage: "trash") + } + } + } label: { + Image(systemName: "ellipsis") + } + } + } } } }