diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index e79f049..a55f58b 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -467,6 +467,7 @@ "accountStatusLastSeen": "Last seen at {}", "postArticle": "Article on the Solar Network", "postStory": "Story on the Solar Network", + "postLocalDraftRestored": "Restored from device", "articleWrittenAt": "Written at {}", "articleEditedAt": "Edited at {}", "attachmentSaved": "Saved to album", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 6b37e74..4cfd3fb 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -465,6 +465,7 @@ "accountStatusLastSeen": "最后一次上线于 {}", "postArticle": "Solar Network 上的文章", "postStory": "Solar Network 上的故事", + "postLocalDraftRestored": "从本地恢复草稿", "articleWrittenAt": "发表于 {}", "articleEditedAt": "编辑于 {}", "attachmentSaved": "已保存到相册", diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index e81be62..b2eb7bc 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -456,6 +456,7 @@ "accountJoinedAt": "加入於 {}", "accountBirthday": "出生於 {}", "accountBadge": "徽章", + "accountCheckInNoRecords": "暫無運勢記錄", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeSiteMigration": "Solar Network 原住民", "accountStatus": "狀態", @@ -464,6 +465,7 @@ "accountStatusLastSeen": "最後一次上線於 {}", "postArticle": "Solar Network 上的文章", "postStory": "Solar Network 上的故事", + "postLocalDraftRestored": "從本地恢復草稿", "articleWrittenAt": "發表於 {}", "articleEditedAt": "編輯於 {}", "attachmentSaved": "已保存到相冊", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index ec2f8ce..9a90654 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -456,6 +456,7 @@ "accountJoinedAt": "加入於 {}", "accountBirthday": "出生於 {}", "accountBadge": "徽章", + "accountCheckInNoRecords": "暫無運勢記錄", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeSiteMigration": "Solar Network 原住民", "accountStatus": "狀態", @@ -464,6 +465,7 @@ "accountStatusLastSeen": "最後一次上線於 {}", "postArticle": "Solar Network 上的文章", "postStory": "Solar Network 上的故事", + "postLocalDraftRestored": "從本地恢復草稿", "articleWrittenAt": "發表於 {}", "articleEditedAt": "編輯於 {}", "attachmentSaved": "已保存到相冊", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4531c84..cad6d16 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -211,9 +211,6 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter @@ -259,7 +256,6 @@ DEPENDENCIES: - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) @@ -347,8 +343,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_compress: @@ -407,7 +401,6 @@ SPEC CHECKSUMS: SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index 6949ce2..3f1329a 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'dart:math' as math; @@ -8,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:surface/providers/post.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; @@ -151,8 +155,18 @@ class PostWriteController extends ChangeNotifier { final TextEditingController aliasController = TextEditingController(); PostWriteController() { - titleController.addListener(() => notifyListeners()); - descriptionController.addListener(() => notifyListeners()); + titleController.addListener(() { + _temporaryPlanSave(); + notifyListeners(); + }); + descriptionController.addListener(() { + _temporaryPlanSave(); + notifyListeners(); + }); + contentController.addListener(() { + _temporaryPlanSave(); + }); + _temporaryLoad(); } String mode = kTitleMap.keys.first; @@ -298,6 +312,81 @@ class PostWriteController extends ChangeNotifier { return compressedAttachment; } + static const kTemporaryStorageKey = 'int_draft_post'; + + Timer? _temporarySaveTimer; + + void _temporaryPlanSave() { + _temporarySaveTimer?.cancel(); + _temporarySaveTimer = Timer(const Duration(seconds: 1), () { + _temporarySave(); + log("[PostWriter] Temporary save saved."); + }); + } + + void _temporarySave() { + SharedPreferences.getInstance().then((prefs) { + if (titleController.text.isEmpty && + descriptionController.text.isEmpty && + contentController.text.isEmpty && + thumbnail == null && + attachments.isEmpty) { + prefs.remove(kTemporaryStorageKey); + return; + } + + prefs.setString( + kTemporaryStorageKey, + jsonEncode({ + 'publisher': publisher, + 'content': contentController.text, + if (aliasController.text.isNotEmpty) 'alias': aliasController.text, + if (titleController.text.isNotEmpty) 'title': titleController.text, + if (descriptionController.text.isNotEmpty) 'description': descriptionController.text, + if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(), + 'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(), + 'tags': tags.map((ele) => {'alias': ele}).toList(), + 'categories': categories.map((ele) => {'alias': ele}).toList(), + 'visibility': visibility, + 'visible_users_list': visibleUsers, + 'invisible_users_list': invisibleUsers, + if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(), + if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(), + if (replyingPost != null) 'reply_to': replyingPost!.toJson(), + if (repostingPost != null) 'repost_to': repostingPost!.toJson(), + }), + ); + }); + } + + bool temporaryRestored = false; + + void _temporaryLoad() { + SharedPreferences.getInstance().then((prefs) { + final raw = prefs.getString(kTemporaryStorageKey); + if (raw == null) return; + final data = jsonDecode(raw); + contentController.text = data['content']; + aliasController.text = data['alias'] ?? ''; + titleController.text = data['title'] ?? ''; + descriptionController.text = data['description'] ?? ''; + if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail'])); + attachments + .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast()); + tags = List.from(data['tags'].map((ele) => ele['alias'])); + categories = List.from(data['categories'].map((ele) => ele['alias'])); + visibility = data['visibility']; + visibleUsers = List.from(data['visible_users_list'] ?? []); + invisibleUsers = List.from(data['invisible_users_list'] ?? []); + if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal(); + if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal(); + replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null; + repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null; + temporaryRestored = true; + notifyListeners(); + }); + } + Future uploadSingleAttachment(BuildContext context, int idx) async { if (isBusy) return; @@ -417,6 +506,7 @@ class PostWriteController extends ChangeNotifier { method: editingPost != null ? 'PUT' : 'POST', ), ); + reset(); } catch (err) { if (!context.mounted) return; context.showErrorDialog(err); @@ -465,56 +555,67 @@ class PostWriteController extends ChangeNotifier { void setPublisher(SnPublisher? item) { publisher = item; + _temporaryPlanSave(); notifyListeners(); } void setPublishedAt(DateTime? value) { publishedAt = value; + _temporaryPlanSave(); notifyListeners(); } void setPublishedUntil(DateTime? value) { publishedUntil = value; + _temporaryPlanSave(); notifyListeners(); } void setTags(List value) { tags = value; + _temporaryPlanSave(); notifyListeners(); } void setCategories(List value) { categories = value; + _temporaryPlanSave(); notifyListeners(); } void setVisibility(int value) { visibility = value; + _temporaryPlanSave(); notifyListeners(); } void setVisibleUsers(List value) { visibleUsers = value; + _temporaryPlanSave(); notifyListeners(); } void setInvisibleUsers(List value) { invisibleUsers = value; + _temporaryPlanSave(); notifyListeners(); } void setProgress(double? value) { progress = value; + _temporaryPlanSave(); notifyListeners(); } void setIsBusy(bool value) { isBusy = value; + _temporaryPlanSave(); notifyListeners(); } void setMode(String value) { mode = value; + _temporaryPlanSave(); notifyListeners(); } @@ -532,6 +633,8 @@ class PostWriteController extends ChangeNotifier { replyingPost = null; repostingPost = null; mode = kTitleMap.keys.first; + temporaryRestored = false; + SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey)); notifyListeners(); } diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 16225e8..1474693 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -364,6 +364,35 @@ class _PostEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1 / MediaQuery.of(context).devicePixelRatio, + ), + ), + ), + child: _writeController.temporaryRestored + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.restore, size: 20), + const Gap(8), + Expanded(child: Text('postLocalDraftRestored').tr()), + InkWell( + child: Text('dialogDismiss').tr(), + onTap: () { + _writeController.reset(); + }, + ), + ], + ) + : const SizedBox.shrink(), + ) + .height(_writeController.temporaryRestored ? 32 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), LoadingIndicator(isActive: _isLoading), if (_writeController.isBusy && _writeController.progress != null) TweenAnimationBuilder(