216 lines
7.9 KiB
Dart
216 lines
7.9 KiB
Dart
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:hive_flutter/hive_flutter.dart';
|
||
import '../models/sake_item.dart'; // SakeItem exports ItemType
|
||
import 'filter_providers.dart';
|
||
import 'theme_provider.dart'; // Phase D6: For isBusinessMode
|
||
|
||
// 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. Phase D6: Mode-based Filtered Provider (セット商品・Draft除外)
|
||
// 個人モード: セット商品とDraftを除外
|
||
// ビジネスモード: 全表示
|
||
final filteredByModeProvider = Provider<AsyncValue<List<SakeItem>>>((ref) {
|
||
final rawListAsync = ref.watch(rawSakeListItemsProvider);
|
||
// Performance optimization: Only watch isBusinessMode field to avoid unnecessary rebuilds
|
||
final isBusinessMode = ref.watch(userProfileProvider.select((p) => p.isBusinessMode));
|
||
|
||
return rawListAsync.when(
|
||
data: (rawList) {
|
||
if (rawList.isEmpty) return const AsyncValue.data([]);
|
||
|
||
// ビジネスモード: 全アイテム表示(セット商品含む)
|
||
if (isBusinessMode) {
|
||
return AsyncValue.data(rawList);
|
||
}
|
||
|
||
// 個人モード: セット商品とDraft(未解析)を除外
|
||
final filtered = rawList.where((item) =>
|
||
item.itemType == ItemType.sake && // セット商品除外
|
||
!item.isPendingAnalysis // Draft除外
|
||
).toList();
|
||
|
||
return AsyncValue.data(filtered);
|
||
},
|
||
loading: () => const AsyncValue.loading(),
|
||
error: (e, s) => AsyncValue.error(e, s),
|
||
);
|
||
});
|
||
|
||
// 3. 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(filteredByModeProvider);
|
||
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.displayName.toLowerCase().contains(searchQuery) ||
|
||
item.displayData.displayBrewery.toLowerCase().contains(searchQuery) ||
|
||
item.displayData.displayPrefecture.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.displayPrefecture != 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.displayName.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.displayName.compareTo(b.displayData.displayName));
|
||
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 (ユーザーフィルタなし、モード別フィルタ適用済み)
|
||
// ソムリエ診断など、ユーザーのフィルタリング操作を無視して全体の傾向を見たい場合に使用
|
||
// Phase D6: filteredByModeProviderを使用(個人モードではセット・Draft除外)
|
||
final allSakeItemsProvider = Provider<AsyncValue<List<SakeItem>>>((ref) {
|
||
final filteredAsync = ref.watch(filteredByModeProvider);
|
||
|
||
return filteredAsync.when(
|
||
data: (filteredList) {
|
||
if (filteredList.isEmpty) return const AsyncValue.data([]);
|
||
|
||
// ソートのみ適用(新しい順)
|
||
final sorted = List<SakeItem>.from(filteredList);
|
||
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;
|
||
}
|
||
}
|