From 2cca84e670b70687a1a89a0156837a880c8c9eec Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 10 Apr 2023 14:01:29 +0200 Subject: [PATCH] Adds log view --- LeCountdown.xcodeproj/project.pbxproj | 22 ++++++ LeCountdown/Conductor.swift | 51 ++++++++------ LeCountdown/Sound/DelaySoundPlayer.swift | 8 ++- LeCountdown/Utils/Codable+Extensions.swift | 59 ++++++++++++++++ LeCountdown/Utils/FileLogger.swift | 82 ++++++++++++++++++++++ LeCountdown/Utils/FileUtils.swift | 54 ++++++++++++++ LeCountdown/Views/ContentView.swift | 11 +++ LeCountdown/Views/LogsView.swift | 39 ++++++++++ 8 files changed, 304 insertions(+), 22 deletions(-) create mode 100644 LeCountdown/Utils/Codable+Extensions.swift create mode 100644 LeCountdown/Utils/FileLogger.swift create mode 100644 LeCountdown/Utils/FileUtils.swift create mode 100644 LeCountdown/Views/LogsView.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index fc269e1..7ff5b34 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -72,6 +72,13 @@ C445FA882984487F0054D761 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4060DC3297AE73D003FAB80 /* Assets.xcassets */; }; C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; }; C445FA922987CC8A0054D761 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA912987CC8A0054D761 /* Sound.swift */; }; + C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6A29E40B7800DEB40B /* FileLogger.swift */; }; + C4556F6F29E40BED00DEB40B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6E29E40BED00DEB40B /* FileUtils.swift */; }; + C4556F7129E40DCF00DEB40B /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */; }; + C4556F7229E40EBF00DEB40B /* FileLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6A29E40B7800DEB40B /* FileLogger.swift */; }; + C4556F7329E40EC200DEB40B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6E29E40BED00DEB40B /* FileUtils.swift */; }; + C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */; }; + C4556F7629E411A400DEB40B /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7529E411A400DEB40B /* LogsView.swift */; }; C4636D9C29AF46BD00994E31 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4636D9B29AF46BD00994E31 /* ActivityKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C4636D9D29AF46D900994E31 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7D02981216200BF3EF9 /* WidgetKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C46926BD29DDC49E0003E310 /* SubscriptionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */; }; @@ -380,6 +387,10 @@ C445FA8E2987B83B0054D761 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = ""; }; C445FA912987CC8A0054D761 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; + C4556F6A29E40B7800DEB40B /* FileLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogger.swift; sourceTree = ""; }; + C4556F6E29E40BED00DEB40B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; + C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = ""; }; + C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; C4636D9B29AF46BD00994E31 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; }; C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionButtonView.swift; sourceTree = ""; }; C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = ""; }; @@ -717,9 +728,12 @@ isa = PBXGroup; children = ( C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */, + C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */, C4742B5629840F6400D5D950 /* CoolPic.swift */, C4BA2B6929A4BE1800CB4FBA /* Date+Extensions.swift */, C4A16DC429D311C800143D5E /* Extensions.swift */, + C4556F6A29E40B7800DEB40B /* FileLogger.swift */, + C4556F6E29E40BED00DEB40B /* FileUtils.swift */, C4BA2B5A299FFAB000CB4FBA /* Logger.swift */, C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */, C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */, @@ -744,6 +758,7 @@ C4F8B1CF298BF2E2005C86A5 /* DialView.swift */, C4BA2B24299D35C100CB4FBA /* HomeView.swift */, C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */, + C4556F7529E411A400DEB40B /* LogsView.swift */, C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */, C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */, C4E5D68929BB7953008E7465 /* SettingsView.swift */, @@ -1173,6 +1188,7 @@ C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */, C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C4BA2B36299F82FB00CB4FBA /* Fakes.swift in Sources */, + C4556F7629E411A400DEB40B /* LogsView.swift in Sources */, C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */, C4BA2B6329A3C34600CB4FBA /* Stat.swift in Sources */, C415D3E229C0C0C20037B215 /* MailView.swift in Sources */, @@ -1212,6 +1228,7 @@ C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */, C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, + C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4BA2B25299D35C100CB4FBA /* HomeView.swift in Sources */, C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */, @@ -1222,8 +1239,10 @@ C49C346929DECA4400AAC6FC /* LiveStopWatch.swift in Sources */, C473C33929ACDBD70056B38A /* TipView.swift in Sources */, C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */, + C4556F7129E40DCF00DEB40B /* Codable+Extensions.swift in Sources */, C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */, + C4556F6F29E40BED00DEB40B /* FileUtils.swift in Sources */, C4BA2B2D299E2DEE00CB4FBA /* Preferences.swift in Sources */, C4F8B185298AC234005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */, C4F8B169298AA236005C86A5 /* StopwatchFormView.swift in Sources */, @@ -1357,6 +1376,7 @@ C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */, C473C33D29ACEC4F0056B38A /* Tip.swift in Sources */, C4F8B19C298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */, + C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */, C4BA2B1C299BE6A100CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */, C4BA2B1F299BE6A100CB4FBA /* Countdown+CoreDataProperties.swift in Sources */, C4F8B1A3298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */, @@ -1378,7 +1398,9 @@ C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */, C48ECC0929DAC47200DE5A66 /* AppGuard.swift in Sources */, C473C2FB29A8DC3A0056B38A /* LaunchWidgetAttributes.swift in Sources */, + C4556F7329E40EC200DEB40B /* FileUtils.swift in Sources */, C4BA2B4B299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, + C4556F7229E40EBF00DEB40B /* FileLogger.swift in Sources */, C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C473C2F129A8DA0B0056B38A /* Conductor.swift in Sources */, C4BA2B1E299BE6A100CB4FBA /* Interval+CoreDataClass.swift in Sources */, diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 70d7ec2..e667704 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -67,6 +67,7 @@ class Conductor: ObservableObject { self.currentStopwatches.removeValue(forKey: id) self.cancelledCountdowns.removeAll(where: { $0 == id }) if let soundPlayer = self._delayedSoundPlayers[id] { + FileLogger.log("Stop sound player: \(id)") soundPlayer.stop() } } @@ -127,38 +128,24 @@ class Conductor: ObservableObject { // MARK: - Countdown - func cancelCountdown(id: TimerID) { - CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) - self.cancelSoundPlayer(id: id) - self.cancelledCountdowns.append(id) - self._endCountdown(countdownId: id, cancel: true) - - if Preferences.playCancellationSound { - self._playCancellationSound() - } - } - - fileprivate func _endCountdown(countdownId: String, cancel: Bool) { - DispatchQueue.main.async { - if !cancel { - self._recordActivity(countdownId: countdownId) - } - self.currentCountdowns.removeValue(forKey: countdownId) - } - } - func startCountdown(countdown: Countdown, handler: @escaping (Result) -> Void) { DispatchQueue.main.async { do { let date = Date(timeIntervalSinceNow: countdown.duration) + FileLogger.log("schedule countdown \(countdown.stringId) at \(date)") self.removeLiveTimer(id: countdown.stringId) let sound = countdown.someSound + let soundPlayer = try DelaySoundPlayer(timerID: countdown.stringId, sound: sound) + + FileLogger.log("a) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") self._delayedSoundPlayers[countdown.stringId] = soundPlayer + FileLogger.log("b) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") + try soundPlayer.start(in: countdown.duration, repeatCount: Int(countdown.repeatCount)) @@ -178,6 +165,28 @@ class Conductor: ObservableObject { } } + func cancelCountdown(id: TimerID) { + CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) + self.cancelSoundPlayer(id: id) + self.cancelledCountdowns.append(id) + self._endCountdown(countdownId: id, cancel: true) + + if Preferences.playCancellationSound { + self._playCancellationSound() + } + + self._endLiveActivity(timerId: id) + } + + fileprivate func _endCountdown(countdownId: String, cancel: Bool) { + DispatchQueue.main.async { + if !cancel { + self._recordActivity(countdownId: countdownId) + } + self.currentCountdowns.removeValue(forKey: countdownId) + } + } + // MARK: - Stopwatch func startStopwatch(_ stopwatch: Stopwatch) { @@ -234,6 +243,7 @@ class Conductor: ObservableObject { let sound: Sound = countdown.someSound let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) self._delayedSoundPlayers[countdown.stringId] = soundPlayer + FileLogger.log("Restored sound player for \(countdown.stringId)") try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount)) } catch { Logger.error(error) @@ -334,6 +344,7 @@ class Conductor: ObservableObject { if let soundPlayer = self._delayedSoundPlayers[id] { soundPlayer.stop() self._delayedSoundPlayers.removeValue(forKey: id) + FileLogger.log("cancelled sound player for \(id)") } self.deactivateAudioSessionIfPossible() diff --git a/LeCountdown/Sound/DelaySoundPlayer.swift b/LeCountdown/Sound/DelaySoundPlayer.swift index a710f0d..b3c69b5 100644 --- a/LeCountdown/Sound/DelaySoundPlayer.swift +++ b/LeCountdown/Sound/DelaySoundPlayer.swift @@ -48,17 +48,21 @@ import AVFoundation self._player.numberOfLoops = repeatCount Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)") - self._player.play(atTime: self._player.deviceCurrentTime + duration) - + let time: TimeInterval = self._player.deviceCurrentTime + duration + self._player.play(atTime: time) + FileLogger.log("self._player.play(atTime: \(time)") } func stop() { self._player.stop() + FileLogger.log("Player stopped") } // MARK: - Delegate func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + FileLogger.log("audioPlayerDidFinishPlaying: successfully = \(flag)") + Logger.log("audioPlayerDidFinishPlaying: successfully = \(flag)") Conductor.maestro.cancelSoundPlayer(id: self._timerID) Conductor.maestro.cleanupLiveActivities() diff --git a/LeCountdown/Utils/Codable+Extensions.swift b/LeCountdown/Utils/Codable+Extensions.swift new file mode 100644 index 0000000..576ea02 --- /dev/null +++ b/LeCountdown/Utils/Codable+Extensions.swift @@ -0,0 +1,59 @@ +// +// Codable+Extensions.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/04/2023. +// + +import Foundation + +extension Encodable { + + var jsonString: String? { + if let data = self.jsonData { + return String(data: data, encoding: .utf8) + } else { + return nil + } + } + + var jsonData: Data? { + let encoder: JSONEncoder = JSONEncoder() + do { + return try encoder.encode(self) + } catch { + Logger.error(error) + return nil + } + } + +} + +extension String { + + func decode() -> T? { + return self.data(using: .utf8)?.decode() + } + + func decodeArray() throws -> [T]? { + return try self.data(using: .utf8)?.decodeArray() + } + +} + +extension Data { + + func decode() -> T? { + do { + return try JSONDecoder().decode(T.self, from: self) + } catch { + Logger.error(error) + return nil + } + } + + func decodeArray() throws -> [T] { + return try JSONDecoder().decode([T].self, from: self) + } + +} diff --git a/LeCountdown/Utils/FileLogger.swift b/LeCountdown/Utils/FileLogger.swift new file mode 100644 index 0000000..fbe80e2 --- /dev/null +++ b/LeCountdown/Utils/FileLogger.swift @@ -0,0 +1,82 @@ +// +// FileLogger.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/04/2023. +// + +import Foundation + +struct Log: Identifiable, Codable { + + var id: String = UUID().uuidString + + var date: Date + var file: String + var line: Int + var function: String + var message: String + + var content: String { + return "\(file).\(line).\(function): \(message)" + } + +} + +class FileLogger { + + fileprivate let fileName = "logs.json" + + static var main: FileLogger = FileLogger() + + var logs: [Log] + + var timer: Timer? = nil + + init() { + self.logs = [] + + do { + let content = try FileUtils.readDocumentFile(fileName: self.fileName) + if let logs: [Log] = try content.decodeArray() { + self.logs = logs + } else { + Logger.w("Log decoding failed") + } + } catch { + Logger.error(error) + } + + } + + func addLog(_ log: Log) { + self.logs.append(log) + self._scheduleWrite() + } + + fileprivate func _scheduleWrite() { + self.timer?.invalidate() + self.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { _ in + self._writeLogs() + }) + } + + fileprivate func _writeLogs() { + DispatchQueue(label: "app.enchant.write", qos: .utility).async { + if let json = self.logs.jsonString { + do { + let _ = try FileUtils.writeToDocumentDirectory(content: json, fileName: self.fileName) + } catch { + Logger.error(error) + } + } + } + } + + @objc static public func log(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + let filestr = NSString(string: file) + let log = Log(date: Date(), file: filestr.lastPathComponent, line: line, function: function, message: message) + FileLogger.main.addLog(log) + } + +} diff --git a/LeCountdown/Utils/FileUtils.swift b/LeCountdown/Utils/FileUtils.swift new file mode 100644 index 0000000..9a33f50 --- /dev/null +++ b/LeCountdown/Utils/FileUtils.swift @@ -0,0 +1,54 @@ +// +// FileUtils.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/04/2023. +// + +import Foundation + +enum FileError : Error { + case documentDirectoryNotFound +} + +class FileUtils { + + static func pathsFromDocumentsDirectory() throws -> [String] { + let documentsURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + return try FileManager.default.contentsOfDirectory(atPath: documentsURL.path) + } + + static func readDocumentFile(fileName: String) throws -> String { + if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let fileURL: URL = dir.appendingPathComponent(fileName) + return try String(contentsOf: fileURL, encoding: .utf8) + } + throw FileError.documentDirectoryNotFound + } + + static func readFile(fileURL: URL) throws -> String { + return try String(contentsOf: fileURL, encoding: .utf8) + } + + static func writeToDocumentDirectory(content: String, fileName: String) throws -> URL { + + if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let fileURL: URL = dir.appendingPathComponent(fileName) + try content.write(to: fileURL, atomically: false, encoding: .utf8) + return fileURL + } + throw FileError.documentDirectoryNotFound + } + + @discardableResult static func writeToDocumentDirectory(data: Data, fileName: String) throws -> URL { + + if let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let fileURL: URL = dir.appendingPathComponent(fileName) + try data.write(to: fileURL) + Logger.log("Wrote file = \(fileURL.absoluteString)") + return fileURL + } + throw FileError.documentDirectoryNotFound + } + +} diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index b64a9eb..8a30fed 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -175,6 +175,7 @@ struct MainToolbarView: ToolbarContent { @State var showAddSheet: Bool = false @State var showTimerSheet: Bool = false @State var showStopwatchSheet: Bool = false + @State var showLogsSheet: Bool = false var body: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { @@ -187,6 +188,16 @@ struct MainToolbarView: ToolbarContent { } } ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + withAnimation { + self.showLogsSheet.toggle() + } + } label: { + Image(systemName: "list.dash") + } + .sheet(isPresented: self.$showLogsSheet, content: { + LogsView() + }) if self.haveRecords() { Button { withAnimation { diff --git a/LeCountdown/Views/LogsView.swift b/LeCountdown/Views/LogsView.swift new file mode 100644 index 0000000..afd23d2 --- /dev/null +++ b/LeCountdown/Views/LogsView.swift @@ -0,0 +1,39 @@ +// +// LogsView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/04/2023. +// + +import SwiftUI + +struct LogsView: View { + var body: some View { + List { + ForEach(FileLogger.main.logs) { log in + LogView(log: log) + } + } + } +} + +struct LogView: View { + + var log: Log + + var body: some View { + + VStack(alignment: .leading) { + Text(log.date, style: .time) + .foregroundColor(.gray) + Text(log.content) + }.font(.footnote) + } +} + +struct LogsView_Previews: PreviewProvider { + + static var previews: some View { + LogView(log: Log(date: Date(), file: "text.txt", line: 100, function: "test()", message: "crazy stuff here")) + } +}