chore: 完成v6.5.58版本迭代更新
本次更新包含多项功能优化与bug修复: 1. 新增flutter_keyboard_visibility依赖替代MediaQuery轮询获取键盘状态 2. 添加远程功能标志API支持与FeatureFlag服务 3. 重构壁纸背景渲染组件,统一全局壁纸展示逻辑 4. 延迟初始化壁纸源健康检测至用户同意协议后 5. 修复预测返回/长按预览锁定问题并移除相关配置项 6. 优化日志输出控制,release模式仅保留错误日志 7. 新增进度模块多语言翻译与相关UI字段 8. 优化稍后读功能,取消时同步删除聊天消息 9. 更新权限说明文档,移除冗余的存储写入权限配置 10. 重构部分UI组件减少参数传递,优化性能
This commit is contained in:
@@ -2,51 +2,17 @@
|
||||
/// 闲言APP — 收藏状态管理(服务端同步 + 本地收藏)
|
||||
/// 创建时间: 2026-04-28
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 作用: 收藏功能服务端同步状态管理,优先Feed API + 降级旧接口 + 本地getByIds + 双向同步
|
||||
/// 上次更新: 修复Feed API返回空结果时不降级+createtime排序错误+未登录本地收藏显示
|
||||
/// 作用: 收藏功能UI状态管理,通过FavoriteRepository统一数据访问
|
||||
/// 上次更新: 迁移到Repository模式,Notifier只管理UI状态
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/storage/kv_storage.dart';
|
||||
import '../../../core/storage/database/app_database.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../models/feed_model.dart';
|
||||
import '../services/feed_service.dart';
|
||||
import '../services/searchall_service.dart';
|
||||
import '../../mine/user_center/services/user_center_service.dart';
|
||||
import '../repositories/favorite_repository.dart';
|
||||
|
||||
class FavoriteItem {
|
||||
const FavoriteItem({
|
||||
required this.id,
|
||||
required this.targetType,
|
||||
required this.targetId,
|
||||
this.title = '',
|
||||
this.content = '',
|
||||
this.createtime = '',
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String targetType;
|
||||
final int targetId;
|
||||
final String title;
|
||||
final String content;
|
||||
final String createtime;
|
||||
|
||||
factory FavoriteItem.fromJson(Map<String, dynamic> json) {
|
||||
return FavoriteItem(
|
||||
id: json['id'] as int? ?? 0,
|
||||
targetType: json['target_type'] as String? ?? '',
|
||||
targetId: json['target_id'] as int? ?? 0,
|
||||
title: json['title'] as String? ?? '',
|
||||
content: json['content'] as String? ?? '',
|
||||
createtime: json['createtime']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
export '../repositories/favorite_repository.dart' show FavoriteItem, SyncResult;
|
||||
|
||||
enum SyncStatus { idle, syncing, success, failed }
|
||||
|
||||
@@ -120,22 +86,37 @@ class FavoriteNotifier extends Notifier<FavoriteState> {
|
||||
FavoriteState build() => const FavoriteState();
|
||||
FavoriteNotifier();
|
||||
|
||||
FavoriteRepository get _repo => ref.read(favoriteRepositoryProvider);
|
||||
|
||||
Future<void> loadFavorites({bool refresh = false}) async {
|
||||
if (state.isLoading) return;
|
||||
final page = refresh ? 1 : state.page;
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
if (state.useFeedApi) {
|
||||
await _loadFeedFavorites(refresh: refresh, page: page);
|
||||
} else {
|
||||
await _loadLegacyFavorites(refresh: refresh, page: page);
|
||||
try {
|
||||
final result = await _repo.fetchFavorites(
|
||||
page: refresh ? 1 : state.page,
|
||||
refresh: refresh,
|
||||
useFeedApi: state.useFeedApi,
|
||||
existingFeedItems: state.feedItems,
|
||||
existingItems: state.items,
|
||||
);
|
||||
state = state.copyWith(
|
||||
items: result.items,
|
||||
feedItems: result.feedItems,
|
||||
total: result.total,
|
||||
page: result.nextPage,
|
||||
isLoading: false,
|
||||
useFeedApi: result.useFeedApi,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('加载收藏失败', e);
|
||||
state = state.copyWith(isLoading: false, error: '加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadLocalFavoritesAsItems() async {
|
||||
try {
|
||||
final feedItems = await loadLocalFavorites();
|
||||
final convertedItems = feedItems.map(_feedItemToFavoriteItem).toList();
|
||||
final convertedItems = await _repo.loadLocalFavoritesAsItems();
|
||||
state = state.copyWith(
|
||||
items: convertedItems,
|
||||
total: convertedItems.length,
|
||||
@@ -147,182 +128,58 @@ class FavoriteNotifier extends Notifier<FavoriteState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFeedFavorites({bool refresh = false, int page = 1}) async {
|
||||
try {
|
||||
final result = await FeedService.fetchFavorites(page: page);
|
||||
|
||||
if (result.list.isEmpty && result.total == 0 && page == 1) {
|
||||
Log.w('Feed收藏API返回空结果(total=0),尝试降级旧接口');
|
||||
state = state.copyWith(useFeedApi: false);
|
||||
await _loadLegacyFavorites(refresh: refresh, page: page);
|
||||
return;
|
||||
}
|
||||
|
||||
final newItems = refresh
|
||||
? result.list
|
||||
: [...state.feedItems, ...result.list];
|
||||
final convertedItems = newItems.map(_feedItemToFavoriteItem).toList();
|
||||
state = state.copyWith(
|
||||
feedItems: newItems,
|
||||
items: convertedItems,
|
||||
total: result.total,
|
||||
page: page + 1,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('Feed收藏列表加载失败,降级旧接口', e);
|
||||
state = state.copyWith(useFeedApi: false);
|
||||
await _loadLegacyFavorites(refresh: refresh, page: page);
|
||||
}
|
||||
}
|
||||
|
||||
FavoriteItem _feedItemToFavoriteItem(FeedItem fi) {
|
||||
return FavoriteItem(
|
||||
id: fi.id,
|
||||
targetType: fi.feedType,
|
||||
targetId: fi.id,
|
||||
title: fi.author.isNotEmpty ? fi.author : fi.title,
|
||||
content: fi.content.isNotEmpty ? fi.content : fi.summary,
|
||||
createtime: fi.createtime?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadLegacyFavorites({
|
||||
bool refresh = false,
|
||||
int page = 1,
|
||||
}) async {
|
||||
try {
|
||||
final result = await UserCenterService.favorite(
|
||||
action: FavoriteAction.list,
|
||||
targetType: 'article',
|
||||
page: page,
|
||||
);
|
||||
final total = result['total'] as int? ?? 0;
|
||||
final list = (result['list'] as List<dynamic>? ?? [])
|
||||
.map((e) => FavoriteItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final newItems = refresh ? list : [...state.items, ...list];
|
||||
state = state.copyWith(
|
||||
items: newItems,
|
||||
total: total,
|
||||
page: page + 1,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.e('加载收藏失败', e);
|
||||
state = state.copyWith(isLoading: false, error: '加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> toggleFavorite({
|
||||
required String targetType,
|
||||
required int targetId,
|
||||
}) async {
|
||||
try {
|
||||
final checkResult = await UserCenterService.favorite(
|
||||
action: FavoriteAction.check,
|
||||
targetType: targetType,
|
||||
targetId: targetId,
|
||||
);
|
||||
final isFav = checkResult['is_favorited'] as bool? ?? false;
|
||||
|
||||
final action = isFav ? FavoriteAction.remove : FavoriteAction.add;
|
||||
await UserCenterService.favorite(
|
||||
action: action,
|
||||
targetType: targetType,
|
||||
targetId: targetId,
|
||||
);
|
||||
Log.i('${isFav ? "取消" : "添加"}收藏成功');
|
||||
await loadFavorites(refresh: true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('收藏操作失败', e);
|
||||
return false;
|
||||
}
|
||||
final success = await _repo.toggleFavorite(
|
||||
targetType: targetType,
|
||||
targetId: targetId,
|
||||
);
|
||||
if (success) await loadFavorites(refresh: true);
|
||||
return success;
|
||||
}
|
||||
|
||||
Future<bool> isFavorited({
|
||||
required String targetType,
|
||||
required int targetId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await UserCenterService.favorite(
|
||||
action: FavoriteAction.check,
|
||||
targetType: targetType,
|
||||
targetId: targetId,
|
||||
);
|
||||
return result['is_favorited'] as bool? ?? false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return _repo.isFavorited(targetType: targetType, targetId: targetId);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 本地收藏(基于 SearchAll getByIds)
|
||||
// 本地收藏(委托给FavoriteRepository)
|
||||
// ============================================================
|
||||
|
||||
static const _localFavKey = 'local_favorites';
|
||||
static List<String> getLocalFavoriteIds() =>
|
||||
FavoriteRepository.getLocalFavoriteIds();
|
||||
|
||||
static List<String> getLocalFavoriteIds() {
|
||||
final raw = KvStorage.getString(_localFavKey);
|
||||
if (raw == null || raw.isEmpty) return [];
|
||||
try {
|
||||
return List<String>.from(jsonDecode(raw) as List);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
static bool isLocalFavorited(String type, int id) =>
|
||||
FavoriteRepository.isLocalFavorited(type, id);
|
||||
|
||||
static Future<void> _saveLocalFavoriteIds(List<String> ids) =>
|
||||
KvStorage.setString(_localFavKey, jsonEncode(ids));
|
||||
Future<void> addLocalFavorite(String type, int id) =>
|
||||
_repo.addLocalFavorite(type, id);
|
||||
|
||||
static bool isLocalFavorited(String type, int id) {
|
||||
final ids = getLocalFavoriteIds();
|
||||
return ids.contains('$type:$id');
|
||||
}
|
||||
Future<void> removeLocalFavorite(String type, int id) =>
|
||||
_repo.removeLocalFavorite(type, id);
|
||||
|
||||
Future<void> addLocalFavorite(String type, int id) async {
|
||||
final ids = getLocalFavoriteIds();
|
||||
final key = '$type:$id';
|
||||
if (!ids.contains(key)) {
|
||||
ids.insert(0, key);
|
||||
await _saveLocalFavoriteIds(ids);
|
||||
}
|
||||
}
|
||||
Future<List<FeedItem>> loadLocalFavorites() => _repo.loadLocalFavorites();
|
||||
|
||||
Future<void> removeLocalFavorite(String type, int id) async {
|
||||
final ids = getLocalFavoriteIds();
|
||||
ids.remove('$type:$id');
|
||||
await _saveLocalFavoriteIds(ids);
|
||||
}
|
||||
|
||||
Future<List<FeedItem>> loadLocalFavorites() async {
|
||||
final ids = getLocalFavoriteIds();
|
||||
if (ids.isEmpty) return [];
|
||||
try {
|
||||
return SearchAllService.getByIds(ids: ids);
|
||||
} catch (e) {
|
||||
Log.e('本地收藏批量获取失败', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleLocalFavorite(String type, int id) async {
|
||||
if (isLocalFavorited(type, id)) {
|
||||
await removeLocalFavorite(type, id);
|
||||
} else {
|
||||
await addLocalFavorite(type, id);
|
||||
}
|
||||
}
|
||||
Future<void> toggleLocalFavorite(String type, int id) =>
|
||||
_repo.toggleLocalFavorite(type, id);
|
||||
|
||||
// ============================================================
|
||||
// 双向同步
|
||||
// 双向同步(委托给FavoriteRepository)
|
||||
// ============================================================
|
||||
|
||||
Future<SyncResult> syncFavorites() async {
|
||||
if (state.isSyncing) {
|
||||
return const SyncResult(pulled: 0, pushed: 0, skipped: 0, message: '正在同步中');
|
||||
return const SyncResult(
|
||||
pulled: 0,
|
||||
pushed: 0,
|
||||
skipped: 0,
|
||||
message: '正在同步中',
|
||||
);
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
@@ -331,250 +188,51 @@ class FavoriteNotifier extends Notifier<FavoriteState> {
|
||||
syncMessage: '准备同步…',
|
||||
);
|
||||
|
||||
int pulled = 0;
|
||||
int pushed = 0;
|
||||
int skipped = 0;
|
||||
|
||||
try {
|
||||
state = state.copyWith(syncMessage: '正在拉取云端收藏…');
|
||||
final serverFavs = await _fetchAllServerFavorites();
|
||||
Log.i('云端收藏总数: ${serverFavs.length}');
|
||||
final result = await _repo.syncFavorites(
|
||||
onProgress: (msg) {
|
||||
state = state.copyWith(syncMessage: msg);
|
||||
},
|
||||
);
|
||||
|
||||
state = state.copyWith(syncMessage: '正在获取本地收藏…');
|
||||
final db = AppDatabase.instance;
|
||||
final localFavs = await db.getFavoriteSentences();
|
||||
Log.i('本地收藏总数: ${localFavs.length}');
|
||||
|
||||
final serverKeySet = <String>{};
|
||||
for (final fav in serverFavs) {
|
||||
final feedType = fav.feedType.isNotEmpty ? fav.feedType : 'feed';
|
||||
serverKeySet.add('$feedType:${fav.id}');
|
||||
}
|
||||
|
||||
final localKeySet = <String>{};
|
||||
for (final s in localFavs) {
|
||||
final serverId = _resolveServerId(s);
|
||||
if (serverId != null) {
|
||||
final feedType = s.feedType.isNotEmpty ? s.feedType : 'feed';
|
||||
localKeySet.add('$feedType:$serverId');
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(syncMessage: '正在上传本地收藏到云端…');
|
||||
for (int i = 0; i < localFavs.length; i++) {
|
||||
final s = localFavs[i];
|
||||
try {
|
||||
int? serverId = _resolveServerId(s);
|
||||
String feedType = s.feedType.isNotEmpty ? s.feedType : 'feed';
|
||||
|
||||
if (serverId == null && s.content.isNotEmpty) {
|
||||
final keyword = s.content.length > 50
|
||||
? s.content.substring(0, 50)
|
||||
: s.content;
|
||||
final result = await SearchAllService.search(
|
||||
keyword: keyword,
|
||||
limit: 3,
|
||||
);
|
||||
for (final item in result.list) {
|
||||
if (item.content.trim() == s.content.trim()) {
|
||||
serverId = item.id;
|
||||
feedType = item.feedType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverId != null && serverId > 0) {
|
||||
final key = '$feedType:$serverId';
|
||||
if (serverKeySet.contains(key)) {
|
||||
skipped++;
|
||||
Log.i('同步跳过(云端已存在): $feedType/$serverId');
|
||||
continue;
|
||||
}
|
||||
|
||||
final success = await FeedService.action(
|
||||
action: 'favorite',
|
||||
feedType: feedType,
|
||||
feedId: serverId,
|
||||
);
|
||||
if (success) {
|
||||
pushed++;
|
||||
serverKeySet.add(key);
|
||||
Log.i('同步上传成功: $feedType/$serverId');
|
||||
} else {
|
||||
skipped++;
|
||||
Log.w('同步上传API返回失败: $feedType/$serverId');
|
||||
}
|
||||
} else {
|
||||
skipped++;
|
||||
Log.w('同步跳过: 无法解析服务端ID, localId=${s.id}');
|
||||
}
|
||||
|
||||
if (localFavs.length > 3 && (i + 1) % 3 == 0) {
|
||||
state = state.copyWith(
|
||||
syncMessage: '上传中 ${i + 1}/${localFavs.length}…',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
skipped++;
|
||||
Log.w('同步单条收藏失败: localId=${s.id}, $e');
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(syncMessage: '正在拉取云端收藏到本地…');
|
||||
for (int i = 0; i < serverFavs.length; i++) {
|
||||
final fav = serverFavs[i];
|
||||
try {
|
||||
final feedType = fav.feedType.isNotEmpty ? fav.feedType : 'feed';
|
||||
final key = '$feedType:${fav.id}';
|
||||
|
||||
if (localKeySet.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final exists = await db.getSentencesById(fav.id.toString());
|
||||
if (exists != null) {
|
||||
if (!exists.isFavorite) {
|
||||
await db.toggleFavorite(fav.id.toString());
|
||||
pulled++;
|
||||
Log.i('同步拉取(标记本地): $feedType/${fav.id}');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.insertOrUpdateSentence(SentencesCompanion(
|
||||
id: Value(fav.id.toString()),
|
||||
content: Value(fav.content),
|
||||
author: Value(fav.author),
|
||||
source: Value(fav.source),
|
||||
tags: const Value(''),
|
||||
feedType: Value(feedType),
|
||||
feedName: Value(fav.feedName),
|
||||
feedIcon: Value(fav.feedIcon),
|
||||
views: Value(fav.views),
|
||||
imageUrl: Value(fav.imageUrl),
|
||||
isFavorite: const Value(true),
|
||||
isLiked: const Value(false),
|
||||
isRead: const Value(false),
|
||||
createdAt: Value(DateTime.now()),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
));
|
||||
pulled++;
|
||||
Log.i('同步拉取(新增本地): $feedType/${fav.id}');
|
||||
|
||||
if (serverFavs.length > 3 && (i + 1) % 5 == 0) {
|
||||
state = state.copyWith(
|
||||
syncMessage: '拉取中 ${i + 1}/${serverFavs.length}…',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('同步拉取单条失败: serverId=${fav.id}, $e');
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(syncMessage: '正在刷新收藏列表…');
|
||||
await loadFavorites(refresh: true);
|
||||
|
||||
final message = '同步完成:上传 $pushed 条,拉取 $pulled 条'
|
||||
'${skipped > 0 ? ",跳过 $skipped 条" : ""}';
|
||||
|
||||
state = state.copyWith(
|
||||
isSyncing: false,
|
||||
syncStatus: SyncStatus.success,
|
||||
syncMessage: message,
|
||||
syncStatus: result.isError ? SyncStatus.failed : SyncStatus.success,
|
||||
syncMessage: result.message,
|
||||
);
|
||||
|
||||
Log.i(message);
|
||||
return SyncResult(
|
||||
pulled: pulled,
|
||||
pushed: pushed,
|
||||
skipped: skipped,
|
||||
message: message,
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
Log.e('同步收藏失败', e);
|
||||
state = state.copyWith(
|
||||
isSyncing: false,
|
||||
syncStatus: SyncStatus.failed,
|
||||
syncMessage: '同步失败: ${_shortError(e)}',
|
||||
syncMessage: '同步失败',
|
||||
);
|
||||
return SyncResult(
|
||||
pulled: pulled,
|
||||
pushed: pushed,
|
||||
skipped: skipped,
|
||||
message: '同步失败: ${_shortError(e)}',
|
||||
pulled: 0,
|
||||
pushed: 0,
|
||||
skipped: 0,
|
||||
message: '同步失败',
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FeedItem>> _fetchAllServerFavorites() async {
|
||||
final allItems = <FeedItem>[];
|
||||
int page = 1;
|
||||
const limit = 50;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
final result = await FeedService.fetchFavorites(page: page, limit: limit);
|
||||
allItems.addAll(result.list);
|
||||
if (!result.hasMore || result.list.length < limit) break;
|
||||
page++;
|
||||
} catch (e) {
|
||||
Log.e('拉取云端收藏第$page页失败', e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
int? _resolveServerId(Sentence s) {
|
||||
final direct = int.tryParse(s.id);
|
||||
if (direct != null && direct > 0) return direct;
|
||||
|
||||
for (final sep in [':', '_']) {
|
||||
if (s.id.contains(sep)) {
|
||||
final parts = s.id.split(sep);
|
||||
if (parts.length == 2) {
|
||||
final parsed = int.tryParse(parts.last);
|
||||
if (parsed != null && parsed > 0) return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String _shortError(dynamic e) {
|
||||
final msg = e.toString();
|
||||
return msg.length > 60 ? '${msg.substring(0, 60)}…' : msg;
|
||||
}
|
||||
|
||||
void resetSyncStatus() {
|
||||
state = state.copyWith(
|
||||
syncStatus: SyncStatus.idle,
|
||||
clearSyncMessage: true,
|
||||
);
|
||||
state = state.copyWith(syncStatus: SyncStatus.idle, clearSyncMessage: true);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分组管理
|
||||
// 分组管理(委托给FavoriteRepository)
|
||||
// ============================================================
|
||||
|
||||
Future<List<String>> loadGroups() async {
|
||||
try {
|
||||
final result = await UserCenterService.favorite(
|
||||
action: FavoriteAction.groups,
|
||||
);
|
||||
final list = (result['groups'] as List<dynamic>? ?? [])
|
||||
.map((e) => e.toString())
|
||||
.toList();
|
||||
state = state.copyWith(groups: list);
|
||||
return list;
|
||||
} catch (e) {
|
||||
Log.e('获取收藏分组失败', e);
|
||||
return [];
|
||||
}
|
||||
final list = await _repo.loadGroups();
|
||||
state = state.copyWith(groups: list);
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<bool> moveToGroup({
|
||||
@@ -582,106 +240,50 @@ class FavoriteNotifier extends Notifier<FavoriteState> {
|
||||
required String targetType,
|
||||
required String newGroup,
|
||||
}) async {
|
||||
try {
|
||||
await UserCenterService.favorite(
|
||||
action: FavoriteAction.move,
|
||||
targetId: targetId,
|
||||
targetType: targetType,
|
||||
groupName: newGroup,
|
||||
);
|
||||
Log.i('移动收藏到分组: $newGroup');
|
||||
await loadFavorites(refresh: true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('移动收藏分组失败', e);
|
||||
return false;
|
||||
}
|
||||
final ok = await _repo.moveToGroup(
|
||||
targetId: targetId,
|
||||
targetType: targetType,
|
||||
newGroup: newGroup,
|
||||
);
|
||||
if (ok) await loadFavorites(refresh: true);
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getCounts() async {
|
||||
try {
|
||||
final result = await UserCenterService.favorite(
|
||||
action: FavoriteAction.count,
|
||||
);
|
||||
final raw = result['counts'] as Map<String, dynamic>? ?? {};
|
||||
final counts = raw.map((k, v) => MapEntry(k, v as int? ?? 0));
|
||||
state = state.copyWith(counts: counts);
|
||||
return counts;
|
||||
} catch (e) {
|
||||
Log.e('获取收藏统计失败', e);
|
||||
return {};
|
||||
}
|
||||
final counts = await _repo.getCounts();
|
||||
state = state.copyWith(counts: counts);
|
||||
return counts;
|
||||
}
|
||||
|
||||
Future<bool> reorderGroups(List<String> newOrder) async {
|
||||
try {
|
||||
await UserCenterService.favorite(
|
||||
action: FavoriteAction.reorder,
|
||||
groupName: newOrder.join(','),
|
||||
);
|
||||
state = state.copyWith(groups: newOrder);
|
||||
Log.i('分组排序已更新');
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('分组排序失败', e);
|
||||
return false;
|
||||
}
|
||||
final ok = await _repo.reorderGroups(newOrder);
|
||||
if (ok) state = state.copyWith(groups: newOrder);
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<bool> deleteGroup(String groupName) async {
|
||||
try {
|
||||
await UserCenterService.favorite(
|
||||
action: FavoriteAction.deleteGroup,
|
||||
groupName: groupName,
|
||||
);
|
||||
final ok = await _repo.deleteGroup(groupName);
|
||||
if (ok) {
|
||||
final updated = List<String>.from(state.groups)..remove(groupName);
|
||||
state = state.copyWith(groups: updated);
|
||||
await loadFavorites(refresh: true);
|
||||
Log.i('分组已删除: $groupName');
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('删除分组失败', e);
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
Future<bool> renameGroup(String oldName, String newName) async {
|
||||
try {
|
||||
await UserCenterService.favorite(
|
||||
action: FavoriteAction.renameGroup,
|
||||
groupName: oldName,
|
||||
newGroup: newName,
|
||||
);
|
||||
final ok = await _repo.renameGroup(oldName, newName);
|
||||
if (ok) {
|
||||
final updated = List<String>.from(state.groups);
|
||||
final idx = updated.indexOf(oldName);
|
||||
if (idx >= 0) updated[idx] = newName;
|
||||
state = state.copyWith(groups: updated);
|
||||
await loadFavorites(refresh: true);
|
||||
Log.i('分组已重命名: $oldName → $newName');
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('重命名分组失败', e);
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResult {
|
||||
const SyncResult({
|
||||
required this.pulled,
|
||||
required this.pushed,
|
||||
required this.skipped,
|
||||
required this.message,
|
||||
this.isError = false,
|
||||
});
|
||||
|
||||
final int pulled;
|
||||
final int pushed;
|
||||
final int skipped;
|
||||
final String message;
|
||||
final bool isError;
|
||||
|
||||
bool get isSuccess => !isError;
|
||||
}
|
||||
|
||||
final favoriteProvider = NotifierProvider<FavoriteNotifier, FavoriteState>(FavoriteNotifier.new);
|
||||
final favoriteProvider = NotifierProvider<FavoriteNotifier, FavoriteState>(
|
||||
FavoriteNotifier.new,
|
||||
);
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
/// 创建时间: 2026-05-12
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 作用: 点赞/收藏/稍后读/已读/评分/屏蔽/举报/不感兴趣/阅读时长
|
||||
/// 上次更新: 稍后读发送成功后弹出CupertinoAlertDialog确认提示
|
||||
/// 上次更新: 取消稍后读时同步删除聊天消息+刷新稍后读页面
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -201,6 +203,33 @@ mixin HomeInteractionMixin on Notifier<HomeState> {
|
||||
Log.w('稍后读句子写入会话失败: $e');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
final db = AppDatabase.instance;
|
||||
final records = await db.getChatMsgRecords(
|
||||
'readlater',
|
||||
limit: 200,
|
||||
);
|
||||
for (final record in records) {
|
||||
if (record.isDeleted) continue;
|
||||
final metaJson = record.metaJson;
|
||||
if (metaJson.isNotEmpty) {
|
||||
try {
|
||||
final meta = jsonDecode(metaJson) as Map<String, dynamic>;
|
||||
final recordSentenceId = meta['sentenceId'] as String?;
|
||||
if (recordSentenceId != null &&
|
||||
(recordSentenceId == id.toString() ||
|
||||
recordSentenceId == id ||
|
||||
_extractNumericId(recordSentenceId) == feedId)) {
|
||||
await db.softDeleteChatMsgRecord(record.id);
|
||||
Log.i('取消稍后读: 已删除聊天消息 ${record.id}');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
notifyReadlaterRefresh();
|
||||
} catch (e) {
|
||||
Log.w('取消稍后读: 删除聊天消息失败: $e');
|
||||
}
|
||||
_showReadLaterConfirmDialog('📖 已移出稍后读');
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -330,6 +359,13 @@ mixin HomeInteractionMixin on Notifier<HomeState> {
|
||||
// 稍后读确认对话框
|
||||
// ============================================================
|
||||
|
||||
int _extractNumericId(String id) {
|
||||
if (id.contains('_')) {
|
||||
return int.tryParse(id.split('_').last) ?? 0;
|
||||
}
|
||||
return int.tryParse(id) ?? 0;
|
||||
}
|
||||
|
||||
void _showReadLaterConfirmDialog(String message) {
|
||||
try {
|
||||
final ctx = rootNavigatorKey.currentContext;
|
||||
|
||||
Reference in New Issue
Block a user