✨ Reworked post draft
This commit is contained in:
		| @@ -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", | ||||
|   | ||||
| @@ -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<Column> get primaryKey => {id}; | ||||
|   | ||||
| @@ -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<void> _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<String>('post'); | ||||
|         final id = row.read<String>('id'); | ||||
|         final lastModified = row.read<DateTime>('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<List<ChatMessage>> getMessagesForRoom( | ||||
|     String roomId, { | ||||
| @@ -69,7 +127,9 @@ class AppDatabase extends _$AppDatabase { | ||||
|   } | ||||
|  | ||||
|   Future<int> 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<List<LocalChatMessage>> 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<List<SnPost>> 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<List<PostDraft>> getAllPostDraftRecords() async { | ||||
|     return await select(postDrafts).get(); | ||||
|   } | ||||
|  | ||||
|   Future<List<PostDraft>> 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<void> addPostDraft(PostDraftsCompanion entry) async { | ||||
|     await into(postDrafts).insert(entry, mode: InsertMode.replace); | ||||
|   } | ||||
| @@ -144,4 +221,9 @@ class AppDatabase extends _$AppDatabase { | ||||
|   Future<void> clearAllPostDrafts() async { | ||||
|     await delete(postDrafts).go(); | ||||
|   } | ||||
|  | ||||
|   Future<PostDraft?> getPostDraftById(String id) async { | ||||
|     return await (select(postDrafts) | ||||
|       ..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<String> post = GeneratedColumn<String>( | ||||
|     'post', | ||||
|   late final GeneratedColumn<String> title = GeneratedColumn<String>( | ||||
|     'title', | ||||
|     aliasedName, | ||||
|     true, | ||||
|     type: DriftSqlType.string, | ||||
|     requiredDuringInsert: false, | ||||
|   ); | ||||
|   static const VerificationMeta _descriptionMeta = const VerificationMeta( | ||||
|     'description', | ||||
|   ); | ||||
|   @override | ||||
|   late final GeneratedColumn<String> description = GeneratedColumn<String>( | ||||
|     'description', | ||||
|     aliasedName, | ||||
|     true, | ||||
|     type: DriftSqlType.string, | ||||
|     requiredDuringInsert: false, | ||||
|   ); | ||||
|   static const VerificationMeta _contentMeta = const VerificationMeta( | ||||
|     'content', | ||||
|   ); | ||||
|   @override | ||||
|   late final GeneratedColumn<String> content = GeneratedColumn<String>( | ||||
|     'content', | ||||
|     aliasedName, | ||||
|     true, | ||||
|     type: DriftSqlType.string, | ||||
|     requiredDuringInsert: false, | ||||
|   ); | ||||
|   static const VerificationMeta _visibilityMeta = const VerificationMeta( | ||||
|     'visibility', | ||||
|   ); | ||||
|   @override | ||||
|   late final GeneratedColumn<int> visibility = GeneratedColumn<int>( | ||||
|     '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<int> type = GeneratedColumn<int>( | ||||
|     '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<GeneratedColumn> get $columns => [id, post, lastModified]; | ||||
|   late final GeneratedColumn<String> postData = GeneratedColumn<String>( | ||||
|     'post_data', | ||||
|     aliasedName, | ||||
|     false, | ||||
|     type: DriftSqlType.string, | ||||
|     requiredDuringInsert: true, | ||||
|   ); | ||||
|   @override | ||||
|   List<GeneratedColumn> 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<PostDraft> { | ||||
|   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<String, Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, Expression>{}; | ||||
|     map['id'] = Variable<String>(id); | ||||
|     map['post'] = Variable<String>(post); | ||||
|     if (!nullToAbsent || title != null) { | ||||
|       map['title'] = Variable<String>(title); | ||||
|     } | ||||
|     if (!nullToAbsent || description != null) { | ||||
|       map['description'] = Variable<String>(description); | ||||
|     } | ||||
|     if (!nullToAbsent || content != null) { | ||||
|       map['content'] = Variable<String>(content); | ||||
|     } | ||||
|     map['visibility'] = Variable<int>(visibility); | ||||
|     map['type'] = Variable<int>(type); | ||||
|     map['last_modified'] = Variable<DateTime>(lastModified); | ||||
|     map['post_data'] = Variable<String>(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<PostDraft> { | ||||
|     serializer ??= driftRuntimeOptions.defaultSerializer; | ||||
|     return PostDraft( | ||||
|       id: serializer.fromJson<String>(json['id']), | ||||
|       post: serializer.fromJson<String>(json['post']), | ||||
|       title: serializer.fromJson<String?>(json['title']), | ||||
|       description: serializer.fromJson<String?>(json['description']), | ||||
|       content: serializer.fromJson<String?>(json['content']), | ||||
|       visibility: serializer.fromJson<int>(json['visibility']), | ||||
|       type: serializer.fromJson<int>(json['type']), | ||||
|       lastModified: serializer.fromJson<DateTime>(json['lastModified']), | ||||
|       postData: serializer.fromJson<String>(json['postData']), | ||||
|     ); | ||||
|   } | ||||
|   @override | ||||
| @@ -717,25 +874,50 @@ class PostDraft extends DataClass implements Insertable<PostDraft> { | ||||
|     serializer ??= driftRuntimeOptions.defaultSerializer; | ||||
|     return <String, dynamic>{ | ||||
|       'id': serializer.toJson<String>(id), | ||||
|       'post': serializer.toJson<String>(post), | ||||
|       'title': serializer.toJson<String?>(title), | ||||
|       'description': serializer.toJson<String?>(description), | ||||
|       'content': serializer.toJson<String?>(content), | ||||
|       'visibility': serializer.toJson<int>(visibility), | ||||
|       'type': serializer.toJson<int>(type), | ||||
|       'lastModified': serializer.toJson<DateTime>(lastModified), | ||||
|       'postData': serializer.toJson<String>(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<String?> title = const Value.absent(), | ||||
|     Value<String?> description = const Value.absent(), | ||||
|     Value<String?> 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<PostDraft> { | ||||
|   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<PostDraft> { | ||||
|   final Value<String> id; | ||||
|   final Value<String> post; | ||||
|   final Value<String?> title; | ||||
|   final Value<String?> description; | ||||
|   final Value<String?> content; | ||||
|   final Value<int> visibility; | ||||
|   final Value<int> type; | ||||
|   final Value<DateTime> lastModified; | ||||
|   final Value<String> postData; | ||||
|   final Value<int> 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<PostDraft> custom({ | ||||
|     Expression<String>? id, | ||||
|     Expression<String>? post, | ||||
|     Expression<String>? title, | ||||
|     Expression<String>? description, | ||||
|     Expression<String>? content, | ||||
|     Expression<int>? visibility, | ||||
|     Expression<int>? type, | ||||
|     Expression<DateTime>? lastModified, | ||||
|     Expression<String>? postData, | ||||
|     Expression<int>? 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<String>? id, | ||||
|     Value<String>? post, | ||||
|     Value<String?>? title, | ||||
|     Value<String?>? description, | ||||
|     Value<String?>? content, | ||||
|     Value<int>? visibility, | ||||
|     Value<int>? type, | ||||
|     Value<DateTime>? lastModified, | ||||
|     Value<String>? postData, | ||||
|     Value<int>? 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<PostDraft> { | ||||
|     if (id.present) { | ||||
|       map['id'] = Variable<String>(id.value); | ||||
|     } | ||||
|     if (post.present) { | ||||
|       map['post'] = Variable<String>(post.value); | ||||
|     if (title.present) { | ||||
|       map['title'] = Variable<String>(title.value); | ||||
|     } | ||||
|     if (description.present) { | ||||
|       map['description'] = Variable<String>(description.value); | ||||
|     } | ||||
|     if (content.present) { | ||||
|       map['content'] = Variable<String>(content.value); | ||||
|     } | ||||
|     if (visibility.present) { | ||||
|       map['visibility'] = Variable<int>(visibility.value); | ||||
|     } | ||||
|     if (type.present) { | ||||
|       map['type'] = Variable<int>(type.value); | ||||
|     } | ||||
|     if (lastModified.present) { | ||||
|       map['last_modified'] = Variable<DateTime>(lastModified.value); | ||||
|     } | ||||
|     if (postData.present) { | ||||
|       map['post_data'] = Variable<String>(postData.value); | ||||
|     } | ||||
|     if (rowid.present) { | ||||
|       map['rowid'] = Variable<int>(rowid.value); | ||||
|     } | ||||
| @@ -829,8 +1080,13 @@ class PostDraftsCompanion extends UpdateCompanion<PostDraft> { | ||||
|   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<String?> title, | ||||
|       Value<String?> description, | ||||
|       Value<String?> content, | ||||
|       Value<int> visibility, | ||||
|       Value<int> type, | ||||
|       required DateTime lastModified, | ||||
|       required String postData, | ||||
|       Value<int> rowid, | ||||
|     }); | ||||
| typedef $$PostDraftsTableUpdateCompanionBuilder = | ||||
|     PostDraftsCompanion Function({ | ||||
|       Value<String> id, | ||||
|       Value<String> post, | ||||
|       Value<String?> title, | ||||
|       Value<String?> description, | ||||
|       Value<String?> content, | ||||
|       Value<int> visibility, | ||||
|       Value<int> type, | ||||
|       Value<DateTime> lastModified, | ||||
|       Value<String> postData, | ||||
|       Value<int> rowid, | ||||
|     }); | ||||
|  | ||||
| @@ -1166,8 +1432,28 @@ class $$PostDraftsTableFilterComposer | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<String> get post => $composableBuilder( | ||||
|     column: $table.post, | ||||
|   ColumnFilters<String> get title => $composableBuilder( | ||||
|     column: $table.title, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<String> get description => $composableBuilder( | ||||
|     column: $table.description, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<String> get content => $composableBuilder( | ||||
|     column: $table.content, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<int> get visibility => $composableBuilder( | ||||
|     column: $table.visibility, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<int> get type => $composableBuilder( | ||||
|     column: $table.type, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
| @@ -1175,6 +1461,11 @@ class $$PostDraftsTableFilterComposer | ||||
|     column: $table.lastModified, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnFilters<String> get postData => $composableBuilder( | ||||
|     column: $table.postData, | ||||
|     builder: (column) => ColumnFilters(column), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class $$PostDraftsTableOrderingComposer | ||||
| @@ -1191,8 +1482,28 @@ class $$PostDraftsTableOrderingComposer | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<String> get post => $composableBuilder( | ||||
|     column: $table.post, | ||||
|   ColumnOrderings<String> get title => $composableBuilder( | ||||
|     column: $table.title, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<String> get description => $composableBuilder( | ||||
|     column: $table.description, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<String> get content => $composableBuilder( | ||||
|     column: $table.content, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<int> get visibility => $composableBuilder( | ||||
|     column: $table.visibility, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<int> get type => $composableBuilder( | ||||
|     column: $table.type, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
| @@ -1200,6 +1511,11 @@ class $$PostDraftsTableOrderingComposer | ||||
|     column: $table.lastModified, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
|  | ||||
|   ColumnOrderings<String> get postData => $composableBuilder( | ||||
|     column: $table.postData, | ||||
|     builder: (column) => ColumnOrderings(column), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class $$PostDraftsTableAnnotationComposer | ||||
| @@ -1214,13 +1530,32 @@ class $$PostDraftsTableAnnotationComposer | ||||
|   GeneratedColumn<String> get id => | ||||
|       $composableBuilder(column: $table.id, builder: (column) => column); | ||||
|  | ||||
|   GeneratedColumn<String> get post => | ||||
|       $composableBuilder(column: $table.post, builder: (column) => column); | ||||
|   GeneratedColumn<String> get title => | ||||
|       $composableBuilder(column: $table.title, builder: (column) => column); | ||||
|  | ||||
|   GeneratedColumn<String> get description => $composableBuilder( | ||||
|     column: $table.description, | ||||
|     builder: (column) => column, | ||||
|   ); | ||||
|  | ||||
|   GeneratedColumn<String> get content => | ||||
|       $composableBuilder(column: $table.content, builder: (column) => column); | ||||
|  | ||||
|   GeneratedColumn<int> get visibility => $composableBuilder( | ||||
|     column: $table.visibility, | ||||
|     builder: (column) => column, | ||||
|   ); | ||||
|  | ||||
|   GeneratedColumn<int> get type => | ||||
|       $composableBuilder(column: $table.type, builder: (column) => column); | ||||
|  | ||||
|   GeneratedColumn<DateTime> get lastModified => $composableBuilder( | ||||
|     column: $table.lastModified, | ||||
|     builder: (column) => column, | ||||
|   ); | ||||
|  | ||||
|   GeneratedColumn<String> get postData => | ||||
|       $composableBuilder(column: $table.postData, builder: (column) => column); | ||||
| } | ||||
|  | ||||
| class $$PostDraftsTableTableManager | ||||
| @@ -1255,25 +1590,45 @@ class $$PostDraftsTableTableManager | ||||
|           updateCompanionCallback: | ||||
|               ({ | ||||
|                 Value<String> id = const Value.absent(), | ||||
|                 Value<String> post = const Value.absent(), | ||||
|                 Value<String?> title = const Value.absent(), | ||||
|                 Value<String?> description = const Value.absent(), | ||||
|                 Value<String?> content = const Value.absent(), | ||||
|                 Value<int> visibility = const Value.absent(), | ||||
|                 Value<int> type = const Value.absent(), | ||||
|                 Value<DateTime> lastModified = const Value.absent(), | ||||
|                 Value<String> postData = const Value.absent(), | ||||
|                 Value<int> 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<String?> title = const Value.absent(), | ||||
|                 Value<String?> description = const Value.absent(), | ||||
|                 Value<String?> content = const Value.absent(), | ||||
|                 Value<int> visibility = const Value.absent(), | ||||
|                 Value<int> type = const Value.absent(), | ||||
|                 required DateTime lastModified, | ||||
|                 required String postData, | ||||
|                 Value<int> 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: | ||||
|   | ||||
| @@ -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<SnCloudFile>() | ||||
|                                     .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 | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'compose_storage_db.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$composeStorageNotifierHash() => | ||||
|     r'4ab4dce85d0a961f096dc3b11505f8f0964dee9d'; | ||||
|     r'8baf17aa06b6f69641c20645ba8a3dfe01c97f8c'; | ||||
|  | ||||
| /// See also [ComposeStorageNotifier]. | ||||
| @ProviderFor(ComposeStorageNotifier) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class ComposeToolbar extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     void saveDraft() { | ||||
|       ComposeLogic.saveDraft(ref, state); | ||||
|       ComposeLogic.saveDraftManually(ref, state, context); | ||||
|     } | ||||
|  | ||||
|     void pickPoll() { | ||||
|   | ||||
| @@ -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<bool>( | ||||
|                                   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<bool>( | ||||
|                           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()), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user