Improvements + does not play last sound

release
Laurent 3 years ago
parent f62d3f23e7
commit 6edf926a7d
  1. 18
      LeCountdown/Model/Model+Extensions.swift
  2. 2
      LeCountdown/Model/NSManagedContext+Extensions.swift
  3. 2
      LeCountdown/Sound/Sound.swift
  4. 12
      LeCountdown/Stats/Filter.swift
  5. 2
      LeCountdown/Utils/Preferences.swift
  6. 15
      LeCountdown/Views/HomeView.swift
  7. 6
      LeCountdown/Views/Stats/ActivitiesView.swift
  8. 101
      LeCountdown/Views/Stats/ActivityView.swift
  9. 128
      LeCountdown/Views/Stats/RecordsView.swift

@ -27,9 +27,21 @@ extension AbstractSoundTimer {
} }
var coolSound: Sound { var coolSound: Sound {
var allSounds: [Sound] = [] var sounds = self.sounds
allSounds.append(contentsOf: self.sounds)
return allSounds.randomElement() ?? Sound.allCases[0] // 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 { var soundName: String {

@ -20,6 +20,7 @@ extension NSManagedObjectContext {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName) let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
request.entity = NSEntityDescription.entity(forEntityName: entityName, in: self)
request.returnsDistinctResults = true request.returnsDistinctResults = true
request.resultType = .dictionaryResultType request.resultType = .dictionaryResultType
if let entity = request.entity { if let entity = request.entity {
@ -34,7 +35,6 @@ extension NSManagedObjectContext {
} }
return try self.fetch(request) return try self.fetch(request)
} }
} }

@ -72,6 +72,8 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
case sbHighChords_Loop_River case sbHighChords_Loop_River
case sbMatriarchFxs_Loop2_Collider case sbMatriarchFxs_Loop2_Collider
static var `default`: Sound { .sbSEM_Synths_Loop4_Nothing_Like_You }
var localizedString: String { var localizedString: String {
switch self { switch self {
case .trainhorn: return NSLocalizedString("Train horn", comment: "") case .trainhorn: return NSLocalizedString("Train horn", comment: "")

@ -9,11 +9,14 @@ import Foundation
enum Filter: Identifiable, Hashable { enum Filter: Identifiable, Hashable {
case none
case year(_ year: Int) case year(_ year: Int)
case month(_ month: Month) case month(_ month: Month)
var predicate: NSPredicate { var predicate: NSPredicate {
switch self { switch self {
case .none:
return NSPredicate(value: true)
case .year(let year): case .year(let year):
return NSPredicate(format: "start >= %@ AND end < %@", year.startOfYear, (year + 1).startOfYear) return NSPredicate(format: "start >= %@ AND end < %@", year.startOfYear, (year + 1).startOfYear)
case .month(let month): case .month(let month):
@ -23,7 +26,8 @@ enum Filter: Identifiable, Hashable {
var localizedString: String { var localizedString: String {
switch self { 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 case .month(let month): return month.localizedString
} }
} }
@ -72,7 +76,11 @@ struct Month {
return NSDate() return NSDate()
} }
fileprivate static let _dateFormatter = DateFormatter() fileprivate static let _dateFormatter = {
let df = DateFormatter()
df.dateFormat = "MMMM yyyy"
return df
}()
var localizedString: String { var localizedString: String {
let components = DateComponents(year: self.year, month: self.month) let components = DateComponents(year: self.year, month: self.month)

@ -12,12 +12,14 @@ enum PreferenceKey: String {
case stopwatches case stopwatches
case showSilentModeAlert case showSilentModeAlert
case soundDurations case soundDurations
case lastSoundPlayed
} }
class Preferences { class Preferences {
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] @UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval]
@UserDefault(PreferenceKey.soundDurations.rawValue, defaultValue: [:]) static var soundDurations: [Int : TimeInterval] @UserDefault(PreferenceKey.soundDurations.rawValue, defaultValue: [:]) static var soundDurations: [Int : TimeInterval]
@UserDefault(PreferenceKey.lastSoundPlayed.rawValue, defaultValue: [:]) static var lastSelectedSound: [String : Int]
static var hideSilentModeAlerts: Bool { static var hideSilentModeAlerts: Bool {
return UserDefaults.standard.bool(forKey: PreferenceKey.showSilentModeAlert.rawValue) return UserDefaults.standard.bool(forKey: PreferenceKey.showSilentModeAlert.rawValue)

@ -62,12 +62,13 @@ struct RegularHomeView: View {
var body: some View { var body: some View {
NavigationStack { NavigationView {
PresetsView(tabSelection: $tabSelection)
.environment(\.managedObjectContext, viewContext)
.tabItem { Label("Presets", systemImage: "globe") }
.tag(0)
TabView(selection: $tabSelection) { TabView(selection: $tabSelection) {
PresetsView(tabSelection: $tabSelection)
.environment(\.managedObjectContext, viewContext)
.tabItem { Label("Presets", systemImage: "globe") }
.tag(0)
ContentView<AbstractTimer>() ContentView<AbstractTimer>()
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro) .environmentObject(Conductor.maestro)
@ -77,9 +78,9 @@ struct RegularHomeView: View {
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") } .tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(2) .tag(2)
} }.tabViewStyle(.page(indexDisplayMode: .never))
} }
.tabViewStyle(.page(indexDisplayMode: .never))
.onOpenURL { _ in .onOpenURL { _ in
self.tabSelection = 1 self.tabSelection = 1
} }

@ -32,15 +32,15 @@ struct ActivitiesView: View {
ForEach(self.activities) { activity in ForEach(self.activities) { activity in
NavigationLink { NavigationLink {
ActivityView(activity: activity) ActivityView(activity: activity)
.environment(\.managedObjectContext, viewContext)
} label: { } label: {
HStack { HStack {
Text(activity.name ?? "no activity") Text(activity.name ?? "no activity")
.foregroundColor(.black).font(.title)
Spacer() Spacer()
Text(activity.recordCount) Text(activity.recordCount)
.font(.system(.title, weight: .bold)) .font(.system(.body, weight: .bold))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
}.padding(.vertical, 4.0) }
} }
} }
} }

@ -8,6 +8,45 @@
import SwiftUI import SwiftUI
import CoreData 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 { enum TimeFrame: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue } var id: Int { return self.rawValue }
@ -29,14 +68,31 @@ enum TimeFrame: Int, Identifiable, CaseIterable {
do { do {
switch self { switch self {
case .all: case .all:
return [] return [.none]
case .year: 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) } return years.map { Filter.year($0) }
} else {
Logger.w("Could not cast \(distinct) as [Int]")
} }
case .month: case .month:
if let months = try context.distinct(entityName: "Record", attributes: ["year", "month"]) as? [Int] { let distinct = try context.distinct(entityName: "Record", attributes: ["year", "month"])
return months.map { Filter.month(Month(month: $0, year: 1)) } 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 { } 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 { struct ActivityView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ActivityView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext)) ActivityView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext))

@ -11,25 +11,67 @@ import CoreData
class RecordsModel: ObservableObject { class RecordsModel: ObservableObject {
@Published var filters: [Filter] = [] @Published var filters: [Filter] = []
@Published var recordsByFilter: [Filter : [Record]] = [:]
func loadFilters(activity: Activity, timeFrame: TimeFrame, context: NSManagedObjectContext) { 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()
if self.recordsByFilter.isEmpty { // var request: FetchRequest<Record>
self.filters = timeFrame.filters(context: context)
for filter in filters { // init(activity: Activity, timeFrame: TimeFrame) {
let records = self._retrieveRecords(activity: activity, filter: filter, context: context) // self.activity = activity
self.recordsByFilter[filter] = records // self.timeFrame = timeFrame
// }
var body: some View {
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] { fileprivate func _retrieveRecords(activity: Activity, filter: Filter, context: NSManagedObjectContext) -> [Record] {
do { do {
let request = Record.fetchRequest() let request = Record.fetchRequest()
let activityPredicate = NSPredicate(format: "activity = %@", activity) let activityPredicate = NSPredicate(format: "activity = %@", activity)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [activityPredicate, filter.predicate]) 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) return try context.fetch(request)
} catch { } catch {
Logger.log(error) Logger.log(error)
@ -37,65 +79,54 @@ class RecordsModel: ObservableObject {
} }
} }
func records(by filter: Filter) -> [Record] { func deleteItem(_ indexSet: IndexSet, context: NSManagedObjectContext) {
return self.recordsByFilter[filter] ?? []
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 @Environment(\.managedObjectContext) private var viewContext
var activity: Activity var activity: Activity
var timeFrame: TimeFrame @State var filter: Filter
var request: FetchRequest<Record>
@StateObject private var model: RecordsModel = RecordsModel()
init(activity: Activity, timeFrame: TimeFrame) { @StateObject var model: RecordsSectionModel = RecordsSectionModel()
self.activity = activity
self.timeFrame = timeFrame
let predicate = NSPredicate(format: "activity = %@", activity)
self.request = FetchRequest(sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)], predicate: predicate)
}
var body: some View { var body: some View {
ForEach(self.model.filters) { filter in Section(filter.localizedString) {
ForEach(self.model.records) { record in
Section(filter.localizedString) { HStack {
ForEach(self.model.records(by: filter)) { record in Text(record.formattedDay)
HStack { Spacer()
Text(record.formattedDay) if let duration = record.duration {
Spacer() Text(duration.minuteSecond)
if let duration = record.duration {
Text(duration.minuteSecond)
}
} }
} }
} }.onDelete(perform: _deleteItem)
}.onAppear {
} self.model.loadRecords(activity: self.activity,
.onDelete(perform: _deleteItem) filter: self.filter,
.onAppear { context: viewContext)
self.model.loadFilters(activity: self.activity, timeFrame: self.timeFrame, context: self.viewContext)
} }
} }
fileprivate func _deleteItem(_ indexSet: IndexSet) { fileprivate func _deleteItem(_ indexSet: IndexSet) {
self.model.deleteItem(indexSet, context: self.viewContext)
for index in indexSet {
let item = self.request.wrappedValue[index]
viewContext.delete(item)
do {
try viewContext.save()
} catch {
Logger.error(error)
}
}
} }
} }
@ -103,6 +134,7 @@ struct RecordsView: View {
struct RecordsView_Previews: PreviewProvider { struct RecordsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
RecordsView(activity: RecordsView(activity:
Activity.fake(context: PersistenceController.preview.container.viewContext), timeFrame: .all) Activity.fake(context: PersistenceController.preview.container.viewContext),
timeFrame: .all)
} }
} }

Loading…
Cancel
Save