feat: optimize label pagination and read progress handling
- 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
This commit is contained in:
parent
1cb87a4fb7
commit
03713230b0
@ -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" : {
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,12 +124,15 @@ class BookmarkDetailViewModel {
|
||||
}
|
||||
|
||||
func updateReadProgress(id: String, progress: Int, anchor: String?) async {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func debouncedUpdateReadProgress(id: String, progress: Double, anchor: String?) {
|
||||
readProgressSubject.send((id, progress, anchor))
|
||||
|
||||
@ -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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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..<min($0 + pageSize, allLabels.count)])
|
||||
}
|
||||
calculatePages()
|
||||
}
|
||||
}
|
||||
|
||||
var labelPages: [[BookmarkLabel]] = []
|
||||
|
||||
// Computed property for available labels (excluding current labels)
|
||||
var availableLabels: [BookmarkLabel] {
|
||||
return allLabels.filter { currentLabels.contains($0.name) == false }
|
||||
}
|
||||
|
||||
var availableLabelPages: [[BookmarkLabel]] = []
|
||||
|
||||
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared, initialLabels: [String] = []) {
|
||||
self.currentLabels = initialLabels
|
||||
|
||||
@ -35,8 +43,8 @@ class BookmarkLabelsViewModel {
|
||||
|
||||
@MainActor
|
||||
func loadAllLabels() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
isInitialLoading = true
|
||||
defer { isInitialLoading = false }
|
||||
do {
|
||||
let labels = try await getLabelsUseCase.execute()
|
||||
allLabels = labels
|
||||
@ -44,6 +52,8 @@ class BookmarkLabelsViewModel {
|
||||
errorMessage = "failed to load labels"
|
||||
showErrorAlert = true
|
||||
}
|
||||
|
||||
calculatePages()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -52,10 +62,10 @@ class BookmarkLabelsViewModel {
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
|
||||
// Update local labels
|
||||
currentLabels.append(contentsOf: labels)
|
||||
currentLabels = Array(Set(currentLabels)) // Remove duplicates
|
||||
|
||||
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
|
||||
} catch let error as BookmarkUpdateError {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
@ -65,6 +75,7 @@ class BookmarkLabelsViewModel {
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
calculatePages()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -94,6 +105,7 @@ class BookmarkLabelsViewModel {
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
calculatePages()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -109,9 +121,35 @@ class BookmarkLabelsViewModel {
|
||||
} else {
|
||||
await addLabel(to: bookmarkId, label: label)
|
||||
}
|
||||
|
||||
calculatePages()
|
||||
}
|
||||
|
||||
func updateLabels(_ labels: [String]) {
|
||||
currentLabels = labels
|
||||
}
|
||||
|
||||
private func calculatePages() {
|
||||
let pageSize = Constants.Labels.pageSize
|
||||
|
||||
// Calculate pages for all labels
|
||||
if allLabels.count <= pageSize {
|
||||
labelPages = [allLabels]
|
||||
} else {
|
||||
// Normal pagination for larger datasets
|
||||
labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map {
|
||||
Array(allLabels[$0..<min($0 + pageSize, allLabels.count)])
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate pages for available labels (excluding current labels)
|
||||
if availableLabels.count <= pageSize {
|
||||
availableLabelPages = [availableLabels]
|
||||
} else {
|
||||
// Normal pagination for larger datasets
|
||||
availableLabelPages = stride(from: 0, to: availableLabels.count, by: pageSize).map {
|
||||
Array(availableLabels[$0..<min($0 + pageSize, availableLabels.count)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user