云同步标签部分

This commit is contained in:
DelLevin-Home
2026-05-28 14:57:19 +08:00
parent e2e0b11d3d
commit 3ac06314c2
5 changed files with 88 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart';
import 'utils/user_prefs.dart';
import 'utils/sync/auto_backup_service.dart';
import 'utils/sync/server_sync_service.dart';
import 'utils/usage_stats_service.dart';
import 'providers/app_provider.dart';
import 'package:flutter/widget_previews.dart';
@@ -18,7 +19,7 @@ void main() async {
await UserPrefs.init();
final appProvider = AppProvider();
runApp(MyApp(appProvider: appProvider));
unawaited(appProvider.initDatabase().then((_) => appProvider.initMainTabIndex()));
unawaited(_validateSyncOnStartup().then((_) => appProvider.initDatabase().then((_) => appProvider.initMainTabIndex())));
unawaited(_initAutoBackup());
unawaited(_initUsageStats());
}
@@ -42,6 +43,34 @@ Future<void> _initUsageStats() async {
}
}
/// 启动时校验同步激活码:有效则继续,过期/失效则下载数据并关闭同步
Future<void> _validateSyncOnStartup() async {
try {
final prefs = UserPrefs();
if (!prefs.syncEnabled || prefs.syncServerUrl.isEmpty || prefs.syncActivationCode.isEmpty) {
return; // 未开启同步,跳过
}
debugPrint('[Startup] 校验同步激活码...');
final result = await ServerSyncService.instance.checkActivation();
if (result != null && result['valid'] == true) {
// 激活码有效,更新有效期信息
await prefs.setSyncExpiresAt(result['expires_at'] ?? '');
await prefs.setSyncIsPermanent(result['is_permanent'] == true);
debugPrint('[Startup] 激活码有效,继续同步模式');
} else {
// 激活码无效/过期,下载服务端数据并关闭同步
debugPrint('[Startup] 激活码失效: ${result?['error'] ?? '未知'},关闭同步并下载数据');
await ServerSyncService.instance.downloadToLocal();
await prefs.setSyncEnabled(false);
debugPrint('[Startup] 已切换到本地模式');
}
} catch (e) {
debugPrint('[Startup] 激活码校验异常: $e');
}
}
class MyApp extends StatefulWidget {
final AppProvider appProvider;
const MyApp({super.key, required this.appProvider});

View File

@@ -549,14 +549,19 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
/// 显示添加标签对话框
void _showAddTagDialog() {
Future<void> _showAddTagDialog() async {
final controller = TextEditingController();
// 获取所有已有标签(从所有笔记中收集
// 从 tags 表获取已有标签sync 模式下走服务端 API
final provider = context.read<AppProvider>();
final allTags = _getAllExistingTags(provider);
final tagRows = await provider.getTags('note_tag');
final allTags = tagRows.map((t) => t['name'] as String).toSet();
// 也从当前笔记内容中收集
for (final note in provider.notes) {
allTags.addAll(note.tags);
}
// 过滤掉已添加的标签
final availableTags = allTags.where((tag) => !_tags.contains(tag)).toList();
final availableTags = allTags.where((tag) => !_tags.contains(tag)).toList()..sort();
showDialog(
context: context,
@@ -713,14 +718,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
}
/// 获取所有已有标签(从所有笔记中收集)
List<String> _getAllExistingTags(AppProvider provider) {
final allTags = <String>{};
for (final note in provider.notes) {
allTags.addAll(note.tags);
}
return allTags.toList()..sort();
}
/// 添加标签
void _addTag(String tag) {
final trimmed = tag.trim();

View File

@@ -48,7 +48,10 @@ class _ServerSyncPageState extends State<ServerSyncPage> {
_isActivated = url.isNotEmpty && code.isNotEmpty;
_syncEnabled = _isActivated && _prefs.syncEnabled;
_updateExpiresText();
if (_isActivated) _startStatusPolling();
if (_isActivated) {
_startStatusPolling();
_checkStatus(); // 立即向服务端验证
}
}
void _updateExpiresText() {

View File

@@ -582,19 +582,34 @@ class AppProvider extends ChangeNotifier {
// ========== 标签管理方法 ==========
Future<List<Map<String, dynamic>>> getTags(String type) async {
if (_useRemote) {
return await ServerDataService.instance.getTags(type.isEmpty ? null : type);
}
return await _tagDao.getTagsByType(type);
}
Future<String> addTag(String name, String type) async {
final id = await _tagDao.addTag(name, type);
if (_useRemote) {
await ServerDataService.instance.saveTag(name, type);
}
await _reloadByTagType(type);
return id;
}
Future<bool> renameTag(String tagId, String newName, String type) async {
final tag = await _tagDao.getTagById(tagId);
final oldName = tag?['name'] as String?;
final result = await _tagDao.renameTag(tagId, newName);
if (result) {
await _reloadByTagType(type);
if (_useRemote) {
if (oldName != null) {
await ServerDataService.instance.deleteTagByName(oldName, type);
}
await ServerDataService.instance.saveTag(newName, type);
await _pushAffectedByType(type); // 先推到服务端
}
await _reloadByTagType(type); // 再从服务端拉最新
}
return result;
}
@@ -602,15 +617,38 @@ class AppProvider extends ChangeNotifier {
Future<void> deleteTag(String tagId, String type,
{String? replacementName}) async {
await _tagDao.deleteTag(tagId, replacementName: replacementName);
await _reloadByTagType(type);
if (_useRemote) {
await ServerDataService.instance.deleteTag(tagId);
if (replacementName != null) {
await ServerDataService.instance.saveTag(replacementName, type);
}
await _pushAffectedByType(type); // 先推到服务端
}
await _reloadByTagType(type); // 再从服务端拉最新
}
/// 仅删除标签本身,不级联影响已有条目
Future<void> deleteTagOnly(String tagId, String type) async {
await _tagDao.deleteTagOnly(tagId);
if (_useRemote) {
await ServerDataService.instance.deleteTag(tagId);
}
await _reloadByTagType(type);
}
/// 把某类型的所有条目推送到服务端(标签级联后调用)
Future<void> _pushAffectedByType(String type) async {
final server = ServerDataService.instance;
switch (type) {
case 'movie_genre':
for (final m in _movies) { await server.saveMovie(m); }
case 'book_genre':
for (final b in _books) { await server.saveBook(b); }
case 'note_tag':
for (final n in _notes) { await server.saveNote(n); }
}
}
/// 从影视/书籍/笔记数据中解析标签,同步到 tags 表
Future<int> syncTagsFromData() async {
final db = await DatabaseHelper.instance.database;

View File

@@ -257,6 +257,11 @@ class ServerDataService {
return data != null;
}
Future<bool> deleteTagByName(String name, String type) async {
final data = await _post('/api/data/tag/delete_by_name', {'name': name, 'type': type});
return data != null;
}
// ─── 图片 ────────────────────────────────────────────────────
/// 是否激活AppProvider 也会用这个检查)