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:
Developer
2026-05-30 05:30:49 +08:00
parent adfa0af825
commit 0da8906f5d
72 changed files with 9137 additions and 3312 deletions

View File

@@ -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,
);

View File

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