generated from dellevin/template
云同步标签部分
This commit is contained in:
@@ -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});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 也会用这个检查)
|
||||
|
||||
Reference in New Issue
Block a user