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>
This commit is contained in:
@@ -14,6 +14,7 @@ struct RecipeListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@EnvironmentObject var mealPlan: MealPlanManager
|
||||
@ObservedObject private var userSettings = UserSettings.shared
|
||||
@State var categoryName: String
|
||||
@State var searchText: String = ""
|
||||
var onCreateNew: () -> Void
|
||||
@@ -22,6 +23,10 @@ struct RecipeListView: View {
|
||||
|
||||
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
||||
|
||||
private var currentRecipeSortMode: RecipeSortMode {
|
||||
RecipeSortMode(rawValue: userSettings.recipeSortMode) ?? .recentlyAdded
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
let recipes = recipesFiltered()
|
||||
@@ -82,6 +87,9 @@ struct RecipeListView: View {
|
||||
.environmentObject(mealPlan)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
recipeSortMenu
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
Button {
|
||||
@@ -113,12 +121,84 @@ struct RecipeListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func recipesFiltered() -> [Recipe] {
|
||||
guard let recipes = appState.recipes[categoryName] else { return [] }
|
||||
guard searchText != "" else { return recipes }
|
||||
return recipes.filter { recipe in
|
||||
recipe.name.lowercased().contains(searchText.lowercased()) || // check name for occurence of search term
|
||||
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased())) // check keywords for search term
|
||||
private var recipeSortMenu: some View {
|
||||
Menu {
|
||||
ForEach(RecipeSortMode.allCases, id: \.self) { mode in
|
||||
Button {
|
||||
userSettings.recipeSortMode = mode.rawValue
|
||||
userSettings.recipeSortAscending = true
|
||||
} label: {
|
||||
if currentRecipeSortMode == mode {
|
||||
Label(mode.descriptor(), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(mode.descriptor())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
Button {
|
||||
userSettings.recipeSortAscending.toggle()
|
||||
} label: {
|
||||
Label(
|
||||
userSettings.recipeSortAscending ? String(localized: "Reverse Order") : String(localized: "Default Order"),
|
||||
systemImage: "arrow.up.arrow.down"
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
}
|
||||
}
|
||||
|
||||
func recipesFiltered() -> [Recipe] {
|
||||
guard let recipes = appState.recipes[categoryName] else { return [] }
|
||||
let filtered: [Recipe]
|
||||
if searchText.isEmpty {
|
||||
filtered = recipes
|
||||
} else {
|
||||
filtered = recipes.filter { recipe in
|
||||
recipe.name.lowercased().contains(searchText.lowercased()) ||
|
||||
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased()))
|
||||
}
|
||||
}
|
||||
return sortRecipes(filtered)
|
||||
}
|
||||
|
||||
private func sortRecipes(_ recipes: [Recipe]) -> [Recipe] {
|
||||
let mode = currentRecipeSortMode
|
||||
let ascending = userSettings.recipeSortAscending
|
||||
switch mode {
|
||||
case .recentlyAdded:
|
||||
return recipes.sorted { a, b in
|
||||
let dateA = parseDate(a.dateModified ?? a.dateCreated) ?? Date(timeIntervalSince1970: Double(a.recipe_id))
|
||||
let dateB = parseDate(b.dateModified ?? b.dateCreated) ?? Date(timeIntervalSince1970: Double(b.recipe_id))
|
||||
return ascending ? dateA > dateB : dateA < dateB
|
||||
}
|
||||
case .alphabetical:
|
||||
return recipes.sorted { a, b in
|
||||
let result = a.name.localizedCaseInsensitiveCompare(b.name)
|
||||
return ascending ? result == .orderedAscending : result == .orderedDescending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
return f
|
||||
}()
|
||||
|
||||
private func parseDate(_ string: String?) -> Date? {
|
||||
guard let string, !string.isEmpty else { return nil }
|
||||
// Try "yyyy-MM-dd HH:mm:ss" first
|
||||
if let date = Self.dateFormatter.date(from: string) {
|
||||
return date
|
||||
}
|
||||
// Try Unix timestamp (integer string)
|
||||
if let timestamp = Double(string), timestamp > 0 {
|
||||
return Date(timeIntervalSince1970: timestamp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user