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

216 lines
7.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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