ponshu-room-lite/lib/providers/sake_list_provider.dart

184 lines
6.4 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/sake_item.dart';
import 'filter_providers.dart';
// 1. Raw List Stream
final rawSakeListItemsProvider = StreamProvider<List<SakeItem>>((ref) {
final box = Hive.box<SakeItem>('sake_items');
// Use startWith to emit current value immediately
return box.watch().map((event) => box.values.toList()).startWith(box.values.toList());
});
// 2. Sort Order Stream
final sakeSortOrderProvider = StreamProvider<List<String>>((ref) {
final box = Hive.box('settings');
return box.watch(key: 'sake_sort_order').map((event) {
final List<dynamic>? stored = box.get('sake_sort_order');
return stored?.cast<String>() ?? <String>[];
}).startWith(box.get('sake_sort_order')?.cast<String>() ?? []);
});
// Sort Mode Enum
enum SortMode { newest, oldest, name, custom }
class SakeSortModeNotifier extends Notifier<SortMode> {
@override
SortMode build() => SortMode.newest;
void set(SortMode mode) => state = mode;
}
final sakeSortModeProvider = NotifierProvider<SakeSortModeNotifier, SortMode>(SakeSortModeNotifier.new);
// 3. Combined Sorted List
final sakeListProvider = Provider<AsyncValue<List<SakeItem>>>((ref) {
final rawListAsync = ref.watch(rawSakeListItemsProvider);
final sortOrderAsync = ref.watch(sakeSortOrderProvider);
final sortMode = ref.watch(sakeSortModeProvider);
// Watch Filters
final searchQuery = ref.watch(sakeSearchQueryProvider).toLowerCase();
final showFavoritesOnly = ref.watch(sakeFilterFavoriteProvider);
final filterTag = ref.watch(sakeFilterTagProvider);
final filterPrefecture = ref.watch(sakeFilterPrefectureProvider);
return rawListAsync.when(
data: (rawList) {
if (rawList.isEmpty) return const AsyncValue.data([]);
// 1. First, apply filters to raw list
var filtered = rawList.where((item) {
// Search Filter
if (searchQuery.isNotEmpty) {
final matches = item.displayData.name.toLowerCase().contains(searchQuery) ||
item.displayData.brewery.toLowerCase().contains(searchQuery) ||
item.displayData.prefecture.toLowerCase().contains(searchQuery);
if (!matches) return false;
}
// Favorite Filter
if (showFavoritesOnly) {
if (!item.userData.isFavorite) return false;
}
// Prefecture Filter
if (filterPrefecture != null) {
if (item.displayData.prefecture != filterPrefecture) return false;
}
// Tag Filter
if (filterTag != null && filterTag != 'All') {
// Special case for 'Set' which effectively filters by itemType if we had it, or implicit tag
if (filterTag == 'Set') {
// Assuming 'Set' tag is manually added or we check itemType if implemented
// For now, check flavorTags for 'Set' or 'セット'
final isSet = item.hiddenSpecs.flavorTags.contains('セット') ||
item.hiddenSpecs.flavorTags.contains('Set') ||
item.displayData.name.contains('セット');
if (!isSet) return false;
} else {
final matchesTag = item.hiddenSpecs.flavorTags.contains(filterTag);
if (!matchesTag) return false;
}
}
return true;
}).toList();
// 2. Then apply sort based on SortMode
switch (sortMode) {
case SortMode.newest:
filtered.sort((a, b) => b.metadata.createdAt.compareTo(a.metadata.createdAt));
break;
case SortMode.oldest:
filtered.sort((a, b) => a.metadata.createdAt.compareTo(b.metadata.createdAt));
break;
case SortMode.name:
filtered.sort((a, b) => a.displayData.name.compareTo(b.displayData.name));
break;
case SortMode.custom:
// Use Manual Sort Order
return sortOrderAsync.when(
data: (sortOrder) {
if (sortOrder.isEmpty) {
// Default fallback if no custom order
filtered.sort((a, b) => b.metadata.createdAt.compareTo(a.metadata.createdAt));
return AsyncValue.data(filtered);
}
final orderMap = {for (var i = 0; i < sortOrder.length; i++) sortOrder[i]: i};
filtered.sort((a, b) {
final idxA = orderMap[a.id];
final idxB = orderMap[b.id];
if (idxA != null && idxB != null) {
return idxA.compareTo(idxB);
} else if (idxA != null) {
return -1;
} else if (idxB != null) {
return 1;
} else {
return b.metadata.createdAt.compareTo(a.metadata.createdAt);
}
});
return AsyncValue.data(filtered);
},
loading: () => AsyncValue.data(filtered),
error: (e, s) => AsyncValue.error(e, s),
);
}
return AsyncValue.data(filtered);
},
loading: () => const AsyncValue.loading(),
error: (e, s) => AsyncValue.error(e, s),
);
});
// 4. Controller for Updating Order
class SakeOrderController extends Notifier<void> {
@override
void build() {}
void updateOrder(List<SakeItem> newOrder) {
final box = Hive.box('settings');
final ids = newOrder.map((e) => e.id).toList();
box.put('sake_sort_order', ids);
}
}
final sakeOrderControllerProvider = NotifierProvider<SakeOrderController, void>(SakeOrderController.new);
// 5. All Items Provider (フィルタなし、全件取得用)
// ソムリエ診断など、フィルタリングせず全体の傾向を見たい場合に使用
final allSakeItemsProvider = Provider<AsyncValue<List<SakeItem>>>((ref) {
final rawListAsync = ref.watch(rawSakeListItemsProvider);
return rawListAsync.when(
data: (rawList) {
if (rawList.isEmpty) return const AsyncValue.data([]);
// ソートのみ適用(新しい順)
final sorted = List<SakeItem>.from(rawList);
sorted.sort((a, b) => b.metadata.createdAt.compareTo(a.metadata.createdAt));
return AsyncValue.data(sorted);
},
loading: () => const AsyncValue.loading(),
error: (e, s) => AsyncValue.error(e, s),
);
});
extension StreamStartWith<T> on Stream<T> {
Stream<T> startWith(T initial) async* {
yield initial;
yield* this;
}
}