Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Recipes/CategoryReorderSheet.swift
Hendrik Hogertz 5307b502e9 Add category and recipe sorting with multiple modes and order inversion
Categories on the main page can be sorted by Recently Used, Alphabetical,
or Manual (drag-to-reorder). The sort menu appears inline next to the
Categories header. All Recipes is included in the sort order and manual
reorder sheet. Recipes within category and all-recipes lists can be sorted
by Recently Added or Alphabetical, with the sort button in the toolbar.
All non-manual sort modes support order inversion via a Reverse/Default
Order toggle. Date parsing handles both formatted strings and Unix
timestamps, with recipe_id as fallback when dates are unavailable.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 07:46:23 +01:00

90 lines
3.2 KiB
Swift

//
// CategoryReorderSheet.swift
// Nextcloud Cookbook iOS Client
//
import SwiftUI
struct CategoryReorderSheet: View {
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) private var dismiss
private static let allRecipesSentinel = "__ALL_RECIPES__"
@State private var orderedNames: [String] = []
var body: some View {
NavigationStack {
List {
ForEach(orderedNames, id: \.self) { name in
HStack {
if name == Self.allRecipesSentinel {
Text("All Recipes")
.bold()
} else {
Text(name)
}
Spacer()
if name == Self.allRecipesSentinel {
let total = appState.categories.reduce(0) { $0 + $1.recipe_count }
Text("\(total)")
.foregroundStyle(.secondary)
.font(.subheadline)
} else if let count = appState.categories.first(where: { $0.name == name })?.recipe_count {
Text("\(count)")
.foregroundStyle(.secondary)
.font(.subheadline)
}
}
}
.onMove { from, to in
orderedNames.move(fromOffsets: from, toOffset: to)
}
}
.environment(\.editMode, .constant(.active))
.navigationTitle(String(localized: "Reorder Categories"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: "Cancel")) {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "Done")) {
appState.updateManualCategoryOrder(orderedNames)
dismiss()
}
.bold()
}
}
}
.onAppear {
let currentCategoryNames = appState.categories
.filter { $0.recipe_count > 0 }
.map { $0.name }
let totalCount = appState.categories.reduce(0) { $0 + $1.recipe_count }
let existing = appState.manualCategoryOrder
// Keep only names that still exist on the server (or are the sentinel)
var reconciled = existing.filter {
$0 == Self.allRecipesSentinel || currentCategoryNames.contains($0)
}
// Ensure the All Recipes sentinel is present
if totalCount > 0 && !reconciled.contains(Self.allRecipesSentinel) {
reconciled.insert(Self.allRecipesSentinel, at: 0)
}
// Append any new categories not yet in the manual order
for name in currentCategoryNames where !reconciled.contains(name) {
reconciled.append(name)
}
orderedNames = reconciled
}
}
}