From b730ccafc1eee1c6d2a421fe928ee65a2ea79ab5 Mon Sep 17 00:00:00 2001 From: Gigi <42325924+g-cqd@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:41:58 +0200 Subject: [PATCH] Implemented ReadSessions - may need refinements --- .../Views/BookInformationView.swift | 2 +- Bilbary for iPad/Views/BookPlaceholder.swift | 4 +- .../Views/GeneralLibraryStructs.swift | 4 +- Bilbary for iPhone/AppModel.swift | 42 ++++++++++ Bilbary for iPhone/Bilbary.swift | 38 +++------ Bilbary for iPhone/BilbaryView.swift | 45 +++++++++- Bilbary for iPhone/determineHigherValue.swift | 83 ++++++++----------- Bilbary for iPhone/timerView.swift | 30 +++---- Bilbary.xcodeproj/project.pbxproj | 15 ++-- .../xcshareddata/swiftpm/Package.resolved | 3 +- Bilbary/Localizable.xcstrings | 3 + Bilbary/Models/Book/Book+BookCover.swift | 16 ++-- Bilbary/Models/Book/Book+Static.swift | 4 +- Bilbary/Models/Book/Book.swift | 2 +- Bilbary/Models/View/RViewModel.swift | 2 +- 15 files changed, 176 insertions(+), 117 deletions(-) create mode 100644 Bilbary for iPhone/AppModel.swift diff --git a/Bilbary for iPad/Views/BookInformationView.swift b/Bilbary for iPad/Views/BookInformationView.swift index a285809..851ae6a 100644 --- a/Bilbary for iPad/Views/BookInformationView.swift +++ b/Bilbary for iPad/Views/BookInformationView.swift @@ -63,7 +63,7 @@ struct BookInformationView: View { GenericInfoCard(onTap: {}, title: "Odes & Ballades", subTitle: "1802 - 1885", contentText: "\"Le GĂ©ant\" is a poem by Victor Hugo in which the speaker is a giant, metaphorically representing great individual power. The giant recounts his life of strength and adventure, from his upbringing in harsh environments, through bouts with nature's elements and beasts, to his enjoyment of warfare. He proudly describes his ability to wield power without any armored protection. In the end, he acknowledges the inevitability of his own death and expresses a wish to be buried among majestic mountains so sublime that people will wonder which one serves as his tomb.", nameImage: "person.fill", number: 0) Text("") - .padding(.horizontal, 30) + .padding(.horizontal, 30) } .frame(minWidth: BConstants.libraryOpenWidth) .frame(maxWidth: BConstants.libraryOpenWidth) diff --git a/Bilbary for iPad/Views/BookPlaceholder.swift b/Bilbary for iPad/Views/BookPlaceholder.swift index fe92930..8dfb016 100644 --- a/Bilbary for iPad/Views/BookPlaceholder.swift +++ b/Bilbary for iPad/Views/BookPlaceholder.swift @@ -7,8 +7,8 @@ import Foundation -struct BookPlaceholder: Hashable { - +struct BookPlaceholder: Hashable { + let url: URL? init(from url: URL?) { diff --git a/Bilbary for iPad/Views/GeneralLibraryStructs.swift b/Bilbary for iPad/Views/GeneralLibraryStructs.swift index 3a4f202..bba2286 100644 --- a/Bilbary for iPad/Views/GeneralLibraryStructs.swift +++ b/Bilbary for iPad/Views/GeneralLibraryStructs.swift @@ -63,9 +63,9 @@ struct CustomButton: View { .foregroundColor(colorScheme == .dark ? .white : .black) // Foreground color .cornerRadius(8) .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: 8) .stroke(Color.primary, lineWidth: 0.2) - ) + ) .padding(.all, 7) } } diff --git a/Bilbary for iPhone/AppModel.swift b/Bilbary for iPhone/AppModel.swift new file mode 100644 index 0000000..7384333 --- /dev/null +++ b/Bilbary for iPhone/AppModel.swift @@ -0,0 +1,42 @@ +// +// AppModel.swift +// Bilbary for iPhone +// +// Created by Guillaume Coquard on 11/07/24. +// + +import Foundation +import SwiftUI +import SwiftData + +@Model +class ReadSession: Identifiable { + + typealias ID = UUID + + @Attribute(.unique) + let id: ID = ID() + + let startTime: Date + private(set) var endTime: Date? + + init() { + self.startTime = .now + } + + func end() -> Self { + self.endTime = .now + return self + } + +} + +@MainActor +let AppModelContainer: ModelContainer = { + do { + let container = try ModelContainer(for: ReadSession.self) + return container + } catch { + fatalError("Failed to create container") + } +}() diff --git a/Bilbary for iPhone/Bilbary.swift b/Bilbary for iPhone/Bilbary.swift index d844ffa..b3da2d4 100644 --- a/Bilbary for iPhone/Bilbary.swift +++ b/Bilbary for iPhone/Bilbary.swift @@ -8,33 +8,17 @@ import SwiftUI @main +@MainActor struct Bilbary: App { - @StateObject private var appUsageTracker = AppUsageTracker() - @Environment(\.scenePhase) private var scenePhase + @State + private var appUsageTracker = AppUsageTracker() - var body: some Scene { - WindowGroup { - TimerView() - .environmentObject(appUsageTracker) - .onAppear { - appUsageTracker.startSession() - } - .onDisappear { - appUsageTracker.endSession() - } - } - .onChange(of: scenePhase) { newPhase in - switch newPhase { - case .active: - appUsageTracker.resumeSession() - case .inactive: - appUsageTracker.pauseSession() - case .background: - appUsageTracker.pauseSession() - @unknown default: - break - } - } - } - } + var body: some Scene { + WindowGroup { + BilbaryView() + .environment(appUsageTracker) + } + .modelContainer(AppModelContainer) + } +} diff --git a/Bilbary for iPhone/BilbaryView.swift b/Bilbary for iPhone/BilbaryView.swift index 761a7de..0757e84 100644 --- a/Bilbary for iPhone/BilbaryView.swift +++ b/Bilbary for iPhone/BilbaryView.swift @@ -6,15 +6,33 @@ // import SwiftUI +import SwiftData struct BilbaryView: View { + @Environment(\.modelContext) + private var context + + @Environment(AppUsageTracker.self) + private var appUsageTracker + @State private var detent: PresentationDetent = .height(64) @State private var tabBarShown: Bool = false + @Environment(\.scenePhase) + private var scenePhase + + @Query + private var sessions: [ReadSession] + + private var timer = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() + + @State + private var timeSpent: TimeInterval? + let columns: [GridItem] = [ .init(.flexible(minimum: 0, maximum: .infinity), alignment: .center), .init(.flexible(minimum: 0, maximum: .infinity), alignment: .center), @@ -70,7 +88,16 @@ struct BilbaryView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) ScrollView { + VStack { + if let timeSpent = timeSpent { + VStack { + Text(timeSpent, format: .number.rounded(rule: .toNearestOrEven, increment: 1)) + .padding() + Text("\(sessions.count) sessions") + .padding() + } + } HStack { Text("Library") .font(.largeTitle) @@ -98,7 +125,23 @@ struct BilbaryView: View { .presentationCompactAdaptation(.automatic) .interactiveDismissDisabled() } - + .onReceive(self.timer) { _ in + timeSpent = appUsageTracker.computeTimeSpentToday(sessions) + } + .onAppear { + appUsageTracker.startSession(to: context) + } + .onDisappear { + appUsageTracker.endSession(to: context) + } + .onChange(of: scenePhase) { _, newPhase in + switch newPhase { + case .active: + appUsageTracker.startSession(to: context) + default: + appUsageTracker.endSession(to: context) + } + } } } diff --git a/Bilbary for iPhone/determineHigherValue.swift b/Bilbary for iPhone/determineHigherValue.swift index 3a92a7a..3c8f6a3 100644 --- a/Bilbary for iPhone/determineHigherValue.swift +++ b/Bilbary for iPhone/determineHigherValue.swift @@ -6,62 +6,45 @@ // import SwiftUI import Combine +import SwiftData -class AppUsageTracker: ObservableObject { - private var timer: Timer? - private var startDate: Date? - private var accumulatedTime: TimeInterval = 0 - - @Published var totalTimeSpent: TimeInterval = 0 - - func startSession() { - startDate = Date() - startTimer() - } - - func endSession() { - stopTimer() - saveAccumulatedTime() +@Observable +class AppUsageTracker { + + var currentSession: ReadSession? + + func startSession(to context: ModelContext) { + guard currentSession == nil else { return } + currentSession = .init() } - - func resumeSession() { - if let startDate = startDate { - accumulatedTime += Date().timeIntervalSince(startDate) + + func endSession(to context: ModelContext) { + if let currentSession = currentSession { + context.insert(currentSession.end()) + do { + try context.save() + } catch { + print(error.localizedDescription) + } } - startTimer() + self.currentSession = nil } - - func pauseSession() { - if let startDate = startDate { - accumulatedTime += Date().timeIntervalSince(startDate) - } - stopTimer() + + func computeTimeSpentToday(_ sessions: ReadSession...) -> TimeInterval { + self.computeTimeSpentToday(sessions) } - - private func startTimer() { - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in - self?.updateTotalTime() + + func computeTimeSpentToday(_ sessions: [ReadSession]) -> TimeInterval { + var duration: TimeInterval = 0 + for session in sessions { + if let endTime = session.endTime { + duration += session.startTime.distance(to: endTime) + } } - } - - private func stopTimer() { - timer?.invalidate() - timer = nil - } - - private func updateTotalTime() { - if let startDate = startDate { - totalTimeSpent = accumulatedTime + Date().timeIntervalSince(startDate) + if let currentSession = currentSession { + duration += currentSession.startTime.distance(to: .now) } + return duration } - - private func saveAccumulatedTime() { - let userDefaults = UserDefaults.standard - userDefaults.set(totalTimeSpent, forKey: "totalTimeSpent") - } - - func loadTotalTimeSpent() { - let userDefaults = UserDefaults.standard - totalTimeSpent = userDefaults.double(forKey: "totalTimeSpent") - } + } diff --git a/Bilbary for iPhone/timerView.swift b/Bilbary for iPhone/timerView.swift index 8f797ca..0351f85 100644 --- a/Bilbary for iPhone/timerView.swift +++ b/Bilbary for iPhone/timerView.swift @@ -7,23 +7,23 @@ import SwiftUI -struct TimerView: View { - @EnvironmentObject var appUsageTracker: AppUsageTracker - - var body: some View { - VStack { - Text("Total Time Spent: \(appUsageTracker.totalTimeSpent, specifier: "%.2f") seconds") - .padding() - } - .onAppear { - appUsageTracker.loadTotalTimeSpent() - } - } -} +// struct TimerView: View { +// @Environment(AppUsageTracker.self) var appUsageTracker: AppUsageTracker +// +// var body: some View { +// VStack { +// Text("Total Time Spent: \(appUsageTracker.totalTimeSpent, specifier: "%.2f") seconds") +// .padding() +// } +// .onAppear { +// appUsageTracker.loadTotalTimeSpent() +// } +// } +// } -//struct timerView: PreviewProvider { +// struct timerView: PreviewProvider { // static var previews: some View { // timerView() // .environmentObject(AppUsageTracker()) // } -//} +// } diff --git a/Bilbary.xcodeproj/project.pbxproj b/Bilbary.xcodeproj/project.pbxproj index d12d790..e591d5e 100644 --- a/Bilbary.xcodeproj/project.pbxproj +++ b/Bilbary.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ 4656F4322B8CD920004DD8BB /* RButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4656F4302B8CD920004DD8BB /* RButton.swift */; }; 4656F4332B8CD920004DD8BB /* RPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4656F4312B8CD920004DD8BB /* RPopover.swift */; }; 466313272B8FEC6B00854AFE /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 466313262B8FEC6B00854AFE /* SwiftSoup */; }; + 46724EBB2C40614B009254EB /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46724EBA2C40614B009254EB /* AppModel.swift */; }; 46A485B12B87382B00C6C7E3 /* EPUBKit in Frameworks */ = {isa = PBXBuildFile; productRef = 46A485B02B87382B00C6C7E3 /* EPUBKit */; }; 46A485B32B87383D00C6C7E3 /* EPUBKit in Frameworks */ = {isa = PBXBuildFile; productRef = 46A485B22B87383D00C6C7E3 /* EPUBKit */; }; 46A485BB2B87392B00C6C7E3 /* EPUBView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A485B82B87392B00C6C7E3 /* EPUBView.swift */; }; @@ -227,6 +228,7 @@ 464FE0AB2B7B86C70005590A /* build.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = build.yml; sourceTree = ""; }; 4656F4302B8CD920004DD8BB /* RButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RButton.swift; sourceTree = ""; }; 4656F4312B8CD920004DD8BB /* RPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RPopover.swift; sourceTree = ""; }; + 46724EBA2C40614B009254EB /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; }; 46A485B82B87392B00C6C7E3 /* EPUBView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EPUBView.swift; sourceTree = ""; }; 650AFE0F2C3E8B4700FEA8D0 /* determineHigherValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = determineHigherValue.swift; sourceTree = ""; }; 650AFE112C3E8B7B00FEA8D0 /* timerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = timerView.swift; sourceTree = ""; }; @@ -516,6 +518,7 @@ 650AFE0F2C3E8B4700FEA8D0 /* determineHigherValue.swift */, 463CEDF92B8569BE001D951C /* BilbaryView.swift */, 463CEDF12B855D00001D951C /* TabButtonItem.swift */, + 46724EBA2C40614B009254EB /* AppModel.swift */, ); path = "Bilbary for iPhone"; sourceTree = ""; @@ -749,6 +752,7 @@ }; 4656F4362B8CDA39004DD8BB /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -763,7 +767,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n"; + shellScript = "if [[ -e /opt/homebrew/bin/swiftlint ]]; then\n /opt/homebrew/bin/swiftlint --fix --format\nelse\n echo \"warning:SwiftLint has not been found on your computer. You should install it with Homebrew (https://brew.sh/)\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -874,6 +878,7 @@ 463EBC1F2B91F7A400BB9A57 /* Book+BookContent.swift in Sources */, 463EBC222B91FA3500BB9A57 /* Book+Equatable.swift in Sources */, 463EBC392B9223E300BB9A57 /* GoalStreakDuration.swift in Sources */, + 46724EBB2C40614B009254EB /* AppModel.swift in Sources */, 463EBC312B9223D900BB9A57 /* RCPreset.swift in Sources */, 464F8F2B2B913BB4009102CB /* NavigationSplitViewVisibility.swift in Sources */, 462E0DD02B87AE2C001D2161 /* ViewModel+NavigationPath.swift in Sources */, @@ -1147,7 +1152,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = NNZWWH5M7V; + DEVELOPMENT_TEAM = NP94WB3P75; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1164,7 +1169,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "studio.aemi.ada23.trial.gcqd.0.iits.Read-for-iPhone"; + PRODUCT_BUNDLE_IDENTIFIER = studio.aemi.ada23.trial.gcqd.Bilbary; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1185,7 +1190,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = NNZWWH5M7V; + DEVELOPMENT_TEAM = NP94WB3P75; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1202,7 +1207,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.0.1; - PRODUCT_BUNDLE_IDENTIFIER = "studio.aemi.ada23.trial.gcqd.0.iits.Read-for-iPhone"; + PRODUCT_BUNDLE_IDENTIFIER = studio.aemi.ada23.trial.gcqd.Bilbary; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Bilbary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bilbary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9717e69..d9a7b12 100644 --- a/Bilbary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bilbary.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "d18a27bb2587ec779a457966860f8f7498b50a118627f740f1308e92725de3ae", "pins" : [ { "identity" : "aexml", @@ -37,5 +38,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Bilbary/Localizable.xcstrings b/Bilbary/Localizable.xcstrings index 8bf89f5..123bf8e 100644 --- a/Bilbary/Localizable.xcstrings +++ b/Bilbary/Localizable.xcstrings @@ -15,6 +15,9 @@ }, "%lld" : { + }, + "%lld sessions" : { + }, "Aa" : { diff --git a/Bilbary/Models/Book/Book+BookCover.swift b/Bilbary/Models/Book/Book+BookCover.swift index 884334f..156b413 100644 --- a/Bilbary/Models/Book/Book+BookCover.swift +++ b/Bilbary/Models/Book/Book+BookCover.swift @@ -26,14 +26,14 @@ extension Book { public func `as`(_ output: T.Type) async -> T? { switch output { - case is Data.Type: - return await Util.getImageData(from: self.url) as? T - case is UIImage.Type: - return await Util.getUIImage(from: self.url) as? T - case is Image.Type: - return await Util.getImage(from: self.url) as? T - default: - return nil + case is Data.Type: + return await Util.getImageData(from: self.url) as? T + case is UIImage.Type: + return await Util.getUIImage(from: self.url) as? T + case is Image.Type: + return await Util.getImage(from: self.url) as? T + default: + return nil } } } diff --git a/Bilbary/Models/Book/Book+Static.swift b/Bilbary/Models/Book/Book+Static.swift index b65a6cc..e42bc1b 100644 --- a/Bilbary/Models/Book/Book+Static.swift +++ b/Bilbary/Models/Book/Book+Static.swift @@ -7,14 +7,12 @@ import Foundation - import OSLog extension Book { static internal let logger: Logger = .init() static internal let delegate: BookDelegate = .default } - import UniformTypeIdentifiers extension Book { @@ -31,5 +29,5 @@ extension Book { public static var localBooks: [Book] { self.localBooksUrls.compactMap(Book.init) } - + } diff --git a/Bilbary/Models/Book/Book.swift b/Bilbary/Models/Book/Book.swift index c0f72f6..3f4426b 100644 --- a/Bilbary/Models/Book/Book.swift +++ b/Bilbary/Models/Book/Book.swift @@ -53,7 +53,7 @@ struct Book: Identifiable { Logger.book.warning("EPUB could not be created for <\(String(describing: url))>") return nil } - + self.cover = BookCover(url: document.cover) self.content = BookContent(from: document.content) } diff --git a/Bilbary/Models/View/RViewModel.swift b/Bilbary/Models/View/RViewModel.swift index a8d71f5..2d2f63a 100644 --- a/Bilbary/Models/View/RViewModel.swift +++ b/Bilbary/Models/View/RViewModel.swift @@ -39,7 +39,7 @@ final class RViewModel { self.libraryWidth = BConstants.libraryClosedWidth } self.setPopover(to: .none) - }else{ + } else { withAnimation { self.libraryWidth = BConstants.libraryOpenWidth }