From dae08d79f6afcf2fadf5df2d1341b0bdd1f70441 Mon Sep 17 00:00:00 2001 From: oliverhnat Date: Fri, 12 Apr 2024 15:32:32 +0200 Subject: [PATCH] Add countdown till when the next flashcard is going to show up --- WordAX.xcodeproj/project.pbxproj | 6 ++ WordAX/Model/DataController.swift | 1 + WordAX/Model/Flashcard+CoreDataClass.swift | 39 ++++++-- .../Model/Flashcard+CoreDataProperties.swift | 2 - .../WordAX0.0.3.xcdatamodel/contents | 13 +++ WordAX/Views/AnkiView.swift | 91 ++++++------------- 6 files changed, 79 insertions(+), 73 deletions(-) create mode 100644 WordAX/Model/WordAXCD.xcdatamodeld/WordAX0.0.3.xcdatamodel/contents diff --git a/WordAX.xcodeproj/project.pbxproj b/WordAX.xcodeproj/project.pbxproj index 43c5a96..6df76dc 100644 --- a/WordAX.xcodeproj/project.pbxproj +++ b/WordAX.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 6CEF7F812BC4694900E205F6 /* WordAXCD.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */; }; 6CEF7F842BC46B5900E205F6 /* Flashcard+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F822BC46B5900E205F6 /* Flashcard+CoreDataClass.swift */; }; 6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */; }; + 6CEF7FA62BC96F2B00E205F6 /* ButtonHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */; }; 6CF439522B83541D004C3543 /* WordAXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439512B83541D004C3543 /* WordAXApp.swift */; }; 6CF439542B83541D004C3543 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439532B83541D004C3543 /* MainView.swift */; }; 6CF439562B83541E004C3543 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6CF439552B83541E004C3543 /* Assets.xcassets */; }; @@ -42,6 +43,8 @@ 6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.1.xcdatamodel; sourceTree = ""; }; 6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.2.xcdatamodel; sourceTree = ""; }; 6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Flashcard+CoreDataProperties.swift"; sourceTree = ""; }; + 6CEF7FA42BC95F0300E205F6 /* WordAX0.0.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.3.xcdatamodel; sourceTree = ""; }; + 6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHStackView.swift; sourceTree = ""; }; 6CF4394E2B83541D004C3543 /* WordAX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WordAX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6CF439512B83541D004C3543 /* WordAXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordAXApp.swift; sourceTree = ""; }; 6CF439532B83541D004C3543 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -70,6 +73,7 @@ 6C81850B2B8BA6BC0033CF46 /* FlashCardListRowView.swift */, 6C8185012B88C9FB0033CF46 /* SettingsView.swift */, 6C8185032B88CA210033CF46 /* AnkiView.swift */, + 6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */, 6C8185052B8A537F0033CF46 /* FlashCardView.swift */, ); path = Views; @@ -200,6 +204,7 @@ 6C8185062B8A537F0033CF46 /* FlashCardView.swift in Sources */, 6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */, 6C81850A2B8BA5740033CF46 /* FlashCardListView.swift in Sources */, + 6CEF7FA62BC96F2B00E205F6 /* ButtonHStackView.swift in Sources */, 6C8185002B88C9660033CF46 /* WordAXModelView.swift in Sources */, 6C8185042B88CA210033CF46 /* AnkiView.swift in Sources */, 6CEF7F7D2BC457E600E205F6 /* DataController.swift in Sources */, @@ -419,6 +424,7 @@ 6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 6CEF7FA42BC95F0300E205F6 /* WordAX0.0.3.xcdatamodel */, 6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */, 6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */, 6CEF7F802BC4694900E205F6 /* WordAX.xcdatamodel */, diff --git a/WordAX/Model/DataController.swift b/WordAX/Model/DataController.swift index 032e15f..ffd8b6e 100644 --- a/WordAX/Model/DataController.swift +++ b/WordAX/Model/DataController.swift @@ -33,6 +33,7 @@ class DataController: ObservableObject { flashcard.lastSeenOn = [Date().addingTimeInterval([86400, 24000, 100000].randomElement()!)].randomElement()! flashcard.shownCount = [0, 1, 2, 3, 4, 5].randomElement()! flashcard.dateAdded = [Date(), Date().addingTimeInterval(-86400), Date().addingTimeInterval(-172800)].randomElement()! +// flashcard.calculatedNextRepetition = flashcard.lastSeenOn ?? Date() + TimeInterval(flashcard.nextSpacedRepetitionMilestone) } do { try viewContext.save() diff --git a/WordAX/Model/Flashcard+CoreDataClass.swift b/WordAX/Model/Flashcard+CoreDataClass.swift index 11534e9..c18c232 100644 --- a/WordAX/Model/Flashcard+CoreDataClass.swift +++ b/WordAX/Model/Flashcard+CoreDataClass.swift @@ -11,6 +11,25 @@ import CoreData @objc(Flashcard) public class Flashcard: NSManagedObject { + + @objc dynamic var calculatedNextRepetition: Date { + if lastSeenOn != nil { + return lastSeenOn!.addSpacedRepetitionMilestone(milestone: self.getSpacedRepetitionMilestone()) + } else { + return Date() + } + } + override public class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { +// public override func value(forKey key: String) -> Any? { + let keyPaths = super.keyPathsForValuesAffectingValue(forKey: key) + if key == "calculatedNextRepetition" { + return keyPaths.union(Set(["lastSeenOn", "nextSpacedRepetitionMilestone"])) + } else { + return keyPaths + } + } + + enum SpacedRepetitionMilestoneEnum: Int64, CaseIterable { case Now = 0 // starting value case OneMinute = 60 // 60 * 1 @@ -31,28 +50,28 @@ public class Flashcard: NSManagedObject { static func getNext(milestone: SpacedRepetitionMilestoneEnum?) -> SpacedRepetitionMilestoneEnum { let sorted = SpacedRepetitionMilestoneEnum.allCasesSorted if milestone == nil { - return SpacedRepetitionMilestoneEnum.TenMinutes + return .TenMinutes } let milestoneIndex = sorted.firstIndex(where: {$0.rawValue == milestone!.rawValue})! if milestoneIndex < SpacedRepetitionMilestoneEnum.allCasesSorted.count { return sorted[milestoneIndex + 1] } - return SpacedRepetitionMilestoneEnum.OneYear + return .OneYear } static func getMilestoneFromInt(value: Int64) -> SpacedRepetitionMilestoneEnum { - return SpacedRepetitionMilestoneEnum.allCasesSorted.first(where: {$0.rawValue == value}) ?? SpacedRepetitionMilestoneEnum.Now + return .allCasesSorted.first(where: {$0.rawValue == value}) ?? .Now } } - public override func didChangeValue(forKey key: String) { - super.didChangeValue(forKey: key) - if key == "lastSeenOn" || key == "nextSpacedRepetitionMilestone" { - // updateCalculatedNextRepetition() - calculatedNextRepetition = lastSeenOn ?? Date() + TimeInterval(nextSpacedRepetitionMilestone) - } - } +// public override func didChangeValue(forKey key: String) { +// super.didChangeValue(forKey: key) +// if key == "lastSeenOn" || key == "nextSpacedRepetitionMilestone" { +// // updateCalculatedNextRepetition() +// calculatedNextRepetition = lastSeenOn ?? Date() + TimeInterval(nextSpacedRepetitionMilestone) +// } +// } // func updateCalculatedNextRepetition() { // if let lastSeen = lastSeenOn { diff --git a/WordAX/Model/Flashcard+CoreDataProperties.swift b/WordAX/Model/Flashcard+CoreDataProperties.swift index ec6dbf7..758e022 100644 --- a/WordAX/Model/Flashcard+CoreDataProperties.swift +++ b/WordAX/Model/Flashcard+CoreDataProperties.swift @@ -24,8 +24,6 @@ extension Flashcard { @NSManaged public var nextSpacedRepetitionMilestone: Int64 @NSManaged public var shown: Bool @NSManaged public var shownCount: Int64 - @NSManaged public var calculatedNextRepetition: Date? - } extension Flashcard : Identifiable { diff --git a/WordAX/Model/WordAXCD.xcdatamodeld/WordAX0.0.3.xcdatamodel/contents b/WordAX/Model/WordAXCD.xcdatamodeld/WordAX0.0.3.xcdatamodel/contents new file mode 100644 index 0000000..d8ed334 --- /dev/null +++ b/WordAX/Model/WordAXCD.xcdatamodeld/WordAX0.0.3.xcdatamodel/contents @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/WordAX/Views/AnkiView.swift b/WordAX/Views/AnkiView.swift index 36d92ef..dfe9f56 100644 --- a/WordAX/Views/AnkiView.swift +++ b/WordAX/Views/AnkiView.swift @@ -21,19 +21,18 @@ struct AnkiView: View { ]), NSPredicate(format: "lastSeenOn == nil") ])) var flashcards: FetchedResults - @FetchRequest( - sortDescriptors: [ - NSSortDescriptor(key: "calculatedNextRepetition", ascending: false) - ], - predicate: - NSCompoundPredicate(type: .and, subpredicates: [ - NSPredicate(format: "%K != nil", "lastSeenOn"), - NSPredicate( - format: "lastSeenOn + nextSpacedRepetitionMilestone > %@", Date() as CVarArg) - ] - )) var soonestFlashcard: FetchedResults + + @State private var timeRemaining = 10 + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + @State var sortedFlashcards: [Flashcard] = [] + + @Environment(\.colorScheme) var colorScheme + // get the most recent flashcard + @FetchRequest(sortDescriptors: [], + predicate: NSPredicate(format: "%K != nil", "lastSeenOn")) var soonestFlashcard: FetchedResults + @State var showDescription = false @@ -86,62 +85,32 @@ struct AnkiView: View { } } } else { - // TODO: Add countdown to the next available word - VStack{ - if !soonestFlashcard.isEmpty { - Text("Time: \(timeRemaining.convertDurationSecondsToCountdown())") - .foregroundStyle(.white) - .padding(.horizontal, 20) - .padding(.vertical, 5) - .background(.black.opacity(0.75)) - .clipShape(.capsule) - } - -// if !soonestFlashcard.isEmpty { -// Text("HERE") -// Text("\(soonestFlashcard.first!.lastSeenOn!.addingTimeInterval(TimeInterval(soonestFlashcard.first!.nextSpacedRepetitionMilestone)).timeIntervalSince(Date()))") -// Text("\(Int64(timeRemaining))") -// Text("\(soonestFlashcard.first!.lastSeenOn!)") -// Text("\(soonestFlashcard.first!.getSpacedRepetitionMilestone().rawValue)") -// Text("\(soonestFlashcard.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone: soonestFlashcard.first!.getSpacedRepetitionMilestone()))") -// } -// if soonestFlashcard.first! { -// Text("HERE") -// } - - Text("There are currently no words to display") + if !soonestFlashcard.isEmpty { + Text("Next flashcard in: \(timeRemaining.convertDurationSecondsToCountdown())") + .foregroundStyle(.black) .padding() .background(.yellow) .clipShape(.buttonBorder) - } - .onAppear { - if !soonestFlashcard.isEmpty { - var lastSeen = soonestFlashcard.first!.lastSeenOn! - var showIn = soonestFlashcard.first!.getSpacedRepetitionMilestone() - var show = lastSeen.addSpacedRepetitionMilestone(milestone: showIn) - timeRemaining = Int(show.timeIntervalSinceNow) - } - } - .onReceive(timer) { time in - if !soonestFlashcard.isEmpty { - var lastSeen = soonestFlashcard.first!.lastSeenOn! - var showIn = soonestFlashcard.first!.getSpacedRepetitionMilestone() - var show = lastSeen.addSpacedRepetitionMilestone(milestone: showIn) - timeRemaining = Int(show.timeIntervalSinceNow) - } -// timeRemaining = Int(soonestFlashcard.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone: soonestFlashcard.first!.getSpacedRepetitionMilestone()).timeIntervalSince(Date())) -// timeRemaining = Int(soonestFlashcard.first!.lastSeenOn!.addingTimeInterval(TimeInterval(soonestFlashcard.first!.nextSpacedRepetitionMilestone)).timeIntervalSince(Date())) -// if timeRemaining > 0 { -// timeRemaining -= 1 -// } -// if !soonestFlashcard.isEmpty { -// timeRemaining = Int(Date().timeIntervalSince(soonestFlashcard.first!.lastSeenOn!)) -// } + .padding(.vertical, 50) + .padding(.horizontal) + .background(.gray.opacity(0.3)) + .clipShape(.buttonBorder) + .onAppear { + if !soonestFlashcard.isEmpty { + sortedFlashcards = soonestFlashcard.sorted(by: { + ($0.lastSeenOn!.addSpacedRepetitionMilestone(milestone:$0.getSpacedRepetitionMilestone()).timeIntervalSinceNow) < ($1.lastSeenOn!.addSpacedRepetitionMilestone(milestone: $1.getSpacedRepetitionMilestone()).timeIntervalSinceNow) + }) + timeRemaining = Int(sortedFlashcards.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone:sortedFlashcards.first!.getSpacedRepetitionMilestone()).timeIntervalSinceNow) + } + } + .onReceive(timer) { time in + if timeRemaining > 0 { + timeRemaining -= 1 + } + } } } } - @State private var timeRemaining = 10 - let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() } #Preview {