♻️ Refactor post draft with drift db
This commit is contained in:
parent
2287995cb4
commit
568d70fffb
@ -409,6 +409,7 @@
|
|||||||
"noDrafts": "No drafts yet",
|
"noDrafts": "No drafts yet",
|
||||||
"articleDrafts": "Article drafts",
|
"articleDrafts": "Article drafts",
|
||||||
"postDrafts": "Post drafts",
|
"postDrafts": "Post drafts",
|
||||||
|
"saveDraft": "Save draft",
|
||||||
"clearAllDrafts": "Clear All Drafts",
|
"clearAllDrafts": "Clear All Drafts",
|
||||||
"clearAllDraftsConfirm": "Are you sure you want to delete all drafts? This action cannot be undone.",
|
"clearAllDraftsConfirm": "Are you sure you want to delete all drafts? This action cannot be undone.",
|
||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
|
26
lib/database/draft.dart
Normal file
26
lib/database/draft.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
class ComposeDrafts extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get title => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get description => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get content => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get attachmentIds => text().withDefault(const Constant('[]'))(); // JSON array as string
|
||||||
|
TextColumn get visibility => text().withDefault(const Constant('public'))();
|
||||||
|
DateTimeColumn get lastModified => dateTime()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleDrafts extends Table {
|
||||||
|
TextColumn get id => text()();
|
||||||
|
TextColumn get title => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get description => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get content => text().withDefault(const Constant(''))();
|
||||||
|
TextColumn get visibility => text().withDefault(const Constant('public'))();
|
||||||
|
DateTimeColumn get lastModified => dateTime()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
@ -1,16 +1,17 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
|
import 'package:island/database/draft.dart';
|
||||||
|
|
||||||
part 'drift_db.g.dart';
|
part 'drift_db.g.dart';
|
||||||
|
|
||||||
// Define the database
|
// Define the database
|
||||||
@DriftDatabase(tables: [ChatMessages])
|
@DriftDatabase(tables: [ChatMessages, ComposeDrafts, ArticleDrafts])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@ -22,6 +23,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
// Add isRead column with default value false
|
// Add isRead column with default value false
|
||||||
await m.addColumn(chatMessages, chatMessages.isRead);
|
await m.addColumn(chatMessages, chatMessages.isRead);
|
||||||
}
|
}
|
||||||
|
if (from < 3) {
|
||||||
|
// Add draft tables
|
||||||
|
await m.createTable(composeDrafts);
|
||||||
|
await m.createTable(articleDrafts);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,4 +97,52 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
isRead: dbMessage.isRead,
|
isRead: dbMessage.isRead,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods for compose drafts
|
||||||
|
Future<List<ComposeDraft>> getAllComposeDrafts() {
|
||||||
|
return (select(composeDrafts)
|
||||||
|
..orderBy([(d) => OrderingTerm.desc(d.lastModified)]))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ComposeDraft?> getComposeDraft(String id) {
|
||||||
|
return (select(composeDrafts)..where((d) => d.id.equals(id)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> saveComposeDraft(ComposeDraftsCompanion draft) {
|
||||||
|
return into(composeDrafts).insert(draft, mode: InsertMode.insertOrReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteComposeDraft(String id) {
|
||||||
|
return (delete(composeDrafts)..where((d) => d.id.equals(id))).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> clearAllComposeDrafts() {
|
||||||
|
return delete(composeDrafts).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods for article drafts
|
||||||
|
Future<List<ArticleDraft>> getAllArticleDrafts() {
|
||||||
|
return (select(articleDrafts)
|
||||||
|
..orderBy([(d) => OrderingTerm.desc(d.lastModified)]))
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ArticleDraft?> getArticleDraft(String id) {
|
||||||
|
return (select(articleDrafts)..where((d) => d.id.equals(id)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> saveArticleDraft(ArticleDraftsCompanion draft) {
|
||||||
|
return into(articleDrafts).insert(draft, mode: InsertMode.insertOrReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> deleteArticleDraft(String id) {
|
||||||
|
return (delete(articleDrafts)..where((d) => d.id.equals(id))).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> clearAllArticleDrafts() {
|
||||||
|
return delete(articleDrafts).go();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ import 'package:island/widgets/post/post_item.dart';
|
|||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
import 'package:island/screens/posts/detail.dart';
|
import 'package:island/screens/posts/detail.dart';
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
import 'package:island/services/compose_storage.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/post/draft_manager.dart';
|
import 'package:island/widgets/post/draft_manager.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@ -65,11 +65,16 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
// Helper method to parse visibility
|
// Helper method to parse visibility
|
||||||
int _parseVisibility(String visibility) {
|
int _parseVisibility(String visibility) {
|
||||||
switch (visibility) {
|
switch (visibility) {
|
||||||
case 'public': return 0;
|
case 'public':
|
||||||
case 'unlisted': return 1;
|
return 0;
|
||||||
case 'friends': return 2;
|
case 'unlisted':
|
||||||
case 'private': return 3;
|
return 1;
|
||||||
default: return 0;
|
case 'friends':
|
||||||
|
return 2;
|
||||||
|
case 'private':
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +108,8 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Start auto-save when component mounts
|
// Start auto-save when component mounts
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (originalPost == null) { // Only auto-save for new posts, not edits
|
if (originalPost == null) {
|
||||||
|
// Only auto-save for new posts, not edits
|
||||||
state.startAutoSave(ref);
|
state.startAutoSave(ref);
|
||||||
}
|
}
|
||||||
return () => state.stopAutoSave();
|
return () => state.stopAutoSave();
|
||||||
@ -119,19 +125,24 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Load draft if available (only for new posts)
|
// Load draft if available (only for new posts)
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (originalPost == null && effectiveForwardedPost == null && effectiveRepliedPost == null) {
|
if (originalPost == null &&
|
||||||
|
effectiveForwardedPost == null &&
|
||||||
|
effectiveRepliedPost == null) {
|
||||||
// Try to load the most recent draft
|
// Try to load the most recent draft
|
||||||
final drafts = ref.read(composeStorageNotifierProvider);
|
final drafts = ref.read(composeStorageNotifierProvider);
|
||||||
if (drafts.isNotEmpty) {
|
if (drafts.isNotEmpty) {
|
||||||
final mostRecentDraft = drafts.values.reduce((a, b) =>
|
final mostRecentDraft = drafts.values.reduce(
|
||||||
a.lastModified.isAfter(b.lastModified) ? a : b);
|
(a, b) => a.lastModified.isAfter(b.lastModified) ? a : b,
|
||||||
|
);
|
||||||
|
|
||||||
// Only load if the draft has meaningful content
|
// Only load if the draft has meaningful content
|
||||||
if (!mostRecentDraft.isEmpty) {
|
if (!mostRecentDraft.isEmpty) {
|
||||||
state.titleController.text = mostRecentDraft.title;
|
state.titleController.text = mostRecentDraft.title;
|
||||||
state.descriptionController.text = mostRecentDraft.description;
|
state.descriptionController.text = mostRecentDraft.description;
|
||||||
state.contentController.text = mostRecentDraft.content;
|
state.contentController.text = mostRecentDraft.content;
|
||||||
state.visibility.value = _parseVisibility(mostRecentDraft.visibility);
|
state.visibility.value = _parseVisibility(
|
||||||
|
mostRecentDraft.visibility,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,12 +152,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
// Dispose state when widget is disposed
|
// Dispose state when widget is disposed
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
return () {
|
return () {
|
||||||
// Stop auto-save first to prevent race conditions
|
|
||||||
state.stopAutoSave();
|
state.stopAutoSave();
|
||||||
// Save final draft before disposing
|
|
||||||
if (originalPost == null) {
|
|
||||||
ComposeLogic.saveDraft(ref, state);
|
|
||||||
}
|
|
||||||
ComposeLogic.dispose(state);
|
ComposeLogic.dispose(state);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@ -235,7 +241,13 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build UI
|
// Build UI
|
||||||
return AppScaffold(
|
return PopScope(
|
||||||
|
onPopInvoked: (_) {
|
||||||
|
if (originalPost == null) {
|
||||||
|
ComposeLogic.saveDraft(ref, state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AppScaffold(
|
||||||
noBackground: false,
|
noBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
@ -247,15 +259,22 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => DraftManagerSheet(
|
builder:
|
||||||
|
(context) => DraftManagerSheet(
|
||||||
isArticle: false,
|
isArticle: false,
|
||||||
onDraftSelected: (draftId) {
|
onDraftSelected: (draftId) {
|
||||||
final draft = ref.read(composeStorageNotifierProvider)[draftId];
|
final draft =
|
||||||
|
ref.read(
|
||||||
|
composeStorageNotifierProvider,
|
||||||
|
)[draftId];
|
||||||
if (draft != null) {
|
if (draft != null) {
|
||||||
state.titleController.text = draft.title;
|
state.titleController.text = draft.title;
|
||||||
state.descriptionController.text = draft.description;
|
state.descriptionController.text =
|
||||||
|
draft.description;
|
||||||
state.contentController.text = draft.content;
|
state.contentController.text = draft.content;
|
||||||
state.visibility.value = _parseVisibility(draft.visibility);
|
state.visibility.value = _parseVisibility(
|
||||||
|
draft.visibility,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -263,6 +282,11 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
tooltip: 'drafts'.tr(),
|
tooltip: 'drafts'.tr(),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
onPressed: () => ComposeLogic.saveDraft(ref, state),
|
||||||
|
tooltip: 'saveDraft'.tr(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.settings),
|
icon: const Icon(Symbols.settings),
|
||||||
onPressed: showSettingsSheet,
|
onPressed: showSettingsSheet,
|
||||||
@ -295,7 +319,9 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
).center()
|
).center()
|
||||||
: Icon(
|
: Icon(
|
||||||
originalPost != null ? Symbols.edit : Symbols.upload,
|
originalPost != null
|
||||||
|
? Symbols.edit
|
||||||
|
: Symbols.upload,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -429,6 +455,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:developer';
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -17,8 +18,8 @@ import 'package:island/widgets/content/cloud_files.dart';
|
|||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||||
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
import 'package:island/services/compose_storage.dart';
|
|
||||||
import 'package:island/widgets/post/draft_manager.dart';
|
import 'package:island/widgets/post/draft_manager.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@ -74,10 +75,13 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return () {
|
return () {
|
||||||
// Save final draft before cancelling timer
|
// Stop auto-save first to prevent race conditions
|
||||||
|
state.stopAutoSave();
|
||||||
|
// Save final draft before disposing
|
||||||
if (originalPost == null) {
|
if (originalPost == null) {
|
||||||
_saveArticleDraft(ref, state);
|
_saveArticleDraft(ref, state);
|
||||||
}
|
}
|
||||||
|
ComposeLogic.dispose(state);
|
||||||
autoSaveTimer?.cancel();
|
autoSaveTimer?.cancel();
|
||||||
};
|
};
|
||||||
}, [state]);
|
}, [state]);
|
||||||
@ -116,13 +120,10 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Dispose state when widget is disposed
|
// Auto-save cleanup
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
return () {
|
return () {
|
||||||
// Save final draft before disposing
|
state.stopAutoSave();
|
||||||
if (originalPost == null) {
|
|
||||||
_saveArticleDraft(ref, state);
|
|
||||||
}
|
|
||||||
ComposeLogic.dispose(state);
|
ComposeLogic.dispose(state);
|
||||||
};
|
};
|
||||||
}, [state]);
|
}, [state]);
|
||||||
@ -273,13 +274,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
child: RawKeyboardListener(
|
child: RawKeyboardListener(
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
onKey:
|
onKey:
|
||||||
(event) => ComposeLogic.handleKeyPress(
|
(event) => _handleKeyPress(
|
||||||
event,
|
event,
|
||||||
state,
|
state,
|
||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
originalPost: originalPost,
|
originalPost: originalPost,
|
||||||
postType: 1, // Article type
|
|
||||||
),
|
),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: state.contentController,
|
controller: state.contentController,
|
||||||
@ -355,7 +355,13 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return PopScope(
|
||||||
|
onPopInvoked: (_) {
|
||||||
|
if (originalPost == null) {
|
||||||
|
_saveArticleDraft(ref, state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AppScaffold(
|
||||||
noBackground: false,
|
noBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
@ -382,7 +388,9 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
isArticle: true,
|
isArticle: true,
|
||||||
onDraftSelected: (draftId) {
|
onDraftSelected: (draftId) {
|
||||||
final draft =
|
final draft =
|
||||||
ref.read(articleStorageNotifierProvider)[draftId];
|
ref.read(
|
||||||
|
articleStorageNotifierProvider,
|
||||||
|
)[draftId];
|
||||||
if (draft != null) {
|
if (draft != null) {
|
||||||
state.titleController.text = draft.title;
|
state.titleController.text = draft.title;
|
||||||
state.descriptionController.text =
|
state.descriptionController.text =
|
||||||
@ -398,6 +406,11 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
tooltip: 'drafts'.tr(),
|
tooltip: 'drafts'.tr(),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
onPressed: () => _saveArticleDraft(ref, state),
|
||||||
|
tooltip: 'saveDraft'.tr(),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.settings),
|
icon: const Icon(Symbols.settings),
|
||||||
onPressed: showSettingsSheet,
|
onPressed: showSettingsSheet,
|
||||||
@ -435,7 +448,9 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
).center()
|
).center()
|
||||||
: Icon(
|
: Icon(
|
||||||
originalPost != null ? Symbols.edit : Symbols.upload,
|
originalPost != null
|
||||||
|
? Symbols.edit
|
||||||
|
: Symbols.upload,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -491,13 +506,44 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to handle keyboard shortcuts
|
||||||
|
void _handleKeyPress(
|
||||||
|
RawKeyEvent event,
|
||||||
|
ComposeState state,
|
||||||
|
WidgetRef ref,
|
||||||
|
BuildContext context, {
|
||||||
|
SnPost? originalPost,
|
||||||
|
}) {
|
||||||
|
if (event is! RawKeyDownEvent) return;
|
||||||
|
|
||||||
|
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||||
|
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
|
||||||
|
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||||
|
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
|
if (isPaste && isModifierPressed) {
|
||||||
|
ComposeLogic.handlePaste(state);
|
||||||
|
} else if (isSave && isModifierPressed) {
|
||||||
|
_saveArticleDraft(ref, state);
|
||||||
|
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
||||||
|
ComposeLogic.performAction(
|
||||||
|
ref,
|
||||||
|
state,
|
||||||
|
context,
|
||||||
|
originalPost: originalPost,
|
||||||
|
postType: 1, // Article type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to save article draft
|
// Helper method to save article draft
|
||||||
Future<void> _saveArticleDraft(WidgetRef ref, ComposeState state) async {
|
Future<void> _saveArticleDraft(WidgetRef ref, ComposeState state) async {
|
||||||
try {
|
try {
|
||||||
final draft = ArticleDraft(
|
final draft = ArticleDraftModel(
|
||||||
id: state.draftId,
|
id: state.draftId,
|
||||||
title: state.titleController.text,
|
title: state.titleController.text,
|
||||||
description: state.descriptionController.text,
|
description: state.descriptionController.text,
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import 'package:island/pods/config.dart';
|
|
||||||
|
|
||||||
part 'compose_storage.g.dart';
|
|
||||||
|
|
||||||
const kComposeDraftStoreKey = 'compose_drafts';
|
|
||||||
const kArticleDraftStoreKey = 'article_drafts';
|
|
||||||
|
|
||||||
class ComposeDraft {
|
|
||||||
final String id;
|
|
||||||
final String title;
|
|
||||||
final String description;
|
|
||||||
final String content;
|
|
||||||
final List<String> attachmentIds;
|
|
||||||
final String visibility;
|
|
||||||
final DateTime lastModified;
|
|
||||||
|
|
||||||
ComposeDraft({
|
|
||||||
required this.id,
|
|
||||||
required this.title,
|
|
||||||
required this.description,
|
|
||||||
required this.content,
|
|
||||||
required this.attachmentIds,
|
|
||||||
required this.visibility,
|
|
||||||
required this.lastModified,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'id': id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'content': content,
|
|
||||||
'attachmentIds': attachmentIds,
|
|
||||||
'visibility': visibility,
|
|
||||||
'lastModified': lastModified.toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
||||||
factory ComposeDraft.fromJson(Map<String, dynamic> json) => ComposeDraft(
|
|
||||||
id: json['id'] as String,
|
|
||||||
title: json['title'] as String? ?? '',
|
|
||||||
description: json['description'] as String? ?? '',
|
|
||||||
content: json['content'] as String? ?? '',
|
|
||||||
attachmentIds: List<String>.from(json['attachmentIds'] as List? ?? []),
|
|
||||||
visibility: json['visibility'] as String? ?? 'public',
|
|
||||||
lastModified: DateTime.parse(json['lastModified'] as String),
|
|
||||||
);
|
|
||||||
|
|
||||||
ComposeDraft copyWith({
|
|
||||||
String? id,
|
|
||||||
String? title,
|
|
||||||
String? description,
|
|
||||||
String? content,
|
|
||||||
List<String>? attachmentIds,
|
|
||||||
String? visibility,
|
|
||||||
DateTime? lastModified,
|
|
||||||
}) {
|
|
||||||
return ComposeDraft(
|
|
||||||
id: id ?? this.id,
|
|
||||||
title: title ?? this.title,
|
|
||||||
description: description ?? this.description,
|
|
||||||
content: content ?? this.content,
|
|
||||||
attachmentIds: attachmentIds ?? this.attachmentIds,
|
|
||||||
visibility: visibility ?? this.visibility,
|
|
||||||
lastModified: lastModified ?? this.lastModified,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isEmpty =>
|
|
||||||
title.isEmpty &&
|
|
||||||
description.isEmpty &&
|
|
||||||
content.isEmpty &&
|
|
||||||
attachmentIds.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArticleDraft {
|
|
||||||
final String id;
|
|
||||||
final String title;
|
|
||||||
final String description;
|
|
||||||
final String content;
|
|
||||||
final String visibility;
|
|
||||||
final DateTime lastModified;
|
|
||||||
|
|
||||||
ArticleDraft({
|
|
||||||
required this.id,
|
|
||||||
required this.title,
|
|
||||||
required this.description,
|
|
||||||
required this.content,
|
|
||||||
required this.visibility,
|
|
||||||
required this.lastModified,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'id': id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'content': content,
|
|
||||||
'visibility': visibility,
|
|
||||||
'lastModified': lastModified.toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
||||||
factory ArticleDraft.fromJson(Map<String, dynamic> json) => ArticleDraft(
|
|
||||||
id: json['id'] as String,
|
|
||||||
title: json['title'] as String? ?? '',
|
|
||||||
description: json['description'] as String? ?? '',
|
|
||||||
content: json['content'] as String? ?? '',
|
|
||||||
visibility: json['visibility'] as String? ?? 'public',
|
|
||||||
lastModified: DateTime.parse(json['lastModified'] as String),
|
|
||||||
);
|
|
||||||
|
|
||||||
ArticleDraft copyWith({
|
|
||||||
String? id,
|
|
||||||
String? title,
|
|
||||||
String? description,
|
|
||||||
String? content,
|
|
||||||
String? visibility,
|
|
||||||
DateTime? lastModified,
|
|
||||||
}) {
|
|
||||||
return ArticleDraft(
|
|
||||||
id: id ?? this.id,
|
|
||||||
title: title ?? this.title,
|
|
||||||
description: description ?? this.description,
|
|
||||||
content: content ?? this.content,
|
|
||||||
visibility: visibility ?? this.visibility,
|
|
||||||
lastModified: lastModified ?? this.lastModified,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isEmpty => title.isEmpty && description.isEmpty && content.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
|
||||||
@override
|
|
||||||
Map<String, ComposeDraft> build() {
|
|
||||||
_loadDrafts();
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadDrafts() {
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
final draftsJson = prefs.getString(kComposeDraftStoreKey);
|
|
||||||
if (draftsJson != null) {
|
|
||||||
try {
|
|
||||||
final Map<String, dynamic> draftsMap = jsonDecode(draftsJson);
|
|
||||||
final drafts = <String, ComposeDraft>{};
|
|
||||||
for (final entry in draftsMap.entries) {
|
|
||||||
drafts[entry.key] = ComposeDraft.fromJson(entry.value);
|
|
||||||
}
|
|
||||||
state = drafts;
|
|
||||||
} catch (e) {
|
|
||||||
// If there's an error loading drafts, start with empty state
|
|
||||||
state = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveDrafts() async {
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
final draftsMap = <String, dynamic>{};
|
|
||||||
for (final entry in state.entries) {
|
|
||||||
draftsMap[entry.key] = entry.value.toJson();
|
|
||||||
}
|
|
||||||
await prefs.setString(kComposeDraftStoreKey, jsonEncode(draftsMap));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveDraft(ComposeDraft draft) async {
|
|
||||||
if (draft.isEmpty) {
|
|
||||||
await deleteDraft(draft.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
|
|
||||||
state = {...state, updatedDraft.id: updatedDraft};
|
|
||||||
await _saveDrafts();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteDraft(String id) async {
|
|
||||||
final newState = Map<String, ComposeDraft>.from(state);
|
|
||||||
newState.remove(id);
|
|
||||||
state = newState;
|
|
||||||
await _saveDrafts();
|
|
||||||
}
|
|
||||||
|
|
||||||
ComposeDraft? getDraft(String id) {
|
|
||||||
return state[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ComposeDraft> getAllDrafts() {
|
|
||||||
final drafts = state.values.toList();
|
|
||||||
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
|
||||||
return drafts;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearAllDrafts() async {
|
|
||||||
state = {};
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
await prefs.remove(kComposeDraftStoreKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
class ArticleStorageNotifier extends _$ArticleStorageNotifier {
|
|
||||||
@override
|
|
||||||
Map<String, ArticleDraft> build() {
|
|
||||||
_loadDrafts();
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadDrafts() {
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
final draftsJson = prefs.getString(kArticleDraftStoreKey);
|
|
||||||
if (draftsJson != null) {
|
|
||||||
try {
|
|
||||||
final Map<String, dynamic> draftsMap = jsonDecode(draftsJson);
|
|
||||||
final drafts = <String, ArticleDraft>{};
|
|
||||||
for (final entry in draftsMap.entries) {
|
|
||||||
drafts[entry.key] = ArticleDraft.fromJson(entry.value);
|
|
||||||
}
|
|
||||||
state = drafts;
|
|
||||||
} catch (e) {
|
|
||||||
// If there's an error loading drafts, start with empty state
|
|
||||||
state = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveDrafts() async {
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
final draftsMap = <String, dynamic>{};
|
|
||||||
for (final entry in state.entries) {
|
|
||||||
draftsMap[entry.key] = entry.value.toJson();
|
|
||||||
}
|
|
||||||
await prefs.setString(kArticleDraftStoreKey, jsonEncode(draftsMap));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveDraft(ArticleDraft draft) async {
|
|
||||||
if (draft.isEmpty) {
|
|
||||||
await deleteDraft(draft.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
|
|
||||||
state = {...state, updatedDraft.id: updatedDraft};
|
|
||||||
await _saveDrafts();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteDraft(String id) async {
|
|
||||||
final newState = Map<String, ArticleDraft>.from(state);
|
|
||||||
newState.remove(id);
|
|
||||||
state = newState;
|
|
||||||
await _saveDrafts();
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleDraft? getDraft(String id) {
|
|
||||||
return state[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ArticleDraft> getAllDrafts() {
|
|
||||||
final drafts = state.values.toList();
|
|
||||||
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
|
||||||
return drafts;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearAllDrafts() async {
|
|
||||||
state = {};
|
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
|
||||||
await prefs.remove(kArticleDraftStoreKey);
|
|
||||||
}
|
|
||||||
}
|
|
341
lib/services/compose_storage_db.dart
Normal file
341
lib/services/compose_storage_db.dart
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:island/database/drift_db.dart';
|
||||||
|
import 'package:island/pods/database.dart';
|
||||||
|
|
||||||
|
part 'compose_storage_db.g.dart';
|
||||||
|
|
||||||
|
class ComposeDraftModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final String content;
|
||||||
|
final List<String> attachmentIds;
|
||||||
|
final String visibility;
|
||||||
|
final DateTime lastModified;
|
||||||
|
|
||||||
|
ComposeDraftModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.content,
|
||||||
|
required this.attachmentIds,
|
||||||
|
required this.visibility,
|
||||||
|
required this.lastModified,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'content': content,
|
||||||
|
'attachmentIds': attachmentIds,
|
||||||
|
'visibility': visibility,
|
||||||
|
'lastModified': lastModified.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ComposeDraftModel.fromJson(Map<String, dynamic> json) => ComposeDraftModel(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String? ?? '',
|
||||||
|
description: json['description'] as String? ?? '',
|
||||||
|
content: json['content'] as String? ?? '',
|
||||||
|
attachmentIds: List<String>.from(json['attachmentIds'] as List? ?? []),
|
||||||
|
visibility: json['visibility'] as String? ?? 'public',
|
||||||
|
lastModified: DateTime.parse(json['lastModified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ComposeDraftModel.fromDbRow(ComposeDraft row) => ComposeDraftModel(
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
content: row.content,
|
||||||
|
attachmentIds: List<String>.from(jsonDecode(row.attachmentIds)),
|
||||||
|
visibility: row.visibility,
|
||||||
|
lastModified: row.lastModified,
|
||||||
|
);
|
||||||
|
|
||||||
|
ComposeDraftsCompanion toDbCompanion() => ComposeDraftsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
title: Value(title),
|
||||||
|
description: Value(description),
|
||||||
|
content: Value(content),
|
||||||
|
attachmentIds: Value(jsonEncode(attachmentIds)),
|
||||||
|
visibility: Value(visibility),
|
||||||
|
lastModified: Value(lastModified),
|
||||||
|
);
|
||||||
|
|
||||||
|
ComposeDraftModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? content,
|
||||||
|
List<String>? attachmentIds,
|
||||||
|
String? visibility,
|
||||||
|
DateTime? lastModified,
|
||||||
|
}) {
|
||||||
|
return ComposeDraftModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
content: content ?? this.content,
|
||||||
|
attachmentIds: attachmentIds ?? this.attachmentIds,
|
||||||
|
visibility: visibility ?? this.visibility,
|
||||||
|
lastModified: lastModified ?? this.lastModified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isEmpty =>
|
||||||
|
title.isEmpty &&
|
||||||
|
description.isEmpty &&
|
||||||
|
content.isEmpty &&
|
||||||
|
attachmentIds.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleDraftModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final String content;
|
||||||
|
final String visibility;
|
||||||
|
final DateTime lastModified;
|
||||||
|
|
||||||
|
ArticleDraftModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.content,
|
||||||
|
required this.visibility,
|
||||||
|
required this.lastModified,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'content': content,
|
||||||
|
'visibility': visibility,
|
||||||
|
'lastModified': lastModified.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ArticleDraftModel.fromJson(Map<String, dynamic> json) => ArticleDraftModel(
|
||||||
|
id: json['id'] as String,
|
||||||
|
title: json['title'] as String? ?? '',
|
||||||
|
description: json['description'] as String? ?? '',
|
||||||
|
content: json['content'] as String? ?? '',
|
||||||
|
visibility: json['visibility'] as String? ?? 'public',
|
||||||
|
lastModified: DateTime.parse(json['lastModified'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ArticleDraftModel.fromDbRow(ArticleDraft row) => ArticleDraftModel(
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
content: row.content,
|
||||||
|
visibility: row.visibility,
|
||||||
|
lastModified: row.lastModified,
|
||||||
|
);
|
||||||
|
|
||||||
|
ArticleDraftsCompanion toDbCompanion() => ArticleDraftsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
title: Value(title),
|
||||||
|
description: Value(description),
|
||||||
|
content: Value(content),
|
||||||
|
visibility: Value(visibility),
|
||||||
|
lastModified: Value(lastModified),
|
||||||
|
);
|
||||||
|
|
||||||
|
ArticleDraftModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? content,
|
||||||
|
String? visibility,
|
||||||
|
DateTime? lastModified,
|
||||||
|
}) {
|
||||||
|
return ArticleDraftModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
content: content ?? this.content,
|
||||||
|
visibility: visibility ?? this.visibility,
|
||||||
|
lastModified: lastModified ?? this.lastModified,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isEmpty => title.isEmpty && description.isEmpty && content.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||||
|
@override
|
||||||
|
Map<String, ComposeDraftModel> build() {
|
||||||
|
_loadDrafts();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadDrafts() async {
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
final dbDrafts = await database.getAllComposeDrafts();
|
||||||
|
final drafts = <String, ComposeDraftModel>{};
|
||||||
|
for (final dbDraft in dbDrafts) {
|
||||||
|
final draft = ComposeDraftModel.fromDbRow(dbDraft);
|
||||||
|
drafts[draft.id] = draft;
|
||||||
|
}
|
||||||
|
state = drafts;
|
||||||
|
} catch (e) {
|
||||||
|
// If there's an error loading drafts, start with empty state
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveDraft(ComposeDraftModel draft) async {
|
||||||
|
if (draft.isEmpty) {
|
||||||
|
await deleteDraft(draft.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
|
||||||
|
state = {...state, updatedDraft.id: updatedDraft};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.saveComposeDraft(updatedDraft.toDbCompanion());
|
||||||
|
} catch (e) {
|
||||||
|
// Revert state on error
|
||||||
|
final newState = Map<String, ComposeDraftModel>.from(state);
|
||||||
|
newState.remove(updatedDraft.id);
|
||||||
|
state = newState;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteDraft(String id) async {
|
||||||
|
final oldDraft = state[id];
|
||||||
|
final newState = Map<String, ComposeDraftModel>.from(state);
|
||||||
|
newState.remove(id);
|
||||||
|
state = newState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.deleteComposeDraft(id);
|
||||||
|
} catch (e) {
|
||||||
|
// Revert state on error
|
||||||
|
if (oldDraft != null) {
|
||||||
|
state = {...state, id: oldDraft};
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeDraftModel? getDraft(String id) {
|
||||||
|
return state[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ComposeDraftModel> getAllDrafts() {
|
||||||
|
final drafts = state.values.toList();
|
||||||
|
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||||
|
return drafts;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllDrafts() async {
|
||||||
|
state = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.clearAllComposeDrafts();
|
||||||
|
} catch (e) {
|
||||||
|
// If clearing fails, we might want to reload from database
|
||||||
|
_loadDrafts();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class ArticleStorageNotifier extends _$ArticleStorageNotifier {
|
||||||
|
@override
|
||||||
|
Map<String, ArticleDraftModel> build() {
|
||||||
|
_loadDrafts();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadDrafts() async {
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
final dbDrafts = await database.getAllArticleDrafts();
|
||||||
|
final drafts = <String, ArticleDraftModel>{};
|
||||||
|
for (final dbDraft in dbDrafts) {
|
||||||
|
final draft = ArticleDraftModel.fromDbRow(dbDraft);
|
||||||
|
drafts[draft.id] = draft;
|
||||||
|
}
|
||||||
|
state = drafts;
|
||||||
|
} catch (e) {
|
||||||
|
// If there's an error loading drafts, start with empty state
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveDraft(ArticleDraftModel draft) async {
|
||||||
|
if (draft.isEmpty) {
|
||||||
|
await deleteDraft(draft.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
|
||||||
|
state = {...state, updatedDraft.id: updatedDraft};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.saveArticleDraft(updatedDraft.toDbCompanion());
|
||||||
|
} catch (e) {
|
||||||
|
// Revert state on error
|
||||||
|
final newState = Map<String, ArticleDraftModel>.from(state);
|
||||||
|
newState.remove(updatedDraft.id);
|
||||||
|
state = newState;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteDraft(String id) async {
|
||||||
|
final oldDraft = state[id];
|
||||||
|
final newState = Map<String, ArticleDraftModel>.from(state);
|
||||||
|
newState.remove(id);
|
||||||
|
state = newState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.deleteArticleDraft(id);
|
||||||
|
} catch (e) {
|
||||||
|
// Revert state on error
|
||||||
|
if (oldDraft != null) {
|
||||||
|
state = {...state, id: oldDraft};
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticleDraftModel? getDraft(String id) {
|
||||||
|
return state[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ArticleDraftModel> getAllDrafts() {
|
||||||
|
final drafts = state.values.toList();
|
||||||
|
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||||
|
return drafts;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllDrafts() async {
|
||||||
|
state = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.clearAllArticleDrafts();
|
||||||
|
} catch (e) {
|
||||||
|
// If clearing fails, we might want to reload from database
|
||||||
|
_loadDrafts();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,19 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'compose_storage.dart';
|
part of 'compose_storage_db.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$composeStorageNotifierHash() =>
|
String _$composeStorageNotifierHash() =>
|
||||||
r'99c5e4070fa8af2771751064b56ca3251dbda27a';
|
r'57d3812b8fd430e6144f72708c694ddceea34c17';
|
||||||
|
|
||||||
/// See also [ComposeStorageNotifier].
|
/// See also [ComposeStorageNotifier].
|
||||||
@ProviderFor(ComposeStorageNotifier)
|
@ProviderFor(ComposeStorageNotifier)
|
||||||
final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
|
final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
|
||||||
ComposeStorageNotifier,
|
ComposeStorageNotifier,
|
||||||
Map<String, ComposeDraft>
|
Map<String, ComposeDraftModel>
|
||||||
>.internal(
|
>.internal(
|
||||||
ComposeStorageNotifier.new,
|
ComposeStorageNotifier.new,
|
||||||
name: r'composeStorageNotifierProvider',
|
name: r'composeStorageNotifierProvider',
|
||||||
@ -26,15 +26,15 @@ final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ComposeStorageNotifier =
|
typedef _$ComposeStorageNotifier =
|
||||||
AutoDisposeNotifier<Map<String, ComposeDraft>>;
|
AutoDisposeNotifier<Map<String, ComposeDraftModel>>;
|
||||||
String _$articleStorageNotifierHash() =>
|
String _$articleStorageNotifierHash() =>
|
||||||
r'4a200878bfe7881fc3afd2164b334e84dc44f338';
|
r'21ee0f8ee87528bebf8f5f4b0b2892cd8058e230';
|
||||||
|
|
||||||
/// See also [ArticleStorageNotifier].
|
/// See also [ArticleStorageNotifier].
|
||||||
@ProviderFor(ArticleStorageNotifier)
|
@ProviderFor(ArticleStorageNotifier)
|
||||||
final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
|
final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
|
||||||
ArticleStorageNotifier,
|
ArticleStorageNotifier,
|
||||||
Map<String, ArticleDraft>
|
Map<String, ArticleDraftModel>
|
||||||
>.internal(
|
>.internal(
|
||||||
ArticleStorageNotifier.new,
|
ArticleStorageNotifier.new,
|
||||||
name: r'articleStorageNotifierProvider',
|
name: r'articleStorageNotifierProvider',
|
||||||
@ -47,6 +47,6 @@ final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ArticleStorageNotifier =
|
typedef _$ArticleStorageNotifier =
|
||||||
AutoDisposeNotifier<Map<String, ArticleDraft>>;
|
AutoDisposeNotifier<Map<String, ArticleDraftModel>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@ -11,7 +11,7 @@ import 'package:island/models/post.dart';
|
|||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/file.dart';
|
import 'package:island/services/file.dart';
|
||||||
import 'package:island/services/compose_storage.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -96,7 +96,7 @@ class ComposeLogic {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ComposeState createStateFromDraft(ComposeDraft draft) {
|
static ComposeState createStateFromDraft(ComposeDraftModel draft) {
|
||||||
return ComposeState(
|
return ComposeState(
|
||||||
attachments: ValueNotifier<List<UniversalFile>>([]),
|
attachments: ValueNotifier<List<UniversalFile>>([]),
|
||||||
titleController: TextEditingController(text: draft.title),
|
titleController: TextEditingController(text: draft.title),
|
||||||
@ -151,7 +151,7 @@ class ComposeLogic {
|
|||||||
return; // Widget has been disposed, don't save
|
return; // Widget has been disposed, don't save
|
||||||
}
|
}
|
||||||
|
|
||||||
final draft = ComposeDraft(
|
final draft = ComposeDraftModel(
|
||||||
id: state.draftId,
|
id: state.draftId,
|
||||||
title: state.titleController.text,
|
title: state.titleController.text,
|
||||||
description: state.descriptionController.text,
|
description: state.descriptionController.text,
|
||||||
@ -182,7 +182,7 @@ class ComposeLogic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<ComposeDraft?> loadDraft(WidgetRef ref, String draftId) async {
|
static Future<ComposeDraftModel?> loadDraft(WidgetRef ref, String draftId) async {
|
||||||
try {
|
try {
|
||||||
return ref
|
return ref
|
||||||
.read(composeStorageNotifierProvider.notifier)
|
.read(composeStorageNotifierProvider.notifier)
|
||||||
@ -410,11 +410,14 @@ class ComposeLogic {
|
|||||||
if (event is! RawKeyDownEvent) return;
|
if (event is! RawKeyDownEvent) return;
|
||||||
|
|
||||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||||
|
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
|
||||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||||
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
|
final isSubmit = event.logicalKey == LogicalKeyboardKey.enter;
|
||||||
|
|
||||||
if (isPaste && isModifierPressed) {
|
if (isPaste && isModifierPressed) {
|
||||||
handlePaste(state);
|
handlePaste(state);
|
||||||
|
} else if (isSave && isModifierPressed) {
|
||||||
|
saveDraft(ref, state);
|
||||||
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
||||||
performAction(
|
performAction(
|
||||||
ref,
|
ref,
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/services/compose_storage.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
class DraftManagerSheet extends HookConsumerWidget {
|
class DraftManagerSheet extends HookConsumerWidget {
|
||||||
@ -28,11 +28,11 @@ class DraftManagerSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final sortedDrafts = useMemoized(() {
|
final sortedDrafts = useMemoized(() {
|
||||||
if (isArticle) {
|
if (isArticle) {
|
||||||
final draftList = drafts.values.cast<ArticleDraft>().toList();
|
final draftList = drafts.values.cast<ArticleDraftModel>().toList();
|
||||||
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||||
return draftList;
|
return draftList;
|
||||||
} else {
|
} else {
|
||||||
final draftList = drafts.values.cast<ComposeDraft>().toList();
|
final draftList = drafts.values.cast<ComposeDraftModel>().toList();
|
||||||
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||||
return draftList;
|
return draftList;
|
||||||
}
|
}
|
||||||
@ -79,15 +79,15 @@ class DraftManagerSheet extends HookConsumerWidget {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
final draftId =
|
final draftId =
|
||||||
isArticle
|
isArticle
|
||||||
? (draft as ArticleDraft).id
|
? (draft as ArticleDraftModel).id
|
||||||
: (draft as ComposeDraft).id;
|
: (draft as ComposeDraftModel).id;
|
||||||
onDraftSelected?.call(draftId);
|
onDraftSelected?.call(draftId);
|
||||||
},
|
},
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
final draftId =
|
final draftId =
|
||||||
isArticle
|
isArticle
|
||||||
? (draft as ArticleDraft).id
|
? (draft as ArticleDraftModel).id
|
||||||
: (draft as ComposeDraft).id;
|
: (draft as ComposeDraftModel).id;
|
||||||
if (isArticle) {
|
if (isArticle) {
|
||||||
await ref
|
await ref
|
||||||
.read(articleStorageNotifierProvider.notifier)
|
.read(articleStorageNotifierProvider.notifier)
|
||||||
@ -182,7 +182,7 @@ class _DraftItem extends StatelessWidget {
|
|||||||
final String visibility;
|
final String visibility;
|
||||||
|
|
||||||
if (isArticle) {
|
if (isArticle) {
|
||||||
final articleDraft = draft as ArticleDraft;
|
final articleDraft = draft as ArticleDraftModel;
|
||||||
title =
|
title =
|
||||||
articleDraft.title.isNotEmpty ? articleDraft.title : 'untitled'.tr();
|
articleDraft.title.isNotEmpty ? articleDraft.title : 'untitled'.tr();
|
||||||
content =
|
content =
|
||||||
@ -194,7 +194,7 @@ class _DraftItem extends StatelessWidget {
|
|||||||
lastModified = articleDraft.lastModified;
|
lastModified = articleDraft.lastModified;
|
||||||
visibility = _parseArticleVisibility(articleDraft.visibility);
|
visibility = _parseArticleVisibility(articleDraft.visibility);
|
||||||
} else {
|
} else {
|
||||||
final postDraft = draft as ComposeDraft;
|
final postDraft = draft as ComposeDraftModel;
|
||||||
title = postDraft.title.isNotEmpty ? postDraft.title : 'untitled'.tr();
|
title = postDraft.title.isNotEmpty ? postDraft.title : 'untitled'.tr();
|
||||||
content =
|
content =
|
||||||
postDraft.content.isNotEmpty
|
postDraft.content.isNotEmpty
|
||||||
|
Loading…
x
Reference in New Issue
Block a user