From 58bb549217a0dfb8fa404f1e413289820cd58492 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 30 Jul 2024 16:29:30 +0800 Subject: [PATCH] :sparkles: Post content local cache --- ios/Podfile.lock | 7 ++ lib/controllers/post_editor_controller.dart | 102 +++++++++++++++++- lib/screens/posts/post_editor.dart | 56 +++++++++- lib/translations/en_us.dart | 3 + lib/translations/zh_cn.dart | 3 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 56 ++++++++++ pubspec.yaml | 1 + 8 files changed, 228 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 982f754..49d73e0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -145,6 +145,9 @@ PODS: - Sentry/HybridSDK (= 8.32.0) - share_plus (0.0.1): - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - sqflite (0.0.3): - Flutter - FlutterMacOS @@ -179,6 +182,7 @@ DEPENDENCIES: - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) @@ -245,6 +249,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: @@ -289,6 +295,7 @@ SPEC CHECKSUMS: Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe diff --git a/lib/controllers/post_editor_controller.dart b/lib/controllers/post_editor_controller.dart index 68b6aad..71c4558 100644 --- a/lib/controllers/post_editor_controller.dart +++ b/lib/controllers/post_editor_controller.dart @@ -1,12 +1,19 @@ +import 'dart:async'; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/get_rx.dart'; import 'package:solian/models/post.dart'; import 'package:solian/models/realm.dart'; import 'package:solian/widgets/attachments/attachment_publish.dart'; import 'package:solian/widgets/posts/editor/post_editor_overview.dart'; import 'package:textfield_tags/textfield_tags.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class PostEditorController extends GetxController { + late final SharedPreferences _prefs; + final titleController = TextEditingController(); final descriptionController = TextEditingController(); final contentController = TextEditingController(); @@ -23,7 +30,28 @@ class PostEditorController extends GetxController { RxBool isDraft = false.obs; + RxBool isRestoreFromLocal = false.obs; + Rx lastSaveTime = Rx(null); + Timer? _saveTimer; + PostEditorController() { + SharedPreferences.getInstance().then((inst) { + _prefs = inst; + localRead(); + _saveTimer = Timer.periodic( + const Duration(seconds: 3), + (Timer t) { + if (isNotEmpty) { + localSave(); + lastSaveTime.value = DateTime.now(); + lastSaveTime.refresh(); + } else if (_prefs.containsKey('post_editor_local_save')) { + localClear(); + lastSaveTime.value = null; + } + }, + ); + }); contentController.addListener(() { contentLength.value = contentController.text.length; }); @@ -57,6 +85,46 @@ class PostEditorController extends GetxController { isDraft.value = !isDraft.value; } + void localSave() { + _prefs.setString( + 'post_editor_local_save', + jsonEncode({ + ...payload, + 'reply_to': replyTo.value?.toJson(), + 'repost_to': repostTo.value?.toJson(), + 'edit_to': editTo.value?.toJson(), + 'realm': realmZone.value?.toJson(), + }), + ); + } + + void localRead() { + if (_prefs.containsKey('post_editor_local_save')) { + isRestoreFromLocal.value = true; + payload = jsonDecode(_prefs.getString('post_editor_local_save')!); + } + } + + void localClear() { + _prefs.remove('post_editor_local_save'); + } + + void currentClear() { + titleController.clear(); + descriptionController.clear(); + contentController.clear(); + tagController.clearTags(); + attachments.clear(); + isDraft.value = false; + isRestoreFromLocal.value = false; + lastSaveTime.value = null; + contentLength.value = 0; + editTo.value = null; + replyTo.value = null; + repostTo.value = null; + realmZone.value = null; + } + set editTarget(Post? value) { if (value == null) { editTo.value = null; @@ -99,14 +167,46 @@ class PostEditorController extends GetxController { }; } + set payload(Map value) { + titleController.text = value['title'] ?? ''; + descriptionController.text = value['description'] ?? ''; + contentController.text = value['content'] ?? ''; + attachments.value = value['attachments'].cast() ?? List.empty(); + attachments.refresh(); + isDraft.value = value['is_draft']; + if (value['reply_to'] != null) { + replyTo.value = Post.fromJson(value['reply_to']); + } + if (value['repost_to'] != null) { + repostTo.value = Post.fromJson(value['repost_to']); + } + if (value['edit_to'] != null) { + editTo.value = Post.fromJson(value['edit_to']); + } + if (value['realm'] != null) { + realmZone.value = Realm.fromJson(value['realm']); + } + } + bool get isEmpty { if (contentController.text.isEmpty) return true; - return false; } + bool get isNotEmpty { + return [ + titleController.text.isNotEmpty, + descriptionController.text.isNotEmpty, + contentController.text.isNotEmpty, + attachments.isNotEmpty, + tagController.getTags?.isNotEmpty ?? false, + ].any((x) => x); + } + @override void dispose() { + _saveTimer?.cancel(); + contentController.dispose(); tagController.dispose(); super.dispose(); diff --git a/lib/screens/posts/post_editor.dart b/lib/screens/posts/post_editor.dart index 09f5d6d..40138fd 100644 --- a/lib/screens/posts/post_editor.dart +++ b/lib/screens/posts/post_editor.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/post.dart'; @@ -70,6 +71,7 @@ class _PostPublishScreenState extends State { if (resp.statusCode != 200) { context.showErrorDialog(resp.bodyString); } else { + _editorController.localClear(); AppRouter.instance.pop(resp.body); } @@ -234,11 +236,63 @@ class _PostPublishScreenState extends State { ), ), Material( - elevation: 8, color: Theme.of(context).colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Obx(() { + final textStyle = TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.75), + ); + final showFactors = [ + _editorController.isRestoreFromLocal.value, + _editorController.lastSaveTime.value != null, + ]; + final doShow = showFactors.any((x) => x); + return Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 16, + ), + child: Row( + children: [ + if (showFactors[0]) + Text('postRestoreFromLocal'.tr, style: textStyle) + .paddingOnly(right: 4), + if (showFactors[0]) + InkWell( + child: Text('clear'.tr, style: textStyle), + onTap: () { + _editorController.localClear(); + _editorController.currentClear(); + setState(() {}); + }, + ), + if (showFactors.where((x) => x).length > 1) + Text( + '·', + style: textStyle, + ).paddingSymmetric(horizontal: 8), + if (showFactors[1]) + Text( + 'postAutoSaveAt'.trParams({ + 'date': DateFormat('HH:mm:ss').format( + _editorController.lastSaveTime.value ?? + DateTime.now(), + ) + }), + style: textStyle, + ), + ], + ), + ) + .animate(target: doShow ? 1 : 0) + .fade(curve: Curves.easeInOut, duration: 300.ms); + }), if (_editorController.mode.value == 0) Obx( () => TweenAnimationBuilder( diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index c5cc87c..788468f 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -93,8 +93,11 @@ const i18nEnglish = { 'totalPostCount': 'Posts', 'totalUpvote': 'Upvote', 'totalDownvote': 'Downvote', + 'clear': 'Clear', 'pinPost': 'Pin this post', 'unpinPost': 'Unpin this post', + 'postRestoreFromLocal': 'Restore from local', + 'postAutoSaveAt': 'Auto saved at @date', 'postOverview': 'Overview', 'postPinned': 'Pinned', 'postListNews': 'News', diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index 7c9252d..0d17d72 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -87,8 +87,11 @@ const i18nSimplifiedChinese = { 'totalPostCount': '总帖数', 'totalUpvote': '获顶数', 'totalDownvote': '获踩数', + 'clear': '清除', 'pinPost': '置顶本帖', 'unpinPost': '取消置顶本帖', + 'postRestoreFromLocal': '内容从本地暂存回复', + 'postAutoSaveAt': '已自动保存于 @date', 'postOverview': '帖子概览', 'postPinned': '已置顶', 'postEditorModeStory': '发个帖子', diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 545592f..20f3330 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,6 +24,7 @@ import protocol_handler_macos import screen_brightness_macos import sentry_flutter import share_plus +import shared_preferences_foundation import sqflite import url_launcher_macos import wakelock_plus @@ -48,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4f7d900..10760c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1384,6 +1384,62 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "3d4571b3c5eb58ce52a419d86e655493d0bc3020672da79f72fa0c16ca3a8ec1" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1462aa1..0a0f91a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: share_plus: ^10.0.0 flutter_cache_manager: ^3.3.3 flutter_markdown_selectionarea: ^0.6.17+1 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: