Skip to content

Commit

Permalink
Merge pull request #1246 from TortugaPower/watch-bookmarks
Browse files Browse the repository at this point in the history
Add bookmarks to the Apple Watch
  • Loading branch information
GianniCarlo authored Feb 1, 2025
2 parents 9a69cbb + 91adebb commit 1bf09db
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 161 deletions.
20 changes: 20 additions & 0 deletions BookPlayer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@
41F1A20D254B0A0C0043FCF3 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 41F1A20C254B0A0C0043FCF3 /* Sentry */; };
41F1A228254B0C6C0043FCF3 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 41F1A227254B0C6C0043FCF3 /* ZipArchive */; };
4645F9FD2D1E46AC00A04257 /* SwipeInlineTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */; };
465D87522D3195D600A4AA47 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465D87512D3195D600A4AA47 /* BookmarksView.swift */; };
465D87542D31965100A4AA47 /* BookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465D87532D31965100A4AA47 /* BookmarksViewModel.swift */; };
4689C06D2D270A7100D6C169 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 419B375423B8D5A500128A8F /* Localizable.strings */; };
46EEDDC92D23154C0063811F /* VoiceOverService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69343D322133844D000C425E /* VoiceOverService.swift */; };
5126F121258E9F18009965DC /* URL+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126F120258E9F18009965DC /* URL+BookPlayer.swift */; };
Expand Down Expand Up @@ -453,6 +455,7 @@
63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85222CE302D200EDBEA8 /* LoginView.swift */; };
63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */; };
63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85422CE3105300EDBEA8 /* ProfileView.swift */; };
63E54C322D494E110040355D /* RemoteItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E54C312D494E110040355D /* RemoteItemListViewModel.swift */; };
63E7DCC02D076185005B5E1F /* View+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */; };
63E893922CAFA89000946CD4 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.swift */; };
63E893932CAFA89000946CD4 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.swift */; };
Expand Down Expand Up @@ -1112,6 +1115,8 @@
41F898AE2402080C00F58B8A /* ZipArchive.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZipArchive.framework; path = Carthage/Build/iOS/ZipArchive.framework; sourceTree = "<group>"; };
41FCA32625E87EC600BFB9E6 /* Audiobook Player 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Audiobook Player 4.xcdatamodel"; sourceTree = "<group>"; };
4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeInlineTip.swift; sourceTree = "<group>"; };
465D87512D3195D600A4AA47 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
465D87532D31965100A4AA47 /* BookmarksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewModel.swift; sourceTree = "<group>"; };
5126F120258E9F18009965DC /* URL+BookPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+BookPlayer.swift"; sourceTree = "<group>"; };
5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZIPFoundation.framework; path = Carthage/Build/iOS/ZIPFoundation.framework; sourceTree = "<group>"; };
620C73C7275DA00300D495AA /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1251,6 +1256,7 @@
63CD85222CE302D200EDBEA8 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BP+ErrorAlerts.swift"; sourceTree = "<group>"; };
63CD85422CE3105300EDBEA8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
63E54C312D494E110040355D /* RemoteItemListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemListViewModel.swift; sourceTree = "<group>"; };
63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+BookPlayer.swift"; sourceTree = "<group>"; };
63E893912CAFA89000946CD4 /* BPPlayerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPPlayerError.swift; sourceTree = "<group>"; };
63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLoaderService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1679,6 +1685,7 @@
6334CF202CFE330300F1FA17 /* RefreshableListView.swift */,
63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */,
4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */,
465D87502D3195B600A4AA47 /* Bookmarks */,
63CD851B2CE2963600EDBEA8 /* Settings */,
6399D06E2CEBA1F900A2E278 /* RemoteItemList */,
9FA334B427C156DB0064E8EA /* ItemList */,
Expand Down Expand Up @@ -2067,6 +2074,15 @@
path = iPad;
sourceTree = "<group>";
};
465D87502D3195B600A4AA47 /* Bookmarks */ = {
isa = PBXGroup;
children = (
465D87512D3195D600A4AA47 /* BookmarksView.swift */,
465D87532D31965100A4AA47 /* BookmarksViewModel.swift */,
);
path = Bookmarks;
sourceTree = "<group>";
};
62793612272CC19E0097837D /* Models */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2362,6 +2378,7 @@
isa = PBXGroup;
children = (
6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */,
63E54C312D494E110040355D /* RemoteItemListViewModel.swift */,
6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */,
6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */,
);
Expand Down Expand Up @@ -3507,9 +3524,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
465D87542D31965100A4AA47 /* BookmarksViewModel.swift in Sources */,
9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */,
6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */,
6334CF212CFE330300F1FA17 /* RefreshableListView.swift in Sources */,
63E54C322D494E110040355D /* RemoteItemListViewModel.swift in Sources */,
9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */,
46EEDDC92D23154C0063811F /* VoiceOverService.swift in Sources */,
6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */,
Expand All @@ -3530,6 +3549,7 @@
418CABB325EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */,
9F82DF7127DF8203001B0EA8 /* WatchConnectivityService.swift in Sources */,
6350E4742CF4D2660077CDC1 /* PlayerToolbarView.swift in Sources */,
465D87522D3195D600A4AA47 /* BookmarksView.swift in Sources */,
41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */,
6350E4762CF4F6E90077CDC1 /* PlaybackFullControlsView.swift in Sources */,
6350E46A2CF429760077CDC1 /* SleepTimer.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
Expand Down
6 changes: 3 additions & 3 deletions BookPlayer/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,10 @@
"voiceover_unknown_author" = "Nieznany autor";
"voiceover_book_info" = "%@ - %@";
"voiceover_book_chapter" = "%@ - %@, rozdział %@";
"voiceover_rewind_time" = "Przewiń wstecz %@";
"voiceover_rewind_time" = "Przewiń do tyłu %@";
"voiceover_forward_time" = "Przewiń do przodu %@";
"watchapp_last_played_title" = "Ostatnio odtwarzane";
"watchapp_refresh_data_title" = "Odśwież dane";
"watchapp_refresh_data_title" = "Odśwież";
"recent_title" = "Ostatnie";
"carplay_library_error" = "Nie można załadować książek";
"siri_invocation_phrase" = "Kontynuuj moją książkę";
Expand All @@ -158,7 +158,7 @@
"voiceover_currently_playing_title" = "Aktualnie odtwarzam %@ - %@";
"voiceover_miniplayer_hint" = "Miniodtwarzacz. Dotknij, aby wyświetlić odtwarzacz";
"voiceover_chapter_time_title" = "Bieżący czas rozdziału: %@";
"voiceover_dismiss_player_title" = "Wyłącz Odtwarzacz";
"voiceover_dismiss_player_title" = "Zamknij odtwarzacz";
"sort_most_recent_button" = "Najczęściej Odtwarzane";
"sort_reversed_button" = "Odwróć kolejność";
"voiceover_continue_playback_title" = "Kontynuuj odtwarzanie";
Expand Down
109 changes: 109 additions & 0 deletions BookPlayerWatch/Bookmarks/BookmarksView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// BookmarksView.swift
// BookPlayerWatch
//
// Created by GC on 1/10/25.
// Copyright © 2025 BookPlayer LLC. All rights reserved.
//

import BookPlayerWatchKit
import SwiftUI

struct BookmarksView: View {
@StateObject var model: BookmarksViewModel

@State var error: Error?

@Environment(\.dismiss) var dismiss

var body: some View {
List {
HStack {
Spacer()
Button {
do {
try model.createBookmark()
} catch {
self.error = error
}
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.buttonStyle(PlainButtonStyle())
Spacer()
}
.frame(height: 24)
.listRowBackground(Color.clear)

Section {
ForEach(model.userBookmarks) { bookmark in
Button {
model.playerManager.jumpTo(bookmark.time + 0.01, recordBookmark: false)
dismiss()
} label: {
VStack(alignment: .leading) {
Text(TimeParser.formatTime(bookmark.time))
.foregroundColor(Color.secondary)
.font(.footnote)
if let note = bookmark.note {
Text(note)
}
}
.frame(minHeight: 24)
.padding(.vertical, Spacing.S4)
}
.swipeActions {
Button(
role: .destructive,
action: { model.deleteBookmark(bookmark) },
label: {
Image(systemName: "trash")
.imageScale(.large)
}
)
.accessibilityLabel("delete_button".localized)
}
}
} header: {
Text("bookmark_type_user_title".localized)
.foregroundStyle(Color.accentColor)
}

Section {
ForEach(model.automaticBookmarks) { bookmark in
Button {
model.playerManager.jumpTo(bookmark.time + 0.01, recordBookmark: false)
dismiss()
} label: {
VStack(alignment: .leading) {
HStack {
Text(TimeParser.formatTime(bookmark.time))
.foregroundColor(Color.secondary)
.font(.footnote)
Spacer()
if let imageName = bookmark.getImageNameForType() {
Image(systemName: imageName)
.foregroundColor(Color.secondary)
}
}
if let note = bookmark.note {
Text(note)
}
}
}
}
} header: {
Text("bookmark_type_automatic_title".localized)
.foregroundStyle(Color.accentColor)
.padding(.top, 10)
}
}
.environment(\.defaultMinListRowHeight, 1)
.customListSectionSpacing(0)
.errorAlert(error: $error)
.navigationTitle("bookmarks_title")
}
}
128 changes: 128 additions & 0 deletions BookPlayerWatch/Bookmarks/BookmarksViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// BookmarksViewModel.swift
// BookPlayerWatch
//
// Created by GC on 1/10/25.
// Copyright © 2025 BookPlayer LLC. All rights reserved.
//

import BookPlayerWatchKit
import Combine
import Foundation

@MainActor
class BookmarksViewModel: ObservableObject {
@Published var automaticBookmarks = [SimpleBookmark]()
@Published var userBookmarks = [SimpleBookmark]()
@Published var selectedBookmarkToDelete: SimpleBookmark?
private var disposeBag = Set<AnyCancellable>()

let playerManager: PlayerManager
let libraryService: LibraryServiceProtocol
let syncService: SyncServiceProtocol

init(coreServices: CoreServices) {
self.playerManager = coreServices.playerManager
self.libraryService = coreServices.libraryService
self.syncService = coreServices.syncService

self.bindCurrentItemObserver()
}

func bindCurrentItemObserver() {
playerManager.currentItemPublisher()
.receive(on: DispatchQueue.main)
.sink { [weak self] currentItem in
guard let self else { return }

if let currentItem {
self.automaticBookmarks = self.getAutomaticBookmarks(for: currentItem.relativePath)
self.userBookmarks = self.getUserBookmarks(for: currentItem.relativePath)
self.syncBookmarks(for: currentItem.relativePath)
} else {
self.automaticBookmarks = []
self.userBookmarks = []
}
}
.store(in: &disposeBag)
}

func syncBookmarks(for relativePath: String) {
Task { @MainActor [weak self] in
guard
let self = self,
let bookmarks = try await self.syncService.syncBookmarksList(relativePath: relativePath)
else { return }

self.userBookmarks = bookmarks
}
}

func getAutomaticBookmarks(for relativePath: String) -> [SimpleBookmark] {
let playBookmarks = self.libraryService.getBookmarks(of: .play, relativePath: relativePath) ?? []
let skipBookmarks = self.libraryService.getBookmarks(of: .skip, relativePath: relativePath) ?? []

let bookmarks = playBookmarks + skipBookmarks

return bookmarks.sorted(by: { $0.time < $1.time })
}

func getUserBookmarks(for relativePath: String) -> [SimpleBookmark] {
return self.libraryService.getBookmarks(of: .user, relativePath: relativePath) ?? []
}

func createBookmark() throws {
guard let currentItem = playerManager.currentItem else { return }

let currentTime = currentItem.currentTime

if let bookmark = libraryService.getBookmark(
at: currentTime,
relativePath: currentItem.relativePath,
type: .user
) {
throw BookmarksAlerts.bookmarkExists(bookmark: bookmark)
}

if let bookmark = libraryService.createBookmark(
at: floor(currentTime),
relativePath: currentItem.relativePath,
type: .user
) {
syncService.scheduleSetBookmark(
relativePath: currentItem.relativePath,
time: floor(currentTime),
note: nil
)
userBookmarks = getUserBookmarks(for: currentItem.relativePath)
throw BookmarksAlerts.bookmarkCreated(bookmark: bookmark)
} else {
throw BookmarksAlerts.fileMissing
}
}

func deleteBookmark(_ bookmark: SimpleBookmark) {
libraryService.deleteBookmark(bookmark)
userBookmarks = getUserBookmarks(for: bookmark.relativePath)
syncService.scheduleDeleteBookmark(bookmark)
}
}

enum BookmarksAlerts: LocalizedError {
case bookmarkExists(bookmark: SimpleBookmark)
case bookmarkCreated(bookmark: SimpleBookmark)
case fileMissing

public var errorDescription: String? {
switch self {
case .bookmarkExists(let bookmark):
let formattedTime = TimeParser.formatTime(bookmark.time)
return String.localizedStringWithFormat("bookmark_exists_title".localized, formattedTime)
case .bookmarkCreated(let bookmark):
let formattedTime = TimeParser.formatTime(bookmark.time)
return String.localizedStringWithFormat("bookmark_created_title".localized, formattedTime)
case .fileMissing:
return "file_missing_title".localized
}
}
}
18 changes: 16 additions & 2 deletions BookPlayerWatch/PlayerToolbarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ final class PlaybackFullControlsViewModel: ObservableObject {
}

struct PlayerToolbarView: View {
/// PlayerManager needs to be an ObservedObject, otherwise the Published property do not reload properly
@ObservedObject var playerManager: PlayerManager
@State var isShowingMoreList: Bool = false

let coreServices: CoreServices

init(coreServices: CoreServices) {
self.coreServices = coreServices
self.playerManager = coreServices.playerManager
}

var body: some View {
HStack {
Spacer()
Expand All @@ -71,8 +79,14 @@ struct PlayerToolbarView: View {

Spacer()

VolumeView(type: .local)
.accessibilityHidden(true)
NavigationLink(
destination: BookmarksView(model: .init(coreServices: coreServices))
) {
ResizeableImageView(name: "bookmark.fill")
.accessibilityLabel("bookmarks_title".localized)
.padding(20)
}
.buttonStyle(PlainButtonStyle())

Spacer()

Expand Down
Loading

0 comments on commit 1bf09db

Please sign in to comment.