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" : {
|
"Close" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Current labels" : {
|
"Current tags" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Data Management" : {
|
"Data Management" : {
|
||||||
@ -187,6 +187,9 @@
|
|||||||
},
|
},
|
||||||
"Loading article..." : {
|
"Loading article..." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Loading tags..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Login & Save" : {
|
"Login & Save" : {
|
||||||
|
|
||||||
@ -214,6 +217,9 @@
|
|||||||
},
|
},
|
||||||
"No bookmarks found in %@." : {
|
"No bookmarks found in %@." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"No tags available" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
|
|||||||
@ -620,7 +620,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 10;
|
CURRENT_PROJECT_VERSION = 11;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@ -664,7 +664,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 10;
|
CURRENT_PROJECT_VERSION = 11;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = 8J69P655GN;
|
DEVELOPMENT_TEAM = 8J69P655GN;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x60",
|
"blue" : "0x5A",
|
||||||
"green" : "0x4E",
|
"green" : "0x4A",
|
||||||
"red" : "0x01"
|
"red" : "0x1F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
@ -49,7 +49,10 @@ class BookmarkDetailViewModel {
|
|||||||
do {
|
do {
|
||||||
settings = try await loadSettingsUseCase.execute()
|
settings = try await loadSettingsUseCase.execute()
|
||||||
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
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 {
|
if settings?.enableTTS == true {
|
||||||
self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase()
|
self.addTextToSpeechQueueUseCase = factory?.makeAddTextToSpeechQueueUseCase()
|
||||||
@ -121,10 +124,13 @@ class BookmarkDetailViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateReadProgress(id: String, progress: Int, anchor: String?) async {
|
func updateReadProgress(id: String, progress: Int, anchor: String?) async {
|
||||||
do {
|
// Only update if the new progress is higher than current
|
||||||
try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor)
|
if progress > readProgress {
|
||||||
} catch {
|
do {
|
||||||
// ignore error in this case
|
try await updateBookmarkUseCase.updateReadProgress(bookmarkId: id, progress: progress, anchor: anchor)
|
||||||
|
} catch {
|
||||||
|
// ignore error in this case
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,21 +42,48 @@ struct BookmarkLabelsView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
// All available labels
|
// All available labels
|
||||||
if !viewModel.allLabels.isEmpty {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
Text("All available tags")
|
||||||
Text("All available tags")
|
.font(.headline)
|
||||||
.font(.headline)
|
.padding(.horizontal)
|
||||||
.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 {
|
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) {
|
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(
|
UnifiedLabelChip(
|
||||||
label: label.name,
|
label: label.name,
|
||||||
isSelected: viewModel.currentLabels.contains(label.name),
|
isSelected: viewModel.currentLabels.contains(label.name),
|
||||||
isRemovable: false,
|
isRemovable: false,
|
||||||
onTap: {
|
onTap: {
|
||||||
|
print("addLabelsUseCase")
|
||||||
Task {
|
Task {
|
||||||
await viewModel.toggleLabel(for: bookmarkId, label: label.name)
|
await viewModel.toggleLabel(for: bookmarkId, label: label.name)
|
||||||
}
|
}
|
||||||
@ -68,7 +95,7 @@ struct BookmarkLabelsView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
|
.tabViewStyle(.page(indexDisplayMode: viewModel.availableLabelPages.count > 1 ? .automatic : .never))
|
||||||
.frame(height: 180)
|
.frame(height: 180)
|
||||||
.padding(.top, -20)
|
.padding(.top, -20)
|
||||||
}
|
}
|
||||||
@ -77,7 +104,7 @@ struct BookmarkLabelsView: View {
|
|||||||
// Current labels
|
// Current labels
|
||||||
if !viewModel.currentLabels.isEmpty {
|
if !viewModel.currentLabels.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Current labels")
|
Text("Current tags")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
|||||||
@ -7,23 +7,31 @@ class BookmarkLabelsViewModel {
|
|||||||
private let getLabelsUseCase: PGetLabelsUseCase
|
private let getLabelsUseCase: PGetLabelsUseCase
|
||||||
|
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
|
var isInitialLoading = false
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var showErrorAlert = false
|
var showErrorAlert = false
|
||||||
var currentLabels: [String] = []
|
var currentLabels: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
calculatePages()
|
||||||
|
}
|
||||||
|
}
|
||||||
var newLabelText = ""
|
var newLabelText = ""
|
||||||
|
|
||||||
|
|
||||||
var allLabels: [BookmarkLabel] = [] {
|
var allLabels: [BookmarkLabel] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
let pageSize = Constants.Labels.pageSize
|
calculatePages()
|
||||||
labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map {
|
|
||||||
Array(allLabels[$0..<min($0 + pageSize, allLabels.count)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelPages: [[BookmarkLabel]] = []
|
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] = []) {
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared, initialLabels: [String] = []) {
|
||||||
self.currentLabels = initialLabels
|
self.currentLabels = initialLabels
|
||||||
|
|
||||||
@ -35,8 +43,8 @@ class BookmarkLabelsViewModel {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func loadAllLabels() async {
|
func loadAllLabels() async {
|
||||||
isLoading = true
|
isInitialLoading = true
|
||||||
defer { isLoading = false }
|
defer { isInitialLoading = false }
|
||||||
do {
|
do {
|
||||||
let labels = try await getLabelsUseCase.execute()
|
let labels = try await getLabelsUseCase.execute()
|
||||||
allLabels = labels
|
allLabels = labels
|
||||||
@ -44,6 +52,8 @@ class BookmarkLabelsViewModel {
|
|||||||
errorMessage = "failed to load labels"
|
errorMessage = "failed to load labels"
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculatePages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -52,10 +62,10 @@ class BookmarkLabelsViewModel {
|
|||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
|
|
||||||
// Update local labels
|
|
||||||
currentLabels.append(contentsOf: labels)
|
currentLabels.append(contentsOf: labels)
|
||||||
currentLabels = Array(Set(currentLabels)) // Remove duplicates
|
currentLabels = Array(Set(currentLabels)) // Remove duplicates
|
||||||
|
|
||||||
|
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
|
||||||
} catch let error as BookmarkUpdateError {
|
} catch let error as BookmarkUpdateError {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
@ -65,6 +75,7 @@ class BookmarkLabelsViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
calculatePages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -94,6 +105,7 @@ class BookmarkLabelsViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
calculatePages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -109,9 +121,35 @@ class BookmarkLabelsViewModel {
|
|||||||
} else {
|
} else {
|
||||||
await addLabel(to: bookmarkId, label: label)
|
await addLabel(to: bookmarkId, label: label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculatePages()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLabels(_ labels: [String]) {
|
func updateLabels(_ labels: [String]) {
|
||||||
currentLabels = labels
|
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