Adds log view

main
Laurent 3 years ago
parent a2f8146a77
commit 2cca84e670
  1. 22
      LeCountdown.xcodeproj/project.pbxproj
  2. 51
      LeCountdown/Conductor.swift
  3. 8
      LeCountdown/Sound/DelaySoundPlayer.swift
  4. 59
      LeCountdown/Utils/Codable+Extensions.swift
  5. 82
      LeCountdown/Utils/FileLogger.swift
  6. 54
      LeCountdown/Utils/FileUtils.swift
  7. 11
      LeCountdown/Views/ContentView.swift
  8. 39
      LeCountdown/Views/LogsView.swift

@ -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 = "<group>"; };
C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = "<group>"; };
C445FA912987CC8A0054D761 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = "<group>"; };
C4556F6A29E40B7800DEB40B /* FileLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogger.swift; sourceTree = "<group>"; };
C4556F6E29E40BED00DEB40B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = "<group>"; };
C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = "<group>"; };
@ -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 */,

@ -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<Date?, Error>) -> 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()

@ -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()

@ -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 : Decodable>() -> T? {
return self.data(using: .utf8)?.decode()
}
func decodeArray<T : Decodable>() throws -> [T]? {
return try self.data(using: .utf8)?.decodeArray()
}
}
extension Data {
func decode<T : Decodable>() -> T? {
do {
return try JSONDecoder().decode(T.self, from: self)
} catch {
Logger.error(error)
return nil
}
}
func decodeArray<T : Decodable>() throws -> [T] {
return try JSONDecoder().decode([T].self, from: self)
}
}

@ -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)
}
}

@ -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
}
}

@ -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 {

@ -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"))
}
}
Loading…
Cancel
Save