From 6edf926a7d80258021957b1c3736ca4789bf9352 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 21 Feb 2023 16:28:13 +0100 Subject: [PATCH] Improvements + does not play last sound --- LeCountdown/Conductor.swift | 2 +- LeCountdown/Model/Model+Extensions.swift | 18 ++- .../Model/NSManagedContext+Extensions.swift | 4 +- LeCountdown/Sound/Sound.swift | 2 + LeCountdown/Stats/Filter.swift | 12 +- LeCountdown/Utils/Preferences.swift | 2 + LeCountdown/Views/HomeView.swift | 15 +- LeCountdown/Views/Stats/ActivitiesView.swift | 6 +- LeCountdown/Views/Stats/ActivityView.swift | 101 ++++++++------ LeCountdown/Views/Stats/RecordsView.swift | 130 +++++++++++------- 10 files changed, 184 insertions(+), 108 deletions(-) diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index d4fd04d..5e4c220 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -15,7 +15,7 @@ class Conductor: ObservableObject { static let maestro: Conductor = Conductor() @Published var soundPlayer: SoundPlayer? = nil - + @UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] @UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date] diff --git a/LeCountdown/Model/Model+Extensions.swift b/LeCountdown/Model/Model+Extensions.swift index e8e3c81..e13ce5e 100644 --- a/LeCountdown/Model/Model+Extensions.swift +++ b/LeCountdown/Model/Model+Extensions.swift @@ -27,9 +27,21 @@ extension AbstractSoundTimer { } var coolSound: Sound { - var allSounds: [Sound] = [] - allSounds.append(contentsOf: self.sounds) - return allSounds.randomElement() ?? Sound.allCases[0] + var sounds = self.sounds + + // remove last played sound if the playlist has at least 3 sounds + if sounds.count > 2, + let lastSoundId = Preferences.lastSelectedSound[self.stringId], + let lastSound = Sound(rawValue: lastSoundId) { + sounds.remove(lastSound) + } + + if let random = sounds.randomElement() { + Preferences.lastSelectedSound[self.stringId] = random.id + return random + } + + return Sound.default } var soundName: String { diff --git a/LeCountdown/Model/NSManagedContext+Extensions.swift b/LeCountdown/Model/NSManagedContext+Extensions.swift index eb01af0..3531876 100644 --- a/LeCountdown/Model/NSManagedContext+Extensions.swift +++ b/LeCountdown/Model/NSManagedContext+Extensions.swift @@ -17,9 +17,10 @@ extension NSManagedObjectContext { } func distinct(entityName: String, attributes: [String]) throws -> [Any] { - + let request = NSFetchRequest(entityName: entityName) + request.entity = NSEntityDescription.entity(forEntityName: entityName, in: self) request.returnsDistinctResults = true request.resultType = .dictionaryResultType if let entity = request.entity { @@ -34,7 +35,6 @@ extension NSManagedObjectContext { } return try self.fetch(request) - } } diff --git a/LeCountdown/Sound/Sound.swift b/LeCountdown/Sound/Sound.swift index adfa63d..48983a1 100644 --- a/LeCountdown/Sound/Sound.swift +++ b/LeCountdown/Sound/Sound.swift @@ -72,6 +72,8 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { case sbHighChords_Loop_River case sbMatriarchFxs_Loop2_Collider + static var `default`: Sound { .sbSEM_Synths_Loop4_Nothing_Like_You } + var localizedString: String { switch self { case .trainhorn: return NSLocalizedString("Train horn", comment: "") diff --git a/LeCountdown/Stats/Filter.swift b/LeCountdown/Stats/Filter.swift index c5a5744..849cc5b 100644 --- a/LeCountdown/Stats/Filter.swift +++ b/LeCountdown/Stats/Filter.swift @@ -9,11 +9,14 @@ import Foundation enum Filter: Identifiable, Hashable { + case none case year(_ year: Int) case month(_ month: Month) var predicate: NSPredicate { switch self { + case .none: + return NSPredicate(value: true) case .year(let year): return NSPredicate(format: "start >= %@ AND end < %@", year.startOfYear, (year + 1).startOfYear) case .month(let month): @@ -23,7 +26,8 @@ enum Filter: Identifiable, Hashable { var localizedString: String { switch self { - case .year(let year): return year.formatted() + case .none: return NSLocalizedString("All", comment: "") + case .year(let year): return "\(year)" case .month(let month): return month.localizedString } } @@ -72,7 +76,11 @@ struct Month { return NSDate() } - fileprivate static let _dateFormatter = DateFormatter() + fileprivate static let _dateFormatter = { + let df = DateFormatter() + df.dateFormat = "MMMM yyyy" + return df + }() var localizedString: String { let components = DateComponents(year: self.year, month: self.month) diff --git a/LeCountdown/Utils/Preferences.swift b/LeCountdown/Utils/Preferences.swift index a9d3fa1..fa5235b 100644 --- a/LeCountdown/Utils/Preferences.swift +++ b/LeCountdown/Utils/Preferences.swift @@ -12,12 +12,14 @@ enum PreferenceKey: String { case stopwatches case showSilentModeAlert case soundDurations + case lastSoundPlayed } class Preferences { @UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] @UserDefault(PreferenceKey.soundDurations.rawValue, defaultValue: [:]) static var soundDurations: [Int : TimeInterval] + @UserDefault(PreferenceKey.lastSoundPlayed.rawValue, defaultValue: [:]) static var lastSelectedSound: [String : Int] static var hideSilentModeAlerts: Bool { return UserDefaults.standard.bool(forKey: PreferenceKey.showSilentModeAlert.rawValue) diff --git a/LeCountdown/Views/HomeView.swift b/LeCountdown/Views/HomeView.swift index f261df8..2fb817d 100644 --- a/LeCountdown/Views/HomeView.swift +++ b/LeCountdown/Views/HomeView.swift @@ -62,12 +62,13 @@ struct RegularHomeView: View { var body: some View { - NavigationStack { + NavigationView { + PresetsView(tabSelection: $tabSelection) + .environment(\.managedObjectContext, viewContext) + .tabItem { Label("Presets", systemImage: "globe") } + .tag(0) TabView(selection: $tabSelection) { - PresetsView(tabSelection: $tabSelection) - .environment(\.managedObjectContext, viewContext) - .tabItem { Label("Presets", systemImage: "globe") } - .tag(0) + ContentView() .environment(\.managedObjectContext, viewContext) .environmentObject(Conductor.maestro) @@ -77,9 +78,9 @@ struct RegularHomeView: View { .environment(\.managedObjectContext, viewContext) .tabItem { Label("Stats", systemImage: "chart.bar.fill") } .tag(2) - } + }.tabViewStyle(.page(indexDisplayMode: .never)) } - .tabViewStyle(.page(indexDisplayMode: .never)) + .onOpenURL { _ in self.tabSelection = 1 } diff --git a/LeCountdown/Views/Stats/ActivitiesView.swift b/LeCountdown/Views/Stats/ActivitiesView.swift index 7a1374b..d6765bc 100644 --- a/LeCountdown/Views/Stats/ActivitiesView.swift +++ b/LeCountdown/Views/Stats/ActivitiesView.swift @@ -32,15 +32,15 @@ struct ActivitiesView: View { ForEach(self.activities) { activity in NavigationLink { ActivityView(activity: activity) + .environment(\.managedObjectContext, viewContext) } label: { HStack { Text(activity.name ?? "no activity") - .foregroundColor(.black).font(.title) Spacer() Text(activity.recordCount) - .font(.system(.title, weight: .bold)) + .font(.system(.body, weight: .bold)) .foregroundColor(.accentColor) - }.padding(.vertical, 4.0) + } } } } diff --git a/LeCountdown/Views/Stats/ActivityView.swift b/LeCountdown/Views/Stats/ActivityView.swift index c63adf1..90f807d 100644 --- a/LeCountdown/Views/Stats/ActivityView.swift +++ b/LeCountdown/Views/Stats/ActivityView.swift @@ -8,6 +8,45 @@ import SwiftUI import CoreData +struct ActivityView: View { + + @Environment(\.managedObjectContext) private var viewContext + + var activity: Activity + + @State var selectedTimeFrame: TimeFrame = .all + + var body: some View { + + VStack { + Picker("Time", selection: $selectedTimeFrame) { + ForEach(TimeFrame.allCases) { timeFrame in + Text(timeFrame.localizedString).tag(timeFrame) + } + } + .pickerStyle(.segmented) + .padding(.horizontal) + +// List { + +// Section { +// StatsView(activity: self.activity, +// timeFrame: self.selectedTimeFrame) +// .environment(\.managedObjectContext, viewContext) +// } + +// Section { + RecordsView(activity: self.activity, + timeFrame: self.selectedTimeFrame) + .environment(\.managedObjectContext, viewContext) +// } +// } + + } + } + +} + enum TimeFrame: Int, Identifiable, CaseIterable { var id: Int { return self.rawValue } @@ -29,14 +68,31 @@ enum TimeFrame: Int, Identifiable, CaseIterable { do { switch self { case .all: - return [] + return [.none] case .year: - if let years = try context.distinct(entityName: "Record", attributes: ["year"]) as? [Int] { + let distinct = try context.distinct(entityName: "Record", attributes: ["year"]) + if let yearsMap = distinct as? [[String : Int]] { + let years = yearsMap.compactMap { $0.first?.value } return years.map { Filter.year($0) } + } else { + Logger.w("Could not cast \(distinct) as [Int]") } case .month: - if let months = try context.distinct(entityName: "Record", attributes: ["year", "month"]) as? [Int] { - return months.map { Filter.month(Month(month: $0, year: 1)) } + let distinct = try context.distinct(entityName: "Record", attributes: ["year", "month"]) + if let distinctMonths = distinct as? [[String : Int]] { + + let months = distinctMonths.compactMap { + if let month = $0["month"], + let year = $0["year"] { + return Month(month: month, year: year) + } else { + Logger.w("issue with dictionary \($0)") + return nil + } + } + return months.map { Filter.month($0) } + } else { + Logger.w("Could not cast \(distinct) as [Int]") } } } catch { @@ -48,43 +104,6 @@ enum TimeFrame: Int, Identifiable, CaseIterable { } -struct ActivityView: View { - - @Environment(\.managedObjectContext) private var viewContext - - var activity: Activity - - @State private var selectedTimeFrame: TimeFrame = .all - - var body: some View { - - VStack { - Picker("", selection: $selectedTimeFrame) { - ForEach(TimeFrame.allCases) { timeFrame in - Text(timeFrame.localizedString).tag(timeFrame) - } - } - .pickerStyle(.segmented) - - List { - - Section { - StatsView(activity: self.activity, timeFrame: self.selectedTimeFrame) - .environment(\.managedObjectContext, viewContext) - } - - Section { - RecordsView(activity: self.activity, timeFrame: self.selectedTimeFrame) - .environment(\.managedObjectContext, viewContext) - } - } - - } - - - } -} - struct ActivityView_Previews: PreviewProvider { static var previews: some View { ActivityView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext)) diff --git a/LeCountdown/Views/Stats/RecordsView.swift b/LeCountdown/Views/Stats/RecordsView.swift index 3217d0e..9c8412a 100644 --- a/LeCountdown/Views/Stats/RecordsView.swift +++ b/LeCountdown/Views/Stats/RecordsView.swift @@ -9,27 +9,69 @@ import SwiftUI import CoreData class RecordsModel: ObservableObject { - + @Published var filters: [Filter] = [] - @Published var recordsByFilter: [Filter : [Record]] = [:] func loadFilters(activity: Activity, timeFrame: TimeFrame, context: NSManagedObjectContext) { + self.filters = timeFrame.filters(context: context) + } + +} + +struct RecordsView: View { + + @Environment(\.managedObjectContext) private var viewContext + + var activity: Activity + + var timeFrame: TimeFrame { + didSet { + self._loadFilters() + } + } + + @StateObject var model: RecordsModel = RecordsModel() + +// var request: FetchRequest + +// init(activity: Activity, timeFrame: TimeFrame) { +// self.activity = activity +// self.timeFrame = timeFrame +// } + + var body: some View { - if self.recordsByFilter.isEmpty { - self.filters = timeFrame.filters(context: context) - for filter in filters { - let records = self._retrieveRecords(activity: activity, filter: filter, context: context) - self.recordsByFilter[filter] = records + List { + ForEach(self.model.filters) { filter in + RecordsSectionView(activity: self.activity, filter: filter) + .environment(\.managedObjectContext, viewContext) } + }.onChange(of: self.timeFrame) { newValue in + self._loadFilters() } } + fileprivate func _loadFilters() { + self.model.loadFilters(activity: self.activity, timeFrame: self.timeFrame, context: self.viewContext) + } + +} + +class RecordsSectionModel: ObservableObject { + + @Published var records: [Record] = [] + + func loadRecords(activity: Activity, filter: Filter, context: NSManagedObjectContext) { + let records = self._retrieveRecords(activity: activity, filter: filter, context: context) + self.records = records + } + fileprivate func _retrieveRecords(activity: Activity, filter: Filter, context: NSManagedObjectContext) -> [Record] { do { let request = Record.fetchRequest() let activityPredicate = NSPredicate(format: "activity = %@", activity) request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [activityPredicate, filter.predicate]) - request.sortDescriptors = [NSSortDescriptor(key: "start", ascending: true)] + request.sortDescriptors = [NSSortDescriptor(key: "start", ascending: false)] return try context.fetch(request) } catch { Logger.log(error) @@ -37,65 +79,54 @@ class RecordsModel: ObservableObject { } } - func records(by filter: Filter) -> [Record] { - return self.recordsByFilter[filter] ?? [] + func deleteItem(_ indexSet: IndexSet, context: NSManagedObjectContext) { + + for index in indexSet { + let item = self.records[index] + context.delete(item) + + do { + try context.save() + } catch { + Logger.error(error) + } + } + } } -struct RecordsView: View { +struct RecordsSectionView: View { @Environment(\.managedObjectContext) private var viewContext var activity: Activity - var timeFrame: TimeFrame - var request: FetchRequest - - @StateObject private var model: RecordsModel = RecordsModel() + @State var filter: Filter - init(activity: Activity, timeFrame: TimeFrame) { - self.activity = activity - self.timeFrame = timeFrame - let predicate = NSPredicate(format: "activity = %@", activity) - self.request = FetchRequest(sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)], predicate: predicate) - } + @StateObject var model: RecordsSectionModel = RecordsSectionModel() var body: some View { - ForEach(self.model.filters) { filter in - - Section(filter.localizedString) { - ForEach(self.model.records(by: filter)) { record in - HStack { - Text(record.formattedDay) - Spacer() - if let duration = record.duration { - Text(duration.minuteSecond) - } + Section(filter.localizedString) { + ForEach(self.model.records) { record in + HStack { + Text(record.formattedDay) + Spacer() + if let duration = record.duration { + Text(duration.minuteSecond) } } - } - - } - .onDelete(perform: _deleteItem) - .onAppear { - self.model.loadFilters(activity: self.activity, timeFrame: self.timeFrame, context: self.viewContext) + }.onDelete(perform: _deleteItem) + }.onAppear { + self.model.loadRecords(activity: self.activity, + filter: self.filter, + context: viewContext) } } fileprivate func _deleteItem(_ indexSet: IndexSet) { - - for index in indexSet { - let item = self.request.wrappedValue[index] - viewContext.delete(item) - - do { - try viewContext.save() - } catch { - Logger.error(error) - } - } + self.model.deleteItem(indexSet, context: self.viewContext) } } @@ -103,6 +134,7 @@ struct RecordsView: View { struct RecordsView_Previews: PreviewProvider { static var previews: some View { RecordsView(activity: - Activity.fake(context: PersistenceController.preview.container.viewContext), timeFrame: .all) + Activity.fake(context: PersistenceController.preview.container.viewContext), + timeFrame: .all) } }