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

216 lines
7.9 KiB
Dart
Raw Normal View History

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;
}
}