Improvements + does not play last sound

release
Laurent 3 years ago
parent f62d3f23e7
commit 6edf926a7d
  1. 2
      LeCountdown/Conductor.swift
  2. 18
      LeCountdown/Model/Model+Extensions.swift
  3. 4
      LeCountdown/Model/NSManagedContext+Extensions.swift
  4. 2
      LeCountdown/Sound/Sound.swift
  5. 12
      LeCountdown/Stats/Filter.swift
  6. 2
      LeCountdown/Utils/Preferences.swift
  7. 15
      LeCountdown/Views/HomeView.swift
  8. 6
      LeCountdown/Views/Stats/ActivitiesView.swift
  9. 101
      LeCountdown/Views/Stats/ActivityView.swift
  10. 130
      LeCountdown/Views/Stats/RecordsView.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]

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

@ -17,9 +17,10 @@ extension NSManagedObjectContext {
}
func distinct(entityName: String, attributes: [String]) throws -> [Any] {
let request = NSFetchRequest<NSFetchRequestResult>(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)
}
}

@ -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: "")

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

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

@ -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<AbstractTimer>()
.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
}

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

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

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

Loading…
Cancel
Save