From 03713230b0d1adaed7597e27b4c4553dcd5b1fad Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Wed, 30 Jul 2025 16:09:40 +0200 Subject: [PATCH] feat: optimize label pagination and read progress handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Optimize calculatePages() to show single page when ≤12 labels - Add loading animation for initial label loading only - Unify label filtering logic in ViewModel instead of UI - Fix read progress regression by always taking higher value - Prevent server updates with lower progress values - Improve UX with better loading states and pagination --- Localizable.xcstrings | 8 ++- readeck.xcodeproj/project.pbxproj | 4 +- .../splashBackground.colorset/Contents.json | 6 +- .../BookmarkDetailViewModel.swift | 16 +++-- .../BookmarkDetail/BookmarkLabelsView.swift | 47 +++++++++++---- .../BookmarkLabelsViewModel.swift | 60 +++++++++++++++---- 6 files changed, 109 insertions(+), 32 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a541074..a687d85 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -101,7 +101,7 @@ "Close" : { }, - "Current labels" : { + "Current tags" : { }, "Data Management" : { @@ -187,6 +187,9 @@ }, "Loading article..." : { + }, + "Loading tags..." : { + }, "Login & Save" : { @@ -214,6 +217,9 @@ }, "No bookmarks found in %@." : { + }, + "No tags available" : { + }, "OK" : { diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 60c7fac..3b6050d 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -620,7 +620,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; @@ -664,7 +664,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; diff --git a/readeck/Assets.xcassets/splashBackground.colorset/Contents.json b/readeck/Assets.xcassets/splashBackground.colorset/Contents.json index 491e23f..e8a5549 100644 --- a/readeck/Assets.xcassets/splashBackground.colorset/Contents.json +++ b/readeck/Assets.xcassets/splashBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x60", - "green" : "0x4E", - "red" : "0x01" + "blue" : "0x5A", + "green" : "0x4A", + "red" : "0x1F" } }, "idiom" : "universal" diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift index b40c8ed..e76899e 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift @@ -49,7 +49,10 @@ class BookmarkDetailViewModel { do { settings = try await loadSettingsUseCase.execute() bookmarkDetail = try await getBookmarkUseCase.execute(id: id) - readProgress = bookmarkDetail.readProgress ?? 0 + + // Always take the higher value between server and local progress + let serverProgress = bookmarkDetail.readProgress ?? 0 + readProgress = max(readProgress, serverProgress) if settings?.enableTTS == true { self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase() @@ -121,10 +124,13 @@ class BookmarkDetailViewModel { } func updateReadProgress(id: String, progress: Int, anchor: String?) async { - do { - try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor) - } catch { - // ignore error in this case + // Only update if the new progress is higher than current + if progress > readProgress { + do { + try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor) + } catch { + // ignore error in this case + } } } diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index 06eda4e..8ef3a81 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -42,21 +42,48 @@ struct BookmarkLabelsView: View { .padding(.horizontal) // All available labels - if !viewModel.allLabels.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("All available tags") - .font(.headline) - .padding(.horizontal) - + VStack(alignment: .leading, spacing: 8) { + Text("All available tags") + .font(.headline) + .padding(.horizontal) + + if viewModel.isInitialLoading { + // Loading state + VStack { + ProgressView() + .scaleEffect(1.2) + .padding(.vertical, 40) + Text("Loading tags...") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .frame(height: 180) + } else if viewModel.allLabels.isEmpty { + // Empty state + VStack { + Image(systemName: "tag") + .font(.system(size: 40)) + .foregroundColor(.secondary) + .padding(.vertical, 20) + Text("No tags available") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .frame(height: 180) + } else { + // Content state TabView { - ForEach(Array(viewModel.labelPages.enumerated()), id: \ .offset) { pageIndex, labelsPage in + ForEach(Array(viewModel.availableLabelPages.enumerated()), id: \.offset) { pageIndex, labelsPage in LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) { - ForEach(labelsPage.filter { !viewModel.currentLabels.contains($0.name) }, id: \ .id) { label in + ForEach(labelsPage, id: \.id) { label in UnifiedLabelChip( label: label.name, isSelected: viewModel.currentLabels.contains(label.name), isRemovable: false, onTap: { + print("addLabelsUseCase") Task { await viewModel.toggleLabel(for: bookmarkId, label: label.name) } @@ -68,7 +95,7 @@ struct BookmarkLabelsView: View { .padding(.horizontal) } } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) + .tabViewStyle(.page(indexDisplayMode: viewModel.availableLabelPages.count > 1 ? .automatic : .never)) .frame(height: 180) .padding(.top, -20) } @@ -77,7 +104,7 @@ struct BookmarkLabelsView: View { // Current labels if !viewModel.currentLabels.isEmpty { VStack(alignment: .leading, spacing: 8) { - Text("Current labels") + Text("Current tags") .font(.headline) .padding(.horizontal) diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift index 4fac783..9cc6852 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift @@ -7,23 +7,31 @@ class BookmarkLabelsViewModel { private let getLabelsUseCase: PGetLabelsUseCase var isLoading = false + var isInitialLoading = false var errorMessage: String? var showErrorAlert = false - var currentLabels: [String] = [] + var currentLabels: [String] = [] { + didSet { + calculatePages() + } + } var newLabelText = "" - var allLabels: [BookmarkLabel] = [] { didSet { - let pageSize = Constants.Labels.pageSize - labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map { - Array(allLabels[$0..