Files
xianyan/lib/features/source/providers/source_provider.dart
2026-06-07 18:20:26 +08:00

353 lines
10 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.
// ============================================================
// 闲言APP — 句子来源状态管理
// 创建时间: 2026-04-29
/// 更新时间: 2026-06-07
/// 作用: 频道数据+开关状态+统计信息+混合规则+偏好设置管理
/// 上次更新: 移除硬编码禁用列表,分类启用状态由服务端推荐权重管理控制
// ============================================================
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/storage/kv_storage.dart';
import '../../../core/utils/logger.dart';
import '../../home/models/feed_model.dart';
import '../../home/services/feed_service.dart';
const _kDisabledChannels = 'source_disabled_channels';
const _kMixConfig = 'source_mix_config';
const _kHomeCardMixConfig = 'home_card_mix_config';
const _kDeduplicateContent = 'source_deduplicate_content';
const _kSortPreference = 'source_sort_preference';
const _kPerPagePreference = 'source_per_page_preference';
/// 后台默认关闭的分类key服务端已根据is_enabled过滤此处仅用于重排序置底
const _kBottomKeys = ['jieqi', 'article', 'lunyu', 'abbr', 'jiufang'];
class SourceState {
const SourceState({
this.channels = const [],
this.stats,
this.disabledKeys = const {},
this.isLoading = false,
this.searchQuery = '',
this.mixConfig = const FeedMixConfig(),
this.homeCardMixConfig = const FeedMixConfig(limit: 5),
this.deduplicateContent = true,
this.sortPreference = 'newest',
this.perPage = 20,
});
final List<FeedChannel> channels;
final FeedStatsResult? stats;
final Set<String> disabledKeys;
final bool isLoading;
final String searchQuery;
final FeedMixConfig mixConfig;
final FeedMixConfig homeCardMixConfig;
final bool deduplicateContent;
final String sortPreference;
final int perPage;
List<FeedChannel> get filteredChannels {
if (searchQuery.isEmpty) return channels;
final q = searchQuery.toLowerCase();
return channels
.where(
(c) =>
c.name.toLowerCase().contains(q) ||
c.key.toLowerCase().contains(q),
)
.toList();
}
int get enabledCount => channels.length - disabledKeys.length;
bool isEnabled(String key) => !disabledKeys.contains(key);
SourceState copyWith({
List<FeedChannel>? channels,
FeedStatsResult? stats,
Set<String>? disabledKeys,
bool? isLoading,
String? searchQuery,
FeedMixConfig? mixConfig,
FeedMixConfig? homeCardMixConfig,
bool? deduplicateContent,
String? sortPreference,
int? perPage,
bool clearStats = false,
}) {
return SourceState(
channels: channels ?? this.channels,
stats: clearStats ? null : (stats ?? this.stats),
disabledKeys: disabledKeys ?? this.disabledKeys,
isLoading: isLoading ?? this.isLoading,
searchQuery: searchQuery ?? this.searchQuery,
mixConfig: mixConfig ?? this.mixConfig,
homeCardMixConfig: homeCardMixConfig ?? this.homeCardMixConfig,
deduplicateContent: deduplicateContent ?? this.deduplicateContent,
sortPreference: sortPreference ?? this.sortPreference,
perPage: perPage ?? this.perPage,
);
}
}
class SourceNotifier extends Notifier<SourceState> {
@override
SourceState build() {
Future.microtask(init).catchError((_) {});
return const SourceState();
}
SourceNotifier();
Future<void> init() async {
state = state.copyWith(isLoading: true);
_loadDisabledKeys();
_loadMixConfig();
_loadHomeCardMixConfig();
_loadDeduplicateContent();
_loadSortPreference();
_loadPerPage();
final results = await Future.wait([
FeedService.fetchChannels(),
FeedService.fetchStats(),
]);
final rawChannels = results[0] as List<FeedChannel>;
final stats = results[1] as FeedStatsResult;
final reordered = _reorderChannels(rawChannels);
// 服务端已根据is_enabled过滤前端无需默认禁用
var disabledKeys = state.disabledKeys;
if (disabledKeys.isEmpty) {
// 首次使用时,不设置默认禁用,所有服务端返回的分类默认启用
disabledKeys = {};
await _saveDisabledKeys({});
}
state = state.copyWith(
channels: reordered,
stats: stats,
disabledKeys: disabledKeys,
isLoading: false,
);
}
List<FeedChannel> _reorderChannels(List<FeedChannel> channels) {
final bottomSet = _kBottomKeys.toSet();
final topChannels = channels
.where((c) => !bottomSet.contains(c.key))
.toList();
final bottomChannels = channels
.where((c) => bottomSet.contains(c.key))
.toList();
final orderedBottom = <FeedChannel>[];
for (final key in _kBottomKeys) {
final ch = bottomChannels.where((c) => c.key == key);
orderedBottom.addAll(ch);
}
return [...topChannels, ...orderedBottom];
}
Future<void> toggleChannel(String key) async {
final newDisabled = Set<String>.from(state.disabledKeys);
if (newDisabled.contains(key)) {
newDisabled.remove(key);
} else {
newDisabled.add(key);
}
state = state.copyWith(disabledKeys: newDisabled);
await _saveDisabledKeys(newDisabled);
_syncPreferencesToServer();
}
Future<void> enableAll() async {
state = state.copyWith(disabledKeys: {});
await _saveDisabledKeys({});
_syncPreferencesToServer();
}
Future<void> disableAll() async {
final allKeys = state.channels.map((c) => c.key).toSet();
state = state.copyWith(disabledKeys: allKeys);
await _saveDisabledKeys(allKeys);
_syncPreferencesToServer();
}
void updateSearch(String query) {
state = state.copyWith(searchQuery: query);
}
void updateMixConfig(FeedMixConfig config) {
state = state.copyWith(mixConfig: config);
_saveMixConfig(config);
_syncPreferencesToServer();
}
void updateHomeCardMixConfig(FeedMixConfig config) {
state = state.copyWith(homeCardMixConfig: config);
_saveHomeCardMixConfig(config);
_syncPreferencesToServer();
}
void setDeduplicateContent(bool enabled) {
state = state.copyWith(deduplicateContent: enabled);
try {
KvStorage.setString(_kDeduplicateContent, enabled.toString());
} catch (e) {
Log.e('内容去重开关保存失败', e);
}
_syncPreferencesToServer();
}
void setSortPreference(String sort) {
state = state.copyWith(sortPreference: sort);
try {
KvStorage.setString(_kSortPreference, sort);
} catch (e) {
Log.e('排序偏好保存失败', e);
}
_syncPreferencesToServer();
}
void setPerPage(int count) {
state = state.copyWith(perPage: count);
try {
KvStorage.setString(_kPerPagePreference, count.toString());
} catch (e) {
Log.e('每页数量保存失败', e);
}
_syncPreferencesToServer();
}
void _loadDisabledKeys() {
try {
final raw = KvStorage.getString(_kDisabledChannels);
if (raw != null && raw.isNotEmpty) {
final list = (jsonDecode(raw) as List<dynamic>)
.map((e) => e.toString())
.toList();
state = state.copyWith(disabledKeys: list.toSet());
}
} catch (e) {
Log.e('来源页禁用频道读取失败', e);
}
}
Future<void> _saveDisabledKeys(Set<String> keys) async {
try {
await KvStorage.setString(_kDisabledChannels, jsonEncode(keys.toList()));
} catch (e) {
Log.e('来源页禁用频道保存失败', e);
}
}
void _loadMixConfig() {
try {
final raw = KvStorage.getString(_kMixConfig);
if (raw != null && raw.isNotEmpty) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
state = state.copyWith(mixConfig: FeedMixConfig.fromJson(decoded));
}
} catch (e) {
Log.e('混合规则读取失败', e);
}
}
void _saveMixConfig(FeedMixConfig config) {
try {
KvStorage.setString(_kMixConfig, jsonEncode(config.toJson()));
} catch (e) {
Log.e('混合规则保存失败', e);
}
}
void _loadHomeCardMixConfig() {
try {
final raw = KvStorage.getString(_kHomeCardMixConfig);
if (raw != null && raw.isNotEmpty) {
final decoded = jsonDecode(raw) as Map<String, dynamic>;
state = state.copyWith(
homeCardMixConfig: FeedMixConfig.fromJson(decoded),
);
}
} catch (e) {
Log.e('首页卡片混合规则读取失败', e);
}
}
void _saveHomeCardMixConfig(FeedMixConfig config) {
try {
KvStorage.setString(_kHomeCardMixConfig, jsonEncode(config.toJson()));
} catch (e) {
Log.e('首页卡片混合规则保存失败', e);
}
}
void _loadDeduplicateContent() {
try {
final raw = KvStorage.getString(_kDeduplicateContent);
if (raw != null && raw.isNotEmpty) {
state = state.copyWith(deduplicateContent: raw == 'true');
}
} catch (e) {
Log.e('内容去重开关读取失败', e);
}
}
void _loadSortPreference() {
try {
final raw = KvStorage.getString(_kSortPreference);
if (raw != null && raw.isNotEmpty) {
state = state.copyWith(sortPreference: raw);
}
} catch (e) {
Log.e('排序偏好读取失败', e);
}
}
void _loadPerPage() {
try {
final raw = KvStorage.getString(_kPerPagePreference);
if (raw != null && raw.isNotEmpty) {
final val = int.tryParse(raw);
if (val != null && val >= 10 && val <= 50) {
state = state.copyWith(perPage: val);
}
}
} catch (e) {
Log.e('每页数量读取失败', e);
}
}
void _syncPreferencesToServer() {
try {
final prefs = FeedPreferences(
disabledChannels: state.disabledKeys.toList(),
mixMode: state.mixConfig.mode,
mixChannels: state.mixConfig.channels,
mixRatios: state.mixConfig.ratios,
groupSize: state.mixConfig.groupSize,
deduplicate: state.deduplicateContent,
sort: state.sortPreference,
homeCardMode: state.homeCardMixConfig.mode,
homeCardChannels: state.homeCardMixConfig.channels,
perPage: state.perPage,
);
FeedService.savePreferences(prefs);
} catch (e) {
Log.e('偏好同步到服务端失败', e);
}
}
}
final sourceProvider = NotifierProvider<SourceNotifier, SourceState>(
SourceNotifier.new,
);