From aa648fec620852fac1b90eff858b8dc35413d3d8 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 8 Sep 2025 22:25:54 +0800 Subject: [PATCH] :sparkles: Reworked post draft --- assets/i18n/en-US.json | 2 + lib/database/draft.dart | 9 +- lib/database/drift_db.dart | 96 ++++- lib/database/drift_db.g.dart | 467 ++++++++++++++++++++++--- lib/screens/posts/compose_article.dart | 50 +-- lib/services/compose_storage_db.dart | 7 +- lib/services/compose_storage_db.g.dart | 2 +- lib/widgets/post/compose_shared.dart | 8 - lib/widgets/post/compose_toolbar.dart | 2 +- lib/widgets/post/draft_manager.dart | 245 ++++++------- 10 files changed, 651 insertions(+), 237 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 36102ecb..6c5fc55c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -471,6 +471,8 @@ "close": "Close", "drafts": "Drafts", "noDrafts": "No drafts yet", + "searchDrafts": "Search drafts...", + "noSearchResults": "No search results", "articleDrafts": "Article drafts", "postDrafts": "Post drafts", "saveDraft": "Save draft", diff --git a/lib/database/draft.dart b/lib/database/draft.dart index e0fe29d7..fd945d15 100644 --- a/lib/database/draft.dart +++ b/lib/database/draft.dart @@ -2,8 +2,15 @@ import 'package:drift/drift.dart'; class PostDrafts extends Table { TextColumn get id => text()(); - TextColumn get post => text()(); // Store SnPost model as JSON string + // Searchable fields stored separately for performance + TextColumn get title => text().nullable()(); + TextColumn get description => text().nullable()(); + TextColumn get content => text().nullable()(); + IntColumn get visibility => integer().withDefault(const Constant(0))(); + IntColumn get type => integer().withDefault(const Constant(0))(); DateTimeColumn get lastModified => dateTime()(); + // Full post data stored as JSON for complete restoration + TextColumn get postData => text()(); @override Set get primaryKey => {id}; diff --git a/lib/database/drift_db.dart b/lib/database/drift_db.dart index b6ad2f1b..1a6206eb 100644 --- a/lib/database/drift_db.dart +++ b/lib/database/drift_db.dart @@ -12,7 +12,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @override - int get schemaVersion => 4; + int get schemaVersion => 6; @override MigrationStrategy get migration => MigrationStrategy( @@ -28,9 +28,67 @@ class AppDatabase extends _$AppDatabase { // Drop old draft tables if they exist await m.createTable(postDrafts); } + if (from < 6) { + // Migrate from old schema to new schema with separate searchable fields + await _migrateToVersion6(m); + } }, ); + Future _migrateToVersion6(Migrator m) async { + // Rename existing table to old if it exists + try { + await customStatement( + 'ALTER TABLE post_drafts RENAME TO post_drafts_old', + ); + } catch (e) { + // Table might not exist + } + + // Drop the table + await customStatement('DROP TABLE IF EXISTS post_drafts'); + + // Create new table + await m.createTable(postDrafts); + + // Migrate existing data if any + try { + final oldDrafts = + await customSelect( + 'SELECT id, post, lastModified FROM post_drafts_old', + readsFrom: {postDrafts}, + ).get(); + + for (final row in oldDrafts) { + final postJson = row.read('post'); + final id = row.read('id'); + final lastModified = row.read('lastModified'); + + if (postJson.isNotEmpty) { + final post = SnPost.fromJson(jsonDecode(postJson)); + await into(postDrafts).insert( + PostDraftsCompanion( + id: Value(id), + title: Value(post.title), + description: Value(post.description), + content: Value(post.content), + visibility: Value(post.visibility), + type: Value(post.type), + lastModified: Value(lastModified), + postData: Value(postJson), + ), + ); + } + } + + // Drop old table + await customStatement('DROP TABLE IF EXISTS post_drafts_old'); + } catch (e) { + // If migration fails, just recreate the table + await m.createTable(postDrafts); + } + } + // Methods for chat messages Future> getMessagesForRoom( String roomId, { @@ -69,7 +127,9 @@ class AppDatabase extends _$AppDatabase { } Future getTotalMessagesForRoom(String roomId) { - return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length); + return (select( + chatMessages, + )..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length); } Future> searchMessages( @@ -85,10 +145,6 @@ class AppDatabase extends _$AppDatabase { ..where((m) => m.content.like('%${query.toLowerCase()}%')); } - - - - final messages = await (selectStatement ..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) @@ -129,10 +185,31 @@ class AppDatabase extends _$AppDatabase { Future> getAllPostDrafts() async { final drafts = await select(postDrafts).get(); return drafts - .map((draft) => SnPost.fromJson(jsonDecode(draft.post))) + .map((draft) => SnPost.fromJson(jsonDecode(draft.postData))) .toList(); } + Future> getAllPostDraftRecords() async { + return await select(postDrafts).get(); + } + + Future> searchPostDrafts(String query) async { + if (query.isEmpty) { + return await select(postDrafts).get(); + } + + final searchTerm = '%${query.toLowerCase()}%'; + return await (select(postDrafts) + ..where( + (draft) => + draft.title.like(searchTerm) | + draft.description.like(searchTerm) | + draft.content.like(searchTerm), + ) + ..orderBy([(draft) => OrderingTerm.desc(draft.lastModified)])) + .get(); + } + Future addPostDraft(PostDraftsCompanion entry) async { await into(postDrafts).insert(entry, mode: InsertMode.replace); } @@ -144,4 +221,9 @@ class AppDatabase extends _$AppDatabase { Future clearAllPostDrafts() async { await delete(postDrafts).go(); } + + Future getPostDraftById(String id) async { + return await (select(postDrafts) + ..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); + } } diff --git a/lib/database/drift_db.g.dart b/lib/database/drift_db.g.dart index d26a862a..053b4a9b 100644 --- a/lib/database/drift_db.g.dart +++ b/lib/database/drift_db.g.dart @@ -584,14 +584,58 @@ class $PostDraftsTable extends PostDrafts type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _postMeta = const VerificationMeta('post'); + static const VerificationMeta _titleMeta = const VerificationMeta('title'); @override - late final GeneratedColumn post = GeneratedColumn( - 'post', + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', + ); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _contentMeta = const VerificationMeta( + 'content', + ); + @override + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _visibilityMeta = const VerificationMeta( + 'visibility', + ); + @override + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), ); static const VerificationMeta _lastModifiedMeta = const VerificationMeta( 'lastModified', @@ -604,8 +648,28 @@ class $PostDraftsTable extends PostDrafts type: DriftSqlType.dateTime, requiredDuringInsert: true, ); + static const VerificationMeta _postDataMeta = const VerificationMeta( + 'postData', + ); @override - List get $columns => [id, post, lastModified]; + late final GeneratedColumn postData = GeneratedColumn( + 'post_data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + title, + description, + content, + visibility, + type, + lastModified, + postData, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -623,13 +687,38 @@ class $PostDraftsTable extends PostDrafts } else if (isInserting) { context.missing(_idMeta); } - if (data.containsKey('post')) { + if (data.containsKey('title')) { context.handle( - _postMeta, - post.isAcceptableOrUnknown(data['post']!, _postMeta), + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), + ); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), + ); + } + if (data.containsKey('content')) { + context.handle( + _contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta), + ); + } + if (data.containsKey('visibility')) { + context.handle( + _visibilityMeta, + visibility.isAcceptableOrUnknown(data['visibility']!, _visibilityMeta), + ); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), ); - } else if (isInserting) { - context.missing(_postMeta); } if (data.containsKey('last_modified')) { context.handle( @@ -642,6 +731,14 @@ class $PostDraftsTable extends PostDrafts } else if (isInserting) { context.missing(_lastModifiedMeta); } + if (data.containsKey('post_data')) { + context.handle( + _postDataMeta, + postData.isAcceptableOrUnknown(data['post_data']!, _postDataMeta), + ); + } else if (isInserting) { + context.missing(_postDataMeta); + } return context; } @@ -656,16 +753,38 @@ class $PostDraftsTable extends PostDrafts DriftSqlType.string, data['${effectivePrefix}id'], )!, - post: + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + content: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + ), + visibility: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}post'], + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + type: + attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], )!, lastModified: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}last_modified'], )!, + postData: + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}post_data'], + )!, ); } @@ -677,27 +796,60 @@ class $PostDraftsTable extends PostDrafts class PostDraft extends DataClass implements Insertable { final String id; - final String post; + final String? title; + final String? description; + final String? content; + final int visibility; + final int type; final DateTime lastModified; + final String postData; const PostDraft({ required this.id, - required this.post, + this.title, + this.description, + this.content, + required this.visibility, + required this.type, required this.lastModified, + required this.postData, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['post'] = Variable(post); + if (!nullToAbsent || title != null) { + map['title'] = Variable(title); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + map['visibility'] = Variable(visibility); + map['type'] = Variable(type); map['last_modified'] = Variable(lastModified); + map['post_data'] = Variable(postData); return map; } PostDraftsCompanion toCompanion(bool nullToAbsent) { return PostDraftsCompanion( id: Value(id), - post: Value(post), + title: + title == null && nullToAbsent ? const Value.absent() : Value(title), + description: + description == null && nullToAbsent + ? const Value.absent() + : Value(description), + content: + content == null && nullToAbsent + ? const Value.absent() + : Value(content), + visibility: Value(visibility), + type: Value(type), lastModified: Value(lastModified), + postData: Value(postData), ); } @@ -708,8 +860,13 @@ class PostDraft extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return PostDraft( id: serializer.fromJson(json['id']), - post: serializer.fromJson(json['post']), + title: serializer.fromJson(json['title']), + description: serializer.fromJson(json['description']), + content: serializer.fromJson(json['content']), + visibility: serializer.fromJson(json['visibility']), + type: serializer.fromJson(json['type']), lastModified: serializer.fromJson(json['lastModified']), + postData: serializer.fromJson(json['postData']), ); } @override @@ -717,25 +874,50 @@ class PostDraft extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'post': serializer.toJson(post), + 'title': serializer.toJson(title), + 'description': serializer.toJson(description), + 'content': serializer.toJson(content), + 'visibility': serializer.toJson(visibility), + 'type': serializer.toJson(type), 'lastModified': serializer.toJson(lastModified), + 'postData': serializer.toJson(postData), }; } - PostDraft copyWith({String? id, String? post, DateTime? lastModified}) => - PostDraft( - id: id ?? this.id, - post: post ?? this.post, - lastModified: lastModified ?? this.lastModified, - ); + PostDraft copyWith({ + String? id, + Value title = const Value.absent(), + Value description = const Value.absent(), + Value content = const Value.absent(), + int? visibility, + int? type, + DateTime? lastModified, + String? postData, + }) => PostDraft( + id: id ?? this.id, + title: title.present ? title.value : this.title, + description: description.present ? description.value : this.description, + content: content.present ? content.value : this.content, + visibility: visibility ?? this.visibility, + type: type ?? this.type, + lastModified: lastModified ?? this.lastModified, + postData: postData ?? this.postData, + ); PostDraft copyWithCompanion(PostDraftsCompanion data) { return PostDraft( id: data.id.present ? data.id.value : this.id, - post: data.post.present ? data.post.value : this.post, + title: data.title.present ? data.title.value : this.title, + description: + data.description.present ? data.description.value : this.description, + content: data.content.present ? data.content.value : this.content, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + type: data.type.present ? data.type.value : this.type, lastModified: data.lastModified.present ? data.lastModified.value : this.lastModified, + postData: data.postData.present ? data.postData.value : this.postData, ); } @@ -743,66 +925,120 @@ class PostDraft extends DataClass implements Insertable { String toString() { return (StringBuffer('PostDraft(') ..write('id: $id, ') - ..write('post: $post, ') - ..write('lastModified: $lastModified') + ..write('title: $title, ') + ..write('description: $description, ') + ..write('content: $content, ') + ..write('visibility: $visibility, ') + ..write('type: $type, ') + ..write('lastModified: $lastModified, ') + ..write('postData: $postData') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, post, lastModified); + int get hashCode => Object.hash( + id, + title, + description, + content, + visibility, + type, + lastModified, + postData, + ); @override bool operator ==(Object other) => identical(this, other) || (other is PostDraft && other.id == this.id && - other.post == this.post && - other.lastModified == this.lastModified); + other.title == this.title && + other.description == this.description && + other.content == this.content && + other.visibility == this.visibility && + other.type == this.type && + other.lastModified == this.lastModified && + other.postData == this.postData); } class PostDraftsCompanion extends UpdateCompanion { final Value id; - final Value post; + final Value title; + final Value description; + final Value content; + final Value visibility; + final Value type; final Value lastModified; + final Value postData; final Value rowid; const PostDraftsCompanion({ this.id = const Value.absent(), - this.post = const Value.absent(), + this.title = const Value.absent(), + this.description = const Value.absent(), + this.content = const Value.absent(), + this.visibility = const Value.absent(), + this.type = const Value.absent(), this.lastModified = const Value.absent(), + this.postData = const Value.absent(), this.rowid = const Value.absent(), }); PostDraftsCompanion.insert({ required String id, - required String post, + this.title = const Value.absent(), + this.description = const Value.absent(), + this.content = const Value.absent(), + this.visibility = const Value.absent(), + this.type = const Value.absent(), required DateTime lastModified, + required String postData, this.rowid = const Value.absent(), }) : id = Value(id), - post = Value(post), - lastModified = Value(lastModified); + lastModified = Value(lastModified), + postData = Value(postData); static Insertable custom({ Expression? id, - Expression? post, + Expression? title, + Expression? description, + Expression? content, + Expression? visibility, + Expression? type, Expression? lastModified, + Expression? postData, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (post != null) 'post': post, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (content != null) 'content': content, + if (visibility != null) 'visibility': visibility, + if (type != null) 'type': type, if (lastModified != null) 'last_modified': lastModified, + if (postData != null) 'post_data': postData, if (rowid != null) 'rowid': rowid, }); } PostDraftsCompanion copyWith({ Value? id, - Value? post, + Value? title, + Value? description, + Value? content, + Value? visibility, + Value? type, Value? lastModified, + Value? postData, Value? rowid, }) { return PostDraftsCompanion( id: id ?? this.id, - post: post ?? this.post, + title: title ?? this.title, + description: description ?? this.description, + content: content ?? this.content, + visibility: visibility ?? this.visibility, + type: type ?? this.type, lastModified: lastModified ?? this.lastModified, + postData: postData ?? this.postData, rowid: rowid ?? this.rowid, ); } @@ -813,12 +1049,27 @@ class PostDraftsCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (post.present) { - map['post'] = Variable(post.value); + if (title.present) { + map['title'] = Variable(title.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (type.present) { + map['type'] = Variable(type.value); } if (lastModified.present) { map['last_modified'] = Variable(lastModified.value); } + if (postData.present) { + map['post_data'] = Variable(postData.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -829,8 +1080,13 @@ class PostDraftsCompanion extends UpdateCompanion { String toString() { return (StringBuffer('PostDraftsCompanion(') ..write('id: $id, ') - ..write('post: $post, ') + ..write('title: $title, ') + ..write('description: $description, ') + ..write('content: $content, ') + ..write('visibility: $visibility, ') + ..write('type: $type, ') ..write('lastModified: $lastModified, ') + ..write('postData: $postData, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -1140,15 +1396,25 @@ typedef $$ChatMessagesTableProcessedTableManager = typedef $$PostDraftsTableCreateCompanionBuilder = PostDraftsCompanion Function({ required String id, - required String post, + Value title, + Value description, + Value content, + Value visibility, + Value type, required DateTime lastModified, + required String postData, Value rowid, }); typedef $$PostDraftsTableUpdateCompanionBuilder = PostDraftsCompanion Function({ Value id, - Value post, + Value title, + Value description, + Value content, + Value visibility, + Value type, Value lastModified, + Value postData, Value rowid, }); @@ -1166,8 +1432,28 @@ class $$PostDraftsTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get post => $composableBuilder( - column: $table.post, + ColumnFilters get title => $composableBuilder( + column: $table.title, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get content => $composableBuilder( + column: $table.content, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnFilters(column), ); @@ -1175,6 +1461,11 @@ class $$PostDraftsTableFilterComposer column: $table.lastModified, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get postData => $composableBuilder( + column: $table.postData, + builder: (column) => ColumnFilters(column), + ); } class $$PostDraftsTableOrderingComposer @@ -1191,8 +1482,28 @@ class $$PostDraftsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get post => $composableBuilder( - column: $table.post, + ColumnOrderings get title => $composableBuilder( + column: $table.title, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column), ); @@ -1200,6 +1511,11 @@ class $$PostDraftsTableOrderingComposer column: $table.lastModified, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get postData => $composableBuilder( + column: $table.postData, + builder: (column) => ColumnOrderings(column), + ); } class $$PostDraftsTableAnnotationComposer @@ -1214,13 +1530,32 @@ class $$PostDraftsTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get post => - $composableBuilder(column: $table.post, builder: (column) => column); + GeneratedColumn get title => + $composableBuilder(column: $table.title, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, + builder: (column) => column, + ); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => column, + ); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumn get lastModified => $composableBuilder( column: $table.lastModified, builder: (column) => column, ); + + GeneratedColumn get postData => + $composableBuilder(column: $table.postData, builder: (column) => column); } class $$PostDraftsTableTableManager @@ -1255,25 +1590,45 @@ class $$PostDraftsTableTableManager updateCompanionCallback: ({ Value id = const Value.absent(), - Value post = const Value.absent(), + Value title = const Value.absent(), + Value description = const Value.absent(), + Value content = const Value.absent(), + Value visibility = const Value.absent(), + Value type = const Value.absent(), Value lastModified = const Value.absent(), + Value postData = const Value.absent(), Value rowid = const Value.absent(), }) => PostDraftsCompanion( id: id, - post: post, + title: title, + description: description, + content: content, + visibility: visibility, + type: type, lastModified: lastModified, + postData: postData, rowid: rowid, ), createCompanionCallback: ({ required String id, - required String post, + Value title = const Value.absent(), + Value description = const Value.absent(), + Value content = const Value.absent(), + Value visibility = const Value.absent(), + Value type = const Value.absent(), required DateTime lastModified, + required String postData, Value rowid = const Value.absent(), }) => PostDraftsCompanion.insert( id: id, - post: post, + title: title, + description: description, + content: content, + visibility: visibility, + type: type, lastModified: lastModified, + postData: postData, rowid: rowid, ), withReferenceMapper: diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index fae84388..00a9bc09 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -128,14 +128,6 @@ class ArticleComposeScreen extends HookConsumerWidget { return null; }, []); - // Auto-save cleanup - useEffect(() { - return () { - state.stopAutoSave(); - ComposeLogic.dispose(state); - }; - }, [state]); - // Helper methods void showSettingsSheet() { showModalBottomSheet( @@ -182,6 +174,12 @@ class ArticleComposeScreen extends HookConsumerWidget { MarkdownTextContent( content: contentValue.text, textStyle: theme.textTheme.bodyMedium, + attachments: + state.attachments.value + .where((e) => e.isOnCloud) + .map((e) => e.data) + .cast() + .toList(), ), ], ); @@ -268,7 +266,7 @@ class ArticleComposeScreen extends HookConsumerWidget { child: KeyboardListener( focusNode: FocusNode(), onKeyEvent: - (event) => _handleKeyPress( + (event) => ComposeLogic.handleKeyPress( event, state, ref, @@ -511,38 +509,4 @@ class ArticleComposeScreen extends HookConsumerWidget { ), ); } - - // Helper method to handle keyboard shortcuts - void _handleKeyPress( - KeyEvent 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 = - HardwareKeyboard.instance.isMetaPressed || - HardwareKeyboard.instance.isControlPressed; - final isSubmit = event.logicalKey == LogicalKeyboardKey.enter; - - if (isPaste && isModifierPressed) { - ComposeLogic.handlePaste(state); - } else if (isSave && isModifierPressed) { - ComposeLogic.saveDraft(ref, state); - ComposeLogic.saveDraft(ref, state); - } else if (isSubmit && isModifierPressed && !state.submitting.value) { - ComposeLogic.performAction( - ref, - state, - context, - originalPost: originalPost, - ); - } - } - - // Helper method to save article draft } diff --git a/lib/services/compose_storage_db.dart b/lib/services/compose_storage_db.dart index 121994d9..b423e175 100644 --- a/lib/services/compose_storage_db.dart +++ b/lib/services/compose_storage_db.dart @@ -39,8 +39,13 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier { await database.addPostDraft( PostDraftsCompanion( id: Value(updatedDraft.id), - post: Value(jsonEncode(updatedDraft.toJson())), + title: Value(updatedDraft.title), + description: Value(updatedDraft.description), + content: Value(updatedDraft.content), + visibility: Value(updatedDraft.visibility), + type: Value(updatedDraft.type), lastModified: Value(updatedDraft.updatedAt ?? DateTime.now()), + postData: Value(jsonEncode(updatedDraft.toJson())), ), ); } catch (e) { diff --git a/lib/services/compose_storage_db.g.dart b/lib/services/compose_storage_db.g.dart index 06dd9c57..03b520bb 100644 --- a/lib/services/compose_storage_db.g.dart +++ b/lib/services/compose_storage_db.g.dart @@ -7,7 +7,7 @@ part of 'compose_storage_db.dart'; // ************************************************************************** String _$composeStorageNotifierHash() => - r'4ab4dce85d0a961f096dc3b11505f8f0964dee9d'; + r'8baf17aa06b6f69641c20645ba8a3dfe01c97f8c'; /// See also [ComposeStorageNotifier]. @ProviderFor(ComposeStorageNotifier) diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index 0eaa45f4..84b36b16 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -173,10 +173,6 @@ class ComposeLogic { } try { - if (state._autoSaveTimer == null) { - return; - } - // Upload any local attachments first final baseUrl = ref.watch(serverUrlProvider); final token = await getToken(ref.watch(tokenProvider)); @@ -284,10 +280,6 @@ class ComposeLogic { } try { - if (state._autoSaveTimer == null) { - return; - } - final draft = SnPost( id: state.draftId, title: state.titleController.text, diff --git a/lib/widgets/post/compose_toolbar.dart b/lib/widgets/post/compose_toolbar.dart index 6754ca82..65d3a354 100644 --- a/lib/widgets/post/compose_toolbar.dart +++ b/lib/widgets/post/compose_toolbar.dart @@ -34,7 +34,7 @@ class ComposeToolbar extends HookConsumerWidget { } void saveDraft() { - ComposeLogic.saveDraft(ref, state); + ComposeLogic.saveDraftManually(ref, state, context); } void pickPoll() { diff --git a/lib/widgets/post/draft_manager.dart b/lib/widgets/post/draft_manager.dart index 692a78a9..541e450d 100644 --- a/lib/widgets/post/draft_manager.dart +++ b/lib/widgets/post/draft_manager.dart @@ -16,138 +16,145 @@ class DraftManagerSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final isLoading = useState(true); + final searchController = useTextEditingController(); + final searchQuery = useState(''); final drafts = ref.watch(composeStorageNotifierProvider); - // Track loading state based on drafts being loaded - useEffect(() { - // Set loading to false after drafts are loaded - // We consider drafts loaded when the provider has been initialized - Future.microtask(() { - if (isLoading.value) { - isLoading.value = false; - } - }); - return null; - }, [drafts]); + // Search functionality + final filteredDrafts = useMemoized(() { + if (searchQuery.value.isEmpty) { + return drafts.values.toList() + ..sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!)); + } - final sortedDrafts = useMemoized( - () { - final draftList = drafts.values.toList(); - draftList.sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!)); - return draftList; - }, - [ - drafts.length, - drafts.values.map((e) => e.updatedAt!.millisecondsSinceEpoch).join(), - ], - ); + final query = searchQuery.value.toLowerCase(); + return drafts.values.where((draft) { + return (draft.title?.toLowerCase().contains(query) ?? false) || + (draft.description?.toLowerCase().contains(query) ?? false) || + (draft.content?.toLowerCase().contains(query) ?? false); + }).toList() + ..sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!)); + }, [drafts, searchQuery.value]); return SheetScaffold( titleText: 'drafts'.tr(), - child: - isLoading.value - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - if (sortedDrafts.isEmpty) - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Symbols.draft, - size: 64, - color: colorScheme.onSurface.withOpacity(0.3), - ), - const Gap(16), - Text( - 'noDrafts'.tr(), - style: theme.textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - ) - else - Expanded( - child: ListView.builder( - itemCount: sortedDrafts.length, - itemBuilder: (context, index) { - final draft = sortedDrafts[index]; - return _DraftItem( - draft: draft, - onTap: () { - Navigator.of(context).pop(); - onDraftSelected?.call(draft.id); - }, - onDelete: () async { - await ref - .read(composeStorageNotifierProvider.notifier) - .deleteDraft(draft.id); - }, - ); - }, - ), - ), - if (sortedDrafts.isNotEmpty) ...[ - const Divider(), - Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () async { - final confirmed = await showDialog( - context: context, - builder: - (context) => AlertDialog( - title: Text('clearAllDrafts'.tr()), - content: Text( - 'clearAllDraftsConfirm'.tr(), - ), - actions: [ - TextButton( - onPressed: - () => Navigator.of( - context, - ).pop(false), - child: Text('cancel'.tr()), - ), - TextButton( - onPressed: - () => Navigator.of( - context, - ).pop(true), - child: Text('confirm'.tr()), - ), - ], - ), - ); + child: Column( + children: [ + // Search bar + Padding( + padding: const EdgeInsets.all(16), + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'searchDrafts'.tr(), + prefixIcon: const Icon(Symbols.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) => searchQuery.value = value, + ), + ), - if (confirmed == true) { - await ref - .read( - composeStorageNotifierProvider.notifier, - ) - .clearAllDrafts(); - } - }, - icon: const Icon(Symbols.delete_sweep), - label: Text('clearAll'.tr()), - ), - ), - ], + // Drafts list + if (filteredDrafts.isEmpty) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Symbols.draft, + size: 64, + color: colorScheme.onSurface.withOpacity(0.3), + ), + const Gap(16), + Text( + searchQuery.value.isEmpty + ? 'noDrafts'.tr() + : 'noSearchResults'.tr(), + style: theme.textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), ), ), ], + ), + ), + ) + else + Expanded( + child: ListView.builder( + itemCount: filteredDrafts.length, + itemBuilder: (context, index) { + final draft = filteredDrafts[index]; + return _DraftItem( + draft: draft, + onTap: () { + Navigator.of(context).pop(); + onDraftSelected?.call(draft.id); + }, + onDelete: () async { + await ref + .read(composeStorageNotifierProvider.notifier) + .deleteDraft(draft.id); + }, + ); + }, + ), + ), + + // Clear all button + if (filteredDrafts.isNotEmpty) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () async { + final confirmed = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('clearAllDrafts'.tr()), + content: Text('clearAllDraftsConfirm'.tr()), + actions: [ + TextButton( + onPressed: + () => Navigator.of(context).pop(false), + child: Text('cancel'.tr()), + ), + TextButton( + onPressed: + () => Navigator.of(context).pop(true), + child: Text('confirm'.tr()), + ), + ], + ), + ); + + if (confirmed == true) { + await ref + .read(composeStorageNotifierProvider.notifier) + .clearAllDrafts(); + } + }, + icon: const Icon(Symbols.delete_sweep), + label: Text('clearAll'.tr()), + ), + ), ], ), + ), + ], + ], + ), ); } }