Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9991e69
uuid tries
Hirobreak Mar 3, 2026
21c5f13
handling events with uuids
Hirobreak Mar 7, 2026
26bc0f4
úuid populate migration
Hirobreak Mar 9, 2026
ba3036c
migration schema versions
Hirobreak Mar 10, 2026
a760612
migration schema single step
Hirobreak Mar 10, 2026
3bd8c37
creating sync uuid task in migration
Hirobreak Mar 11, 2026
6e88d47
missing uuids when receiving content from server
Hirobreak Mar 11, 2026
60cbd27
working move
Hirobreak Mar 12, 2026
47640b5
post payload uuids inside items property
Hirobreak Mar 13, 2026
aa5f537
ia review fixes
Hirobreak Apr 7, 2026
8378744
fix rebase errors
Hirobreak Apr 8, 2026
ce6f3e3
fixing tests
Hirobreak Apr 8, 2026
2c1d1b5
update mapping model
GianniCarlo Apr 11, 2026
456a9c3
fixing sneding autogenerated uuid
Hirobreak Apr 15, 2026
5c89871
Fix reference for upload task progress
GianniCarlo Apr 21, 2026
b4f4230
avoid adding uuid in payload if placeholder
GianniCarlo Apr 22, 2026
c458b5e
change decodeIfPresent for playableitem
GianniCarlo Apr 22, 2026
f9bd3f1
Fix actor threads issues
GianniCarlo Apr 22, 2026
7396cfa
fix missing uuid
GianniCarlo Apr 22, 2026
a259b9d
collapse matchUuid tasks
GianniCarlo Apr 23, 2026
8970070
move to main actor
GianniCarlo Apr 23, 2026
0649f1f
insert once
GianniCarlo Apr 23, 2026
087df39
remove maps
GianniCarlo Apr 23, 2026
c71470e
fix ci
GianniCarlo Apr 23, 2026
62305f5
Fix first app launch not using system mode properly
GianniCarlo Apr 25, 2026
38f8c56
Fix sending relative path again for fallback
GianniCarlo Apr 25, 2026
511690a
put back relative path for debug file info
GianniCarlo Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
xcodebuild \
-scheme BookPlayer \
-configuration Debug \
-destination "platform=iOS Simulator,name=iPhone 17,OS=26.4" \
-destination "platform=iOS Simulator,name=iPhone 17" \
build-for-testing

- name: Run unit tests
Expand All @@ -53,6 +53,6 @@ jobs:
-configuration Debug \
-testPlan "Unit Tests" \
-only-testing:BookPlayerTests \
-destination "platform=iOS Simulator,name=iPhone 17,OS=26.4" \
-destination "platform=iOS Simulator,name=iPhone 17" \
-destination-timeout 120 \
test-without-building
88 changes: 78 additions & 10 deletions BookPlayer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

6 changes: 6 additions & 0 deletions BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
) -> Bool {
Self.shared = self

// Register fallback defaults before anything reads UserDefaults, so a fresh
// install follows the system appearance instead of defaulting to light mode.
UserDefaults.standard.register(defaults: [
Constants.UserDefaults.systemThemeVariantEnabled: true
])

NotificationCenter.default.addObserver(
self,
selector: #selector(self.messageReceived),
Expand Down
4 changes: 3 additions & 1 deletion BookPlayer/AppIntents/CreateBookmarkIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ struct CreateBookmarkIntent: AppIntent {
if let bookmark = libraryService.createBookmark(
at: floor(currentTime),
relativePath: currentItem.relativePath,
uuid: currentItem.uuid,
type: .user
) {

Expand All @@ -66,7 +67,8 @@ struct CreateBookmarkIntent: AppIntent {
playerLoaderService.syncService.scheduleSetBookmark(
relativePath: currentItem.relativePath,
time: floor(currentTime),
note: note
note: note,
uuid: currentItem.uuid
)

let formattedTime = TimeParser.formatTime(bookmark.time)
Expand Down
26 changes: 13 additions & 13 deletions BookPlayer/Generated/AutoMockable.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ class LibraryServiceProtocolMock: LibraryServiceProtocol {
var moveItemsInsideCalled: Bool {
return moveItemsInsideCallsCount > 0
}
var moveItemsInsideReceivedArguments: (items: [String], relativePath: String?)?
var moveItemsInsideReceivedInvocations: [(items: [String], relativePath: String?)] = []
var moveItemsInsideClosure: (([String], String?) throws -> Void)?
func moveItems(_ items: [String], inside relativePath: String?) throws {
var moveItemsInsideReceivedArguments: (items: [PathUuidPair], relativePath: String?)?
var moveItemsInsideReceivedInvocations: [(items: [PathUuidPair], relativePath: String?)] = []
var moveItemsInsideClosure: (([PathUuidPair], String?) throws -> Void)?
func moveItems(_ items: [PathUuidPair], inside relativePath: String?) throws {
if let error = moveItemsInsideThrowableError {
throw error
}
Expand Down Expand Up @@ -845,7 +845,7 @@ class LibraryServiceProtocolMock: LibraryServiceProtocol {
var createBookmarkAtRelativePathTypeReceivedInvocations: [(time: Double, relativePath: String, type: BookmarkType)] = []
var createBookmarkAtRelativePathTypeReturnValue: SimpleBookmark?
var createBookmarkAtRelativePathTypeClosure: ((Double, String, BookmarkType) -> SimpleBookmark?)?
func createBookmark(at time: Double, relativePath: String, type: BookmarkType) -> SimpleBookmark? {
func createBookmark(at time: Double, relativePath: String, uuid: String, type: BookmarkType) -> SimpleBookmark? {
createBookmarkAtRelativePathTypeCallsCount += 1
createBookmarkAtRelativePathTypeReceivedArguments = (time: time, relativePath: relativePath, type: type)
createBookmarkAtRelativePathTypeReceivedInvocations.append((time: time, relativePath: relativePath, type: type))
Expand Down Expand Up @@ -1627,7 +1627,7 @@ class SyncServiceProtocolMock: SyncServiceProtocol {
var getRemoteFileURLsOfTypeReceivedInvocations: [(relativePath: String, type: SimpleItemType)] = []
var getRemoteFileURLsOfTypeReturnValue: [RemoteFileURL]!
var getRemoteFileURLsOfTypeClosure: ((String, SimpleItemType) async throws -> [RemoteFileURL])?
func getRemoteFileURLs(of relativePath: String, type: SimpleItemType) async throws -> [RemoteFileURL] {
func getRemoteFileURLs(of relativePath: String, for uuid: String?, type: SimpleItemType) async throws -> [RemoteFileURL] {
if let error = getRemoteFileURLsOfTypeThrowableError {
throw error
}
Expand Down Expand Up @@ -1695,10 +1695,10 @@ class SyncServiceProtocolMock: SyncServiceProtocol {
var scheduleMoveItemsToCalled: Bool {
return scheduleMoveItemsToCallsCount > 0
}
var scheduleMoveItemsToReceivedArguments: (items: [String], parentFolder: String?)?
var scheduleMoveItemsToReceivedInvocations: [(items: [String], parentFolder: String?)] = []
var scheduleMoveItemsToClosure: (([String], String?) -> Void)?
func scheduleMove(items: [String], to parentFolder: String?) {
var scheduleMoveItemsToReceivedArguments: (items: [PathUuidPair], parentFolder: PathUuidPair?)?
var scheduleMoveItemsToReceivedInvocations: [(items: [PathUuidPair], parentFolder: PathUuidPair?)] = []
var scheduleMoveItemsToClosure: (([PathUuidPair], PathUuidPair?) -> Void)?
func scheduleMove(items: [PathUuidPair], to parentFolder: PathUuidPair?) {
scheduleMoveItemsToCallsCount += 1
scheduleMoveItemsToReceivedArguments = (items: items, parentFolder: parentFolder)
scheduleMoveItemsToReceivedInvocations.append((items: items, parentFolder: parentFolder))
Expand All @@ -1713,7 +1713,7 @@ class SyncServiceProtocolMock: SyncServiceProtocol {
var scheduleRenameFolderAtNameReceivedArguments: (relativePath: String, name: String)?
var scheduleRenameFolderAtNameReceivedInvocations: [(relativePath: String, name: String)] = []
var scheduleRenameFolderAtNameClosure: ((String, String) -> Void)?
func scheduleRenameFolder(at relativePath: String, name: String) {
func scheduleRenameFolder(at relativePath: String, name: String, for uuid: String) {
scheduleRenameFolderAtNameCallsCount += 1
scheduleRenameFolderAtNameReceivedArguments = (relativePath: relativePath, name: name)
scheduleRenameFolderAtNameReceivedInvocations.append((relativePath: relativePath, name: name))
Expand All @@ -1728,7 +1728,7 @@ class SyncServiceProtocolMock: SyncServiceProtocol {
var scheduleSetBookmarkRelativePathTimeNoteReceivedArguments: (relativePath: String, time: Double, note: String?)?
var scheduleSetBookmarkRelativePathTimeNoteReceivedInvocations: [(relativePath: String, time: Double, note: String?)] = []
var scheduleSetBookmarkRelativePathTimeNoteClosure: ((String, Double, String?) -> Void)?
func scheduleSetBookmark(relativePath: String, time: Double, note: String?) {
func scheduleSetBookmark(relativePath: String, time: Double, note: String?, uuid: String) {
scheduleSetBookmarkRelativePathTimeNoteCallsCount += 1
scheduleSetBookmarkRelativePathTimeNoteReceivedArguments = (relativePath: relativePath, time: time, note: note)
scheduleSetBookmarkRelativePathTimeNoteReceivedInvocations.append((relativePath: relativePath, time: time, note: note))
Expand Down Expand Up @@ -1758,7 +1758,7 @@ class SyncServiceProtocolMock: SyncServiceProtocol {
var scheduleUploadArtworkRelativePathReceivedRelativePath: String?
var scheduleUploadArtworkRelativePathReceivedInvocations: [String] = []
var scheduleUploadArtworkRelativePathClosure: ((String) -> Void)?
func scheduleUploadArtwork(relativePath: String) {
func scheduleUploadArtwork(relativePath: String, uuid: String) {
scheduleUploadArtworkRelativePathCallsCount += 1
scheduleUploadArtworkRelativePathReceivedRelativePath = relativePath
scheduleUploadArtworkRelativePathReceivedInvocations.append(relativePath)
Expand Down
4 changes: 2 additions & 2 deletions BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ final class ItemDetailsViewModel: ObservableObject {

await ArtworkService.removeCache(for: item.relativePath)
await ArtworkService.storeInCache(imageData, for: cacheKey)
syncService.scheduleUploadArtwork(relativePath: cacheKey)
syncService.scheduleUploadArtwork(relativePath: cacheKey, uuid: item.uuid)

loadingState.show = false
listState.reload(.path(item.parentFolder ?? ""))
Expand Down Expand Up @@ -196,7 +196,7 @@ final class ItemDetailsViewModel: ObservableObject {
case .bound, .folder:
let newRelativePath = try libraryService.renameFolder(at: relativePath, with: cleanedTitle)
cacheKey = newRelativePath
syncService.scheduleRenameFolder(at: relativePath, name: cleanedTitle)
syncService.scheduleRenameFolder(at: relativePath, name: cleanedTitle, for: item.uuid)
}

return cacheKey
Expand Down
10 changes: 6 additions & 4 deletions BookPlayer/Library/ItemList/ItemListView+Alerts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension ItemListView {

Button("new_playlist_button") {
folderInput.prepareForFolder(title: suggestedFolderName, placeholder: suggestedFolderName)
model.selectedSetItems = Set(alertParameters.itemIdentifiers)
model.selectedSetItems = Set(alertParameters.itemIdentifiers.map({ $0.relativePath }))
activeAlert = nil
Task { @MainActor in
activeAlert = .createFolder(type: .folder, placeholder: suggestedFolderName)
Expand All @@ -70,15 +70,15 @@ extension ItemListView {

Button("existing_playlist_button") {
model.pendingMoveItemIdentifiers = alertParameters.itemIdentifiers
model.selectedSetItems = Set(alertParameters.itemIdentifiers)
model.selectedSetItems = Set(alertParameters.itemIdentifiers.map({ $0.relativePath }))
activeSheet = .foldersSelection
}
.disabled(alertParameters.availableFolders.isEmpty)

Button("bound_books_create_button") {
if alertParameters.hasOnlyBooks {
folderInput.prepareForBound(title: suggestedFolderName, placeholder: suggestedFolderName)
model.selectedSetItems = Set(alertParameters.itemIdentifiers)
model.selectedSetItems = Set(alertParameters.itemIdentifiers.map({ $0.relativePath }))
activeAlert = nil
Task { @MainActor in
activeAlert = .createFolder(type: .bound, placeholder: suggestedFolderName)
Expand Down Expand Up @@ -145,7 +145,9 @@ extension ItemListView {
Button("create_button") {
model.createFolder(
with: folderInput.name,
items: selectedItems,
items: model.selectedItems.map { item in
PathUuidPair(relativePath: item.relativePath, uuid: item.uuid)
},
type: folderInput.type
)
folderInput.reset()
Expand Down
22 changes: 11 additions & 11 deletions BookPlayer/Library/ItemList/ItemListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class ItemListViewModel: ObservableObject {
var reloadScope: ListStateManager.Scope {
switch libraryNode {
case .root: return .path("")
case .book(_, let relativePath), .folder(_, let relativePath):
case .book(_, let relativePath, _), .folder(_, let relativePath, _):
return .path(relativePath)
}
}
Expand Down Expand Up @@ -79,7 +79,7 @@ final class ItemListViewModel: ObservableObject {
@Published var selectedItems = [SimpleLibraryItem]()
/// Stores item identifiers from import operations to avoid race condition
/// where items may not be loaded in the UI yet when moving to a folder
var pendingMoveItemIdentifiers: [String]?
var pendingMoveItemIdentifiers: [PathUuidPair]?

/// Search
@Published var scope: ItemListSearchScope = .all
Expand Down Expand Up @@ -230,7 +230,7 @@ final class ItemListViewModel: ObservableObject {
.map { String(path.prefix(upTo: $0.lowerBound)) }
.reversed()

guard case .folder(_, let folderRelativePath) = libraryNode else {
guard case .folder(_, let folderRelativePath, _) = libraryNode else {
return parentFolders.last
}

Expand Down Expand Up @@ -347,7 +347,7 @@ extension ItemListViewModel {
}

func handleMoveIntoLibrary() {
let selectedItemPaths = selectedItems.compactMap({ $0.relativePath })
let selectedItemPaths = selectedItems.compactMap({ PathUuidPair(relativePath: $0.relativePath, uuid: $0.uuid) })
let parentFolder = selectedItems.first?.parentFolder

do {
Expand All @@ -364,7 +364,7 @@ extension ItemListViewModel {
editMode = .inactive
}

func importIntoLibrary(_ items: [String]) {
func importIntoLibrary(_ items: [PathUuidPair]) {
do {
try libraryService.moveItems(items, inside: nil)
syncService.scheduleMove(items: items, to: nil)
Expand All @@ -375,7 +375,7 @@ extension ItemListViewModel {
listState.reloadAll(padding: items.count)
}

func createFolder(with title: String, items: [String]? = nil, type: SimpleItemType) {
func createFolder(with title: String, items: [PathUuidPair]? = nil, type: SimpleItemType) {
Task { @MainActor in
do {
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -391,15 +391,15 @@ extension ItemListViewModel {
await syncService.scheduleUpload(items: [folder])
if let fetchedItems = items {
try libraryService.moveItems(fetchedItems, inside: folder.relativePath)
syncService.scheduleMove(items: fetchedItems, to: folder.relativePath)
syncService.scheduleMove(items: fetchedItems, to: PathUuidPair(relativePath: folder.relativePath, uuid: folder.uuid))
}
try libraryService.updateFolder(at: folder.relativePath, type: type)
libraryService.rebuildFolderDetails(folder.relativePath)

// stop playback if folder items contain that current item
if let items = items,
let currentRelativePath = playerManager.currentItem?.relativePath,
items.contains(currentRelativePath)
items.contains(where: { $0.relativePath == currentRelativePath })
{
playerManager.stop()
}
Expand All @@ -415,17 +415,17 @@ extension ItemListViewModel {
func handleMoveIntoFolder(_ folder: SimpleLibraryItem) {
// Use pendingMoveItemIdentifiers if available (from import operations),
// otherwise fall back to selectedItems (from manual selection)
let fetchedItems: [String]
let fetchedItems: [PathUuidPair]
if let pendingItems = pendingMoveItemIdentifiers {
fetchedItems = pendingItems
pendingMoveItemIdentifiers = nil
} else {
fetchedItems = selectedItems.compactMap({ $0.relativePath })
fetchedItems = selectedItems.compactMap({ PathUuidPair(relativePath: $0.relativePath, uuid: $0.uuid) })
}

do {
try libraryService.moveItems(fetchedItems, inside: folder.relativePath)
syncService.scheduleMove(items: fetchedItems, to: folder.relativePath)
syncService.scheduleMove(items: fetchedItems, to: PathUuidPair(relativePath: folder.relativePath, uuid: folder.uuid))
} catch {
loadingState.error = error
}
Expand Down
Loading
Loading