mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: compose with draft management, media attachments, and reply context

+5391 -483
+5
android/app/src/main/AndroidManifest.xml
··· 30 30 <meta-data 31 31 android:name="flutterEmbedding" 32 32 android:value="2" /> 33 + <!-- WorkManager background service for scheduled posts --> 34 + <service 35 + android:name="be.tramckrijte.workmanager.BackgroundWorker" 36 + android:exported="false" 37 + android:permission="android.permission.BIND_JOB_SERVICE" /> 33 38 </application> 34 39 <!-- Required to query activities that can process text, see: 35 40 https://developer.android.com/training/package-visibility and
+88 -81
docs/specs/phase-3.md
··· 2 2 3 3 ## Post Composition 4 4 5 - Posts are created via `com.atproto.repo.createRecord` with collection 6 - `app.bsky.feed.post`. The compose screen is a full-screen modal opened from a 7 - floating action button on the home screen. 5 + Posts are created via `com.atproto.repo.createRecord` with collection `app.bsky.feed.post`. 6 + The compose screen is a full-screen modal opened from a floating action button on the home screen. 8 7 9 8 ### Post Record Structure 10 9 ··· 19 18 20 19 ### Media Uploads 21 20 22 - Upload images via `com.atproto.repo.uploadBlob`. Returns a `blob` ref used in 23 - the embed object. No GIF support — images only. 21 + Upload images via `com.atproto.repo.uploadBlob`. Returns a `blob` ref used in the embed object. 24 22 25 23 | Constraint | Value | 26 24 | -------------- | ---------------------------------- | ··· 29 27 | Accepted types | JPEG, PNG, WebP | 30 28 | Alt text | Required UI field, optional in API | 31 29 32 - Embed type for images: `app.bsky.embed.images`. Each image entry has `image` 33 - (blob ref), `alt` (string), and optional `aspectRatio` (`width` / `height`). 30 + Embed type for images: `app.bsky.embed.images`. 31 + Each image entry has `image` (blob ref), `alt` (string), and optional `aspectRatio` (`width` / `height`). 32 + 33 + ### Video Uploads 34 + 35 + Upload video via `app.bsky.video.uploadVideo` on the `video.bsky.app` service. 36 + Video processing is asynchronous — the endpoint returns a `JobStatus` with a `jobId`. 37 + Poll `app.bsky.video.getJobStatus` until `state` is `JOB_STATE_COMPLETED` (returns the final `blob` ref) or `JOB_STATE_FAILED`. 38 + 39 + Before uploading, call `app.bsky.video.getUploadLimits` to check the user's remaining daily quota (`canUpload`, `remainingDailyVideos`, `remainingDailyBytes`). 40 + If the user cannot upload, show the server-provided `message` or a fallback explaining the daily limit. 41 + 42 + | Constraint | Value | 43 + | -------------- | -------------------------------------------- | 44 + | Max videos | 1 per post (mutually exclusive with images) | 45 + | Max file size | 100 MB | 46 + | Accepted types | MP4 | 47 + | Alt text | Optional UI field | 48 + | Captions | Optional; `EmbedVideoCaption` with lang code | 49 + 50 + Embed type for video: `app.bsky.embed.video`. Fields: 51 + 52 + | Field | Type | Description | 53 + | -------------- | ------------------- | -------------------------------------- | 54 + | `video` | blob | Processed video blob from job status | 55 + | `alt` | string | Accessibility description | 56 + | `aspectRatio` | object | `width` / `height` integers | 57 + | `captions` | array | Caption files with BCP-47 `lang` codes | 58 + | `presentation` | string | `"default"` or `"gif"` playback hint | 59 + 60 + The compose screen must show a progress indicator during video upload and processing. 61 + Disable the submit button until processing completes. If the job fails, display the error message from `JobStatus.error` and allow the user to retry or remove the video. 62 + 63 + A post embeds either images **or** a video, never both. When a video is attached, the image picker should be disabled (and vice versa). 64 + Switching media type should prompt the user to confirm replacing the existing attachment(s). 34 65 35 66 ### Facet Detection 36 67 37 - Use **bluesky_text** to detect mentions, links, and hashtags in the post text 38 - and produce the `facets` array automatically before submission. The compose 39 - screen should render a live preview of detected facets with colour-coded 40 - highlights as the user types. 68 + Use **bluesky_text** to detect mentions, links, and hashtags in the post text and produce the `facets` array automatically before submission. 69 + The compose screen should render a live preview of detected facets with color-coded highlights as the user types. 41 70 42 71 ### Grapheme Counter 43 72 44 - Display a live character counter showing remaining graphemes (300 max). Use 45 - Dart's `Characters` class for accurate grapheme cluster counting. Disable the 46 - submit button when the count exceeds 300 or the text is empty. 73 + Display a live character counter showing remaining graphemes (300 max). 74 + Use Dart's `Characters` class for accurate grapheme cluster counting. 75 + Disable the submit button when the count exceeds 300 or the text is empty. 47 76 48 77 ### Drafts 49 78 50 - Persist unsent posts locally in a Drift `drafts` table. On network failure or 51 - explicit save, always store the draft. Drafts are account-scoped. 79 + Persist unsent posts locally in a Drift `drafts` table. 80 + On network failure or explicit save, always store the draft. Drafts are account-scoped. 52 81 53 82 | Column | Type | Notes | 54 83 | ------------- | -------- | ---------------------------------------- | ··· 66 95 67 96 ### Scheduled Posts 68 97 69 - Schedule posts for future publication using a local scheduler. Store the 70 - scheduled time alongside the draft. Use a `WorkManager` (Android) / 71 - `BGTaskScheduler` (iOS) background task to submit the post at the scheduled 72 - time. If the device is offline at the scheduled time, queue the post and retry 73 - when connectivity resumes. 98 + Schedule posts for future publication using a local scheduler. 99 + Store the scheduled time alongside the draft. Use a `WorkManager` (Android) / `BGTaskScheduler` (iOS) background task to submit the post at the scheduled time. 100 + If the device is offline at the scheduled time, queue the post and retry when connectivity resumes. 74 101 75 - Add a `scheduled_at` (nullable datetime) column to the `drafts` table. When 76 - non-null, the draft is treated as scheduled rather than a regular draft. 102 + Add a `scheduled_at` (nullable datetime) column to the `drafts` table. 103 + When non-null, the draft is treated as scheduled rather than a regular draft. 77 104 78 - Build a `ComposeBloc` with events: `TextChanged`, `MediaAttached`, 79 - `MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`, 80 - `PostScheduled`, `PostSubmitted`. 105 + Build a `ComposeBloc` with events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `VideoAttached`, `VideoRemoved`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted`. 81 106 82 107 ## Notifications 83 108 ··· 108 133 109 134 ### Rendering 110 135 111 - Group notifications by day. Each notification row shows the author avatar, the 112 - reason icon, a summary line, and an optional post preview snippet. Tapping a 113 - notification navigates to the relevant post or profile. 136 + Group notifications by day. Each notification row shows the author avatar, the reason icon, a summary line, and an optional post preview snippet. 137 + Tapping a notification navigates to the relevant post or profile. 114 138 115 - Display an unread count badge on the Notifications nav bar item. Poll 116 - `getUnreadCount` on a 30-second interval when the app is foregrounded. Call 117 - `updateSeen` when the notifications screen is opened. 139 + Display an unread count badge on the Notifications nav bar item. 140 + Poll `getUnreadCount` on a 30-second interval when the app is foregrounded. 141 + Call `updateSeen` when the notifications screen is opened. 118 142 119 - Build a `NotificationBloc` with events: `NotificationsRequested`, 120 - `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead`. 143 + Build a `NotificationBloc` with events: `NotificationsRequested`, `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead`. 121 144 122 145 ## Post & Profile Actions 123 146 124 - All post and profile interactions use the AT Protocol record model. Actions 125 - that create a relationship (like, repost, follow, block) write a record via 126 - `com.atproto.repo.createRecord` and undo by deleting via 127 - `com.atproto.repo.deleteRecord`. Muting is a server-side procedure call with 128 - no persistent record. 147 + All post and profile interactions use the AT Protocol record model. 148 + Actions that create a relationship (like, repost, follow, block) write a record via `com.atproto.repo.createRecord` and undo by deleting via `com.atproto.repo.deleteRecord`. 149 + Muting is a server-side procedure call with no persistent record. 129 150 130 151 ### API 131 152 ··· 139 160 140 161 ### Like 141 162 142 - Collection: `app.bsky.feed.like`. Record contains a `subject` (RepoStrongRef 143 - with the post's AT-URI and CID) and `createdAt`. 163 + Collection: `app.bsky.feed.like`. 164 + Record contains a `subject` (RepoStrongRef with the post's AT-URI and CID) and `createdAt`. 144 165 145 - To unlike, extract the record key (rkey) from the `viewer.like` AT-URI and 146 - call `deleteRecord` with collection `app.bsky.feed.like` and that rkey. 166 + To unlike, extract the record key (rkey) from the `viewer.like` AT-URI and call `deleteRecord` with collection `app.bsky.feed.like` and that rkey. 147 167 148 - The `PostView.viewer.like` field is non-null when the current user has liked 149 - the post. Use this to drive the filled/outlined heart icon state. 168 + The `PostView.viewer.like` field is non-null when the current user has liked the post. 169 + Use this to drive the filled/outlined heart icon state. 150 170 151 171 ### Repost 152 172 153 - Collection: `app.bsky.feed.repost`. Record structure is identical to like — 154 - `subject` (RepoStrongRef) + `createdAt`. 173 + Collection: `app.bsky.feed.repost`. Record structure is identical to like — `subject` (RepoStrongRef) + `createdAt`. 155 174 156 175 To un-repost, extract the rkey from `viewer.repost` and delete the record. 157 176 158 - The `PostView.viewer.repost` field is non-null when the current user has 159 - reposted. Use this for the repost icon state. 177 + The `PostView.viewer.repost` field is non-null when the current user has reposted. 178 + Use this for the repost icon state. 160 179 161 180 ### Follow 162 181 163 - Collection: `app.bsky.graph.follow`. Record contains `subject` (the target 164 - user's DID as a string) and `createdAt`. 182 + Collection: `app.bsky.graph.follow`. Record contains `subject` (the target user's DID as a string) and `createdAt`. 165 183 166 184 To unfollow, extract the rkey from `viewer.following` and delete the record. 167 185 ··· 177 195 - `app.bsky.graph.muteActor` — input: `{ actor: DID }` 178 196 - `app.bsky.graph.unmuteActor` — input: `{ actor: DID }` 179 197 180 - Both return empty responses. The `viewer.muted` boolean on profiles reflects 181 - the current mute state. Muted accounts' posts are still fetched but should be 182 - visually de-emphasised or filtered in the UI based on user preference. 198 + Both return empty responses. The `viewer.muted` boolean on profiles reflects the current mute state. 199 + Muted accounts' posts are still fetched but should be visually de-emphasised or filtered in the UI based on user preference. 183 200 184 201 ### Block 185 202 186 - Collection: `app.bsky.graph.block`. Record contains `subject` (the target 187 - user's DID) and `createdAt`. 203 + Collection: `app.bsky.graph.block`. Record contains `subject` (the target user's DID) and `createdAt`. 188 204 189 205 To unblock, extract the rkey from `viewer.blocking` and delete the record. 190 206 ··· 216 232 | `reasonRude` | Harassment or rude behaviour | 217 233 | `reasonOther` | Other (requires text explanation) | 218 234 219 - The report dialog should present the reason picker and an optional free-text 220 - description field. Submitting returns a report ID for confirmation. 235 + The report dialog should present the reason picker and an optional free-text description field. 236 + Submitting returns a report ID for confirmation. 221 237 222 238 ### Optimistic Updates 223 239 224 - All toggle actions (like, repost, follow, mute, block) should use optimistic 225 - UI updates: 240 + All toggle actions (like, repost, follow, mute, block) should use optimistic UI updates: 226 241 227 242 1. Immediately update the local state (icon, count, button label). 228 243 2. Fire the API call in the background. 229 244 3. On success, reconcile with the server response (update the viewer URI). 230 245 4. On failure, roll back the local state and show a snackbar error. 231 246 232 - Build a `PostActionCubit` that manages per-post action state (like, repost, 233 - save). It accepts the initial `ViewerState` from the post and exposes 247 + Build a `PostActionCubit` that manages per-post action state (like, repost, save). 248 + It accepts the initial `ViewerState` from the post and exposes 234 249 toggleable methods. 235 250 236 - Build a `ProfileActionCubit` that manages per-profile action state (follow, 237 - mute, block). It accepts the initial `ViewerState` from the profile and 238 - exposes toggleable methods. 251 + Build a `ProfileActionCubit` that manages per-profile action state (follow, mute, block). 252 + It accepts the initial `ViewerState` from the profile and exposes toggleable methods. 239 253 240 254 ### Post Action Bar 241 255 ··· 248 262 | Like | heart | Toggle like | — | 249 263 | Share | share | Share sheet | — | 250 264 251 - The bookmark (save) icon is placed in the post overflow menu alongside 252 - "Report" and "Copy link". 265 + The bookmark (save) icon is placed in the post overflow menu alongside "Report" and "Copy link". 253 266 254 - Like and repost counts are displayed next to their respective icons. Counts 255 - update optimistically. The repost long-press opens a bottom sheet with 256 - "Repost" and "Quote Post" options. 267 + Like and repost counts are displayed next to their respective icons. Counts update optimistically. 268 + The repost long-press opens a bottom sheet with "Repost" and "Quote Post" options. 257 269 258 270 ### Profile Action Buttons 259 271 ··· 266 278 | Blocked by them | — | No button | 267 279 | You blocked them | "Unblock" | Delete block | 268 280 269 - The profile overflow menu (three-dot icon) contains: Mute / Unmute, Block / 270 - Unblock, Report, Copy DID, Share profile. 281 + The profile overflow menu (three-dot icon) contains: Mute / Unmute, Block / Unblock, Report, Copy DID, Share profile. 271 282 272 283 Mute and block actions should show a confirmation dialog before proceeding. 273 284 274 285 ## Saved Posts 275 286 276 - Allow users to bookmark posts locally for later reading. Saved posts are stored 277 - only in Drift — nothing is written to the network. This is intentionally 278 - private and local-only. 287 + Allow users to bookmark posts locally for later reading. Saved posts are stored only in Drift — nothing is written to the network. 288 + This is intentionally private and local-only. 279 289 280 290 ### Drift Table 281 291 ··· 291 301 292 302 ### UI 293 303 294 - Add a "Save" action (bookmark icon) to the post action bar. Tapping toggles 295 - the saved state. Saved posts are viewable from a "Saved" section in the 296 - profile screen or settings. 304 + Add a "Save" action (bookmark icon) to the post action bar. Tapping toggles the saved state. 305 + Saved posts are viewable from a "Saved" section in the profile screen or settings. 297 306 298 - Build a `SavedPostsCubit` that reads/writes the `saved_posts` table and 299 - exposes a stream of saved post URIs for quick lookup (to show filled vs 300 - outlined bookmark icons in the feed). 307 + Build a `SavedPostsCubit` that reads/writes the `saved_posts` table and exposes a stream of saved post URIs for quick lookup (to show filled vs outlined bookmark icons in the feed).
+15 -11
docs/tasks/phase-3.md
··· 2 2 3 3 ## M8 — Post Composition 4 4 5 - - [ ] Full-screen compose modal with text input, live grapheme counter (300 max), and submit button 6 - - [ ] `ComposeBloc` — events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted` 7 - - [ ] Image attachment via `uploadBlob` — up to 4 images, alt text input per image 8 - - [ ] Live facet detection and preview via `bluesky_text` (mentions, links, hashtags) 9 - - [ ] Post creation via `com.atproto.repo.createRecord` with `app.bsky.feed.post` collection 10 - - [ ] Reply support — pass `parent` + `root` refs when composing from a post thread 11 - - [ ] Drift migration: add `drafts` table (id, account_did, text, reply_uri, embed_json, media_paths, created_at, updated_at, scheduled_at) 12 - - [ ] Draft save on network failure, explicit save, and back-navigation 13 - - [ ] Drafts list UI accessible from compose toolbar 14 - - [ ] Scheduled posts — date/time picker, background task via WorkManager / BGTaskScheduler 15 - - [ ] Floating action button on home screen to open compose modal 5 + - [x] Full-screen compose modal with text input, live grapheme counter (300 max), and submit button 6 + - [x] `ComposeBloc` — events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `VideoAttached`, `VideoRemoved`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted` 7 + - [x] Image attachment via `uploadBlob` — up to 4 images, alt text input per image, file size (1 MB max) and MIME type (JPEG, PNG, WebP) validation 8 + - [x] Video attachment via `app.bsky.video.uploadVideo` — 1 per post (mutually exclusive with images), 100 MB max, MP4 only 9 + - [x] Video upload quota check via `getUploadLimits` before upload; show limit message if `canUpload` is false 10 + - [x] Video processing job polling via `getJobStatus` with progress indicator; handle `JOB_STATE_FAILED` with error display and retry 11 + - [x] Video embed via `app.bsky.embed.video` with alt text, aspectRatio, and optional captions 12 + - [x] Live facet detection and preview via `bluesky_text` (mentions, links, hashtags) 13 + - [x] Post creation via `com.atproto.repo.createRecord` with `app.bsky.feed.post` collection 14 + - [x] Reply support — pass `parent` + `root` refs when composing from a post thread 15 + - [x] Drift migration: add `drafts` table (id, account_did, text, reply_uri, embed_json, media_paths, created_at, updated_at, scheduled_at) 16 + - [x] Draft save on network failure, explicit save, and back-navigation 17 + - [x] Drafts list UI accessible from compose toolbar 18 + - [x] Scheduled posts — date/time picker, background task via WorkManager / BGTaskScheduler 19 + - [x] Floating action button on home screen to open compose modal 16 20 17 21 ## M9 — Notifications 18 22
+10
ios/Runner/Info.plist
··· 45 45 <true/> 46 46 <key>UIApplicationSupportsIndirectInputEvents</key> 47 47 <true/> 48 + <!-- WorkManager / BGTaskScheduler task identifiers for scheduled posts --> 49 + <key>BGTaskSchedulerPermittedIdentifiers</key> 50 + <array> 51 + <string>lazurite.scheduled_post</string> 52 + </array> 53 + <key>UIBackgroundModes</key> 54 + <array> 55 + <string>processing</string> 56 + <string>fetch</string> 57 + </array> 48 58 </dict> 49 59 </plist>
+40 -2
lib/core/database/app_database.dart
··· 6 6 7 7 part 'app_database.g.dart'; 8 8 9 - @DriftDatabase(tables: [Accounts, CachedProfiles, CachedPosts, Settings, SavedFeeds, SearchHistory]) 9 + @DriftDatabase(tables: [Accounts, CachedProfiles, CachedPosts, Settings, SavedFeeds, SearchHistory, Drafts]) 10 10 class AppDatabase extends _$AppDatabase { 11 11 AppDatabase({QueryExecutor? executor}) : super(executor ?? _openConnection()); 12 12 13 13 @override 14 - int get schemaVersion => 4; 14 + int get schemaVersion => 5; 15 15 16 16 @override 17 17 MigrationStrategy get migration => MigrationStrategy( ··· 31 31 } 32 32 if (from < 4) { 33 33 await migrator.createTable(searchHistory); 34 + } 35 + if (from < 5) { 36 + await migrator.createTable(drafts); 34 37 } 35 38 }, 36 39 ); ··· 175 178 await deleteSearchHistoryEntry(entry.id); 176 179 } 177 180 } 181 + } 182 + 183 + Future<List<DraftEntry>> getDrafts(String accountDid) async { 184 + return (select(drafts) 185 + ..where((d) => d.accountDid.equals(accountDid)) 186 + ..orderBy([ 187 + (d) => OrderingTerm(expression: d.scheduledAt, mode: OrderingMode.desc), 188 + (d) => OrderingTerm(expression: d.updatedAt, mode: OrderingMode.desc), 189 + ])) 190 + .get(); 191 + } 192 + 193 + Future<DraftEntry?> getDraft(int id) async { 194 + return (select(drafts)..where((d) => d.id.equals(id))).getSingleOrNull(); 195 + } 196 + 197 + Future<int> saveDraft(DraftsCompanion draft) async { 198 + if (draft.id.present) { 199 + await updateDraft(draft.id.value, draft); 200 + return draft.id.value; 201 + } 202 + return into(drafts).insert(draft); 203 + } 204 + 205 + Future<int> updateDraft(int id, DraftsCompanion draft) async { 206 + final query = update(drafts)..where((d) => d.id.equals(id)); 207 + return query.write(draft.copyWith(updatedAt: Value(DateTime.now()))); 208 + } 209 + 210 + Future<int> deleteDraft(int id) async { 211 + return (delete(drafts)..where((d) => d.id.equals(id))).go(); 212 + } 213 + 214 + Future<int> deleteAllDrafts(String accountDid) async { 215 + return (delete(drafts)..where((d) => d.accountDid.equals(accountDid))).go(); 178 216 } 179 217 }
+2120 -387
lib/core/database/app_database.g.dart
··· 26 26 type: DriftSqlType.string, 27 27 requiredDuringInsert: true, 28 28 ); 29 - static const VerificationMeta _displayNameMeta = const VerificationMeta('displayName'); 29 + static const VerificationMeta _displayNameMeta = const VerificationMeta( 30 + 'displayName', 31 + ); 30 32 @override 31 33 late final GeneratedColumn<String> displayName = GeneratedColumn<String>( 32 34 'display_name', ··· 35 37 type: DriftSqlType.string, 36 38 requiredDuringInsert: false, 37 39 ); 38 - static const VerificationMeta _serviceMeta = const VerificationMeta('service'); 40 + static const VerificationMeta _serviceMeta = const VerificationMeta( 41 + 'service', 42 + ); 39 43 @override 40 44 late final GeneratedColumn<String> service = GeneratedColumn<String>( 41 45 'service', ··· 44 48 type: DriftSqlType.string, 45 49 requiredDuringInsert: false, 46 50 ); 47 - static const VerificationMeta _accessTokenMeta = const VerificationMeta('accessToken'); 51 + static const VerificationMeta _accessTokenMeta = const VerificationMeta( 52 + 'accessToken', 53 + ); 48 54 @override 49 55 late final GeneratedColumn<String> accessToken = GeneratedColumn<String>( 50 56 'access_token', ··· 53 59 type: DriftSqlType.string, 54 60 requiredDuringInsert: true, 55 61 ); 56 - static const VerificationMeta _refreshTokenMeta = const VerificationMeta('refreshToken'); 62 + static const VerificationMeta _refreshTokenMeta = const VerificationMeta( 63 + 'refreshToken', 64 + ); 57 65 @override 58 66 late final GeneratedColumn<String> refreshToken = GeneratedColumn<String>( 59 67 'refresh_token', ··· 62 70 type: DriftSqlType.string, 63 71 requiredDuringInsert: false, 64 72 ); 65 - static const VerificationMeta _dpopPublicKeyMeta = const VerificationMeta('dpopPublicKey'); 73 + static const VerificationMeta _dpopPublicKeyMeta = const VerificationMeta( 74 + 'dpopPublicKey', 75 + ); 66 76 @override 67 77 late final GeneratedColumn<String> dpopPublicKey = GeneratedColumn<String>( 68 78 'dpop_public_key', ··· 71 81 type: DriftSqlType.string, 72 82 requiredDuringInsert: false, 73 83 ); 74 - static const VerificationMeta _dpopPrivateKeyMeta = const VerificationMeta('dpopPrivateKey'); 84 + static const VerificationMeta _dpopPrivateKeyMeta = const VerificationMeta( 85 + 'dpopPrivateKey', 86 + ); 75 87 @override 76 88 late final GeneratedColumn<String> dpopPrivateKey = GeneratedColumn<String>( 77 89 'dpop_private_key', ··· 80 92 type: DriftSqlType.string, 81 93 requiredDuringInsert: false, 82 94 ); 83 - static const VerificationMeta _dpopNonceMeta = const VerificationMeta('dpopNonce'); 95 + static const VerificationMeta _dpopNonceMeta = const VerificationMeta( 96 + 'dpopNonce', 97 + ); 84 98 @override 85 99 late final GeneratedColumn<String> dpopNonce = GeneratedColumn<String>( 86 100 'dpop_nonce', ··· 89 103 type: DriftSqlType.string, 90 104 requiredDuringInsert: false, 91 105 ); 92 - static const VerificationMeta _expiresAtMeta = const VerificationMeta('expiresAt'); 106 + static const VerificationMeta _expiresAtMeta = const VerificationMeta( 107 + 'expiresAt', 108 + ); 93 109 @override 94 110 late final GeneratedColumn<DateTime> expiresAt = GeneratedColumn<DateTime>( 95 111 'expires_at', ··· 98 114 type: DriftSqlType.dateTime, 99 115 requiredDuringInsert: false, 100 116 ); 101 - static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); 117 + static const VerificationMeta _createdAtMeta = const VerificationMeta( 118 + 'createdAt', 119 + ); 102 120 @override 103 121 late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( 104 122 'created_at', ··· 108 126 requiredDuringInsert: false, 109 127 defaultValue: currentDateAndTime, 110 128 ); 111 - static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); 129 + static const VerificationMeta _updatedAtMeta = const VerificationMeta( 130 + 'updatedAt', 131 + ); 112 132 @override 113 133 late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 114 134 'updated_at', ··· 139 159 String get actualTableName => $name; 140 160 static const String $name = 'accounts'; 141 161 @override 142 - VerificationContext validateIntegrity(Insertable<Account> instance, {bool isInserting = false}) { 162 + VerificationContext validateIntegrity( 163 + Insertable<Account> instance, { 164 + bool isInserting = false, 165 + }) { 143 166 final context = VerificationContext(); 144 167 final data = instance.toColumns(true); 145 168 if (data.containsKey('did')) { 146 - context.handle(_didMeta, did.isAcceptableOrUnknown(data['did']!, _didMeta)); 169 + context.handle( 170 + _didMeta, 171 + did.isAcceptableOrUnknown(data['did']!, _didMeta), 172 + ); 147 173 } else if (isInserting) { 148 174 context.missing(_didMeta); 149 175 } 150 176 if (data.containsKey('handle')) { 151 - context.handle(_handleMeta, handle.isAcceptableOrUnknown(data['handle']!, _handleMeta)); 177 + context.handle( 178 + _handleMeta, 179 + handle.isAcceptableOrUnknown(data['handle']!, _handleMeta), 180 + ); 152 181 } else if (isInserting) { 153 182 context.missing(_handleMeta); 154 183 } 155 184 if (data.containsKey('display_name')) { 156 - context.handle(_displayNameMeta, displayName.isAcceptableOrUnknown(data['display_name']!, _displayNameMeta)); 185 + context.handle( 186 + _displayNameMeta, 187 + displayName.isAcceptableOrUnknown( 188 + data['display_name']!, 189 + _displayNameMeta, 190 + ), 191 + ); 157 192 } 158 193 if (data.containsKey('service')) { 159 - context.handle(_serviceMeta, service.isAcceptableOrUnknown(data['service']!, _serviceMeta)); 194 + context.handle( 195 + _serviceMeta, 196 + service.isAcceptableOrUnknown(data['service']!, _serviceMeta), 197 + ); 160 198 } 161 199 if (data.containsKey('access_token')) { 162 - context.handle(_accessTokenMeta, accessToken.isAcceptableOrUnknown(data['access_token']!, _accessTokenMeta)); 200 + context.handle( 201 + _accessTokenMeta, 202 + accessToken.isAcceptableOrUnknown( 203 + data['access_token']!, 204 + _accessTokenMeta, 205 + ), 206 + ); 163 207 } else if (isInserting) { 164 208 context.missing(_accessTokenMeta); 165 209 } 166 210 if (data.containsKey('refresh_token')) { 167 - context.handle(_refreshTokenMeta, refreshToken.isAcceptableOrUnknown(data['refresh_token']!, _refreshTokenMeta)); 211 + context.handle( 212 + _refreshTokenMeta, 213 + refreshToken.isAcceptableOrUnknown( 214 + data['refresh_token']!, 215 + _refreshTokenMeta, 216 + ), 217 + ); 168 218 } 169 219 if (data.containsKey('dpop_public_key')) { 170 220 context.handle( 171 221 _dpopPublicKeyMeta, 172 - dpopPublicKey.isAcceptableOrUnknown(data['dpop_public_key']!, _dpopPublicKeyMeta), 222 + dpopPublicKey.isAcceptableOrUnknown( 223 + data['dpop_public_key']!, 224 + _dpopPublicKeyMeta, 225 + ), 173 226 ); 174 227 } 175 228 if (data.containsKey('dpop_private_key')) { 176 229 context.handle( 177 230 _dpopPrivateKeyMeta, 178 - dpopPrivateKey.isAcceptableOrUnknown(data['dpop_private_key']!, _dpopPrivateKeyMeta), 231 + dpopPrivateKey.isAcceptableOrUnknown( 232 + data['dpop_private_key']!, 233 + _dpopPrivateKeyMeta, 234 + ), 179 235 ); 180 236 } 181 237 if (data.containsKey('dpop_nonce')) { 182 - context.handle(_dpopNonceMeta, dpopNonce.isAcceptableOrUnknown(data['dpop_nonce']!, _dpopNonceMeta)); 238 + context.handle( 239 + _dpopNonceMeta, 240 + dpopNonce.isAcceptableOrUnknown(data['dpop_nonce']!, _dpopNonceMeta), 241 + ); 183 242 } 184 243 if (data.containsKey('expires_at')) { 185 - context.handle(_expiresAtMeta, expiresAt.isAcceptableOrUnknown(data['expires_at']!, _expiresAtMeta)); 244 + context.handle( 245 + _expiresAtMeta, 246 + expiresAt.isAcceptableOrUnknown(data['expires_at']!, _expiresAtMeta), 247 + ); 186 248 } 187 249 if (data.containsKey('created_at')) { 188 - context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); 250 + context.handle( 251 + _createdAtMeta, 252 + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), 253 + ); 189 254 } 190 255 if (data.containsKey('updated_at')) { 191 - context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); 256 + context.handle( 257 + _updatedAtMeta, 258 + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), 259 + ); 192 260 } 193 261 return context; 194 262 } ··· 199 267 Account map(Map<String, dynamic> data, {String? tablePrefix}) { 200 268 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 201 269 return Account( 202 - did: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}did'])!, 203 - handle: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}handle'])!, 204 - displayName: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}display_name']), 205 - service: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}service']), 206 - accessToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}access_token'])!, 207 - refreshToken: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}refresh_token']), 208 - dpopPublicKey: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}dpop_public_key']), 270 + did: attachedDatabase.typeMapping.read( 271 + DriftSqlType.string, 272 + data['${effectivePrefix}did'], 273 + )!, 274 + handle: attachedDatabase.typeMapping.read( 275 + DriftSqlType.string, 276 + data['${effectivePrefix}handle'], 277 + )!, 278 + displayName: attachedDatabase.typeMapping.read( 279 + DriftSqlType.string, 280 + data['${effectivePrefix}display_name'], 281 + ), 282 + service: attachedDatabase.typeMapping.read( 283 + DriftSqlType.string, 284 + data['${effectivePrefix}service'], 285 + ), 286 + accessToken: attachedDatabase.typeMapping.read( 287 + DriftSqlType.string, 288 + data['${effectivePrefix}access_token'], 289 + )!, 290 + refreshToken: attachedDatabase.typeMapping.read( 291 + DriftSqlType.string, 292 + data['${effectivePrefix}refresh_token'], 293 + ), 294 + dpopPublicKey: attachedDatabase.typeMapping.read( 295 + DriftSqlType.string, 296 + data['${effectivePrefix}dpop_public_key'], 297 + ), 209 298 dpopPrivateKey: attachedDatabase.typeMapping.read( 210 299 DriftSqlType.string, 211 300 data['${effectivePrefix}dpop_private_key'], 212 301 ), 213 - dpopNonce: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}dpop_nonce']), 214 - expiresAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}expires_at']), 215 - createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, 216 - updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, 302 + dpopNonce: attachedDatabase.typeMapping.read( 303 + DriftSqlType.string, 304 + data['${effectivePrefix}dpop_nonce'], 305 + ), 306 + expiresAt: attachedDatabase.typeMapping.read( 307 + DriftSqlType.dateTime, 308 + data['${effectivePrefix}expires_at'], 309 + ), 310 + createdAt: attachedDatabase.typeMapping.read( 311 + DriftSqlType.dateTime, 312 + data['${effectivePrefix}created_at'], 313 + )!, 314 + updatedAt: attachedDatabase.typeMapping.read( 315 + DriftSqlType.dateTime, 316 + data['${effectivePrefix}updated_at'], 317 + )!, 217 318 ); 218 319 } 219 320 ··· 286 387 return AccountsCompanion( 287 388 did: Value(did), 288 389 handle: Value(handle), 289 - displayName: displayName == null && nullToAbsent ? const Value.absent() : Value(displayName), 290 - service: service == null && nullToAbsent ? const Value.absent() : Value(service), 390 + displayName: displayName == null && nullToAbsent 391 + ? const Value.absent() 392 + : Value(displayName), 393 + service: service == null && nullToAbsent 394 + ? const Value.absent() 395 + : Value(service), 291 396 accessToken: Value(accessToken), 292 - refreshToken: refreshToken == null && nullToAbsent ? const Value.absent() : Value(refreshToken), 293 - dpopPublicKey: dpopPublicKey == null && nullToAbsent ? const Value.absent() : Value(dpopPublicKey), 294 - dpopPrivateKey: dpopPrivateKey == null && nullToAbsent ? const Value.absent() : Value(dpopPrivateKey), 295 - dpopNonce: dpopNonce == null && nullToAbsent ? const Value.absent() : Value(dpopNonce), 296 - expiresAt: expiresAt == null && nullToAbsent ? const Value.absent() : Value(expiresAt), 397 + refreshToken: refreshToken == null && nullToAbsent 398 + ? const Value.absent() 399 + : Value(refreshToken), 400 + dpopPublicKey: dpopPublicKey == null && nullToAbsent 401 + ? const Value.absent() 402 + : Value(dpopPublicKey), 403 + dpopPrivateKey: dpopPrivateKey == null && nullToAbsent 404 + ? const Value.absent() 405 + : Value(dpopPrivateKey), 406 + dpopNonce: dpopNonce == null && nullToAbsent 407 + ? const Value.absent() 408 + : Value(dpopNonce), 409 + expiresAt: expiresAt == null && nullToAbsent 410 + ? const Value.absent() 411 + : Value(expiresAt), 297 412 createdAt: Value(createdAt), 298 413 updatedAt: Value(updatedAt), 299 414 ); 300 415 } 301 416 302 - factory Account.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 417 + factory Account.fromJson( 418 + Map<String, dynamic> json, { 419 + ValueSerializer? serializer, 420 + }) { 303 421 serializer ??= driftRuntimeOptions.defaultSerializer; 304 422 return Account( 305 423 did: serializer.fromJson<String>(json['did']), ··· 355 473 service: service.present ? service.value : this.service, 356 474 accessToken: accessToken ?? this.accessToken, 357 475 refreshToken: refreshToken.present ? refreshToken.value : this.refreshToken, 358 - dpopPublicKey: dpopPublicKey.present ? dpopPublicKey.value : this.dpopPublicKey, 359 - dpopPrivateKey: dpopPrivateKey.present ? dpopPrivateKey.value : this.dpopPrivateKey, 476 + dpopPublicKey: dpopPublicKey.present 477 + ? dpopPublicKey.value 478 + : this.dpopPublicKey, 479 + dpopPrivateKey: dpopPrivateKey.present 480 + ? dpopPrivateKey.value 481 + : this.dpopPrivateKey, 360 482 dpopNonce: dpopNonce.present ? dpopNonce.value : this.dpopNonce, 361 483 expiresAt: expiresAt.present ? expiresAt.value : this.expiresAt, 362 484 createdAt: createdAt ?? this.createdAt, ··· 366 488 return Account( 367 489 did: data.did.present ? data.did.value : this.did, 368 490 handle: data.handle.present ? data.handle.value : this.handle, 369 - displayName: data.displayName.present ? data.displayName.value : this.displayName, 491 + displayName: data.displayName.present 492 + ? data.displayName.value 493 + : this.displayName, 370 494 service: data.service.present ? data.service.value : this.service, 371 - accessToken: data.accessToken.present ? data.accessToken.value : this.accessToken, 372 - refreshToken: data.refreshToken.present ? data.refreshToken.value : this.refreshToken, 373 - dpopPublicKey: data.dpopPublicKey.present ? data.dpopPublicKey.value : this.dpopPublicKey, 374 - dpopPrivateKey: data.dpopPrivateKey.present ? data.dpopPrivateKey.value : this.dpopPrivateKey, 495 + accessToken: data.accessToken.present 496 + ? data.accessToken.value 497 + : this.accessToken, 498 + refreshToken: data.refreshToken.present 499 + ? data.refreshToken.value 500 + : this.refreshToken, 501 + dpopPublicKey: data.dpopPublicKey.present 502 + ? data.dpopPublicKey.value 503 + : this.dpopPublicKey, 504 + dpopPrivateKey: data.dpopPrivateKey.present 505 + ? data.dpopPrivateKey.value 506 + : this.dpopPrivateKey, 375 507 dpopNonce: data.dpopNonce.present ? data.dpopNonce.value : this.dpopNonce, 376 508 expiresAt: data.expiresAt.present ? data.expiresAt.value : this.expiresAt, 377 509 createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ··· 607 739 } 608 740 } 609 741 610 - class $CachedProfilesTable extends CachedProfiles with TableInfo<$CachedProfilesTable, CachedProfile> { 742 + class $CachedProfilesTable extends CachedProfiles 743 + with TableInfo<$CachedProfilesTable, CachedProfile> { 611 744 @override 612 745 final GeneratedDatabase attachedDatabase; 613 746 final String? _alias; ··· 630 763 type: DriftSqlType.string, 631 764 requiredDuringInsert: true, 632 765 ); 633 - static const VerificationMeta _payloadMeta = const VerificationMeta('payload'); 766 + static const VerificationMeta _payloadMeta = const VerificationMeta( 767 + 'payload', 768 + ); 634 769 @override 635 770 late final GeneratedColumn<String> payload = GeneratedColumn<String>( 636 771 'payload', ··· 639 774 type: DriftSqlType.string, 640 775 requiredDuringInsert: true, 641 776 ); 642 - static const VerificationMeta _fetchedAtMeta = const VerificationMeta('fetchedAt'); 777 + static const VerificationMeta _fetchedAtMeta = const VerificationMeta( 778 + 'fetchedAt', 779 + ); 643 780 @override 644 781 late final GeneratedColumn<DateTime> fetchedAt = GeneratedColumn<DateTime>( 645 782 'fetched_at', ··· 657 794 String get actualTableName => $name; 658 795 static const String $name = 'cached_profiles'; 659 796 @override 660 - VerificationContext validateIntegrity(Insertable<CachedProfile> instance, {bool isInserting = false}) { 797 + VerificationContext validateIntegrity( 798 + Insertable<CachedProfile> instance, { 799 + bool isInserting = false, 800 + }) { 661 801 final context = VerificationContext(); 662 802 final data = instance.toColumns(true); 663 803 if (data.containsKey('did')) { 664 - context.handle(_didMeta, did.isAcceptableOrUnknown(data['did']!, _didMeta)); 804 + context.handle( 805 + _didMeta, 806 + did.isAcceptableOrUnknown(data['did']!, _didMeta), 807 + ); 665 808 } else if (isInserting) { 666 809 context.missing(_didMeta); 667 810 } 668 811 if (data.containsKey('handle')) { 669 - context.handle(_handleMeta, handle.isAcceptableOrUnknown(data['handle']!, _handleMeta)); 812 + context.handle( 813 + _handleMeta, 814 + handle.isAcceptableOrUnknown(data['handle']!, _handleMeta), 815 + ); 670 816 } else if (isInserting) { 671 817 context.missing(_handleMeta); 672 818 } 673 819 if (data.containsKey('payload')) { 674 - context.handle(_payloadMeta, payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta)); 820 + context.handle( 821 + _payloadMeta, 822 + payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta), 823 + ); 675 824 } else if (isInserting) { 676 825 context.missing(_payloadMeta); 677 826 } 678 827 if (data.containsKey('fetched_at')) { 679 - context.handle(_fetchedAtMeta, fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta)); 828 + context.handle( 829 + _fetchedAtMeta, 830 + fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta), 831 + ); 680 832 } 681 833 return context; 682 834 } ··· 687 839 CachedProfile map(Map<String, dynamic> data, {String? tablePrefix}) { 688 840 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 689 841 return CachedProfile( 690 - did: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}did'])!, 691 - handle: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}handle'])!, 692 - payload: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}payload'])!, 693 - fetchedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}fetched_at'])!, 842 + did: attachedDatabase.typeMapping.read( 843 + DriftSqlType.string, 844 + data['${effectivePrefix}did'], 845 + )!, 846 + handle: attachedDatabase.typeMapping.read( 847 + DriftSqlType.string, 848 + data['${effectivePrefix}handle'], 849 + )!, 850 + payload: attachedDatabase.typeMapping.read( 851 + DriftSqlType.string, 852 + data['${effectivePrefix}payload'], 853 + )!, 854 + fetchedAt: attachedDatabase.typeMapping.read( 855 + DriftSqlType.dateTime, 856 + data['${effectivePrefix}fetched_at'], 857 + )!, 694 858 ); 695 859 } 696 860 ··· 705 869 final String handle; 706 870 final String payload; 707 871 final DateTime fetchedAt; 708 - const CachedProfile({required this.did, required this.handle, required this.payload, required this.fetchedAt}); 872 + const CachedProfile({ 873 + required this.did, 874 + required this.handle, 875 + required this.payload, 876 + required this.fetchedAt, 877 + }); 709 878 @override 710 879 Map<String, Expression> toColumns(bool nullToAbsent) { 711 880 final map = <String, Expression>{}; ··· 725 894 ); 726 895 } 727 896 728 - factory CachedProfile.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 897 + factory CachedProfile.fromJson( 898 + Map<String, dynamic> json, { 899 + ValueSerializer? serializer, 900 + }) { 729 901 serializer ??= driftRuntimeOptions.defaultSerializer; 730 902 return CachedProfile( 731 903 did: serializer.fromJson<String>(json['did']), ··· 745 917 }; 746 918 } 747 919 748 - CachedProfile copyWith({String? did, String? handle, String? payload, DateTime? fetchedAt}) => CachedProfile( 920 + CachedProfile copyWith({ 921 + String? did, 922 + String? handle, 923 + String? payload, 924 + DateTime? fetchedAt, 925 + }) => CachedProfile( 749 926 did: did ?? this.did, 750 927 handle: handle ?? this.handle, 751 928 payload: payload ?? this.payload, ··· 871 1048 } 872 1049 } 873 1050 874 - class $CachedPostsTable extends CachedPosts with TableInfo<$CachedPostsTable, CachedPost> { 1051 + class $CachedPostsTable extends CachedPosts 1052 + with TableInfo<$CachedPostsTable, CachedPost> { 875 1053 @override 876 1054 final GeneratedDatabase attachedDatabase; 877 1055 final String? _alias; ··· 885 1063 type: DriftSqlType.string, 886 1064 requiredDuringInsert: true, 887 1065 ); 888 - static const VerificationMeta _authorDidMeta = const VerificationMeta('authorDid'); 1066 + static const VerificationMeta _authorDidMeta = const VerificationMeta( 1067 + 'authorDid', 1068 + ); 889 1069 @override 890 1070 late final GeneratedColumn<String> authorDid = GeneratedColumn<String>( 891 1071 'author_did', ··· 894 1074 type: DriftSqlType.string, 895 1075 requiredDuringInsert: true, 896 1076 ); 897 - static const VerificationMeta _payloadMeta = const VerificationMeta('payload'); 1077 + static const VerificationMeta _payloadMeta = const VerificationMeta( 1078 + 'payload', 1079 + ); 898 1080 @override 899 1081 late final GeneratedColumn<String> payload = GeneratedColumn<String>( 900 1082 'payload', ··· 903 1085 type: DriftSqlType.string, 904 1086 requiredDuringInsert: true, 905 1087 ); 906 - static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); 1088 + static const VerificationMeta _createdAtMeta = const VerificationMeta( 1089 + 'createdAt', 1090 + ); 907 1091 @override 908 1092 late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( 909 1093 'created_at', ··· 912 1096 type: DriftSqlType.dateTime, 913 1097 requiredDuringInsert: false, 914 1098 ); 915 - static const VerificationMeta _fetchedAtMeta = const VerificationMeta('fetchedAt'); 1099 + static const VerificationMeta _fetchedAtMeta = const VerificationMeta( 1100 + 'fetchedAt', 1101 + ); 916 1102 @override 917 1103 late final GeneratedColumn<DateTime> fetchedAt = GeneratedColumn<DateTime>( 918 1104 'fetched_at', ··· 923 1109 defaultValue: currentDateAndTime, 924 1110 ); 925 1111 @override 926 - List<GeneratedColumn> get $columns => [uri, authorDid, payload, createdAt, fetchedAt]; 1112 + List<GeneratedColumn> get $columns => [ 1113 + uri, 1114 + authorDid, 1115 + payload, 1116 + createdAt, 1117 + fetchedAt, 1118 + ]; 927 1119 @override 928 1120 String get aliasedName => _alias ?? actualTableName; 929 1121 @override 930 1122 String get actualTableName => $name; 931 1123 static const String $name = 'cached_posts'; 932 1124 @override 933 - VerificationContext validateIntegrity(Insertable<CachedPost> instance, {bool isInserting = false}) { 1125 + VerificationContext validateIntegrity( 1126 + Insertable<CachedPost> instance, { 1127 + bool isInserting = false, 1128 + }) { 934 1129 final context = VerificationContext(); 935 1130 final data = instance.toColumns(true); 936 1131 if (data.containsKey('uri')) { 937 - context.handle(_uriMeta, uri.isAcceptableOrUnknown(data['uri']!, _uriMeta)); 1132 + context.handle( 1133 + _uriMeta, 1134 + uri.isAcceptableOrUnknown(data['uri']!, _uriMeta), 1135 + ); 938 1136 } else if (isInserting) { 939 1137 context.missing(_uriMeta); 940 1138 } 941 1139 if (data.containsKey('author_did')) { 942 - context.handle(_authorDidMeta, authorDid.isAcceptableOrUnknown(data['author_did']!, _authorDidMeta)); 1140 + context.handle( 1141 + _authorDidMeta, 1142 + authorDid.isAcceptableOrUnknown(data['author_did']!, _authorDidMeta), 1143 + ); 943 1144 } else if (isInserting) { 944 1145 context.missing(_authorDidMeta); 945 1146 } 946 1147 if (data.containsKey('payload')) { 947 - context.handle(_payloadMeta, payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta)); 1148 + context.handle( 1149 + _payloadMeta, 1150 + payload.isAcceptableOrUnknown(data['payload']!, _payloadMeta), 1151 + ); 948 1152 } else if (isInserting) { 949 1153 context.missing(_payloadMeta); 950 1154 } 951 1155 if (data.containsKey('created_at')) { 952 - context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); 1156 + context.handle( 1157 + _createdAtMeta, 1158 + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), 1159 + ); 953 1160 } 954 1161 if (data.containsKey('fetched_at')) { 955 - context.handle(_fetchedAtMeta, fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta)); 1162 + context.handle( 1163 + _fetchedAtMeta, 1164 + fetchedAt.isAcceptableOrUnknown(data['fetched_at']!, _fetchedAtMeta), 1165 + ); 956 1166 } 957 1167 return context; 958 1168 } ··· 963 1173 CachedPost map(Map<String, dynamic> data, {String? tablePrefix}) { 964 1174 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 965 1175 return CachedPost( 966 - uri: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}uri'])!, 967 - authorDid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}author_did'])!, 968 - payload: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}payload'])!, 969 - createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), 970 - fetchedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}fetched_at'])!, 1176 + uri: attachedDatabase.typeMapping.read( 1177 + DriftSqlType.string, 1178 + data['${effectivePrefix}uri'], 1179 + )!, 1180 + authorDid: attachedDatabase.typeMapping.read( 1181 + DriftSqlType.string, 1182 + data['${effectivePrefix}author_did'], 1183 + )!, 1184 + payload: attachedDatabase.typeMapping.read( 1185 + DriftSqlType.string, 1186 + data['${effectivePrefix}payload'], 1187 + )!, 1188 + createdAt: attachedDatabase.typeMapping.read( 1189 + DriftSqlType.dateTime, 1190 + data['${effectivePrefix}created_at'], 1191 + ), 1192 + fetchedAt: attachedDatabase.typeMapping.read( 1193 + DriftSqlType.dateTime, 1194 + data['${effectivePrefix}fetched_at'], 1195 + )!, 971 1196 ); 972 1197 } 973 1198 ··· 1008 1233 uri: Value(uri), 1009 1234 authorDid: Value(authorDid), 1010 1235 payload: Value(payload), 1011 - createdAt: createdAt == null && nullToAbsent ? const Value.absent() : Value(createdAt), 1236 + createdAt: createdAt == null && nullToAbsent 1237 + ? const Value.absent() 1238 + : Value(createdAt), 1012 1239 fetchedAt: Value(fetchedAt), 1013 1240 ); 1014 1241 } 1015 1242 1016 - factory CachedPost.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 1243 + factory CachedPost.fromJson( 1244 + Map<String, dynamic> json, { 1245 + ValueSerializer? serializer, 1246 + }) { 1017 1247 serializer ??= driftRuntimeOptions.defaultSerializer; 1018 1248 return CachedPost( 1019 1249 uri: serializer.fromJson<String>(json['uri']), ··· 1071 1301 } 1072 1302 1073 1303 @override 1074 - int get hashCode => Object.hash(uri, authorDid, payload, createdAt, fetchedAt); 1304 + int get hashCode => 1305 + Object.hash(uri, authorDid, payload, createdAt, fetchedAt); 1075 1306 @override 1076 1307 bool operator ==(Object other) => 1077 1308 identical(this, other) || ··· 1182 1413 } 1183 1414 } 1184 1415 1185 - class $SettingsTable extends Settings with TableInfo<$SettingsTable, SettingsEntry> { 1416 + class $SettingsTable extends Settings 1417 + with TableInfo<$SettingsTable, SettingsEntry> { 1186 1418 @override 1187 1419 final GeneratedDatabase attachedDatabase; 1188 1420 final String? _alias; ··· 1205 1437 type: DriftSqlType.string, 1206 1438 requiredDuringInsert: true, 1207 1439 ); 1208 - static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); 1440 + static const VerificationMeta _updatedAtMeta = const VerificationMeta( 1441 + 'updatedAt', 1442 + ); 1209 1443 @override 1210 1444 late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 1211 1445 'updated_at', ··· 1223 1457 String get actualTableName => $name; 1224 1458 static const String $name = 'settings'; 1225 1459 @override 1226 - VerificationContext validateIntegrity(Insertable<SettingsEntry> instance, {bool isInserting = false}) { 1460 + VerificationContext validateIntegrity( 1461 + Insertable<SettingsEntry> instance, { 1462 + bool isInserting = false, 1463 + }) { 1227 1464 final context = VerificationContext(); 1228 1465 final data = instance.toColumns(true); 1229 1466 if (data.containsKey('key')) { 1230 - context.handle(_keyMeta, key.isAcceptableOrUnknown(data['key']!, _keyMeta)); 1467 + context.handle( 1468 + _keyMeta, 1469 + key.isAcceptableOrUnknown(data['key']!, _keyMeta), 1470 + ); 1231 1471 } else if (isInserting) { 1232 1472 context.missing(_keyMeta); 1233 1473 } 1234 1474 if (data.containsKey('value')) { 1235 - context.handle(_valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); 1475 + context.handle( 1476 + _valueMeta, 1477 + value.isAcceptableOrUnknown(data['value']!, _valueMeta), 1478 + ); 1236 1479 } else if (isInserting) { 1237 1480 context.missing(_valueMeta); 1238 1481 } 1239 1482 if (data.containsKey('updated_at')) { 1240 - context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); 1483 + context.handle( 1484 + _updatedAtMeta, 1485 + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), 1486 + ); 1241 1487 } 1242 1488 return context; 1243 1489 } ··· 1248 1494 SettingsEntry map(Map<String, dynamic> data, {String? tablePrefix}) { 1249 1495 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 1250 1496 return SettingsEntry( 1251 - key: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}key'])!, 1252 - value: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}value'])!, 1253 - updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, 1497 + key: attachedDatabase.typeMapping.read( 1498 + DriftSqlType.string, 1499 + data['${effectivePrefix}key'], 1500 + )!, 1501 + value: attachedDatabase.typeMapping.read( 1502 + DriftSqlType.string, 1503 + data['${effectivePrefix}value'], 1504 + )!, 1505 + updatedAt: attachedDatabase.typeMapping.read( 1506 + DriftSqlType.dateTime, 1507 + data['${effectivePrefix}updated_at'], 1508 + )!, 1254 1509 ); 1255 1510 } 1256 1511 ··· 1264 1519 final String key; 1265 1520 final String value; 1266 1521 final DateTime updatedAt; 1267 - const SettingsEntry({required this.key, required this.value, required this.updatedAt}); 1522 + const SettingsEntry({ 1523 + required this.key, 1524 + required this.value, 1525 + required this.updatedAt, 1526 + }); 1268 1527 @override 1269 1528 Map<String, Expression> toColumns(bool nullToAbsent) { 1270 1529 final map = <String, Expression>{}; ··· 1275 1534 } 1276 1535 1277 1536 SettingsCompanion toCompanion(bool nullToAbsent) { 1278 - return SettingsCompanion(key: Value(key), value: Value(value), updatedAt: Value(updatedAt)); 1537 + return SettingsCompanion( 1538 + key: Value(key), 1539 + value: Value(value), 1540 + updatedAt: Value(updatedAt), 1541 + ); 1279 1542 } 1280 1543 1281 - factory SettingsEntry.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 1544 + factory SettingsEntry.fromJson( 1545 + Map<String, dynamic> json, { 1546 + ValueSerializer? serializer, 1547 + }) { 1282 1548 serializer ??= driftRuntimeOptions.defaultSerializer; 1283 1549 return SettingsEntry( 1284 1550 key: serializer.fromJson<String>(json['key']), ··· 1297 1563 } 1298 1564 1299 1565 SettingsEntry copyWith({String? key, String? value, DateTime? updatedAt}) => 1300 - SettingsEntry(key: key ?? this.key, value: value ?? this.value, updatedAt: updatedAt ?? this.updatedAt); 1566 + SettingsEntry( 1567 + key: key ?? this.key, 1568 + value: value ?? this.value, 1569 + updatedAt: updatedAt ?? this.updatedAt, 1570 + ); 1301 1571 SettingsEntry copyWithCompanion(SettingsCompanion data) { 1302 1572 return SettingsEntry( 1303 1573 key: data.key.present ? data.key.value : this.key, ··· 1403 1673 } 1404 1674 } 1405 1675 1406 - class $SavedFeedsTable extends SavedFeeds with TableInfo<$SavedFeedsTable, SavedFeedEntry> { 1676 + class $SavedFeedsTable extends SavedFeeds 1677 + with TableInfo<$SavedFeedsTable, SavedFeedEntry> { 1407 1678 @override 1408 1679 final GeneratedDatabase attachedDatabase; 1409 1680 final String? _alias; ··· 1417 1688 type: DriftSqlType.string, 1418 1689 requiredDuringInsert: true, 1419 1690 ); 1420 - static const VerificationMeta _accountDidMeta = const VerificationMeta('accountDid'); 1691 + static const VerificationMeta _accountDidMeta = const VerificationMeta( 1692 + 'accountDid', 1693 + ); 1421 1694 @override 1422 1695 late final GeneratedColumn<String> accountDid = GeneratedColumn<String>( 1423 1696 'account_did', ··· 1452 1725 false, 1453 1726 type: DriftSqlType.bool, 1454 1727 requiredDuringInsert: false, 1455 - defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), 1728 + defaultConstraints: GeneratedColumn.constraintIsAlways( 1729 + 'CHECK ("pinned" IN (0, 1))', 1730 + ), 1456 1731 defaultValue: const Constant(false), 1457 1732 ); 1458 - static const VerificationMeta _sortOrderMeta = const VerificationMeta('sortOrder'); 1733 + static const VerificationMeta _sortOrderMeta = const VerificationMeta( 1734 + 'sortOrder', 1735 + ); 1459 1736 @override 1460 1737 late final GeneratedColumn<int> sortOrder = GeneratedColumn<int>( 1461 1738 'sort_order', ··· 1465 1742 requiredDuringInsert: false, 1466 1743 defaultValue: const Constant(0), 1467 1744 ); 1468 - static const VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); 1745 + static const VerificationMeta _updatedAtMeta = const VerificationMeta( 1746 + 'updatedAt', 1747 + ); 1469 1748 @override 1470 1749 late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 1471 1750 'updated_at', ··· 1476 1755 defaultValue: currentDateAndTime, 1477 1756 ); 1478 1757 @override 1479 - List<GeneratedColumn> get $columns => [id, accountDid, type, value, pinned, sortOrder, updatedAt]; 1758 + List<GeneratedColumn> get $columns => [ 1759 + id, 1760 + accountDid, 1761 + type, 1762 + value, 1763 + pinned, 1764 + sortOrder, 1765 + updatedAt, 1766 + ]; 1480 1767 @override 1481 1768 String get aliasedName => _alias ?? actualTableName; 1482 1769 @override 1483 1770 String get actualTableName => $name; 1484 1771 static const String $name = 'saved_feeds'; 1485 1772 @override 1486 - VerificationContext validateIntegrity(Insertable<SavedFeedEntry> instance, {bool isInserting = false}) { 1773 + VerificationContext validateIntegrity( 1774 + Insertable<SavedFeedEntry> instance, { 1775 + bool isInserting = false, 1776 + }) { 1487 1777 final context = VerificationContext(); 1488 1778 final data = instance.toColumns(true); 1489 1779 if (data.containsKey('id')) { ··· 1492 1782 context.missing(_idMeta); 1493 1783 } 1494 1784 if (data.containsKey('account_did')) { 1495 - context.handle(_accountDidMeta, accountDid.isAcceptableOrUnknown(data['account_did']!, _accountDidMeta)); 1785 + context.handle( 1786 + _accountDidMeta, 1787 + accountDid.isAcceptableOrUnknown(data['account_did']!, _accountDidMeta), 1788 + ); 1496 1789 } else if (isInserting) { 1497 1790 context.missing(_accountDidMeta); 1498 1791 } 1499 1792 if (data.containsKey('type')) { 1500 - context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); 1793 + context.handle( 1794 + _typeMeta, 1795 + type.isAcceptableOrUnknown(data['type']!, _typeMeta), 1796 + ); 1501 1797 } else if (isInserting) { 1502 1798 context.missing(_typeMeta); 1503 1799 } 1504 1800 if (data.containsKey('value')) { 1505 - context.handle(_valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); 1801 + context.handle( 1802 + _valueMeta, 1803 + value.isAcceptableOrUnknown(data['value']!, _valueMeta), 1804 + ); 1506 1805 } else if (isInserting) { 1507 1806 context.missing(_valueMeta); 1508 1807 } 1509 1808 if (data.containsKey('pinned')) { 1510 - context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); 1809 + context.handle( 1810 + _pinnedMeta, 1811 + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta), 1812 + ); 1511 1813 } 1512 1814 if (data.containsKey('sort_order')) { 1513 - context.handle(_sortOrderMeta, sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta)); 1815 + context.handle( 1816 + _sortOrderMeta, 1817 + sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta), 1818 + ); 1514 1819 } 1515 1820 if (data.containsKey('updated_at')) { 1516 - context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); 1821 + context.handle( 1822 + _updatedAtMeta, 1823 + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), 1824 + ); 1517 1825 } 1518 1826 return context; 1519 1827 } ··· 1524 1832 SavedFeedEntry map(Map<String, dynamic> data, {String? tablePrefix}) { 1525 1833 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 1526 1834 return SavedFeedEntry( 1527 - id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, 1528 - accountDid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}account_did'])!, 1529 - type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, 1530 - value: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}value'])!, 1531 - pinned: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, 1532 - sortOrder: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}sort_order'])!, 1533 - updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, 1835 + id: attachedDatabase.typeMapping.read( 1836 + DriftSqlType.string, 1837 + data['${effectivePrefix}id'], 1838 + )!, 1839 + accountDid: attachedDatabase.typeMapping.read( 1840 + DriftSqlType.string, 1841 + data['${effectivePrefix}account_did'], 1842 + )!, 1843 + type: attachedDatabase.typeMapping.read( 1844 + DriftSqlType.string, 1845 + data['${effectivePrefix}type'], 1846 + )!, 1847 + value: attachedDatabase.typeMapping.read( 1848 + DriftSqlType.string, 1849 + data['${effectivePrefix}value'], 1850 + )!, 1851 + pinned: attachedDatabase.typeMapping.read( 1852 + DriftSqlType.bool, 1853 + data['${effectivePrefix}pinned'], 1854 + )!, 1855 + sortOrder: attachedDatabase.typeMapping.read( 1856 + DriftSqlType.int, 1857 + data['${effectivePrefix}sort_order'], 1858 + )!, 1859 + updatedAt: attachedDatabase.typeMapping.read( 1860 + DriftSqlType.dateTime, 1861 + data['${effectivePrefix}updated_at'], 1862 + )!, 1534 1863 ); 1535 1864 } 1536 1865 ··· 1582 1911 ); 1583 1912 } 1584 1913 1585 - factory SavedFeedEntry.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 1914 + factory SavedFeedEntry.fromJson( 1915 + Map<String, dynamic> json, { 1916 + ValueSerializer? serializer, 1917 + }) { 1586 1918 serializer ??= driftRuntimeOptions.defaultSerializer; 1587 1919 return SavedFeedEntry( 1588 1920 id: serializer.fromJson<String>(json['id']), ··· 1628 1960 SavedFeedEntry copyWithCompanion(SavedFeedsCompanion data) { 1629 1961 return SavedFeedEntry( 1630 1962 id: data.id.present ? data.id.value : this.id, 1631 - accountDid: data.accountDid.present ? data.accountDid.value : this.accountDid, 1963 + accountDid: data.accountDid.present 1964 + ? data.accountDid.value 1965 + : this.accountDid, 1632 1966 type: data.type.present ? data.type.value : this.type, 1633 1967 value: data.value.present ? data.value.value : this.value, 1634 1968 pinned: data.pinned.present ? data.pinned.value : this.pinned, ··· 1652 1986 } 1653 1987 1654 1988 @override 1655 - int get hashCode => Object.hash(id, accountDid, type, value, pinned, sortOrder, updatedAt); 1989 + int get hashCode => 1990 + Object.hash(id, accountDid, type, value, pinned, sortOrder, updatedAt); 1656 1991 @override 1657 1992 bool operator ==(Object other) => 1658 1993 identical(this, other) || ··· 1788 2123 } 1789 2124 } 1790 2125 1791 - class $SearchHistoryTable extends SearchHistory with TableInfo<$SearchHistoryTable, SearchHistoryEntry> { 2126 + class $SearchHistoryTable extends SearchHistory 2127 + with TableInfo<$SearchHistoryTable, SearchHistoryEntry> { 1792 2128 @override 1793 2129 final GeneratedDatabase attachedDatabase; 1794 2130 final String? _alias; ··· 1802 2138 hasAutoIncrement: true, 1803 2139 type: DriftSqlType.int, 1804 2140 requiredDuringInsert: false, 1805 - defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'), 2141 + defaultConstraints: GeneratedColumn.constraintIsAlways( 2142 + 'PRIMARY KEY AUTOINCREMENT', 2143 + ), 1806 2144 ); 1807 2145 static const VerificationMeta _queryMeta = const VerificationMeta('query'); 1808 2146 @override ··· 1822 2160 type: DriftSqlType.string, 1823 2161 requiredDuringInsert: true, 1824 2162 ); 1825 - static const VerificationMeta _searchedAtMeta = const VerificationMeta('searchedAt'); 2163 + static const VerificationMeta _searchedAtMeta = const VerificationMeta( 2164 + 'searchedAt', 2165 + ); 1826 2166 @override 1827 2167 late final GeneratedColumn<DateTime> searchedAt = GeneratedColumn<DateTime>( 1828 2168 'searched_at', ··· 1832 2172 requiredDuringInsert: false, 1833 2173 defaultValue: currentDateAndTime, 1834 2174 ); 1835 - static const VerificationMeta _accountDidMeta = const VerificationMeta('accountDid'); 2175 + static const VerificationMeta _accountDidMeta = const VerificationMeta( 2176 + 'accountDid', 2177 + ); 1836 2178 @override 1837 2179 late final GeneratedColumn<String> accountDid = GeneratedColumn<String>( 1838 2180 'account_did', ··· 1842 2184 requiredDuringInsert: true, 1843 2185 ); 1844 2186 @override 1845 - List<GeneratedColumn> get $columns => [id, query, type, searchedAt, accountDid]; 2187 + List<GeneratedColumn> get $columns => [ 2188 + id, 2189 + query, 2190 + type, 2191 + searchedAt, 2192 + accountDid, 2193 + ]; 1846 2194 @override 1847 2195 String get aliasedName => _alias ?? actualTableName; 1848 2196 @override 1849 2197 String get actualTableName => $name; 1850 2198 static const String $name = 'search_history'; 1851 2199 @override 1852 - VerificationContext validateIntegrity(Insertable<SearchHistoryEntry> instance, {bool isInserting = false}) { 2200 + VerificationContext validateIntegrity( 2201 + Insertable<SearchHistoryEntry> instance, { 2202 + bool isInserting = false, 2203 + }) { 1853 2204 final context = VerificationContext(); 1854 2205 final data = instance.toColumns(true); 1855 2206 if (data.containsKey('id')) { 1856 2207 context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); 1857 2208 } 1858 2209 if (data.containsKey('query')) { 1859 - context.handle(_queryMeta, query.isAcceptableOrUnknown(data['query']!, _queryMeta)); 2210 + context.handle( 2211 + _queryMeta, 2212 + query.isAcceptableOrUnknown(data['query']!, _queryMeta), 2213 + ); 1860 2214 } else if (isInserting) { 1861 2215 context.missing(_queryMeta); 1862 2216 } 1863 2217 if (data.containsKey('type')) { 1864 - context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); 2218 + context.handle( 2219 + _typeMeta, 2220 + type.isAcceptableOrUnknown(data['type']!, _typeMeta), 2221 + ); 1865 2222 } else if (isInserting) { 1866 2223 context.missing(_typeMeta); 1867 2224 } 1868 2225 if (data.containsKey('searched_at')) { 1869 - context.handle(_searchedAtMeta, searchedAt.isAcceptableOrUnknown(data['searched_at']!, _searchedAtMeta)); 2226 + context.handle( 2227 + _searchedAtMeta, 2228 + searchedAt.isAcceptableOrUnknown(data['searched_at']!, _searchedAtMeta), 2229 + ); 1870 2230 } 1871 2231 if (data.containsKey('account_did')) { 1872 - context.handle(_accountDidMeta, accountDid.isAcceptableOrUnknown(data['account_did']!, _accountDidMeta)); 2232 + context.handle( 2233 + _accountDidMeta, 2234 + accountDid.isAcceptableOrUnknown(data['account_did']!, _accountDidMeta), 2235 + ); 1873 2236 } else if (isInserting) { 1874 2237 context.missing(_accountDidMeta); 1875 2238 } ··· 1882 2245 SearchHistoryEntry map(Map<String, dynamic> data, {String? tablePrefix}) { 1883 2246 final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 1884 2247 return SearchHistoryEntry( 1885 - id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, 1886 - query: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}query'])!, 1887 - type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, 1888 - searchedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}searched_at'])!, 1889 - accountDid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}account_did'])!, 2248 + id: attachedDatabase.typeMapping.read( 2249 + DriftSqlType.int, 2250 + data['${effectivePrefix}id'], 2251 + )!, 2252 + query: attachedDatabase.typeMapping.read( 2253 + DriftSqlType.string, 2254 + data['${effectivePrefix}query'], 2255 + )!, 2256 + type: attachedDatabase.typeMapping.read( 2257 + DriftSqlType.string, 2258 + data['${effectivePrefix}type'], 2259 + )!, 2260 + searchedAt: attachedDatabase.typeMapping.read( 2261 + DriftSqlType.dateTime, 2262 + data['${effectivePrefix}searched_at'], 2263 + )!, 2264 + accountDid: attachedDatabase.typeMapping.read( 2265 + DriftSqlType.string, 2266 + data['${effectivePrefix}account_did'], 2267 + )!, 1890 2268 ); 1891 2269 } 1892 2270 ··· 1896 2274 } 1897 2275 } 1898 2276 1899 - class SearchHistoryEntry extends DataClass implements Insertable<SearchHistoryEntry> { 2277 + class SearchHistoryEntry extends DataClass 2278 + implements Insertable<SearchHistoryEntry> { 1900 2279 final int id; 1901 2280 final String query; 1902 2281 final String type; ··· 1930 2309 ); 1931 2310 } 1932 2311 1933 - factory SearchHistoryEntry.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) { 2312 + factory SearchHistoryEntry.fromJson( 2313 + Map<String, dynamic> json, { 2314 + ValueSerializer? serializer, 2315 + }) { 1934 2316 serializer ??= driftRuntimeOptions.defaultSerializer; 1935 2317 return SearchHistoryEntry( 1936 2318 id: serializer.fromJson<int>(json['id']), ··· 1952 2334 }; 1953 2335 } 1954 2336 1955 - SearchHistoryEntry copyWith({int? id, String? query, String? type, DateTime? searchedAt, String? accountDid}) => 1956 - SearchHistoryEntry( 1957 - id: id ?? this.id, 1958 - query: query ?? this.query, 1959 - type: type ?? this.type, 1960 - searchedAt: searchedAt ?? this.searchedAt, 1961 - accountDid: accountDid ?? this.accountDid, 1962 - ); 2337 + SearchHistoryEntry copyWith({ 2338 + int? id, 2339 + String? query, 2340 + String? type, 2341 + DateTime? searchedAt, 2342 + String? accountDid, 2343 + }) => SearchHistoryEntry( 2344 + id: id ?? this.id, 2345 + query: query ?? this.query, 2346 + type: type ?? this.type, 2347 + searchedAt: searchedAt ?? this.searchedAt, 2348 + accountDid: accountDid ?? this.accountDid, 2349 + ); 1963 2350 SearchHistoryEntry copyWithCompanion(SearchHistoryCompanion data) { 1964 2351 return SearchHistoryEntry( 1965 2352 id: data.id.present ? data.id.value : this.id, 1966 2353 query: data.query.present ? data.query.value : this.query, 1967 2354 type: data.type.present ? data.type.value : this.type, 1968 - searchedAt: data.searchedAt.present ? data.searchedAt.value : this.searchedAt, 1969 - accountDid: data.accountDid.present ? data.accountDid.value : this.accountDid, 2355 + searchedAt: data.searchedAt.present 2356 + ? data.searchedAt.value 2357 + : this.searchedAt, 2358 + accountDid: data.accountDid.present 2359 + ? data.accountDid.value 2360 + : this.accountDid, 1970 2361 ); 1971 2362 } 1972 2363 ··· 2083 2474 } 2084 2475 } 2085 2476 2477 + class $DraftsTable extends Drafts with TableInfo<$DraftsTable, DraftEntry> { 2478 + @override 2479 + final GeneratedDatabase attachedDatabase; 2480 + final String? _alias; 2481 + $DraftsTable(this.attachedDatabase, [this._alias]); 2482 + static const VerificationMeta _idMeta = const VerificationMeta('id'); 2483 + @override 2484 + late final GeneratedColumn<int> id = GeneratedColumn<int>( 2485 + 'id', 2486 + aliasedName, 2487 + false, 2488 + hasAutoIncrement: true, 2489 + type: DriftSqlType.int, 2490 + requiredDuringInsert: false, 2491 + defaultConstraints: GeneratedColumn.constraintIsAlways( 2492 + 'PRIMARY KEY AUTOINCREMENT', 2493 + ), 2494 + ); 2495 + static const VerificationMeta _accountDidMeta = const VerificationMeta( 2496 + 'accountDid', 2497 + ); 2498 + @override 2499 + late final GeneratedColumn<String> accountDid = GeneratedColumn<String>( 2500 + 'account_did', 2501 + aliasedName, 2502 + false, 2503 + type: DriftSqlType.string, 2504 + requiredDuringInsert: true, 2505 + ); 2506 + static const VerificationMeta _contentMeta = const VerificationMeta( 2507 + 'content', 2508 + ); 2509 + @override 2510 + late final GeneratedColumn<String> content = GeneratedColumn<String>( 2511 + 'content', 2512 + aliasedName, 2513 + false, 2514 + type: DriftSqlType.string, 2515 + requiredDuringInsert: true, 2516 + ); 2517 + static const VerificationMeta _replyUriMeta = const VerificationMeta( 2518 + 'replyUri', 2519 + ); 2520 + @override 2521 + late final GeneratedColumn<String> replyUri = GeneratedColumn<String>( 2522 + 'reply_uri', 2523 + aliasedName, 2524 + true, 2525 + type: DriftSqlType.string, 2526 + requiredDuringInsert: false, 2527 + ); 2528 + static const VerificationMeta _replyCidMeta = const VerificationMeta( 2529 + 'replyCid', 2530 + ); 2531 + @override 2532 + late final GeneratedColumn<String> replyCid = GeneratedColumn<String>( 2533 + 'reply_cid', 2534 + aliasedName, 2535 + true, 2536 + type: DriftSqlType.string, 2537 + requiredDuringInsert: false, 2538 + ); 2539 + static const VerificationMeta _rootUriMeta = const VerificationMeta( 2540 + 'rootUri', 2541 + ); 2542 + @override 2543 + late final GeneratedColumn<String> rootUri = GeneratedColumn<String>( 2544 + 'root_uri', 2545 + aliasedName, 2546 + true, 2547 + type: DriftSqlType.string, 2548 + requiredDuringInsert: false, 2549 + ); 2550 + static const VerificationMeta _rootCidMeta = const VerificationMeta( 2551 + 'rootCid', 2552 + ); 2553 + @override 2554 + late final GeneratedColumn<String> rootCid = GeneratedColumn<String>( 2555 + 'root_cid', 2556 + aliasedName, 2557 + true, 2558 + type: DriftSqlType.string, 2559 + requiredDuringInsert: false, 2560 + ); 2561 + static const VerificationMeta _embedJsonMeta = const VerificationMeta( 2562 + 'embedJson', 2563 + ); 2564 + @override 2565 + late final GeneratedColumn<String> embedJson = GeneratedColumn<String>( 2566 + 'embed_json', 2567 + aliasedName, 2568 + true, 2569 + type: DriftSqlType.string, 2570 + requiredDuringInsert: false, 2571 + ); 2572 + static const VerificationMeta _mediaPathsMeta = const VerificationMeta( 2573 + 'mediaPaths', 2574 + ); 2575 + @override 2576 + late final GeneratedColumn<String> mediaPaths = GeneratedColumn<String>( 2577 + 'media_paths', 2578 + aliasedName, 2579 + true, 2580 + type: DriftSqlType.string, 2581 + requiredDuringInsert: false, 2582 + ); 2583 + static const VerificationMeta _createdAtMeta = const VerificationMeta( 2584 + 'createdAt', 2585 + ); 2586 + @override 2587 + late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>( 2588 + 'created_at', 2589 + aliasedName, 2590 + false, 2591 + type: DriftSqlType.dateTime, 2592 + requiredDuringInsert: false, 2593 + defaultValue: currentDateAndTime, 2594 + ); 2595 + static const VerificationMeta _updatedAtMeta = const VerificationMeta( 2596 + 'updatedAt', 2597 + ); 2598 + @override 2599 + late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>( 2600 + 'updated_at', 2601 + aliasedName, 2602 + false, 2603 + type: DriftSqlType.dateTime, 2604 + requiredDuringInsert: false, 2605 + defaultValue: currentDateAndTime, 2606 + ); 2607 + static const VerificationMeta _scheduledAtMeta = const VerificationMeta( 2608 + 'scheduledAt', 2609 + ); 2610 + @override 2611 + late final GeneratedColumn<DateTime> scheduledAt = GeneratedColumn<DateTime>( 2612 + 'scheduled_at', 2613 + aliasedName, 2614 + true, 2615 + type: DriftSqlType.dateTime, 2616 + requiredDuringInsert: false, 2617 + ); 2618 + @override 2619 + List<GeneratedColumn> get $columns => [ 2620 + id, 2621 + accountDid, 2622 + content, 2623 + replyUri, 2624 + replyCid, 2625 + rootUri, 2626 + rootCid, 2627 + embedJson, 2628 + mediaPaths, 2629 + createdAt, 2630 + updatedAt, 2631 + scheduledAt, 2632 + ]; 2633 + @override 2634 + String get aliasedName => _alias ?? actualTableName; 2635 + @override 2636 + String get actualTableName => $name; 2637 + static const String $name = 'drafts'; 2638 + @override 2639 + VerificationContext validateIntegrity( 2640 + Insertable<DraftEntry> instance, { 2641 + bool isInserting = false, 2642 + }) { 2643 + final context = VerificationContext(); 2644 + final data = instance.toColumns(true); 2645 + if (data.containsKey('id')) { 2646 + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); 2647 + } 2648 + if (data.containsKey('account_did')) { 2649 + context.handle( 2650 + _accountDidMeta, 2651 + accountDid.isAcceptableOrUnknown(data['account_did']!, _accountDidMeta), 2652 + ); 2653 + } else if (isInserting) { 2654 + context.missing(_accountDidMeta); 2655 + } 2656 + if (data.containsKey('content')) { 2657 + context.handle( 2658 + _contentMeta, 2659 + content.isAcceptableOrUnknown(data['content']!, _contentMeta), 2660 + ); 2661 + } else if (isInserting) { 2662 + context.missing(_contentMeta); 2663 + } 2664 + if (data.containsKey('reply_uri')) { 2665 + context.handle( 2666 + _replyUriMeta, 2667 + replyUri.isAcceptableOrUnknown(data['reply_uri']!, _replyUriMeta), 2668 + ); 2669 + } 2670 + if (data.containsKey('reply_cid')) { 2671 + context.handle( 2672 + _replyCidMeta, 2673 + replyCid.isAcceptableOrUnknown(data['reply_cid']!, _replyCidMeta), 2674 + ); 2675 + } 2676 + if (data.containsKey('root_uri')) { 2677 + context.handle( 2678 + _rootUriMeta, 2679 + rootUri.isAcceptableOrUnknown(data['root_uri']!, _rootUriMeta), 2680 + ); 2681 + } 2682 + if (data.containsKey('root_cid')) { 2683 + context.handle( 2684 + _rootCidMeta, 2685 + rootCid.isAcceptableOrUnknown(data['root_cid']!, _rootCidMeta), 2686 + ); 2687 + } 2688 + if (data.containsKey('embed_json')) { 2689 + context.handle( 2690 + _embedJsonMeta, 2691 + embedJson.isAcceptableOrUnknown(data['embed_json']!, _embedJsonMeta), 2692 + ); 2693 + } 2694 + if (data.containsKey('media_paths')) { 2695 + context.handle( 2696 + _mediaPathsMeta, 2697 + mediaPaths.isAcceptableOrUnknown(data['media_paths']!, _mediaPathsMeta), 2698 + ); 2699 + } 2700 + if (data.containsKey('created_at')) { 2701 + context.handle( 2702 + _createdAtMeta, 2703 + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), 2704 + ); 2705 + } 2706 + if (data.containsKey('updated_at')) { 2707 + context.handle( 2708 + _updatedAtMeta, 2709 + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), 2710 + ); 2711 + } 2712 + if (data.containsKey('scheduled_at')) { 2713 + context.handle( 2714 + _scheduledAtMeta, 2715 + scheduledAt.isAcceptableOrUnknown( 2716 + data['scheduled_at']!, 2717 + _scheduledAtMeta, 2718 + ), 2719 + ); 2720 + } 2721 + return context; 2722 + } 2723 + 2724 + @override 2725 + Set<GeneratedColumn> get $primaryKey => {id}; 2726 + @override 2727 + DraftEntry map(Map<String, dynamic> data, {String? tablePrefix}) { 2728 + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 2729 + return DraftEntry( 2730 + id: attachedDatabase.typeMapping.read( 2731 + DriftSqlType.int, 2732 + data['${effectivePrefix}id'], 2733 + )!, 2734 + accountDid: attachedDatabase.typeMapping.read( 2735 + DriftSqlType.string, 2736 + data['${effectivePrefix}account_did'], 2737 + )!, 2738 + content: attachedDatabase.typeMapping.read( 2739 + DriftSqlType.string, 2740 + data['${effectivePrefix}content'], 2741 + )!, 2742 + replyUri: attachedDatabase.typeMapping.read( 2743 + DriftSqlType.string, 2744 + data['${effectivePrefix}reply_uri'], 2745 + ), 2746 + replyCid: attachedDatabase.typeMapping.read( 2747 + DriftSqlType.string, 2748 + data['${effectivePrefix}reply_cid'], 2749 + ), 2750 + rootUri: attachedDatabase.typeMapping.read( 2751 + DriftSqlType.string, 2752 + data['${effectivePrefix}root_uri'], 2753 + ), 2754 + rootCid: attachedDatabase.typeMapping.read( 2755 + DriftSqlType.string, 2756 + data['${effectivePrefix}root_cid'], 2757 + ), 2758 + embedJson: attachedDatabase.typeMapping.read( 2759 + DriftSqlType.string, 2760 + data['${effectivePrefix}embed_json'], 2761 + ), 2762 + mediaPaths: attachedDatabase.typeMapping.read( 2763 + DriftSqlType.string, 2764 + data['${effectivePrefix}media_paths'], 2765 + ), 2766 + createdAt: attachedDatabase.typeMapping.read( 2767 + DriftSqlType.dateTime, 2768 + data['${effectivePrefix}created_at'], 2769 + )!, 2770 + updatedAt: attachedDatabase.typeMapping.read( 2771 + DriftSqlType.dateTime, 2772 + data['${effectivePrefix}updated_at'], 2773 + )!, 2774 + scheduledAt: attachedDatabase.typeMapping.read( 2775 + DriftSqlType.dateTime, 2776 + data['${effectivePrefix}scheduled_at'], 2777 + ), 2778 + ); 2779 + } 2780 + 2781 + @override 2782 + $DraftsTable createAlias(String alias) { 2783 + return $DraftsTable(attachedDatabase, alias); 2784 + } 2785 + } 2786 + 2787 + class DraftEntry extends DataClass implements Insertable<DraftEntry> { 2788 + final int id; 2789 + final String accountDid; 2790 + final String content; 2791 + final String? replyUri; 2792 + final String? replyCid; 2793 + final String? rootUri; 2794 + final String? rootCid; 2795 + final String? embedJson; 2796 + final String? mediaPaths; 2797 + final DateTime createdAt; 2798 + final DateTime updatedAt; 2799 + final DateTime? scheduledAt; 2800 + const DraftEntry({ 2801 + required this.id, 2802 + required this.accountDid, 2803 + required this.content, 2804 + this.replyUri, 2805 + this.replyCid, 2806 + this.rootUri, 2807 + this.rootCid, 2808 + this.embedJson, 2809 + this.mediaPaths, 2810 + required this.createdAt, 2811 + required this.updatedAt, 2812 + this.scheduledAt, 2813 + }); 2814 + @override 2815 + Map<String, Expression> toColumns(bool nullToAbsent) { 2816 + final map = <String, Expression>{}; 2817 + map['id'] = Variable<int>(id); 2818 + map['account_did'] = Variable<String>(accountDid); 2819 + map['content'] = Variable<String>(content); 2820 + if (!nullToAbsent || replyUri != null) { 2821 + map['reply_uri'] = Variable<String>(replyUri); 2822 + } 2823 + if (!nullToAbsent || replyCid != null) { 2824 + map['reply_cid'] = Variable<String>(replyCid); 2825 + } 2826 + if (!nullToAbsent || rootUri != null) { 2827 + map['root_uri'] = Variable<String>(rootUri); 2828 + } 2829 + if (!nullToAbsent || rootCid != null) { 2830 + map['root_cid'] = Variable<String>(rootCid); 2831 + } 2832 + if (!nullToAbsent || embedJson != null) { 2833 + map['embed_json'] = Variable<String>(embedJson); 2834 + } 2835 + if (!nullToAbsent || mediaPaths != null) { 2836 + map['media_paths'] = Variable<String>(mediaPaths); 2837 + } 2838 + map['created_at'] = Variable<DateTime>(createdAt); 2839 + map['updated_at'] = Variable<DateTime>(updatedAt); 2840 + if (!nullToAbsent || scheduledAt != null) { 2841 + map['scheduled_at'] = Variable<DateTime>(scheduledAt); 2842 + } 2843 + return map; 2844 + } 2845 + 2846 + DraftsCompanion toCompanion(bool nullToAbsent) { 2847 + return DraftsCompanion( 2848 + id: Value(id), 2849 + accountDid: Value(accountDid), 2850 + content: Value(content), 2851 + replyUri: replyUri == null && nullToAbsent 2852 + ? const Value.absent() 2853 + : Value(replyUri), 2854 + replyCid: replyCid == null && nullToAbsent 2855 + ? const Value.absent() 2856 + : Value(replyCid), 2857 + rootUri: rootUri == null && nullToAbsent 2858 + ? const Value.absent() 2859 + : Value(rootUri), 2860 + rootCid: rootCid == null && nullToAbsent 2861 + ? const Value.absent() 2862 + : Value(rootCid), 2863 + embedJson: embedJson == null && nullToAbsent 2864 + ? const Value.absent() 2865 + : Value(embedJson), 2866 + mediaPaths: mediaPaths == null && nullToAbsent 2867 + ? const Value.absent() 2868 + : Value(mediaPaths), 2869 + createdAt: Value(createdAt), 2870 + updatedAt: Value(updatedAt), 2871 + scheduledAt: scheduledAt == null && nullToAbsent 2872 + ? const Value.absent() 2873 + : Value(scheduledAt), 2874 + ); 2875 + } 2876 + 2877 + factory DraftEntry.fromJson( 2878 + Map<String, dynamic> json, { 2879 + ValueSerializer? serializer, 2880 + }) { 2881 + serializer ??= driftRuntimeOptions.defaultSerializer; 2882 + return DraftEntry( 2883 + id: serializer.fromJson<int>(json['id']), 2884 + accountDid: serializer.fromJson<String>(json['accountDid']), 2885 + content: serializer.fromJson<String>(json['content']), 2886 + replyUri: serializer.fromJson<String?>(json['replyUri']), 2887 + replyCid: serializer.fromJson<String?>(json['replyCid']), 2888 + rootUri: serializer.fromJson<String?>(json['rootUri']), 2889 + rootCid: serializer.fromJson<String?>(json['rootCid']), 2890 + embedJson: serializer.fromJson<String?>(json['embedJson']), 2891 + mediaPaths: serializer.fromJson<String?>(json['mediaPaths']), 2892 + createdAt: serializer.fromJson<DateTime>(json['createdAt']), 2893 + updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), 2894 + scheduledAt: serializer.fromJson<DateTime?>(json['scheduledAt']), 2895 + ); 2896 + } 2897 + @override 2898 + Map<String, dynamic> toJson({ValueSerializer? serializer}) { 2899 + serializer ??= driftRuntimeOptions.defaultSerializer; 2900 + return <String, dynamic>{ 2901 + 'id': serializer.toJson<int>(id), 2902 + 'accountDid': serializer.toJson<String>(accountDid), 2903 + 'content': serializer.toJson<String>(content), 2904 + 'replyUri': serializer.toJson<String?>(replyUri), 2905 + 'replyCid': serializer.toJson<String?>(replyCid), 2906 + 'rootUri': serializer.toJson<String?>(rootUri), 2907 + 'rootCid': serializer.toJson<String?>(rootCid), 2908 + 'embedJson': serializer.toJson<String?>(embedJson), 2909 + 'mediaPaths': serializer.toJson<String?>(mediaPaths), 2910 + 'createdAt': serializer.toJson<DateTime>(createdAt), 2911 + 'updatedAt': serializer.toJson<DateTime>(updatedAt), 2912 + 'scheduledAt': serializer.toJson<DateTime?>(scheduledAt), 2913 + }; 2914 + } 2915 + 2916 + DraftEntry copyWith({ 2917 + int? id, 2918 + String? accountDid, 2919 + String? content, 2920 + Value<String?> replyUri = const Value.absent(), 2921 + Value<String?> replyCid = const Value.absent(), 2922 + Value<String?> rootUri = const Value.absent(), 2923 + Value<String?> rootCid = const Value.absent(), 2924 + Value<String?> embedJson = const Value.absent(), 2925 + Value<String?> mediaPaths = const Value.absent(), 2926 + DateTime? createdAt, 2927 + DateTime? updatedAt, 2928 + Value<DateTime?> scheduledAt = const Value.absent(), 2929 + }) => DraftEntry( 2930 + id: id ?? this.id, 2931 + accountDid: accountDid ?? this.accountDid, 2932 + content: content ?? this.content, 2933 + replyUri: replyUri.present ? replyUri.value : this.replyUri, 2934 + replyCid: replyCid.present ? replyCid.value : this.replyCid, 2935 + rootUri: rootUri.present ? rootUri.value : this.rootUri, 2936 + rootCid: rootCid.present ? rootCid.value : this.rootCid, 2937 + embedJson: embedJson.present ? embedJson.value : this.embedJson, 2938 + mediaPaths: mediaPaths.present ? mediaPaths.value : this.mediaPaths, 2939 + createdAt: createdAt ?? this.createdAt, 2940 + updatedAt: updatedAt ?? this.updatedAt, 2941 + scheduledAt: scheduledAt.present ? scheduledAt.value : this.scheduledAt, 2942 + ); 2943 + DraftEntry copyWithCompanion(DraftsCompanion data) { 2944 + return DraftEntry( 2945 + id: data.id.present ? data.id.value : this.id, 2946 + accountDid: data.accountDid.present 2947 + ? data.accountDid.value 2948 + : this.accountDid, 2949 + content: data.content.present ? data.content.value : this.content, 2950 + replyUri: data.replyUri.present ? data.replyUri.value : this.replyUri, 2951 + replyCid: data.replyCid.present ? data.replyCid.value : this.replyCid, 2952 + rootUri: data.rootUri.present ? data.rootUri.value : this.rootUri, 2953 + rootCid: data.rootCid.present ? data.rootCid.value : this.rootCid, 2954 + embedJson: data.embedJson.present ? data.embedJson.value : this.embedJson, 2955 + mediaPaths: data.mediaPaths.present 2956 + ? data.mediaPaths.value 2957 + : this.mediaPaths, 2958 + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 2959 + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, 2960 + scheduledAt: data.scheduledAt.present 2961 + ? data.scheduledAt.value 2962 + : this.scheduledAt, 2963 + ); 2964 + } 2965 + 2966 + @override 2967 + String toString() { 2968 + return (StringBuffer('DraftEntry(') 2969 + ..write('id: $id, ') 2970 + ..write('accountDid: $accountDid, ') 2971 + ..write('content: $content, ') 2972 + ..write('replyUri: $replyUri, ') 2973 + ..write('replyCid: $replyCid, ') 2974 + ..write('rootUri: $rootUri, ') 2975 + ..write('rootCid: $rootCid, ') 2976 + ..write('embedJson: $embedJson, ') 2977 + ..write('mediaPaths: $mediaPaths, ') 2978 + ..write('createdAt: $createdAt, ') 2979 + ..write('updatedAt: $updatedAt, ') 2980 + ..write('scheduledAt: $scheduledAt') 2981 + ..write(')')) 2982 + .toString(); 2983 + } 2984 + 2985 + @override 2986 + int get hashCode => Object.hash( 2987 + id, 2988 + accountDid, 2989 + content, 2990 + replyUri, 2991 + replyCid, 2992 + rootUri, 2993 + rootCid, 2994 + embedJson, 2995 + mediaPaths, 2996 + createdAt, 2997 + updatedAt, 2998 + scheduledAt, 2999 + ); 3000 + @override 3001 + bool operator ==(Object other) => 3002 + identical(this, other) || 3003 + (other is DraftEntry && 3004 + other.id == this.id && 3005 + other.accountDid == this.accountDid && 3006 + other.content == this.content && 3007 + other.replyUri == this.replyUri && 3008 + other.replyCid == this.replyCid && 3009 + other.rootUri == this.rootUri && 3010 + other.rootCid == this.rootCid && 3011 + other.embedJson == this.embedJson && 3012 + other.mediaPaths == this.mediaPaths && 3013 + other.createdAt == this.createdAt && 3014 + other.updatedAt == this.updatedAt && 3015 + other.scheduledAt == this.scheduledAt); 3016 + } 3017 + 3018 + class DraftsCompanion extends UpdateCompanion<DraftEntry> { 3019 + final Value<int> id; 3020 + final Value<String> accountDid; 3021 + final Value<String> content; 3022 + final Value<String?> replyUri; 3023 + final Value<String?> replyCid; 3024 + final Value<String?> rootUri; 3025 + final Value<String?> rootCid; 3026 + final Value<String?> embedJson; 3027 + final Value<String?> mediaPaths; 3028 + final Value<DateTime> createdAt; 3029 + final Value<DateTime> updatedAt; 3030 + final Value<DateTime?> scheduledAt; 3031 + const DraftsCompanion({ 3032 + this.id = const Value.absent(), 3033 + this.accountDid = const Value.absent(), 3034 + this.content = const Value.absent(), 3035 + this.replyUri = const Value.absent(), 3036 + this.replyCid = const Value.absent(), 3037 + this.rootUri = const Value.absent(), 3038 + this.rootCid = const Value.absent(), 3039 + this.embedJson = const Value.absent(), 3040 + this.mediaPaths = const Value.absent(), 3041 + this.createdAt = const Value.absent(), 3042 + this.updatedAt = const Value.absent(), 3043 + this.scheduledAt = const Value.absent(), 3044 + }); 3045 + DraftsCompanion.insert({ 3046 + this.id = const Value.absent(), 3047 + required String accountDid, 3048 + required String content, 3049 + this.replyUri = const Value.absent(), 3050 + this.replyCid = const Value.absent(), 3051 + this.rootUri = const Value.absent(), 3052 + this.rootCid = const Value.absent(), 3053 + this.embedJson = const Value.absent(), 3054 + this.mediaPaths = const Value.absent(), 3055 + this.createdAt = const Value.absent(), 3056 + this.updatedAt = const Value.absent(), 3057 + this.scheduledAt = const Value.absent(), 3058 + }) : accountDid = Value(accountDid), 3059 + content = Value(content); 3060 + static Insertable<DraftEntry> custom({ 3061 + Expression<int>? id, 3062 + Expression<String>? accountDid, 3063 + Expression<String>? content, 3064 + Expression<String>? replyUri, 3065 + Expression<String>? replyCid, 3066 + Expression<String>? rootUri, 3067 + Expression<String>? rootCid, 3068 + Expression<String>? embedJson, 3069 + Expression<String>? mediaPaths, 3070 + Expression<DateTime>? createdAt, 3071 + Expression<DateTime>? updatedAt, 3072 + Expression<DateTime>? scheduledAt, 3073 + }) { 3074 + return RawValuesInsertable({ 3075 + if (id != null) 'id': id, 3076 + if (accountDid != null) 'account_did': accountDid, 3077 + if (content != null) 'content': content, 3078 + if (replyUri != null) 'reply_uri': replyUri, 3079 + if (replyCid != null) 'reply_cid': replyCid, 3080 + if (rootUri != null) 'root_uri': rootUri, 3081 + if (rootCid != null) 'root_cid': rootCid, 3082 + if (embedJson != null) 'embed_json': embedJson, 3083 + if (mediaPaths != null) 'media_paths': mediaPaths, 3084 + if (createdAt != null) 'created_at': createdAt, 3085 + if (updatedAt != null) 'updated_at': updatedAt, 3086 + if (scheduledAt != null) 'scheduled_at': scheduledAt, 3087 + }); 3088 + } 3089 + 3090 + DraftsCompanion copyWith({ 3091 + Value<int>? id, 3092 + Value<String>? accountDid, 3093 + Value<String>? content, 3094 + Value<String?>? replyUri, 3095 + Value<String?>? replyCid, 3096 + Value<String?>? rootUri, 3097 + Value<String?>? rootCid, 3098 + Value<String?>? embedJson, 3099 + Value<String?>? mediaPaths, 3100 + Value<DateTime>? createdAt, 3101 + Value<DateTime>? updatedAt, 3102 + Value<DateTime?>? scheduledAt, 3103 + }) { 3104 + return DraftsCompanion( 3105 + id: id ?? this.id, 3106 + accountDid: accountDid ?? this.accountDid, 3107 + content: content ?? this.content, 3108 + replyUri: replyUri ?? this.replyUri, 3109 + replyCid: replyCid ?? this.replyCid, 3110 + rootUri: rootUri ?? this.rootUri, 3111 + rootCid: rootCid ?? this.rootCid, 3112 + embedJson: embedJson ?? this.embedJson, 3113 + mediaPaths: mediaPaths ?? this.mediaPaths, 3114 + createdAt: createdAt ?? this.createdAt, 3115 + updatedAt: updatedAt ?? this.updatedAt, 3116 + scheduledAt: scheduledAt ?? this.scheduledAt, 3117 + ); 3118 + } 3119 + 3120 + @override 3121 + Map<String, Expression> toColumns(bool nullToAbsent) { 3122 + final map = <String, Expression>{}; 3123 + if (id.present) { 3124 + map['id'] = Variable<int>(id.value); 3125 + } 3126 + if (accountDid.present) { 3127 + map['account_did'] = Variable<String>(accountDid.value); 3128 + } 3129 + if (content.present) { 3130 + map['content'] = Variable<String>(content.value); 3131 + } 3132 + if (replyUri.present) { 3133 + map['reply_uri'] = Variable<String>(replyUri.value); 3134 + } 3135 + if (replyCid.present) { 3136 + map['reply_cid'] = Variable<String>(replyCid.value); 3137 + } 3138 + if (rootUri.present) { 3139 + map['root_uri'] = Variable<String>(rootUri.value); 3140 + } 3141 + if (rootCid.present) { 3142 + map['root_cid'] = Variable<String>(rootCid.value); 3143 + } 3144 + if (embedJson.present) { 3145 + map['embed_json'] = Variable<String>(embedJson.value); 3146 + } 3147 + if (mediaPaths.present) { 3148 + map['media_paths'] = Variable<String>(mediaPaths.value); 3149 + } 3150 + if (createdAt.present) { 3151 + map['created_at'] = Variable<DateTime>(createdAt.value); 3152 + } 3153 + if (updatedAt.present) { 3154 + map['updated_at'] = Variable<DateTime>(updatedAt.value); 3155 + } 3156 + if (scheduledAt.present) { 3157 + map['scheduled_at'] = Variable<DateTime>(scheduledAt.value); 3158 + } 3159 + return map; 3160 + } 3161 + 3162 + @override 3163 + String toString() { 3164 + return (StringBuffer('DraftsCompanion(') 3165 + ..write('id: $id, ') 3166 + ..write('accountDid: $accountDid, ') 3167 + ..write('content: $content, ') 3168 + ..write('replyUri: $replyUri, ') 3169 + ..write('replyCid: $replyCid, ') 3170 + ..write('rootUri: $rootUri, ') 3171 + ..write('rootCid: $rootCid, ') 3172 + ..write('embedJson: $embedJson, ') 3173 + ..write('mediaPaths: $mediaPaths, ') 3174 + ..write('createdAt: $createdAt, ') 3175 + ..write('updatedAt: $updatedAt, ') 3176 + ..write('scheduledAt: $scheduledAt') 3177 + ..write(')')) 3178 + .toString(); 3179 + } 3180 + } 3181 + 2086 3182 abstract class _$AppDatabase extends GeneratedDatabase { 2087 3183 _$AppDatabase(QueryExecutor e) : super(e); 2088 3184 $AppDatabaseManager get managers => $AppDatabaseManager(this); ··· 2092 3188 late final $SettingsTable settings = $SettingsTable(this); 2093 3189 late final $SavedFeedsTable savedFeeds = $SavedFeedsTable(this); 2094 3190 late final $SearchHistoryTable searchHistory = $SearchHistoryTable(this); 3191 + late final $DraftsTable drafts = $DraftsTable(this); 2095 3192 @override 2096 - Iterable<TableInfo<Table, Object?>> get allTables => allSchemaEntities.whereType<TableInfo<Table, Object?>>(); 3193 + Iterable<TableInfo<Table, Object?>> get allTables => 3194 + allSchemaEntities.whereType<TableInfo<Table, Object?>>(); 2097 3195 @override 2098 3196 List<DatabaseSchemaEntity> get allSchemaEntities => [ 2099 3197 accounts, ··· 2102 3200 settings, 2103 3201 savedFeeds, 2104 3202 searchHistory, 3203 + drafts, 2105 3204 ]; 2106 3205 } 2107 3206 ··· 2138 3237 Value<int> rowid, 2139 3238 }); 2140 3239 2141 - class $$AccountsTableFilterComposer extends Composer<_$AppDatabase, $AccountsTable> { 3240 + class $$AccountsTableFilterComposer 3241 + extends Composer<_$AppDatabase, $AccountsTable> { 2142 3242 $$AccountsTableFilterComposer({ 2143 3243 required super.$db, 2144 3244 required super.$table, ··· 2146 3246 super.$addJoinBuilderToRootComposer, 2147 3247 super.$removeJoinBuilderFromRootComposer, 2148 3248 }); 2149 - ColumnFilters<String> get did => $composableBuilder(column: $table.did, builder: (column) => ColumnFilters(column)); 3249 + ColumnFilters<String> get did => $composableBuilder( 3250 + column: $table.did, 3251 + builder: (column) => ColumnFilters(column), 3252 + ); 2150 3253 2151 - ColumnFilters<String> get handle => 2152 - $composableBuilder(column: $table.handle, builder: (column) => ColumnFilters(column)); 3254 + ColumnFilters<String> get handle => $composableBuilder( 3255 + column: $table.handle, 3256 + builder: (column) => ColumnFilters(column), 3257 + ); 2153 3258 2154 - ColumnFilters<String> get displayName => 2155 - $composableBuilder(column: $table.displayName, builder: (column) => ColumnFilters(column)); 3259 + ColumnFilters<String> get displayName => $composableBuilder( 3260 + column: $table.displayName, 3261 + builder: (column) => ColumnFilters(column), 3262 + ); 2156 3263 2157 - ColumnFilters<String> get service => 2158 - $composableBuilder(column: $table.service, builder: (column) => ColumnFilters(column)); 3264 + ColumnFilters<String> get service => $composableBuilder( 3265 + column: $table.service, 3266 + builder: (column) => ColumnFilters(column), 3267 + ); 2159 3268 2160 - ColumnFilters<String> get accessToken => 2161 - $composableBuilder(column: $table.accessToken, builder: (column) => ColumnFilters(column)); 3269 + ColumnFilters<String> get accessToken => $composableBuilder( 3270 + column: $table.accessToken, 3271 + builder: (column) => ColumnFilters(column), 3272 + ); 2162 3273 2163 - ColumnFilters<String> get refreshToken => 2164 - $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnFilters(column)); 3274 + ColumnFilters<String> get refreshToken => $composableBuilder( 3275 + column: $table.refreshToken, 3276 + builder: (column) => ColumnFilters(column), 3277 + ); 2165 3278 2166 - ColumnFilters<String> get dpopPublicKey => 2167 - $composableBuilder(column: $table.dpopPublicKey, builder: (column) => ColumnFilters(column)); 3279 + ColumnFilters<String> get dpopPublicKey => $composableBuilder( 3280 + column: $table.dpopPublicKey, 3281 + builder: (column) => ColumnFilters(column), 3282 + ); 2168 3283 2169 - ColumnFilters<String> get dpopPrivateKey => 2170 - $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnFilters(column)); 3284 + ColumnFilters<String> get dpopPrivateKey => $composableBuilder( 3285 + column: $table.dpopPrivateKey, 3286 + builder: (column) => ColumnFilters(column), 3287 + ); 2171 3288 2172 - ColumnFilters<String> get dpopNonce => 2173 - $composableBuilder(column: $table.dpopNonce, builder: (column) => ColumnFilters(column)); 3289 + ColumnFilters<String> get dpopNonce => $composableBuilder( 3290 + column: $table.dpopNonce, 3291 + builder: (column) => ColumnFilters(column), 3292 + ); 2174 3293 2175 - ColumnFilters<DateTime> get expiresAt => 2176 - $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnFilters(column)); 3294 + ColumnFilters<DateTime> get expiresAt => $composableBuilder( 3295 + column: $table.expiresAt, 3296 + builder: (column) => ColumnFilters(column), 3297 + ); 2177 3298 2178 - ColumnFilters<DateTime> get createdAt => 2179 - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); 3299 + ColumnFilters<DateTime> get createdAt => $composableBuilder( 3300 + column: $table.createdAt, 3301 + builder: (column) => ColumnFilters(column), 3302 + ); 2180 3303 2181 - ColumnFilters<DateTime> get updatedAt => 2182 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); 3304 + ColumnFilters<DateTime> get updatedAt => $composableBuilder( 3305 + column: $table.updatedAt, 3306 + builder: (column) => ColumnFilters(column), 3307 + ); 2183 3308 } 2184 3309 2185 - class $$AccountsTableOrderingComposer extends Composer<_$AppDatabase, $AccountsTable> { 3310 + class $$AccountsTableOrderingComposer 3311 + extends Composer<_$AppDatabase, $AccountsTable> { 2186 3312 $$AccountsTableOrderingComposer({ 2187 3313 required super.$db, 2188 3314 required super.$table, ··· 2190 3316 super.$addJoinBuilderToRootComposer, 2191 3317 super.$removeJoinBuilderFromRootComposer, 2192 3318 }); 2193 - ColumnOrderings<String> get did => 2194 - $composableBuilder(column: $table.did, builder: (column) => ColumnOrderings(column)); 3319 + ColumnOrderings<String> get did => $composableBuilder( 3320 + column: $table.did, 3321 + builder: (column) => ColumnOrderings(column), 3322 + ); 2195 3323 2196 - ColumnOrderings<String> get handle => 2197 - $composableBuilder(column: $table.handle, builder: (column) => ColumnOrderings(column)); 3324 + ColumnOrderings<String> get handle => $composableBuilder( 3325 + column: $table.handle, 3326 + builder: (column) => ColumnOrderings(column), 3327 + ); 2198 3328 2199 - ColumnOrderings<String> get displayName => 2200 - $composableBuilder(column: $table.displayName, builder: (column) => ColumnOrderings(column)); 3329 + ColumnOrderings<String> get displayName => $composableBuilder( 3330 + column: $table.displayName, 3331 + builder: (column) => ColumnOrderings(column), 3332 + ); 2201 3333 2202 - ColumnOrderings<String> get service => 2203 - $composableBuilder(column: $table.service, builder: (column) => ColumnOrderings(column)); 3334 + ColumnOrderings<String> get service => $composableBuilder( 3335 + column: $table.service, 3336 + builder: (column) => ColumnOrderings(column), 3337 + ); 2204 3338 2205 - ColumnOrderings<String> get accessToken => 2206 - $composableBuilder(column: $table.accessToken, builder: (column) => ColumnOrderings(column)); 3339 + ColumnOrderings<String> get accessToken => $composableBuilder( 3340 + column: $table.accessToken, 3341 + builder: (column) => ColumnOrderings(column), 3342 + ); 2207 3343 2208 - ColumnOrderings<String> get refreshToken => 2209 - $composableBuilder(column: $table.refreshToken, builder: (column) => ColumnOrderings(column)); 3344 + ColumnOrderings<String> get refreshToken => $composableBuilder( 3345 + column: $table.refreshToken, 3346 + builder: (column) => ColumnOrderings(column), 3347 + ); 2210 3348 2211 - ColumnOrderings<String> get dpopPublicKey => 2212 - $composableBuilder(column: $table.dpopPublicKey, builder: (column) => ColumnOrderings(column)); 3349 + ColumnOrderings<String> get dpopPublicKey => $composableBuilder( 3350 + column: $table.dpopPublicKey, 3351 + builder: (column) => ColumnOrderings(column), 3352 + ); 2213 3353 2214 - ColumnOrderings<String> get dpopPrivateKey => 2215 - $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => ColumnOrderings(column)); 3354 + ColumnOrderings<String> get dpopPrivateKey => $composableBuilder( 3355 + column: $table.dpopPrivateKey, 3356 + builder: (column) => ColumnOrderings(column), 3357 + ); 2216 3358 2217 - ColumnOrderings<String> get dpopNonce => 2218 - $composableBuilder(column: $table.dpopNonce, builder: (column) => ColumnOrderings(column)); 3359 + ColumnOrderings<String> get dpopNonce => $composableBuilder( 3360 + column: $table.dpopNonce, 3361 + builder: (column) => ColumnOrderings(column), 3362 + ); 2219 3363 2220 - ColumnOrderings<DateTime> get expiresAt => 2221 - $composableBuilder(column: $table.expiresAt, builder: (column) => ColumnOrderings(column)); 3364 + ColumnOrderings<DateTime> get expiresAt => $composableBuilder( 3365 + column: $table.expiresAt, 3366 + builder: (column) => ColumnOrderings(column), 3367 + ); 2222 3368 2223 - ColumnOrderings<DateTime> get createdAt => 2224 - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); 3369 + ColumnOrderings<DateTime> get createdAt => $composableBuilder( 3370 + column: $table.createdAt, 3371 + builder: (column) => ColumnOrderings(column), 3372 + ); 2225 3373 2226 - ColumnOrderings<DateTime> get updatedAt => 2227 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); 3374 + ColumnOrderings<DateTime> get updatedAt => $composableBuilder( 3375 + column: $table.updatedAt, 3376 + builder: (column) => ColumnOrderings(column), 3377 + ); 2228 3378 } 2229 3379 2230 - class $$AccountsTableAnnotationComposer extends Composer<_$AppDatabase, $AccountsTable> { 3380 + class $$AccountsTableAnnotationComposer 3381 + extends Composer<_$AppDatabase, $AccountsTable> { 2231 3382 $$AccountsTableAnnotationComposer({ 2232 3383 required super.$db, 2233 3384 required super.$table, ··· 2235 3386 super.$addJoinBuilderToRootComposer, 2236 3387 super.$removeJoinBuilderFromRootComposer, 2237 3388 }); 2238 - GeneratedColumn<String> get did => $composableBuilder(column: $table.did, builder: (column) => column); 3389 + GeneratedColumn<String> get did => 3390 + $composableBuilder(column: $table.did, builder: (column) => column); 2239 3391 2240 - GeneratedColumn<String> get handle => $composableBuilder(column: $table.handle, builder: (column) => column); 3392 + GeneratedColumn<String> get handle => 3393 + $composableBuilder(column: $table.handle, builder: (column) => column); 2241 3394 2242 - GeneratedColumn<String> get displayName => 2243 - $composableBuilder(column: $table.displayName, builder: (column) => column); 3395 + GeneratedColumn<String> get displayName => $composableBuilder( 3396 + column: $table.displayName, 3397 + builder: (column) => column, 3398 + ); 2244 3399 2245 - GeneratedColumn<String> get service => $composableBuilder(column: $table.service, builder: (column) => column); 3400 + GeneratedColumn<String> get service => 3401 + $composableBuilder(column: $table.service, builder: (column) => column); 2246 3402 2247 - GeneratedColumn<String> get accessToken => 2248 - $composableBuilder(column: $table.accessToken, builder: (column) => column); 3403 + GeneratedColumn<String> get accessToken => $composableBuilder( 3404 + column: $table.accessToken, 3405 + builder: (column) => column, 3406 + ); 2249 3407 2250 - GeneratedColumn<String> get refreshToken => 2251 - $composableBuilder(column: $table.refreshToken, builder: (column) => column); 3408 + GeneratedColumn<String> get refreshToken => $composableBuilder( 3409 + column: $table.refreshToken, 3410 + builder: (column) => column, 3411 + ); 2252 3412 2253 - GeneratedColumn<String> get dpopPublicKey => 2254 - $composableBuilder(column: $table.dpopPublicKey, builder: (column) => column); 3413 + GeneratedColumn<String> get dpopPublicKey => $composableBuilder( 3414 + column: $table.dpopPublicKey, 3415 + builder: (column) => column, 3416 + ); 2255 3417 2256 - GeneratedColumn<String> get dpopPrivateKey => 2257 - $composableBuilder(column: $table.dpopPrivateKey, builder: (column) => column); 3418 + GeneratedColumn<String> get dpopPrivateKey => $composableBuilder( 3419 + column: $table.dpopPrivateKey, 3420 + builder: (column) => column, 3421 + ); 2258 3422 2259 - GeneratedColumn<String> get dpopNonce => $composableBuilder(column: $table.dpopNonce, builder: (column) => column); 3423 + GeneratedColumn<String> get dpopNonce => 3424 + $composableBuilder(column: $table.dpopNonce, builder: (column) => column); 2260 3425 2261 - GeneratedColumn<DateTime> get expiresAt => $composableBuilder(column: $table.expiresAt, builder: (column) => column); 3426 + GeneratedColumn<DateTime> get expiresAt => 3427 + $composableBuilder(column: $table.expiresAt, builder: (column) => column); 2262 3428 2263 - GeneratedColumn<DateTime> get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); 3429 + GeneratedColumn<DateTime> get createdAt => 3430 + $composableBuilder(column: $table.createdAt, builder: (column) => column); 2264 3431 2265 - GeneratedColumn<DateTime> get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); 3432 + GeneratedColumn<DateTime> get updatedAt => 3433 + $composableBuilder(column: $table.updatedAt, builder: (column) => column); 2266 3434 } 2267 3435 2268 3436 class $$AccountsTableTableManager ··· 2285 3453 TableManagerState( 2286 3454 db: db, 2287 3455 table: table, 2288 - createFilteringComposer: () => $$AccountsTableFilterComposer($db: db, $table: table), 2289 - createOrderingComposer: () => $$AccountsTableOrderingComposer($db: db, $table: table), 2290 - createComputedFieldComposer: () => $$AccountsTableAnnotationComposer($db: db, $table: table), 3456 + createFilteringComposer: () => 3457 + $$AccountsTableFilterComposer($db: db, $table: table), 3458 + createOrderingComposer: () => 3459 + $$AccountsTableOrderingComposer($db: db, $table: table), 3460 + createComputedFieldComposer: () => 3461 + $$AccountsTableAnnotationComposer($db: db, $table: table), 2291 3462 updateCompanionCallback: 2292 3463 ({ 2293 3464 Value<String> did = const Value.absent(), ··· 2348 3519 updatedAt: updatedAt, 2349 3520 rowid: rowid, 2350 3521 ), 2351 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 3522 + withReferenceMapper: (p0) => p0 3523 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 3524 + .toList(), 2352 3525 prefetchHooksCallback: null, 2353 3526 ), 2354 3527 ); ··· 2385 3558 Value<int> rowid, 2386 3559 }); 2387 3560 2388 - class $$CachedProfilesTableFilterComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 3561 + class $$CachedProfilesTableFilterComposer 3562 + extends Composer<_$AppDatabase, $CachedProfilesTable> { 2389 3563 $$CachedProfilesTableFilterComposer({ 2390 3564 required super.$db, 2391 3565 required super.$table, ··· 2393 3567 super.$addJoinBuilderToRootComposer, 2394 3568 super.$removeJoinBuilderFromRootComposer, 2395 3569 }); 2396 - ColumnFilters<String> get did => $composableBuilder(column: $table.did, builder: (column) => ColumnFilters(column)); 3570 + ColumnFilters<String> get did => $composableBuilder( 3571 + column: $table.did, 3572 + builder: (column) => ColumnFilters(column), 3573 + ); 2397 3574 2398 - ColumnFilters<String> get handle => 2399 - $composableBuilder(column: $table.handle, builder: (column) => ColumnFilters(column)); 3575 + ColumnFilters<String> get handle => $composableBuilder( 3576 + column: $table.handle, 3577 + builder: (column) => ColumnFilters(column), 3578 + ); 2400 3579 2401 - ColumnFilters<String> get payload => 2402 - $composableBuilder(column: $table.payload, builder: (column) => ColumnFilters(column)); 3580 + ColumnFilters<String> get payload => $composableBuilder( 3581 + column: $table.payload, 3582 + builder: (column) => ColumnFilters(column), 3583 + ); 2403 3584 2404 - ColumnFilters<DateTime> get fetchedAt => 2405 - $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnFilters(column)); 3585 + ColumnFilters<DateTime> get fetchedAt => $composableBuilder( 3586 + column: $table.fetchedAt, 3587 + builder: (column) => ColumnFilters(column), 3588 + ); 2406 3589 } 2407 3590 2408 - class $$CachedProfilesTableOrderingComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 3591 + class $$CachedProfilesTableOrderingComposer 3592 + extends Composer<_$AppDatabase, $CachedProfilesTable> { 2409 3593 $$CachedProfilesTableOrderingComposer({ 2410 3594 required super.$db, 2411 3595 required super.$table, ··· 2413 3597 super.$addJoinBuilderToRootComposer, 2414 3598 super.$removeJoinBuilderFromRootComposer, 2415 3599 }); 2416 - ColumnOrderings<String> get did => 2417 - $composableBuilder(column: $table.did, builder: (column) => ColumnOrderings(column)); 3600 + ColumnOrderings<String> get did => $composableBuilder( 3601 + column: $table.did, 3602 + builder: (column) => ColumnOrderings(column), 3603 + ); 2418 3604 2419 - ColumnOrderings<String> get handle => 2420 - $composableBuilder(column: $table.handle, builder: (column) => ColumnOrderings(column)); 3605 + ColumnOrderings<String> get handle => $composableBuilder( 3606 + column: $table.handle, 3607 + builder: (column) => ColumnOrderings(column), 3608 + ); 2421 3609 2422 - ColumnOrderings<String> get payload => 2423 - $composableBuilder(column: $table.payload, builder: (column) => ColumnOrderings(column)); 3610 + ColumnOrderings<String> get payload => $composableBuilder( 3611 + column: $table.payload, 3612 + builder: (column) => ColumnOrderings(column), 3613 + ); 2424 3614 2425 - ColumnOrderings<DateTime> get fetchedAt => 2426 - $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnOrderings(column)); 3615 + ColumnOrderings<DateTime> get fetchedAt => $composableBuilder( 3616 + column: $table.fetchedAt, 3617 + builder: (column) => ColumnOrderings(column), 3618 + ); 2427 3619 } 2428 3620 2429 - class $$CachedProfilesTableAnnotationComposer extends Composer<_$AppDatabase, $CachedProfilesTable> { 3621 + class $$CachedProfilesTableAnnotationComposer 3622 + extends Composer<_$AppDatabase, $CachedProfilesTable> { 2430 3623 $$CachedProfilesTableAnnotationComposer({ 2431 3624 required super.$db, 2432 3625 required super.$table, ··· 2434 3627 super.$addJoinBuilderToRootComposer, 2435 3628 super.$removeJoinBuilderFromRootComposer, 2436 3629 }); 2437 - GeneratedColumn<String> get did => $composableBuilder(column: $table.did, builder: (column) => column); 3630 + GeneratedColumn<String> get did => 3631 + $composableBuilder(column: $table.did, builder: (column) => column); 2438 3632 2439 - GeneratedColumn<String> get handle => $composableBuilder(column: $table.handle, builder: (column) => column); 3633 + GeneratedColumn<String> get handle => 3634 + $composableBuilder(column: $table.handle, builder: (column) => column); 2440 3635 2441 - GeneratedColumn<String> get payload => $composableBuilder(column: $table.payload, builder: (column) => column); 3636 + GeneratedColumn<String> get payload => 3637 + $composableBuilder(column: $table.payload, builder: (column) => column); 2442 3638 2443 - GeneratedColumn<DateTime> get fetchedAt => $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 3639 + GeneratedColumn<DateTime> get fetchedAt => 3640 + $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 2444 3641 } 2445 3642 2446 3643 class $$CachedProfilesTableTableManager ··· 2454 3651 $$CachedProfilesTableAnnotationComposer, 2455 3652 $$CachedProfilesTableCreateCompanionBuilder, 2456 3653 $$CachedProfilesTableUpdateCompanionBuilder, 2457 - (CachedProfile, BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>), 3654 + ( 3655 + CachedProfile, 3656 + BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>, 3657 + ), 2458 3658 CachedProfile, 2459 3659 PrefetchHooks Function() 2460 3660 > { 2461 - $$CachedProfilesTableTableManager(_$AppDatabase db, $CachedProfilesTable table) 2462 - : super( 3661 + $$CachedProfilesTableTableManager( 3662 + _$AppDatabase db, 3663 + $CachedProfilesTable table, 3664 + ) : super( 2463 3665 TableManagerState( 2464 3666 db: db, 2465 3667 table: table, 2466 - createFilteringComposer: () => $$CachedProfilesTableFilterComposer($db: db, $table: table), 2467 - createOrderingComposer: () => $$CachedProfilesTableOrderingComposer($db: db, $table: table), 2468 - createComputedFieldComposer: () => $$CachedProfilesTableAnnotationComposer($db: db, $table: table), 3668 + createFilteringComposer: () => 3669 + $$CachedProfilesTableFilterComposer($db: db, $table: table), 3670 + createOrderingComposer: () => 3671 + $$CachedProfilesTableOrderingComposer($db: db, $table: table), 3672 + createComputedFieldComposer: () => 3673 + $$CachedProfilesTableAnnotationComposer($db: db, $table: table), 2469 3674 updateCompanionCallback: 2470 3675 ({ 2471 3676 Value<String> did = const Value.absent(), ··· 2494 3699 fetchedAt: fetchedAt, 2495 3700 rowid: rowid, 2496 3701 ), 2497 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 3702 + withReferenceMapper: (p0) => p0 3703 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 3704 + .toList(), 2498 3705 prefetchHooksCallback: null, 2499 3706 ), 2500 3707 ); ··· 2510 3717 $$CachedProfilesTableAnnotationComposer, 2511 3718 $$CachedProfilesTableCreateCompanionBuilder, 2512 3719 $$CachedProfilesTableUpdateCompanionBuilder, 2513 - (CachedProfile, BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>), 3720 + ( 3721 + CachedProfile, 3722 + BaseReferences<_$AppDatabase, $CachedProfilesTable, CachedProfile>, 3723 + ), 2514 3724 CachedProfile, 2515 3725 PrefetchHooks Function() 2516 3726 >; ··· 2533 3743 Value<int> rowid, 2534 3744 }); 2535 3745 2536 - class $$CachedPostsTableFilterComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 3746 + class $$CachedPostsTableFilterComposer 3747 + extends Composer<_$AppDatabase, $CachedPostsTable> { 2537 3748 $$CachedPostsTableFilterComposer({ 2538 3749 required super.$db, 2539 3750 required super.$table, ··· 2541 3752 super.$addJoinBuilderToRootComposer, 2542 3753 super.$removeJoinBuilderFromRootComposer, 2543 3754 }); 2544 - ColumnFilters<String> get uri => $composableBuilder(column: $table.uri, builder: (column) => ColumnFilters(column)); 3755 + ColumnFilters<String> get uri => $composableBuilder( 3756 + column: $table.uri, 3757 + builder: (column) => ColumnFilters(column), 3758 + ); 2545 3759 2546 - ColumnFilters<String> get authorDid => 2547 - $composableBuilder(column: $table.authorDid, builder: (column) => ColumnFilters(column)); 3760 + ColumnFilters<String> get authorDid => $composableBuilder( 3761 + column: $table.authorDid, 3762 + builder: (column) => ColumnFilters(column), 3763 + ); 2548 3764 2549 - ColumnFilters<String> get payload => 2550 - $composableBuilder(column: $table.payload, builder: (column) => ColumnFilters(column)); 3765 + ColumnFilters<String> get payload => $composableBuilder( 3766 + column: $table.payload, 3767 + builder: (column) => ColumnFilters(column), 3768 + ); 2551 3769 2552 - ColumnFilters<DateTime> get createdAt => 2553 - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); 3770 + ColumnFilters<DateTime> get createdAt => $composableBuilder( 3771 + column: $table.createdAt, 3772 + builder: (column) => ColumnFilters(column), 3773 + ); 2554 3774 2555 - ColumnFilters<DateTime> get fetchedAt => 2556 - $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnFilters(column)); 3775 + ColumnFilters<DateTime> get fetchedAt => $composableBuilder( 3776 + column: $table.fetchedAt, 3777 + builder: (column) => ColumnFilters(column), 3778 + ); 2557 3779 } 2558 3780 2559 - class $$CachedPostsTableOrderingComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 3781 + class $$CachedPostsTableOrderingComposer 3782 + extends Composer<_$AppDatabase, $CachedPostsTable> { 2560 3783 $$CachedPostsTableOrderingComposer({ 2561 3784 required super.$db, 2562 3785 required super.$table, ··· 2564 3787 super.$addJoinBuilderToRootComposer, 2565 3788 super.$removeJoinBuilderFromRootComposer, 2566 3789 }); 2567 - ColumnOrderings<String> get uri => 2568 - $composableBuilder(column: $table.uri, builder: (column) => ColumnOrderings(column)); 3790 + ColumnOrderings<String> get uri => $composableBuilder( 3791 + column: $table.uri, 3792 + builder: (column) => ColumnOrderings(column), 3793 + ); 2569 3794 2570 - ColumnOrderings<String> get authorDid => 2571 - $composableBuilder(column: $table.authorDid, builder: (column) => ColumnOrderings(column)); 3795 + ColumnOrderings<String> get authorDid => $composableBuilder( 3796 + column: $table.authorDid, 3797 + builder: (column) => ColumnOrderings(column), 3798 + ); 2572 3799 2573 - ColumnOrderings<String> get payload => 2574 - $composableBuilder(column: $table.payload, builder: (column) => ColumnOrderings(column)); 3800 + ColumnOrderings<String> get payload => $composableBuilder( 3801 + column: $table.payload, 3802 + builder: (column) => ColumnOrderings(column), 3803 + ); 2575 3804 2576 - ColumnOrderings<DateTime> get createdAt => 2577 - $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); 3805 + ColumnOrderings<DateTime> get createdAt => $composableBuilder( 3806 + column: $table.createdAt, 3807 + builder: (column) => ColumnOrderings(column), 3808 + ); 2578 3809 2579 - ColumnOrderings<DateTime> get fetchedAt => 2580 - $composableBuilder(column: $table.fetchedAt, builder: (column) => ColumnOrderings(column)); 3810 + ColumnOrderings<DateTime> get fetchedAt => $composableBuilder( 3811 + column: $table.fetchedAt, 3812 + builder: (column) => ColumnOrderings(column), 3813 + ); 2581 3814 } 2582 3815 2583 - class $$CachedPostsTableAnnotationComposer extends Composer<_$AppDatabase, $CachedPostsTable> { 3816 + class $$CachedPostsTableAnnotationComposer 3817 + extends Composer<_$AppDatabase, $CachedPostsTable> { 2584 3818 $$CachedPostsTableAnnotationComposer({ 2585 3819 required super.$db, 2586 3820 required super.$table, ··· 2588 3822 super.$addJoinBuilderToRootComposer, 2589 3823 super.$removeJoinBuilderFromRootComposer, 2590 3824 }); 2591 - GeneratedColumn<String> get uri => $composableBuilder(column: $table.uri, builder: (column) => column); 3825 + GeneratedColumn<String> get uri => 3826 + $composableBuilder(column: $table.uri, builder: (column) => column); 2592 3827 2593 - GeneratedColumn<String> get authorDid => $composableBuilder(column: $table.authorDid, builder: (column) => column); 3828 + GeneratedColumn<String> get authorDid => 3829 + $composableBuilder(column: $table.authorDid, builder: (column) => column); 2594 3830 2595 - GeneratedColumn<String> get payload => $composableBuilder(column: $table.payload, builder: (column) => column); 3831 + GeneratedColumn<String> get payload => 3832 + $composableBuilder(column: $table.payload, builder: (column) => column); 2596 3833 2597 - GeneratedColumn<DateTime> get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); 3834 + GeneratedColumn<DateTime> get createdAt => 3835 + $composableBuilder(column: $table.createdAt, builder: (column) => column); 2598 3836 2599 - GeneratedColumn<DateTime> get fetchedAt => $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 3837 + GeneratedColumn<DateTime> get fetchedAt => 3838 + $composableBuilder(column: $table.fetchedAt, builder: (column) => column); 2600 3839 } 2601 3840 2602 3841 class $$CachedPostsTableTableManager ··· 2610 3849 $$CachedPostsTableAnnotationComposer, 2611 3850 $$CachedPostsTableCreateCompanionBuilder, 2612 3851 $$CachedPostsTableUpdateCompanionBuilder, 2613 - (CachedPost, BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>), 3852 + ( 3853 + CachedPost, 3854 + BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>, 3855 + ), 2614 3856 CachedPost, 2615 3857 PrefetchHooks Function() 2616 3858 > { ··· 2619 3861 TableManagerState( 2620 3862 db: db, 2621 3863 table: table, 2622 - createFilteringComposer: () => $$CachedPostsTableFilterComposer($db: db, $table: table), 2623 - createOrderingComposer: () => $$CachedPostsTableOrderingComposer($db: db, $table: table), 2624 - createComputedFieldComposer: () => $$CachedPostsTableAnnotationComposer($db: db, $table: table), 3864 + createFilteringComposer: () => 3865 + $$CachedPostsTableFilterComposer($db: db, $table: table), 3866 + createOrderingComposer: () => 3867 + $$CachedPostsTableOrderingComposer($db: db, $table: table), 3868 + createComputedFieldComposer: () => 3869 + $$CachedPostsTableAnnotationComposer($db: db, $table: table), 2625 3870 updateCompanionCallback: 2626 3871 ({ 2627 3872 Value<String> uri = const Value.absent(), ··· 2654 3899 fetchedAt: fetchedAt, 2655 3900 rowid: rowid, 2656 3901 ), 2657 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 3902 + withReferenceMapper: (p0) => p0 3903 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 3904 + .toList(), 2658 3905 prefetchHooksCallback: null, 2659 3906 ), 2660 3907 ); ··· 2670 3917 $$CachedPostsTableAnnotationComposer, 2671 3918 $$CachedPostsTableCreateCompanionBuilder, 2672 3919 $$CachedPostsTableUpdateCompanionBuilder, 2673 - (CachedPost, BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>), 3920 + ( 3921 + CachedPost, 3922 + BaseReferences<_$AppDatabase, $CachedPostsTable, CachedPost>, 3923 + ), 2674 3924 CachedPost, 2675 3925 PrefetchHooks Function() 2676 3926 >; ··· 2682 3932 Value<int> rowid, 2683 3933 }); 2684 3934 typedef $$SettingsTableUpdateCompanionBuilder = 2685 - SettingsCompanion Function({Value<String> key, Value<String> value, Value<DateTime> updatedAt, Value<int> rowid}); 3935 + SettingsCompanion Function({ 3936 + Value<String> key, 3937 + Value<String> value, 3938 + Value<DateTime> updatedAt, 3939 + Value<int> rowid, 3940 + }); 2686 3941 2687 - class $$SettingsTableFilterComposer extends Composer<_$AppDatabase, $SettingsTable> { 3942 + class $$SettingsTableFilterComposer 3943 + extends Composer<_$AppDatabase, $SettingsTable> { 2688 3944 $$SettingsTableFilterComposer({ 2689 3945 required super.$db, 2690 3946 required super.$table, ··· 2692 3948 super.$addJoinBuilderToRootComposer, 2693 3949 super.$removeJoinBuilderFromRootComposer, 2694 3950 }); 2695 - ColumnFilters<String> get key => $composableBuilder(column: $table.key, builder: (column) => ColumnFilters(column)); 3951 + ColumnFilters<String> get key => $composableBuilder( 3952 + column: $table.key, 3953 + builder: (column) => ColumnFilters(column), 3954 + ); 2696 3955 2697 - ColumnFilters<String> get value => 2698 - $composableBuilder(column: $table.value, builder: (column) => ColumnFilters(column)); 3956 + ColumnFilters<String> get value => $composableBuilder( 3957 + column: $table.value, 3958 + builder: (column) => ColumnFilters(column), 3959 + ); 2699 3960 2700 - ColumnFilters<DateTime> get updatedAt => 2701 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); 3961 + ColumnFilters<DateTime> get updatedAt => $composableBuilder( 3962 + column: $table.updatedAt, 3963 + builder: (column) => ColumnFilters(column), 3964 + ); 2702 3965 } 2703 3966 2704 - class $$SettingsTableOrderingComposer extends Composer<_$AppDatabase, $SettingsTable> { 3967 + class $$SettingsTableOrderingComposer 3968 + extends Composer<_$AppDatabase, $SettingsTable> { 2705 3969 $$SettingsTableOrderingComposer({ 2706 3970 required super.$db, 2707 3971 required super.$table, ··· 2709 3973 super.$addJoinBuilderToRootComposer, 2710 3974 super.$removeJoinBuilderFromRootComposer, 2711 3975 }); 2712 - ColumnOrderings<String> get key => 2713 - $composableBuilder(column: $table.key, builder: (column) => ColumnOrderings(column)); 3976 + ColumnOrderings<String> get key => $composableBuilder( 3977 + column: $table.key, 3978 + builder: (column) => ColumnOrderings(column), 3979 + ); 2714 3980 2715 - ColumnOrderings<String> get value => 2716 - $composableBuilder(column: $table.value, builder: (column) => ColumnOrderings(column)); 3981 + ColumnOrderings<String> get value => $composableBuilder( 3982 + column: $table.value, 3983 + builder: (column) => ColumnOrderings(column), 3984 + ); 2717 3985 2718 - ColumnOrderings<DateTime> get updatedAt => 2719 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); 3986 + ColumnOrderings<DateTime> get updatedAt => $composableBuilder( 3987 + column: $table.updatedAt, 3988 + builder: (column) => ColumnOrderings(column), 3989 + ); 2720 3990 } 2721 3991 2722 - class $$SettingsTableAnnotationComposer extends Composer<_$AppDatabase, $SettingsTable> { 3992 + class $$SettingsTableAnnotationComposer 3993 + extends Composer<_$AppDatabase, $SettingsTable> { 2723 3994 $$SettingsTableAnnotationComposer({ 2724 3995 required super.$db, 2725 3996 required super.$table, ··· 2727 3998 super.$addJoinBuilderToRootComposer, 2728 3999 super.$removeJoinBuilderFromRootComposer, 2729 4000 }); 2730 - GeneratedColumn<String> get key => $composableBuilder(column: $table.key, builder: (column) => column); 4001 + GeneratedColumn<String> get key => 4002 + $composableBuilder(column: $table.key, builder: (column) => column); 2731 4003 2732 - GeneratedColumn<String> get value => $composableBuilder(column: $table.value, builder: (column) => column); 4004 + GeneratedColumn<String> get value => 4005 + $composableBuilder(column: $table.value, builder: (column) => column); 2733 4006 2734 - GeneratedColumn<DateTime> get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); 4007 + GeneratedColumn<DateTime> get updatedAt => 4008 + $composableBuilder(column: $table.updatedAt, builder: (column) => column); 2735 4009 } 2736 4010 2737 4011 class $$SettingsTableTableManager ··· 2745 4019 $$SettingsTableAnnotationComposer, 2746 4020 $$SettingsTableCreateCompanionBuilder, 2747 4021 $$SettingsTableUpdateCompanionBuilder, 2748 - (SettingsEntry, BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>), 4022 + ( 4023 + SettingsEntry, 4024 + BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>, 4025 + ), 2749 4026 SettingsEntry, 2750 4027 PrefetchHooks Function() 2751 4028 > { ··· 2754 4031 TableManagerState( 2755 4032 db: db, 2756 4033 table: table, 2757 - createFilteringComposer: () => $$SettingsTableFilterComposer($db: db, $table: table), 2758 - createOrderingComposer: () => $$SettingsTableOrderingComposer($db: db, $table: table), 2759 - createComputedFieldComposer: () => $$SettingsTableAnnotationComposer($db: db, $table: table), 4034 + createFilteringComposer: () => 4035 + $$SettingsTableFilterComposer($db: db, $table: table), 4036 + createOrderingComposer: () => 4037 + $$SettingsTableOrderingComposer($db: db, $table: table), 4038 + createComputedFieldComposer: () => 4039 + $$SettingsTableAnnotationComposer($db: db, $table: table), 2760 4040 updateCompanionCallback: 2761 4041 ({ 2762 4042 Value<String> key = const Value.absent(), 2763 4043 Value<String> value = const Value.absent(), 2764 4044 Value<DateTime> updatedAt = const Value.absent(), 2765 4045 Value<int> rowid = const Value.absent(), 2766 - }) => SettingsCompanion(key: key, value: value, updatedAt: updatedAt, rowid: rowid), 4046 + }) => SettingsCompanion( 4047 + key: key, 4048 + value: value, 4049 + updatedAt: updatedAt, 4050 + rowid: rowid, 4051 + ), 2767 4052 createCompanionCallback: 2768 4053 ({ 2769 4054 required String key, 2770 4055 required String value, 2771 4056 Value<DateTime> updatedAt = const Value.absent(), 2772 4057 Value<int> rowid = const Value.absent(), 2773 - }) => SettingsCompanion.insert(key: key, value: value, updatedAt: updatedAt, rowid: rowid), 2774 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 4058 + }) => SettingsCompanion.insert( 4059 + key: key, 4060 + value: value, 4061 + updatedAt: updatedAt, 4062 + rowid: rowid, 4063 + ), 4064 + withReferenceMapper: (p0) => p0 4065 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 4066 + .toList(), 2775 4067 prefetchHooksCallback: null, 2776 4068 ), 2777 4069 ); ··· 2787 4079 $$SettingsTableAnnotationComposer, 2788 4080 $$SettingsTableCreateCompanionBuilder, 2789 4081 $$SettingsTableUpdateCompanionBuilder, 2790 - (SettingsEntry, BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>), 4082 + ( 4083 + SettingsEntry, 4084 + BaseReferences<_$AppDatabase, $SettingsTable, SettingsEntry>, 4085 + ), 2791 4086 SettingsEntry, 2792 4087 PrefetchHooks Function() 2793 4088 >; ··· 2814 4109 Value<int> rowid, 2815 4110 }); 2816 4111 2817 - class $$SavedFeedsTableFilterComposer extends Composer<_$AppDatabase, $SavedFeedsTable> { 4112 + class $$SavedFeedsTableFilterComposer 4113 + extends Composer<_$AppDatabase, $SavedFeedsTable> { 2818 4114 $$SavedFeedsTableFilterComposer({ 2819 4115 required super.$db, 2820 4116 required super.$table, ··· 2822 4118 super.$addJoinBuilderToRootComposer, 2823 4119 super.$removeJoinBuilderFromRootComposer, 2824 4120 }); 2825 - ColumnFilters<String> get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); 4121 + ColumnFilters<String> get id => $composableBuilder( 4122 + column: $table.id, 4123 + builder: (column) => ColumnFilters(column), 4124 + ); 2826 4125 2827 - ColumnFilters<String> get accountDid => 2828 - $composableBuilder(column: $table.accountDid, builder: (column) => ColumnFilters(column)); 4126 + ColumnFilters<String> get accountDid => $composableBuilder( 4127 + column: $table.accountDid, 4128 + builder: (column) => ColumnFilters(column), 4129 + ); 2829 4130 2830 - ColumnFilters<String> get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); 4131 + ColumnFilters<String> get type => $composableBuilder( 4132 + column: $table.type, 4133 + builder: (column) => ColumnFilters(column), 4134 + ); 2831 4135 2832 - ColumnFilters<String> get value => 2833 - $composableBuilder(column: $table.value, builder: (column) => ColumnFilters(column)); 4136 + ColumnFilters<String> get value => $composableBuilder( 4137 + column: $table.value, 4138 + builder: (column) => ColumnFilters(column), 4139 + ); 2834 4140 2835 - ColumnFilters<bool> get pinned => 2836 - $composableBuilder(column: $table.pinned, builder: (column) => ColumnFilters(column)); 4141 + ColumnFilters<bool> get pinned => $composableBuilder( 4142 + column: $table.pinned, 4143 + builder: (column) => ColumnFilters(column), 4144 + ); 2837 4145 2838 - ColumnFilters<int> get sortOrder => 2839 - $composableBuilder(column: $table.sortOrder, builder: (column) => ColumnFilters(column)); 4146 + ColumnFilters<int> get sortOrder => $composableBuilder( 4147 + column: $table.sortOrder, 4148 + builder: (column) => ColumnFilters(column), 4149 + ); 2840 4150 2841 - ColumnFilters<DateTime> get updatedAt => 2842 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); 4151 + ColumnFilters<DateTime> get updatedAt => $composableBuilder( 4152 + column: $table.updatedAt, 4153 + builder: (column) => ColumnFilters(column), 4154 + ); 2843 4155 } 2844 4156 2845 - class $$SavedFeedsTableOrderingComposer extends Composer<_$AppDatabase, $SavedFeedsTable> { 4157 + class $$SavedFeedsTableOrderingComposer 4158 + extends Composer<_$AppDatabase, $SavedFeedsTable> { 2846 4159 $$SavedFeedsTableOrderingComposer({ 2847 4160 required super.$db, 2848 4161 required super.$table, ··· 2850 4163 super.$addJoinBuilderToRootComposer, 2851 4164 super.$removeJoinBuilderFromRootComposer, 2852 4165 }); 2853 - ColumnOrderings<String> get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); 4166 + ColumnOrderings<String> get id => $composableBuilder( 4167 + column: $table.id, 4168 + builder: (column) => ColumnOrderings(column), 4169 + ); 2854 4170 2855 - ColumnOrderings<String> get accountDid => 2856 - $composableBuilder(column: $table.accountDid, builder: (column) => ColumnOrderings(column)); 4171 + ColumnOrderings<String> get accountDid => $composableBuilder( 4172 + column: $table.accountDid, 4173 + builder: (column) => ColumnOrderings(column), 4174 + ); 2857 4175 2858 - ColumnOrderings<String> get type => 2859 - $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); 4176 + ColumnOrderings<String> get type => $composableBuilder( 4177 + column: $table.type, 4178 + builder: (column) => ColumnOrderings(column), 4179 + ); 2860 4180 2861 - ColumnOrderings<String> get value => 2862 - $composableBuilder(column: $table.value, builder: (column) => ColumnOrderings(column)); 4181 + ColumnOrderings<String> get value => $composableBuilder( 4182 + column: $table.value, 4183 + builder: (column) => ColumnOrderings(column), 4184 + ); 2863 4185 2864 - ColumnOrderings<bool> get pinned => 2865 - $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); 4186 + ColumnOrderings<bool> get pinned => $composableBuilder( 4187 + column: $table.pinned, 4188 + builder: (column) => ColumnOrderings(column), 4189 + ); 2866 4190 2867 - ColumnOrderings<int> get sortOrder => 2868 - $composableBuilder(column: $table.sortOrder, builder: (column) => ColumnOrderings(column)); 4191 + ColumnOrderings<int> get sortOrder => $composableBuilder( 4192 + column: $table.sortOrder, 4193 + builder: (column) => ColumnOrderings(column), 4194 + ); 2869 4195 2870 - ColumnOrderings<DateTime> get updatedAt => 2871 - $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); 4196 + ColumnOrderings<DateTime> get updatedAt => $composableBuilder( 4197 + column: $table.updatedAt, 4198 + builder: (column) => ColumnOrderings(column), 4199 + ); 2872 4200 } 2873 4201 2874 - class $$SavedFeedsTableAnnotationComposer extends Composer<_$AppDatabase, $SavedFeedsTable> { 4202 + class $$SavedFeedsTableAnnotationComposer 4203 + extends Composer<_$AppDatabase, $SavedFeedsTable> { 2875 4204 $$SavedFeedsTableAnnotationComposer({ 2876 4205 required super.$db, 2877 4206 required super.$table, ··· 2879 4208 super.$addJoinBuilderToRootComposer, 2880 4209 super.$removeJoinBuilderFromRootComposer, 2881 4210 }); 2882 - GeneratedColumn<String> get id => $composableBuilder(column: $table.id, builder: (column) => column); 4211 + GeneratedColumn<String> get id => 4212 + $composableBuilder(column: $table.id, builder: (column) => column); 2883 4213 2884 - GeneratedColumn<String> get accountDid => $composableBuilder(column: $table.accountDid, builder: (column) => column); 4214 + GeneratedColumn<String> get accountDid => $composableBuilder( 4215 + column: $table.accountDid, 4216 + builder: (column) => column, 4217 + ); 2885 4218 2886 - GeneratedColumn<String> get type => $composableBuilder(column: $table.type, builder: (column) => column); 4219 + GeneratedColumn<String> get type => 4220 + $composableBuilder(column: $table.type, builder: (column) => column); 2887 4221 2888 - GeneratedColumn<String> get value => $composableBuilder(column: $table.value, builder: (column) => column); 4222 + GeneratedColumn<String> get value => 4223 + $composableBuilder(column: $table.value, builder: (column) => column); 2889 4224 2890 - GeneratedColumn<bool> get pinned => $composableBuilder(column: $table.pinned, builder: (column) => column); 4225 + GeneratedColumn<bool> get pinned => 4226 + $composableBuilder(column: $table.pinned, builder: (column) => column); 2891 4227 2892 - GeneratedColumn<int> get sortOrder => $composableBuilder(column: $table.sortOrder, builder: (column) => column); 4228 + GeneratedColumn<int> get sortOrder => 4229 + $composableBuilder(column: $table.sortOrder, builder: (column) => column); 2893 4230 2894 - GeneratedColumn<DateTime> get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); 4231 + GeneratedColumn<DateTime> get updatedAt => 4232 + $composableBuilder(column: $table.updatedAt, builder: (column) => column); 2895 4233 } 2896 4234 2897 4235 class $$SavedFeedsTableTableManager ··· 2905 4243 $$SavedFeedsTableAnnotationComposer, 2906 4244 $$SavedFeedsTableCreateCompanionBuilder, 2907 4245 $$SavedFeedsTableUpdateCompanionBuilder, 2908 - (SavedFeedEntry, BaseReferences<_$AppDatabase, $SavedFeedsTable, SavedFeedEntry>), 4246 + ( 4247 + SavedFeedEntry, 4248 + BaseReferences<_$AppDatabase, $SavedFeedsTable, SavedFeedEntry>, 4249 + ), 2909 4250 SavedFeedEntry, 2910 4251 PrefetchHooks Function() 2911 4252 > { ··· 2914 4255 TableManagerState( 2915 4256 db: db, 2916 4257 table: table, 2917 - createFilteringComposer: () => $$SavedFeedsTableFilterComposer($db: db, $table: table), 2918 - createOrderingComposer: () => $$SavedFeedsTableOrderingComposer($db: db, $table: table), 2919 - createComputedFieldComposer: () => $$SavedFeedsTableAnnotationComposer($db: db, $table: table), 4258 + createFilteringComposer: () => 4259 + $$SavedFeedsTableFilterComposer($db: db, $table: table), 4260 + createOrderingComposer: () => 4261 + $$SavedFeedsTableOrderingComposer($db: db, $table: table), 4262 + createComputedFieldComposer: () => 4263 + $$SavedFeedsTableAnnotationComposer($db: db, $table: table), 2920 4264 updateCompanionCallback: 2921 4265 ({ 2922 4266 Value<String> id = const Value.absent(), ··· 2957 4301 updatedAt: updatedAt, 2958 4302 rowid: rowid, 2959 4303 ), 2960 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 4304 + withReferenceMapper: (p0) => p0 4305 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 4306 + .toList(), 2961 4307 prefetchHooksCallback: null, 2962 4308 ), 2963 4309 ); ··· 2973 4319 $$SavedFeedsTableAnnotationComposer, 2974 4320 $$SavedFeedsTableCreateCompanionBuilder, 2975 4321 $$SavedFeedsTableUpdateCompanionBuilder, 2976 - (SavedFeedEntry, BaseReferences<_$AppDatabase, $SavedFeedsTable, SavedFeedEntry>), 4322 + ( 4323 + SavedFeedEntry, 4324 + BaseReferences<_$AppDatabase, $SavedFeedsTable, SavedFeedEntry>, 4325 + ), 2977 4326 SavedFeedEntry, 2978 4327 PrefetchHooks Function() 2979 4328 >; ··· 2994 4343 Value<String> accountDid, 2995 4344 }); 2996 4345 2997 - class $$SearchHistoryTableFilterComposer extends Composer<_$AppDatabase, $SearchHistoryTable> { 4346 + class $$SearchHistoryTableFilterComposer 4347 + extends Composer<_$AppDatabase, $SearchHistoryTable> { 2998 4348 $$SearchHistoryTableFilterComposer({ 2999 4349 required super.$db, 3000 4350 required super.$table, ··· 3002 4352 super.$addJoinBuilderToRootComposer, 3003 4353 super.$removeJoinBuilderFromRootComposer, 3004 4354 }); 3005 - ColumnFilters<int> get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); 4355 + ColumnFilters<int> get id => $composableBuilder( 4356 + column: $table.id, 4357 + builder: (column) => ColumnFilters(column), 4358 + ); 3006 4359 3007 - ColumnFilters<String> get query => 3008 - $composableBuilder(column: $table.query, builder: (column) => ColumnFilters(column)); 4360 + ColumnFilters<String> get query => $composableBuilder( 4361 + column: $table.query, 4362 + builder: (column) => ColumnFilters(column), 4363 + ); 3009 4364 3010 - ColumnFilters<String> get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); 4365 + ColumnFilters<String> get type => $composableBuilder( 4366 + column: $table.type, 4367 + builder: (column) => ColumnFilters(column), 4368 + ); 3011 4369 3012 - ColumnFilters<DateTime> get searchedAt => 3013 - $composableBuilder(column: $table.searchedAt, builder: (column) => ColumnFilters(column)); 4370 + ColumnFilters<DateTime> get searchedAt => $composableBuilder( 4371 + column: $table.searchedAt, 4372 + builder: (column) => ColumnFilters(column), 4373 + ); 3014 4374 3015 - ColumnFilters<String> get accountDid => 3016 - $composableBuilder(column: $table.accountDid, builder: (column) => ColumnFilters(column)); 4375 + ColumnFilters<String> get accountDid => $composableBuilder( 4376 + column: $table.accountDid, 4377 + builder: (column) => ColumnFilters(column), 4378 + ); 3017 4379 } 3018 4380 3019 - class $$SearchHistoryTableOrderingComposer extends Composer<_$AppDatabase, $SearchHistoryTable> { 4381 + class $$SearchHistoryTableOrderingComposer 4382 + extends Composer<_$AppDatabase, $SearchHistoryTable> { 3020 4383 $$SearchHistoryTableOrderingComposer({ 3021 4384 required super.$db, 3022 4385 required super.$table, ··· 3024 4387 super.$addJoinBuilderToRootComposer, 3025 4388 super.$removeJoinBuilderFromRootComposer, 3026 4389 }); 3027 - ColumnOrderings<int> get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); 4390 + ColumnOrderings<int> get id => $composableBuilder( 4391 + column: $table.id, 4392 + builder: (column) => ColumnOrderings(column), 4393 + ); 3028 4394 3029 - ColumnOrderings<String> get query => 3030 - $composableBuilder(column: $table.query, builder: (column) => ColumnOrderings(column)); 4395 + ColumnOrderings<String> get query => $composableBuilder( 4396 + column: $table.query, 4397 + builder: (column) => ColumnOrderings(column), 4398 + ); 3031 4399 3032 - ColumnOrderings<String> get type => 3033 - $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); 4400 + ColumnOrderings<String> get type => $composableBuilder( 4401 + column: $table.type, 4402 + builder: (column) => ColumnOrderings(column), 4403 + ); 3034 4404 3035 - ColumnOrderings<DateTime> get searchedAt => 3036 - $composableBuilder(column: $table.searchedAt, builder: (column) => ColumnOrderings(column)); 4405 + ColumnOrderings<DateTime> get searchedAt => $composableBuilder( 4406 + column: $table.searchedAt, 4407 + builder: (column) => ColumnOrderings(column), 4408 + ); 3037 4409 3038 - ColumnOrderings<String> get accountDid => 3039 - $composableBuilder(column: $table.accountDid, builder: (column) => ColumnOrderings(column)); 4410 + ColumnOrderings<String> get accountDid => $composableBuilder( 4411 + column: $table.accountDid, 4412 + builder: (column) => ColumnOrderings(column), 4413 + ); 3040 4414 } 3041 4415 3042 - class $$SearchHistoryTableAnnotationComposer extends Composer<_$AppDatabase, $SearchHistoryTable> { 4416 + class $$SearchHistoryTableAnnotationComposer 4417 + extends Composer<_$AppDatabase, $SearchHistoryTable> { 3043 4418 $$SearchHistoryTableAnnotationComposer({ 3044 4419 required super.$db, 3045 4420 required super.$table, ··· 3047 4422 super.$addJoinBuilderToRootComposer, 3048 4423 super.$removeJoinBuilderFromRootComposer, 3049 4424 }); 3050 - GeneratedColumn<int> get id => $composableBuilder(column: $table.id, builder: (column) => column); 4425 + GeneratedColumn<int> get id => 4426 + $composableBuilder(column: $table.id, builder: (column) => column); 3051 4427 3052 - GeneratedColumn<String> get query => $composableBuilder(column: $table.query, builder: (column) => column); 4428 + GeneratedColumn<String> get query => 4429 + $composableBuilder(column: $table.query, builder: (column) => column); 3053 4430 3054 - GeneratedColumn<String> get type => $composableBuilder(column: $table.type, builder: (column) => column); 4431 + GeneratedColumn<String> get type => 4432 + $composableBuilder(column: $table.type, builder: (column) => column); 3055 4433 3056 - GeneratedColumn<DateTime> get searchedAt => 3057 - $composableBuilder(column: $table.searchedAt, builder: (column) => column); 4434 + GeneratedColumn<DateTime> get searchedAt => $composableBuilder( 4435 + column: $table.searchedAt, 4436 + builder: (column) => column, 4437 + ); 3058 4438 3059 - GeneratedColumn<String> get accountDid => $composableBuilder(column: $table.accountDid, builder: (column) => column); 4439 + GeneratedColumn<String> get accountDid => $composableBuilder( 4440 + column: $table.accountDid, 4441 + builder: (column) => column, 4442 + ); 3060 4443 } 3061 4444 3062 4445 class $$SearchHistoryTableTableManager ··· 3070 4453 $$SearchHistoryTableAnnotationComposer, 3071 4454 $$SearchHistoryTableCreateCompanionBuilder, 3072 4455 $$SearchHistoryTableUpdateCompanionBuilder, 3073 - (SearchHistoryEntry, BaseReferences<_$AppDatabase, $SearchHistoryTable, SearchHistoryEntry>), 4456 + ( 4457 + SearchHistoryEntry, 4458 + BaseReferences< 4459 + _$AppDatabase, 4460 + $SearchHistoryTable, 4461 + SearchHistoryEntry 4462 + >, 4463 + ), 3074 4464 SearchHistoryEntry, 3075 4465 PrefetchHooks Function() 3076 4466 > { ··· 3079 4469 TableManagerState( 3080 4470 db: db, 3081 4471 table: table, 3082 - createFilteringComposer: () => $$SearchHistoryTableFilterComposer($db: db, $table: table), 3083 - createOrderingComposer: () => $$SearchHistoryTableOrderingComposer($db: db, $table: table), 3084 - createComputedFieldComposer: () => $$SearchHistoryTableAnnotationComposer($db: db, $table: table), 4472 + createFilteringComposer: () => 4473 + $$SearchHistoryTableFilterComposer($db: db, $table: table), 4474 + createOrderingComposer: () => 4475 + $$SearchHistoryTableOrderingComposer($db: db, $table: table), 4476 + createComputedFieldComposer: () => 4477 + $$SearchHistoryTableAnnotationComposer($db: db, $table: table), 3085 4478 updateCompanionCallback: 3086 4479 ({ 3087 4480 Value<int> id = const Value.absent(), ··· 3110 4503 searchedAt: searchedAt, 3111 4504 accountDid: accountDid, 3112 4505 ), 3113 - withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), 4506 + withReferenceMapper: (p0) => p0 4507 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 4508 + .toList(), 3114 4509 prefetchHooksCallback: null, 3115 4510 ), 3116 4511 ); ··· 3126 4521 $$SearchHistoryTableAnnotationComposer, 3127 4522 $$SearchHistoryTableCreateCompanionBuilder, 3128 4523 $$SearchHistoryTableUpdateCompanionBuilder, 3129 - (SearchHistoryEntry, BaseReferences<_$AppDatabase, $SearchHistoryTable, SearchHistoryEntry>), 4524 + ( 4525 + SearchHistoryEntry, 4526 + BaseReferences<_$AppDatabase, $SearchHistoryTable, SearchHistoryEntry>, 4527 + ), 3130 4528 SearchHistoryEntry, 3131 4529 PrefetchHooks Function() 3132 4530 >; 4531 + typedef $$DraftsTableCreateCompanionBuilder = 4532 + DraftsCompanion Function({ 4533 + Value<int> id, 4534 + required String accountDid, 4535 + required String content, 4536 + Value<String?> replyUri, 4537 + Value<String?> replyCid, 4538 + Value<String?> rootUri, 4539 + Value<String?> rootCid, 4540 + Value<String?> embedJson, 4541 + Value<String?> mediaPaths, 4542 + Value<DateTime> createdAt, 4543 + Value<DateTime> updatedAt, 4544 + Value<DateTime?> scheduledAt, 4545 + }); 4546 + typedef $$DraftsTableUpdateCompanionBuilder = 4547 + DraftsCompanion Function({ 4548 + Value<int> id, 4549 + Value<String> accountDid, 4550 + Value<String> content, 4551 + Value<String?> replyUri, 4552 + Value<String?> replyCid, 4553 + Value<String?> rootUri, 4554 + Value<String?> rootCid, 4555 + Value<String?> embedJson, 4556 + Value<String?> mediaPaths, 4557 + Value<DateTime> createdAt, 4558 + Value<DateTime> updatedAt, 4559 + Value<DateTime?> scheduledAt, 4560 + }); 4561 + 4562 + class $$DraftsTableFilterComposer 4563 + extends Composer<_$AppDatabase, $DraftsTable> { 4564 + $$DraftsTableFilterComposer({ 4565 + required super.$db, 4566 + required super.$table, 4567 + super.joinBuilder, 4568 + super.$addJoinBuilderToRootComposer, 4569 + super.$removeJoinBuilderFromRootComposer, 4570 + }); 4571 + ColumnFilters<int> get id => $composableBuilder( 4572 + column: $table.id, 4573 + builder: (column) => ColumnFilters(column), 4574 + ); 4575 + 4576 + ColumnFilters<String> get accountDid => $composableBuilder( 4577 + column: $table.accountDid, 4578 + builder: (column) => ColumnFilters(column), 4579 + ); 4580 + 4581 + ColumnFilters<String> get content => $composableBuilder( 4582 + column: $table.content, 4583 + builder: (column) => ColumnFilters(column), 4584 + ); 4585 + 4586 + ColumnFilters<String> get replyUri => $composableBuilder( 4587 + column: $table.replyUri, 4588 + builder: (column) => ColumnFilters(column), 4589 + ); 4590 + 4591 + ColumnFilters<String> get replyCid => $composableBuilder( 4592 + column: $table.replyCid, 4593 + builder: (column) => ColumnFilters(column), 4594 + ); 4595 + 4596 + ColumnFilters<String> get rootUri => $composableBuilder( 4597 + column: $table.rootUri, 4598 + builder: (column) => ColumnFilters(column), 4599 + ); 4600 + 4601 + ColumnFilters<String> get rootCid => $composableBuilder( 4602 + column: $table.rootCid, 4603 + builder: (column) => ColumnFilters(column), 4604 + ); 4605 + 4606 + ColumnFilters<String> get embedJson => $composableBuilder( 4607 + column: $table.embedJson, 4608 + builder: (column) => ColumnFilters(column), 4609 + ); 4610 + 4611 + ColumnFilters<String> get mediaPaths => $composableBuilder( 4612 + column: $table.mediaPaths, 4613 + builder: (column) => ColumnFilters(column), 4614 + ); 4615 + 4616 + ColumnFilters<DateTime> get createdAt => $composableBuilder( 4617 + column: $table.createdAt, 4618 + builder: (column) => ColumnFilters(column), 4619 + ); 4620 + 4621 + ColumnFilters<DateTime> get updatedAt => $composableBuilder( 4622 + column: $table.updatedAt, 4623 + builder: (column) => ColumnFilters(column), 4624 + ); 4625 + 4626 + ColumnFilters<DateTime> get scheduledAt => $composableBuilder( 4627 + column: $table.scheduledAt, 4628 + builder: (column) => ColumnFilters(column), 4629 + ); 4630 + } 4631 + 4632 + class $$DraftsTableOrderingComposer 4633 + extends Composer<_$AppDatabase, $DraftsTable> { 4634 + $$DraftsTableOrderingComposer({ 4635 + required super.$db, 4636 + required super.$table, 4637 + super.joinBuilder, 4638 + super.$addJoinBuilderToRootComposer, 4639 + super.$removeJoinBuilderFromRootComposer, 4640 + }); 4641 + ColumnOrderings<int> get id => $composableBuilder( 4642 + column: $table.id, 4643 + builder: (column) => ColumnOrderings(column), 4644 + ); 4645 + 4646 + ColumnOrderings<String> get accountDid => $composableBuilder( 4647 + column: $table.accountDid, 4648 + builder: (column) => ColumnOrderings(column), 4649 + ); 4650 + 4651 + ColumnOrderings<String> get content => $composableBuilder( 4652 + column: $table.content, 4653 + builder: (column) => ColumnOrderings(column), 4654 + ); 4655 + 4656 + ColumnOrderings<String> get replyUri => $composableBuilder( 4657 + column: $table.replyUri, 4658 + builder: (column) => ColumnOrderings(column), 4659 + ); 4660 + 4661 + ColumnOrderings<String> get replyCid => $composableBuilder( 4662 + column: $table.replyCid, 4663 + builder: (column) => ColumnOrderings(column), 4664 + ); 4665 + 4666 + ColumnOrderings<String> get rootUri => $composableBuilder( 4667 + column: $table.rootUri, 4668 + builder: (column) => ColumnOrderings(column), 4669 + ); 4670 + 4671 + ColumnOrderings<String> get rootCid => $composableBuilder( 4672 + column: $table.rootCid, 4673 + builder: (column) => ColumnOrderings(column), 4674 + ); 4675 + 4676 + ColumnOrderings<String> get embedJson => $composableBuilder( 4677 + column: $table.embedJson, 4678 + builder: (column) => ColumnOrderings(column), 4679 + ); 4680 + 4681 + ColumnOrderings<String> get mediaPaths => $composableBuilder( 4682 + column: $table.mediaPaths, 4683 + builder: (column) => ColumnOrderings(column), 4684 + ); 4685 + 4686 + ColumnOrderings<DateTime> get createdAt => $composableBuilder( 4687 + column: $table.createdAt, 4688 + builder: (column) => ColumnOrderings(column), 4689 + ); 4690 + 4691 + ColumnOrderings<DateTime> get updatedAt => $composableBuilder( 4692 + column: $table.updatedAt, 4693 + builder: (column) => ColumnOrderings(column), 4694 + ); 4695 + 4696 + ColumnOrderings<DateTime> get scheduledAt => $composableBuilder( 4697 + column: $table.scheduledAt, 4698 + builder: (column) => ColumnOrderings(column), 4699 + ); 4700 + } 4701 + 4702 + class $$DraftsTableAnnotationComposer 4703 + extends Composer<_$AppDatabase, $DraftsTable> { 4704 + $$DraftsTableAnnotationComposer({ 4705 + required super.$db, 4706 + required super.$table, 4707 + super.joinBuilder, 4708 + super.$addJoinBuilderToRootComposer, 4709 + super.$removeJoinBuilderFromRootComposer, 4710 + }); 4711 + GeneratedColumn<int> get id => 4712 + $composableBuilder(column: $table.id, builder: (column) => column); 4713 + 4714 + GeneratedColumn<String> get accountDid => $composableBuilder( 4715 + column: $table.accountDid, 4716 + builder: (column) => column, 4717 + ); 4718 + 4719 + GeneratedColumn<String> get content => 4720 + $composableBuilder(column: $table.content, builder: (column) => column); 4721 + 4722 + GeneratedColumn<String> get replyUri => 4723 + $composableBuilder(column: $table.replyUri, builder: (column) => column); 4724 + 4725 + GeneratedColumn<String> get replyCid => 4726 + $composableBuilder(column: $table.replyCid, builder: (column) => column); 4727 + 4728 + GeneratedColumn<String> get rootUri => 4729 + $composableBuilder(column: $table.rootUri, builder: (column) => column); 4730 + 4731 + GeneratedColumn<String> get rootCid => 4732 + $composableBuilder(column: $table.rootCid, builder: (column) => column); 4733 + 4734 + GeneratedColumn<String> get embedJson => 4735 + $composableBuilder(column: $table.embedJson, builder: (column) => column); 4736 + 4737 + GeneratedColumn<String> get mediaPaths => $composableBuilder( 4738 + column: $table.mediaPaths, 4739 + builder: (column) => column, 4740 + ); 4741 + 4742 + GeneratedColumn<DateTime> get createdAt => 4743 + $composableBuilder(column: $table.createdAt, builder: (column) => column); 4744 + 4745 + GeneratedColumn<DateTime> get updatedAt => 4746 + $composableBuilder(column: $table.updatedAt, builder: (column) => column); 4747 + 4748 + GeneratedColumn<DateTime> get scheduledAt => $composableBuilder( 4749 + column: $table.scheduledAt, 4750 + builder: (column) => column, 4751 + ); 4752 + } 4753 + 4754 + class $$DraftsTableTableManager 4755 + extends 4756 + RootTableManager< 4757 + _$AppDatabase, 4758 + $DraftsTable, 4759 + DraftEntry, 4760 + $$DraftsTableFilterComposer, 4761 + $$DraftsTableOrderingComposer, 4762 + $$DraftsTableAnnotationComposer, 4763 + $$DraftsTableCreateCompanionBuilder, 4764 + $$DraftsTableUpdateCompanionBuilder, 4765 + (DraftEntry, BaseReferences<_$AppDatabase, $DraftsTable, DraftEntry>), 4766 + DraftEntry, 4767 + PrefetchHooks Function() 4768 + > { 4769 + $$DraftsTableTableManager(_$AppDatabase db, $DraftsTable table) 4770 + : super( 4771 + TableManagerState( 4772 + db: db, 4773 + table: table, 4774 + createFilteringComposer: () => 4775 + $$DraftsTableFilterComposer($db: db, $table: table), 4776 + createOrderingComposer: () => 4777 + $$DraftsTableOrderingComposer($db: db, $table: table), 4778 + createComputedFieldComposer: () => 4779 + $$DraftsTableAnnotationComposer($db: db, $table: table), 4780 + updateCompanionCallback: 4781 + ({ 4782 + Value<int> id = const Value.absent(), 4783 + Value<String> accountDid = const Value.absent(), 4784 + Value<String> content = const Value.absent(), 4785 + Value<String?> replyUri = const Value.absent(), 4786 + Value<String?> replyCid = const Value.absent(), 4787 + Value<String?> rootUri = const Value.absent(), 4788 + Value<String?> rootCid = const Value.absent(), 4789 + Value<String?> embedJson = const Value.absent(), 4790 + Value<String?> mediaPaths = const Value.absent(), 4791 + Value<DateTime> createdAt = const Value.absent(), 4792 + Value<DateTime> updatedAt = const Value.absent(), 4793 + Value<DateTime?> scheduledAt = const Value.absent(), 4794 + }) => DraftsCompanion( 4795 + id: id, 4796 + accountDid: accountDid, 4797 + content: content, 4798 + replyUri: replyUri, 4799 + replyCid: replyCid, 4800 + rootUri: rootUri, 4801 + rootCid: rootCid, 4802 + embedJson: embedJson, 4803 + mediaPaths: mediaPaths, 4804 + createdAt: createdAt, 4805 + updatedAt: updatedAt, 4806 + scheduledAt: scheduledAt, 4807 + ), 4808 + createCompanionCallback: 4809 + ({ 4810 + Value<int> id = const Value.absent(), 4811 + required String accountDid, 4812 + required String content, 4813 + Value<String?> replyUri = const Value.absent(), 4814 + Value<String?> replyCid = const Value.absent(), 4815 + Value<String?> rootUri = const Value.absent(), 4816 + Value<String?> rootCid = const Value.absent(), 4817 + Value<String?> embedJson = const Value.absent(), 4818 + Value<String?> mediaPaths = const Value.absent(), 4819 + Value<DateTime> createdAt = const Value.absent(), 4820 + Value<DateTime> updatedAt = const Value.absent(), 4821 + Value<DateTime?> scheduledAt = const Value.absent(), 4822 + }) => DraftsCompanion.insert( 4823 + id: id, 4824 + accountDid: accountDid, 4825 + content: content, 4826 + replyUri: replyUri, 4827 + replyCid: replyCid, 4828 + rootUri: rootUri, 4829 + rootCid: rootCid, 4830 + embedJson: embedJson, 4831 + mediaPaths: mediaPaths, 4832 + createdAt: createdAt, 4833 + updatedAt: updatedAt, 4834 + scheduledAt: scheduledAt, 4835 + ), 4836 + withReferenceMapper: (p0) => p0 4837 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) 4838 + .toList(), 4839 + prefetchHooksCallback: null, 4840 + ), 4841 + ); 4842 + } 4843 + 4844 + typedef $$DraftsTableProcessedTableManager = 4845 + ProcessedTableManager< 4846 + _$AppDatabase, 4847 + $DraftsTable, 4848 + DraftEntry, 4849 + $$DraftsTableFilterComposer, 4850 + $$DraftsTableOrderingComposer, 4851 + $$DraftsTableAnnotationComposer, 4852 + $$DraftsTableCreateCompanionBuilder, 4853 + $$DraftsTableUpdateCompanionBuilder, 4854 + (DraftEntry, BaseReferences<_$AppDatabase, $DraftsTable, DraftEntry>), 4855 + DraftEntry, 4856 + PrefetchHooks Function() 4857 + >; 3133 4858 3134 4859 class $AppDatabaseManager { 3135 4860 final _$AppDatabase _db; 3136 4861 $AppDatabaseManager(this._db); 3137 - $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db, _db.accounts); 3138 - $$CachedProfilesTableTableManager get cachedProfiles => $$CachedProfilesTableTableManager(_db, _db.cachedProfiles); 3139 - $$CachedPostsTableTableManager get cachedPosts => $$CachedPostsTableTableManager(_db, _db.cachedPosts); 3140 - $$SettingsTableTableManager get settings => $$SettingsTableTableManager(_db, _db.settings); 3141 - $$SavedFeedsTableTableManager get savedFeeds => $$SavedFeedsTableTableManager(_db, _db.savedFeeds); 3142 - $$SearchHistoryTableTableManager get searchHistory => $$SearchHistoryTableTableManager(_db, _db.searchHistory); 4862 + $$AccountsTableTableManager get accounts => 4863 + $$AccountsTableTableManager(_db, _db.accounts); 4864 + $$CachedProfilesTableTableManager get cachedProfiles => 4865 + $$CachedProfilesTableTableManager(_db, _db.cachedProfiles); 4866 + $$CachedPostsTableTableManager get cachedPosts => 4867 + $$CachedPostsTableTableManager(_db, _db.cachedPosts); 4868 + $$SettingsTableTableManager get settings => 4869 + $$SettingsTableTableManager(_db, _db.settings); 4870 + $$SavedFeedsTableTableManager get savedFeeds => 4871 + $$SavedFeedsTableTableManager(_db, _db.savedFeeds); 4872 + $$SearchHistoryTableTableManager get searchHistory => 4873 + $$SearchHistoryTableTableManager(_db, _db.searchHistory); 4874 + $$DraftsTableTableManager get drafts => 4875 + $$DraftsTableTableManager(_db, _db.drafts); 3143 4876 }
+16
lib/core/database/tables.dart
··· 74 74 DateTimeColumn get searchedAt => dateTime().withDefault(currentDateAndTime)(); 75 75 TextColumn get accountDid => text()(); 76 76 } 77 + 78 + @DataClassName('DraftEntry') 79 + class Drafts extends Table { 80 + IntColumn get id => integer().autoIncrement()(); 81 + TextColumn get accountDid => text()(); 82 + TextColumn get content => text()(); 83 + TextColumn get replyUri => text().nullable()(); 84 + TextColumn get replyCid => text().nullable()(); 85 + TextColumn get rootUri => text().nullable()(); 86 + TextColumn get rootCid => text().nullable()(); 87 + TextColumn get embedJson => text().nullable()(); 88 + TextColumn get mediaPaths => text().nullable()(); 89 + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); 90 + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); 91 + DateTimeColumn get scheduledAt => dateTime().nullable()(); 92 + }
+28
lib/core/router/app_router.dart
··· 2 2 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 + import 'package:bluesky/bluesky.dart'; 6 + import 'package:flutter_bloc/flutter_bloc.dart'; 7 + import 'package:lazurite/core/database/app_database.dart'; 5 8 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 6 9 import 'package:lazurite/core/router/app_shell.dart'; 7 10 import 'package:lazurite/features/auth/presentation/login_screen.dart'; 11 + import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 12 + import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; 13 + import 'package:lazurite/features/compose/presentation/compose_screen.dart'; 8 14 import 'package:lazurite/features/devtools/presentation/dev_tools_screen.dart'; 9 15 import 'package:lazurite/features/feed/presentation/feed_management_screen.dart'; 10 16 import 'package:lazurite/features/feed/presentation/home_feed_screen.dart'; ··· 44 50 }, 45 51 routes: [ 46 52 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 53 + GoRoute( 54 + path: '/compose', 55 + parentNavigatorKey: _rootNavigatorKey, 56 + builder: (context, state) { 57 + final args = state.extra as ComposeRouteArgs?; 58 + return BlocProvider( 59 + create: (_) => ComposeBloc( 60 + composeRepository: ComposeRepository(bluesky: context.read<Bluesky>()), 61 + database: context.read<AppDatabase>(), 62 + accountDid: context.read<String>(), 63 + ), 64 + child: ComposeScreen( 65 + replyParentUri: args?.replyParentUri, 66 + replyParentCid: args?.replyParentCid, 67 + replyRootUri: args?.replyRootUri, 68 + replyRootCid: args?.replyRootCid, 69 + replyAuthorHandle: args?.replyAuthorHandle, 70 + draftId: args?.draftId, 71 + ), 72 + ); 73 + }, 74 + ), 47 75 StatefulShellRoute.indexedStack( 48 76 builder: (context, state, navigationShell) => AppShell(navigationShell: navigationShell), 49 77 branches: [
+273
lib/core/scheduler/post_scheduler.dart
··· 1 + import 'dart:convert'; 2 + import 'dart:io'; 3 + import 'dart:typed_data'; 4 + 5 + import 'package:atproto_core/atproto_core.dart' show Blob; 6 + import 'package:bluesky/app_bsky_video_defs.dart' show KnownJobStatusState; 7 + import 'package:bluesky_text/bluesky_text.dart'; 8 + import 'package:flutter/widgets.dart'; 9 + import 'package:lazurite/core/database/app_database.dart'; 10 + import 'package:lazurite/core/logging/app_logger.dart'; 11 + import 'package:lazurite/core/network/xrpc_client_factory.dart'; 12 + import 'package:lazurite/features/auth/data/auth_repository.dart'; 13 + import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 14 + import 'package:workmanager/workmanager.dart'; 15 + 16 + const _kTaskName = 'lazurite.scheduled_post'; 17 + const _kTaskTag = 'scheduled_post'; 18 + const _kVideoPollInterval = Duration(seconds: 2); 19 + const _kVideoPollTimeout = Duration(minutes: 5); 20 + 21 + /// Top-level callback required by WorkManager. 22 + /// Runs in an isolate; must be a top-level function. 23 + @pragma('vm:entry-point') 24 + void callbackDispatcher() { 25 + Workmanager().executeTask((taskName, inputData) async { 26 + if (taskName != _kTaskName) return Future.value(true); 27 + 28 + final draftId = inputData?['draftId'] as int?; 29 + if (draftId == null) return Future.value(false); 30 + 31 + try { 32 + await _submitScheduledDraft(draftId); 33 + return Future.value(true); 34 + } catch (e, stackTrace) { 35 + log.e('Scheduled post failed for draft $draftId', error: e, stackTrace: stackTrace); 36 + return Future.value(false); 37 + } 38 + }); 39 + } 40 + 41 + /// Executes a scheduled post from a persisted draft. 42 + /// 43 + /// Opens a fresh [AppDatabase], restores the user session, rebuilds the post 44 + /// (uploading any media blobs as needed), creates the AT Protocol record, then 45 + /// deletes the draft on success. 46 + Future<void> _submitScheduledDraft(int draftId) async { 47 + WidgetsFlutterBinding.ensureInitialized(); 48 + 49 + final database = AppDatabase(); 50 + try { 51 + final authRepo = AuthRepository(database: database); 52 + final tokens = await authRepo.restoreSession(); 53 + if (tokens == null) { 54 + throw Exception('No authenticated session for scheduled draft $draftId'); 55 + } 56 + 57 + final bluesky = createBlueskyClient(tokens); 58 + if (bluesky == null) { 59 + throw Exception('Could not build Bluesky client for scheduled draft $draftId'); 60 + } 61 + 62 + final draft = await database.getDraft(draftId); 63 + if (draft == null) { 64 + log.w('Scheduled post: draft $draftId not found — may have been manually deleted'); 65 + return; 66 + } 67 + 68 + final composeRepo = ComposeRepository(bluesky: bluesky); 69 + 70 + final facets = <Map<String, dynamic>>[]; 71 + for (final entity in BlueskyText(draft.content).entities) { 72 + try { 73 + final facet = await entity.toFacet().timeout( 74 + const Duration(seconds: 5), 75 + onTimeout: () { 76 + log.w('Scheduled post: timeout resolving facet for "${entity.value}"'); 77 + return {}; 78 + }, 79 + ); 80 + if (facet.isNotEmpty) facets.add(facet); 81 + } catch (e) { 82 + log.w('Scheduled post: could not resolve facet for "${entity.value}": $e'); 83 + } 84 + } 85 + 86 + Map<String, dynamic>? embed; 87 + 88 + if (draft.embedJson != null) { 89 + final decoded = jsonDecode(draft.embedJson!) as Map<String, dynamic>; 90 + final type = decoded['type'] as String?; 91 + 92 + if (type == 'images') { 93 + embed = await _buildImageEmbed(composeRepo, decoded); 94 + } else if (type == 'video') { 95 + embed = await _buildVideoEmbed(composeRepo, decoded); 96 + } 97 + } 98 + 99 + Map<String, dynamic>? reply; 100 + if (draft.replyUri != null && draft.replyCid != null) { 101 + reply = { 102 + 'parent': {'uri': draft.replyUri, 'cid': draft.replyCid}, 103 + 'root': {'uri': draft.rootUri ?? draft.replyUri, 'cid': draft.rootCid ?? draft.replyCid}, 104 + }; 105 + } 106 + 107 + final success = await composeRepo.createPost( 108 + text: draft.content, 109 + facets: facets, 110 + embed: embed, 111 + reply: reply, 112 + repo: tokens.did, 113 + ); 114 + 115 + if (!success) { 116 + throw Exception('createPost returned false for scheduled draft $draftId'); 117 + } 118 + 119 + await database.deleteDraft(draftId); 120 + log.i('Scheduled post: sent draft $draftId successfully'); 121 + } finally { 122 + await database.close(); 123 + } 124 + } 125 + 126 + /// Uploads images from [embedJson] `{ "paths": [...], "altTexts": [...] }`. 127 + Future<Map<String, dynamic>?> _buildImageEmbed(ComposeRepository repo, Map<String, dynamic> embedJson) async { 128 + final paths = (embedJson['paths'] as List<dynamic>? ?? []).cast<String>(); 129 + final alts = (embedJson['altTexts'] as List<dynamic>? ?? []).cast<String>(); 130 + final images = <Map<String, dynamic>>[]; 131 + 132 + for (var i = 0; i < paths.length; i++) { 133 + final file = File(paths[i]); 134 + if (!file.existsSync()) { 135 + log.w('Scheduled post: image not found at ${paths[i]}, skipping'); 136 + continue; 137 + } 138 + 139 + final bytes = await file.readAsBytes(); 140 + final mime = _detectMime(bytes); 141 + if (mime == null) { 142 + log.w('Scheduled post: unsupported image format at ${paths[i]}, skipping'); 143 + continue; 144 + } 145 + 146 + final blobRef = await repo.uploadBlob(bytes.toList(), mimeType: mime); 147 + if (blobRef == null) { 148 + throw Exception('Failed to upload image ${paths[i]}'); 149 + } 150 + 151 + final altText = i < alts.length ? alts[i] : ''; 152 + final entry = <String, dynamic>{'image': blobRef.toJson(), 'alt': altText}; 153 + 154 + try { 155 + final dims = await readImageDimensions(bytes.toList()); 156 + if (dims != null) { 157 + entry['aspectRatio'] = {'width': dims.width, 'height': dims.height}; 158 + } 159 + } catch (_) { 160 + log.w('Scheduled post: could not read image dimensions for ${paths[i]}'); 161 + } 162 + 163 + images.add(entry); 164 + } 165 + 166 + if (images.isEmpty) return null; 167 + return {'\$type': 'app.bsky.embed.images', 'images': images}; 168 + } 169 + 170 + /// Re-uploads a video from its local path and polls the processing job. 171 + Future<Map<String, dynamic>?> _buildVideoEmbed(ComposeRepository repo, Map<String, dynamic> embedJson) async { 172 + final path = embedJson['path'] as String?; 173 + final altText = (embedJson['alt'] as String?) ?? ''; 174 + 175 + if (path == null) return null; 176 + 177 + final file = File(path); 178 + if (!file.existsSync()) { 179 + log.w('Scheduled post: video not found at $path'); 180 + return null; 181 + } 182 + 183 + final bytes = await file.readAsBytes(); 184 + if (bytes.length > kMaxVideoBytes) { 185 + throw Exception('Video exceeds 100 MB limit: $path'); 186 + } 187 + 188 + final jobId = await repo.uploadVideo(bytes); 189 + if (jobId == null) throw Exception('Failed to upload video for scheduled post'); 190 + 191 + final blob = await _pollVideoJob(repo, jobId); 192 + if (blob == null) throw Exception('Video processing failed for scheduled post'); 193 + 194 + return {'\$type': 'app.bsky.embed.video', 'video': blob.toJson(), 'alt': altText}; 195 + } 196 + 197 + /// Polls `getJobStatus` until the video is ready or the deadline passes. 198 + Future<Blob?> _pollVideoJob(ComposeRepository repo, String jobId) async { 199 + final deadline = DateTime.now().add(_kVideoPollTimeout); 200 + 201 + while (DateTime.now().isBefore(deadline)) { 202 + await Future<void>.delayed(_kVideoPollInterval); 203 + try { 204 + final status = await repo.getJobStatus(jobId); 205 + if (status == null) continue; 206 + 207 + final knownState = (status as dynamic).state.knownValue; 208 + if (knownState == KnownJobStatusState.jOB_STATE_COMPLETED && status.blob != null) { 209 + return status.blob as Blob; 210 + } 211 + if (knownState == KnownJobStatusState.jOB_STATE_FAILED) { 212 + final error = (status as dynamic).error as String?; 213 + log.e('Scheduled post: video processing failed — ${error ?? 'unknown error'}'); 214 + return null; 215 + } 216 + } catch (e) { 217 + log.w('Scheduled post: video job poll error (retrying)', error: e); 218 + } 219 + } 220 + 221 + log.e('Scheduled post: video processing timed out for job $jobId'); 222 + return null; 223 + } 224 + 225 + /// Returns the MIME type from magic bytes, or null for unsupported formats. 226 + String? _detectMime(Uint8List bytes) { 227 + if (bytes.length < 12) return null; 228 + if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) return 'image/jpeg'; 229 + if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) return 'image/png'; 230 + if (bytes[0] == 0x52 && 231 + bytes[1] == 0x49 && 232 + bytes[2] == 0x46 && 233 + bytes[3] == 0x46 && 234 + bytes[8] == 0x57 && 235 + bytes[9] == 0x45 && 236 + bytes[10] == 0x42 && 237 + bytes[11] == 0x50) { 238 + return 'image/webp'; 239 + } 240 + return null; 241 + } 242 + 243 + class PostScheduler { 244 + PostScheduler._(); 245 + 246 + /// Initialises WorkManager. Call once from main() before runApp(). 247 + static Future<void> initialize() async { 248 + await Workmanager().initialize(callbackDispatcher, isInDebugMode: false); 249 + } 250 + 251 + /// Registers a one-off background task to fire at [scheduledAt]. 252 + static Future<void> schedulePost({required int draftId, required DateTime scheduledAt}) async { 253 + final delay = scheduledAt.difference(DateTime.now()); 254 + if (delay.isNegative) return; 255 + 256 + await Workmanager().registerOneOffTask( 257 + 'scheduled_post_$draftId', 258 + _kTaskName, 259 + initialDelay: delay, 260 + tag: _kTaskTag, 261 + constraints: Constraints(networkType: NetworkType.connected), 262 + inputData: {'draftId': draftId}, 263 + existingWorkPolicy: ExistingWorkPolicy.replace, 264 + ); 265 + 266 + log.i('Scheduled post for draft $draftId at $scheduledAt'); 267 + } 268 + 269 + /// Cancels a previously scheduled post task. 270 + static Future<void> cancelPost(int draftId) async { 271 + await Workmanager().cancelByUniqueName('scheduled_post_$draftId'); 272 + } 273 + }
+644
lib/features/compose/bloc/compose_bloc.dart
··· 1 + import 'dart:async'; 2 + import 'dart:convert'; 3 + import 'dart:io'; 4 + import 'dart:ui' as ui; 5 + 6 + import 'package:atproto_core/atproto_core.dart' show Blob, BlobRef; 7 + import 'package:bluesky/bluesky.dart'; 8 + import 'package:bluesky/app_bsky_video_defs.dart' show KnownJobStatusState; 9 + import 'package:bluesky_text/bluesky_text.dart'; 10 + import 'package:characters/characters.dart'; 11 + import 'package:drift/drift.dart'; 12 + import 'package:equatable/equatable.dart'; 13 + import 'package:flutter_bloc/flutter_bloc.dart'; 14 + import 'package:lazurite/core/database/app_database.dart'; 15 + import 'package:lazurite/core/logging/app_logger.dart'; 16 + import 'package:lazurite/core/scheduler/post_scheduler.dart'; 17 + 18 + part 'compose_event.dart'; 19 + part 'compose_state.dart'; 20 + 21 + const int kMaxGraphemes = 300; 22 + const int kMaxImages = 4; 23 + 24 + /// 1 MB 25 + const int kMaxImageBytes = 1 * 1024 * 1024; 26 + 27 + /// 100 MB 28 + const int kMaxVideoBytes = 100 * 1024 * 1024; 29 + const _kVideoJobPollInterval = Duration(seconds: 2); 30 + const _kVideoJobPollTimeout = Duration(minutes: 5); 31 + 32 + class ComposeBloc extends Bloc<ComposeEvent, ComposeState> { 33 + ComposeBloc({required ComposeRepository composeRepository, required AppDatabase database, required String accountDid}) 34 + : _composeRepository = composeRepository, 35 + _database = database, 36 + _accountDid = accountDid, 37 + super(const ComposeState.ready()) { 38 + on<TextChanged>(_onTextChanged); 39 + on<MediaAttached>(_onMediaAttached); 40 + on<MediaRemoved>(_onMediaRemoved); 41 + on<AltTextUpdated>(_onAltTextUpdated); 42 + on<VideoAttached>(_onVideoAttached); 43 + on<VideoRemoved>(_onVideoRemoved); 44 + on<VideoAltTextUpdated>(_onVideoAltTextUpdated); 45 + on<DraftSaved>(_onDraftSaved); 46 + on<DraftLoaded>(_onDraftLoaded); 47 + on<DraftsRequested>(_onDraftsRequested); 48 + on<DraftDeleted>(_onDraftDeleted); 49 + on<PostScheduled>(_onPostScheduled); 50 + on<ScheduleCleared>(_onScheduleCleared); 51 + on<PostSubmitted>(_onPostSubmitted); 52 + on<ReplyContextSet>(_onReplyContextSet); 53 + on<ReplyContextCleared>(_onReplyContextCleared); 54 + } 55 + 56 + final ComposeRepository _composeRepository; 57 + final AppDatabase _database; 58 + final String _accountDid; 59 + 60 + Future<void> _onTextChanged(TextChanged event, Emitter<ComposeState> emit) async { 61 + final text = event.text; 62 + final graphemeCount = text.characters.length; 63 + final isOverLimit = graphemeCount > kMaxGraphemes; 64 + final isEmpty = text.trim().isEmpty && state.mediaAttachments.isEmpty && state.videoAttachment == null; 65 + 66 + emit( 67 + state.copyWith( 68 + text: text, 69 + graphemeCount: graphemeCount, 70 + isOverLimit: isOverLimit, 71 + isEmpty: isEmpty, 72 + canSubmit: !isOverLimit && !isEmpty && !(state.videoAttachment?.isActive ?? false), 73 + ), 74 + ); 75 + } 76 + 77 + Future<void> _onMediaAttached(MediaAttached event, Emitter<ComposeState> emit) async { 78 + if (!state.canAddMoreMedia) return; 79 + 80 + final attachments = List<MediaAttachment>.from(state.mediaAttachments) 81 + ..add(MediaAttachment(localPath: event.path, width: event.width, height: event.height)); 82 + final isEmpty = state.text.trim().isEmpty && attachments.isEmpty; 83 + 84 + emit(state.copyWith(mediaAttachments: attachments, isEmpty: isEmpty, canSubmit: !state.isOverLimit && !isEmpty)); 85 + } 86 + 87 + Future<void> _onMediaRemoved(MediaRemoved event, Emitter<ComposeState> emit) async { 88 + if (event.index < 0 || event.index >= state.mediaAttachments.length) return; 89 + 90 + final attachments = List<MediaAttachment>.from(state.mediaAttachments)..removeAt(event.index); 91 + final isEmpty = state.text.trim().isEmpty && attachments.isEmpty && state.videoAttachment == null; 92 + 93 + emit(state.copyWith(mediaAttachments: attachments, isEmpty: isEmpty, canSubmit: !state.isOverLimit && !isEmpty)); 94 + } 95 + 96 + Future<void> _onAltTextUpdated(AltTextUpdated event, Emitter<ComposeState> emit) async { 97 + if (event.index < 0 || event.index >= state.mediaAttachments.length) return; 98 + 99 + final attachments = List<MediaAttachment>.from(state.mediaAttachments); 100 + attachments[event.index] = attachments[event.index].copyWith(altText: event.altText); 101 + emit(state.copyWith(mediaAttachments: attachments)); 102 + } 103 + 104 + Future<void> _onVideoAttached(VideoAttached event, Emitter<ComposeState> emit) async { 105 + if (!state.canAddVideo) return; 106 + 107 + final file = File(event.path); 108 + if (!file.existsSync()) return; 109 + 110 + final sizeBytes = file.lengthSync(); 111 + if (sizeBytes > kMaxVideoBytes) { 112 + final mb = (sizeBytes / 1024 / 1024).toStringAsFixed(1); 113 + _emitError(emit, 'Video is $mb MB — exceeds the 100 MB limit.'); 114 + return; 115 + } 116 + 117 + final pendingVideo = VideoAttachment(localPath: event.path, status: VideoUploadStatus.checkingLimits); 118 + emit(state.copyWith(videoAttachment: pendingVideo, canSubmit: false, isEmpty: false)); 119 + 120 + try { 121 + final limits = await _composeRepository.getUploadLimits(); 122 + if (limits != null && !limits.canUpload) { 123 + _setVideoError(emit, limits.message ?? 'Daily video upload limit reached.'); 124 + return; 125 + } 126 + 127 + emit(state.copyWith(videoAttachment: pendingVideo.copyWith(status: VideoUploadStatus.uploading))); 128 + final bytes = await file.readAsBytes(); 129 + final jobId = await _composeRepository.uploadVideo(bytes); 130 + if (jobId == null) { 131 + _setVideoError(emit, 'Upload failed — please try again.'); 132 + return; 133 + } 134 + 135 + emit( 136 + state.copyWith( 137 + videoAttachment: pendingVideo.copyWith(status: VideoUploadStatus.processing, jobId: jobId), 138 + ), 139 + ); 140 + final blob = await _pollVideoJob(jobId, emit); 141 + if (blob == null) return; 142 + 143 + final readyVideo = VideoAttachment( 144 + localPath: event.path, 145 + status: VideoUploadStatus.ready, 146 + uploadProgress: 100, 147 + blob: blob, 148 + altText: state.videoAttachment?.altText ?? '', 149 + jobId: jobId, 150 + ); 151 + final isEmpty = state.text.trim().isEmpty && state.mediaAttachments.isEmpty; 152 + emit(state.copyWith(videoAttachment: readyVideo, isEmpty: isEmpty, canSubmit: !state.isOverLimit && !isEmpty)); 153 + } catch (e, stackTrace) { 154 + log.e('Video upload failed', error: e, stackTrace: stackTrace); 155 + _setVideoError(emit, 'Upload failed: $e'); 156 + } 157 + } 158 + 159 + Future<Blob?> _pollVideoJob(String jobId, Emitter<ComposeState> emit) async { 160 + final deadline = DateTime.now().add(_kVideoJobPollTimeout); 161 + 162 + while (DateTime.now().isBefore(deadline)) { 163 + await Future.delayed(_kVideoJobPollInterval); 164 + try { 165 + final status = await _composeRepository.getJobStatus(jobId); 166 + if (status == null) continue; 167 + 168 + final progress = status.progress ?? 0; 169 + final current = state.videoAttachment; 170 + if (current != null) { 171 + emit(state.copyWith(videoAttachment: current.copyWith(uploadProgress: progress))); 172 + } 173 + 174 + final knownState = status.state.knownValue; 175 + if (knownState == KnownJobStatusState.jOB_STATE_COMPLETED && status.blob != null) { 176 + return status.blob; 177 + } 178 + if (knownState == KnownJobStatusState.jOB_STATE_FAILED) { 179 + _setVideoError(emit, status.error ?? 'Video processing failed.'); 180 + return null; 181 + } 182 + } catch (e) { 183 + log.w('Video job poll error (retrying)', error: e); 184 + } 185 + } 186 + 187 + _setVideoError(emit, 'Video processing timed out.'); 188 + return null; 189 + } 190 + 191 + void _setVideoError(Emitter<ComposeState> emit, String message) { 192 + final current = state.videoAttachment; 193 + if (current != null) { 194 + emit( 195 + state.copyWith( 196 + videoAttachment: current.copyWith(status: VideoUploadStatus.error, errorMessage: message), 197 + ), 198 + ); 199 + } 200 + } 201 + 202 + Future<void> _onVideoRemoved(VideoRemoved event, Emitter<ComposeState> emit) async { 203 + final isEmpty = state.text.trim().isEmpty && state.mediaAttachments.isEmpty; 204 + emit(state.copyWith(videoAttachment: null, isEmpty: isEmpty, canSubmit: !state.isOverLimit && !isEmpty)); 205 + } 206 + 207 + Future<void> _onVideoAltTextUpdated(VideoAltTextUpdated event, Emitter<ComposeState> emit) async { 208 + final updated = state.videoAttachment?.copyWith(altText: event.altText); 209 + if (updated != null) emit(state.copyWith(videoAttachment: updated)); 210 + } 211 + 212 + Future<void> _onDraftSaved(DraftSaved event, Emitter<ComposeState> emit) async { 213 + emit(state.copyWith(isSavingDraft: true)); 214 + try { 215 + final embedJson = _buildEmbedJson(); 216 + final draft = DraftsCompanion( 217 + id: state.draftId != null ? Value(state.draftId!) : const Value.absent(), 218 + accountDid: Value(_accountDid), 219 + content: Value(state.text), 220 + replyUri: state.replyParentUri != null ? Value(state.replyParentUri!) : const Value.absent(), 221 + replyCid: state.replyParentCid != null ? Value(state.replyParentCid!) : const Value.absent(), 222 + rootUri: state.replyRootUri != null ? Value(state.replyRootUri!) : const Value.absent(), 223 + rootCid: state.replyRootCid != null ? Value(state.replyRootCid!) : const Value.absent(), 224 + embedJson: embedJson != null ? Value(jsonEncode(embedJson)) : const Value.absent(), 225 + mediaPaths: state.mediaAttachments.isNotEmpty 226 + ? Value(jsonEncode(state.mediaAttachments.map((m) => m.localPath).toList())) 227 + : const Value.absent(), 228 + scheduledAt: state.scheduledAt != null ? Value(state.scheduledAt!) : const Value.absent(), 229 + updatedAt: Value(DateTime.now()), 230 + ); 231 + final id = await _database.saveDraft(draft); 232 + emit(state.copyWith(draftId: id, isSavingDraft: false)); 233 + } catch (e, stackTrace) { 234 + log.e('Failed to save draft', error: e, stackTrace: stackTrace); 235 + emit(state.copyWith(isSavingDraft: false)); 236 + } 237 + } 238 + 239 + Future<void> _onDraftLoaded(DraftLoaded event, Emitter<ComposeState> emit) async { 240 + try { 241 + final draft = await _database.getDraft(event.draftId); 242 + if (draft == null) return; 243 + 244 + List<MediaAttachment> attachments = []; 245 + 246 + if (draft.embedJson != null) { 247 + try { 248 + final decoded = jsonDecode(draft.embedJson!) as Map<String, dynamic>; 249 + final type = decoded['type'] as String?; 250 + if (type == 'images') { 251 + final paths = decoded['paths'] as List<dynamic>? ?? []; 252 + final alts = decoded['altTexts'] as List<dynamic>? ?? []; 253 + attachments = paths.asMap().entries.where((e) => File(e.value as String).existsSync()).map((e) { 254 + return MediaAttachment( 255 + localPath: e.value as String, 256 + altText: e.key < alts.length ? (alts[e.key] as String? ?? '') : '', 257 + ); 258 + }).toList(); 259 + } 260 + } catch (e) { 261 + log.w('Failed to parse embedJson from draft', error: e); 262 + } 263 + } 264 + 265 + if (attachments.isEmpty && draft.mediaPaths != null) { 266 + try { 267 + final paths = jsonDecode(draft.mediaPaths!) as List<dynamic>; 268 + attachments = paths 269 + .whereType<String>() 270 + .where((path) => File(path).existsSync()) 271 + .map((path) => MediaAttachment(localPath: path)) 272 + .toList(); 273 + } catch (e) { 274 + log.w('Failed to parse mediaPaths from draft', error: e); 275 + } 276 + } 277 + 278 + final text = draft.content; 279 + final graphemeCount = text.characters.length; 280 + final isOverLimit = graphemeCount > kMaxGraphemes; 281 + final isEmpty = text.trim().isEmpty && attachments.isEmpty; 282 + 283 + emit( 284 + ComposeState.ready( 285 + text: text, 286 + mediaAttachments: attachments, 287 + draftId: draft.id, 288 + scheduledAt: draft.scheduledAt, 289 + replyParentUri: draft.replyUri, 290 + replyParentCid: draft.replyCid, 291 + replyRootUri: draft.rootUri, 292 + replyRootCid: draft.rootCid, 293 + ).copyWith( 294 + graphemeCount: graphemeCount, 295 + isOverLimit: isOverLimit, 296 + isEmpty: isEmpty, 297 + canSubmit: !isOverLimit && !isEmpty, 298 + ), 299 + ); 300 + } catch (e, stackTrace) { 301 + log.e('Failed to load draft', error: e, stackTrace: stackTrace); 302 + } 303 + } 304 + 305 + Future<void> _onDraftsRequested(DraftsRequested event, Emitter<ComposeState> emit) async { 306 + emit(state.copyWith(isLoadingDrafts: true)); 307 + try { 308 + final drafts = await _database.getDrafts(_accountDid); 309 + emit(state.copyWith(drafts: drafts, isLoadingDrafts: false)); 310 + } catch (e, stackTrace) { 311 + log.e('Failed to load drafts', error: e, stackTrace: stackTrace); 312 + emit(state.copyWith(isLoadingDrafts: false)); 313 + } 314 + } 315 + 316 + Future<void> _onDraftDeleted(DraftDeleted event, Emitter<ComposeState> emit) async { 317 + try { 318 + await _database.deleteDraft(event.draftId); 319 + final drafts = List<DraftEntry>.from(state.drafts)..removeWhere((d) => d.id == event.draftId); 320 + emit(state.copyWith(drafts: drafts)); 321 + } catch (e, stackTrace) { 322 + log.e('Failed to delete draft', error: e, stackTrace: stackTrace); 323 + } 324 + } 325 + 326 + Future<void> _onPostScheduled(PostScheduled event, Emitter<ComposeState> emit) async { 327 + emit(state.copyWith(scheduledAt: event.scheduledAt)); 328 + } 329 + 330 + Future<void> _onScheduleCleared(ScheduleCleared event, Emitter<ComposeState> emit) async { 331 + emit(state.copyWith(scheduledAt: null)); 332 + } 333 + 334 + Future<void> _onReplyContextSet(ReplyContextSet event, Emitter<ComposeState> emit) async { 335 + emit( 336 + state.copyWith( 337 + replyParentUri: event.parentUri, 338 + replyParentCid: event.parentCid, 339 + replyRootUri: event.rootUri, 340 + replyRootCid: event.rootCid, 341 + ), 342 + ); 343 + } 344 + 345 + Future<void> _onReplyContextCleared(ReplyContextCleared event, Emitter<ComposeState> emit) async { 346 + emit(state.copyWith(replyParentUri: null, replyParentCid: null, replyRootUri: null, replyRootCid: null)); 347 + } 348 + 349 + Future<void> _onPostSubmitted(PostSubmitted event, Emitter<ComposeState> emit) async { 350 + if (!state.canSubmit || state.isOverLimit) return; 351 + 352 + emit(state.copyWith(status: ComposeStatus.submitting, canSubmit: false)); 353 + 354 + try { 355 + if (state.scheduledAt != null && state.scheduledAt!.isAfter(DateTime.now())) { 356 + final embedJson = _buildEmbedJson(); 357 + final draft = DraftsCompanion( 358 + accountDid: Value(_accountDid), 359 + content: Value(state.text), 360 + replyUri: state.replyParentUri != null ? Value(state.replyParentUri!) : const Value.absent(), 361 + replyCid: state.replyParentCid != null ? Value(state.replyParentCid!) : const Value.absent(), 362 + rootUri: state.replyRootUri != null ? Value(state.replyRootUri!) : const Value.absent(), 363 + rootCid: state.replyRootCid != null ? Value(state.replyRootCid!) : const Value.absent(), 364 + embedJson: embedJson != null ? Value(jsonEncode(embedJson)) : const Value.absent(), 365 + mediaPaths: state.mediaAttachments.isNotEmpty 366 + ? Value(jsonEncode(state.mediaAttachments.map((m) => m.localPath).toList())) 367 + : const Value.absent(), 368 + scheduledAt: Value(state.scheduledAt!), 369 + updatedAt: Value(DateTime.now()), 370 + ); 371 + final draftId = await _database.saveDraft(draft); 372 + await PostScheduler.schedulePost(draftId: draftId, scheduledAt: state.scheduledAt!); 373 + emit(state.copyWith(status: ComposeStatus.success, canSubmit: false)); 374 + return; 375 + } 376 + 377 + final blueskyText = BlueskyText(state.text); 378 + final facets = <Map<String, dynamic>>[]; 379 + for (final entity in blueskyText.entities) { 380 + try { 381 + final facet = await entity.toFacet().timeout( 382 + const Duration(seconds: 5), 383 + onTimeout: () { 384 + log.w('Timeout resolving @${entity.value}; facet dropped.'); 385 + return {}; 386 + }, 387 + ); 388 + if (facet.isNotEmpty) facets.add(facet); 389 + } catch (e) { 390 + log.w('Could not resolve facet for "${entity.value}": $e'); 391 + } 392 + } 393 + 394 + Map<String, dynamic>? embed; 395 + 396 + if (state.mediaAttachments.isNotEmpty) { 397 + final uploaded = <_UploadedImage>[]; 398 + for (final attachment in state.mediaAttachments) { 399 + final file = File(attachment.localPath); 400 + if (!file.existsSync()) { 401 + _emitError(emit, 'Image file not found. Please re-attach and try again.'); 402 + return; 403 + } 404 + final bytes = await file.readAsBytes(); 405 + 406 + if (bytes.length > kMaxImageBytes) { 407 + final mb = (bytes.length / 1024 / 1024).toStringAsFixed(1); 408 + _emitError(emit, 'Image "${attachment.localPath.split('/').last}" is $mb MB — max 1 MB.'); 409 + return; 410 + } 411 + 412 + final mime = _detectImageMime(bytes); 413 + if (mime == null) { 414 + _emitError(emit, 'Unsupported image format. Use JPEG, PNG, or WebP.'); 415 + return; 416 + } 417 + 418 + final blob = await _composeRepository.uploadBlob(bytes, mimeType: mime); 419 + if (blob == null) { 420 + _emitError(emit, 'Failed to upload image. Please try again.'); 421 + return; 422 + } 423 + uploaded.add( 424 + _UploadedImage( 425 + blobRef: blob, 426 + altText: attachment.altText, 427 + width: attachment.width, 428 + height: attachment.height, 429 + ), 430 + ); 431 + } 432 + 433 + embed = { 434 + '\$type': 'app.bsky.embed.images', 435 + 'images': uploaded.map((img) { 436 + final entry = <String, dynamic>{'image': img.blobRef.toJson(), 'alt': img.altText}; 437 + if (img.width != null && img.height != null) { 438 + entry['aspectRatio'] = {'width': img.width, 'height': img.height}; 439 + } 440 + return entry; 441 + }).toList(), 442 + }; 443 + } else if (state.videoAttachment?.isReady == true) { 444 + final blob = state.videoAttachment!.blob!; 445 + embed = {'\$type': 'app.bsky.embed.video', 'video': blob.toJson(), 'alt': state.videoAttachment!.altText}; 446 + } 447 + 448 + Map<String, dynamic>? reply; 449 + if (state.replyParentUri != null && state.replyParentCid != null) { 450 + reply = { 451 + 'parent': {'uri': state.replyParentUri, 'cid': state.replyParentCid}, 452 + 'root': { 453 + 'uri': state.replyRootUri ?? state.replyParentUri, 454 + 'cid': state.replyRootCid ?? state.replyParentCid, 455 + }, 456 + }; 457 + } 458 + 459 + final success = await _composeRepository.createPost( 460 + text: state.text, 461 + facets: facets, 462 + embed: embed, 463 + reply: reply, 464 + repo: _accountDid, 465 + ); 466 + 467 + if (success) { 468 + if (state.draftId != null) await _database.deleteDraft(state.draftId!); 469 + emit(state.copyWith(status: ComposeStatus.success, canSubmit: false)); 470 + } else { 471 + _emitError(emit, 'Failed to create post. Please try again.'); 472 + } 473 + } catch (e, stackTrace) { 474 + log.e('Failed to submit post', error: e, stackTrace: stackTrace); 475 + 476 + try { 477 + final embedJson = _buildEmbedJson(); 478 + final draft = DraftsCompanion( 479 + accountDid: Value(_accountDid), 480 + content: Value(state.text), 481 + replyUri: state.replyParentUri != null ? Value(state.replyParentUri!) : const Value.absent(), 482 + replyCid: state.replyParentCid != null ? Value(state.replyParentCid!) : const Value.absent(), 483 + rootUri: state.replyRootUri != null ? Value(state.replyRootUri!) : const Value.absent(), 484 + rootCid: state.replyRootCid != null ? Value(state.replyRootCid!) : const Value.absent(), 485 + embedJson: embedJson != null ? Value(jsonEncode(embedJson)) : const Value.absent(), 486 + mediaPaths: state.mediaAttachments.isNotEmpty 487 + ? Value(jsonEncode(state.mediaAttachments.map((m) => m.localPath).toList())) 488 + : const Value.absent(), 489 + scheduledAt: state.scheduledAt != null ? Value(state.scheduledAt!) : const Value.absent(), 490 + updatedAt: Value(DateTime.now()), 491 + ); 492 + await _database.saveDraft(draft); 493 + _emitError(emit, 'Network error — post saved as draft.'); 494 + } catch (_) { 495 + _emitError(emit, 'Failed to submit post: $e'); 496 + } 497 + } 498 + } 499 + 500 + /// Emits error state (preserving content), then transitions back to ready 501 + /// so the user can retry without losing their work. 502 + void _emitError(Emitter<ComposeState> emit, String message) { 503 + emit(state.copyWith(status: ComposeStatus.error, errorMessage: message, canSubmit: false)); 504 + emit( 505 + state.copyWith( 506 + status: ComposeStatus.ready, 507 + errorMessage: message, 508 + canSubmit: !state.isOverLimit && !state.isEmpty, 509 + ), 510 + ); 511 + } 512 + 513 + Map<String, dynamic>? _buildEmbedJson() { 514 + if (state.mediaAttachments.isNotEmpty) { 515 + return { 516 + 'type': 'images', 517 + 'paths': state.mediaAttachments.map((m) => m.localPath).toList(), 518 + 'altTexts': state.mediaAttachments.map((m) => m.altText).toList(), 519 + }; 520 + } 521 + if (state.videoAttachment != null) { 522 + return {'type': 'video', 'path': state.videoAttachment!.localPath, 'alt': state.videoAttachment!.altText}; 523 + } 524 + return null; 525 + } 526 + 527 + /// Returns MIME type from magic bytes, or null if not an accepted image type. 528 + static String? _detectImageMime(List<int> bytes) { 529 + if (bytes.length < 12) return null; 530 + if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) return 'image/jpeg'; 531 + if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) return 'image/png'; 532 + if (bytes[0] == 0x52 && 533 + bytes[1] == 0x49 && 534 + bytes[2] == 0x46 && 535 + bytes[3] == 0x46 && 536 + bytes[8] == 0x57 && 537 + bytes[9] == 0x45 && 538 + bytes[10] == 0x42 && 539 + bytes[11] == 0x50) { 540 + return 'image/webp'; 541 + } 542 + return null; 543 + } 544 + } 545 + 546 + class _UploadedImage { 547 + const _UploadedImage({required this.blobRef, required this.altText, this.width, this.height}); 548 + 549 + final BlobRef blobRef; 550 + final String altText; 551 + final int? width; 552 + final int? height; 553 + } 554 + 555 + class ComposeRepository { 556 + ComposeRepository({required Bluesky bluesky}) : _bluesky = bluesky; 557 + 558 + final Bluesky _bluesky; 559 + 560 + Future<BlobRef?> uploadBlob(List<int> bytes, {String mimeType = 'image/jpeg'}) async { 561 + try { 562 + final response = await _bluesky.atproto.repo.uploadBlob( 563 + bytes: Uint8List.fromList(bytes), 564 + $headers: {'Content-Type': mimeType}, 565 + ); 566 + return response.data.blob.ref; 567 + } catch (e, stackTrace) { 568 + log.e('Failed to upload blob', error: e, stackTrace: stackTrace); 569 + return null; 570 + } 571 + } 572 + 573 + /// Uploads video bytes and returns the job ID, or null on failure. 574 + Future<String?> uploadVideo(Uint8List bytes) async { 575 + try { 576 + final response = await _bluesky.video.uploadVideo(bytes: bytes); 577 + return response.data.jobId; 578 + } catch (e, stackTrace) { 579 + log.e('Failed to upload video', error: e, stackTrace: stackTrace); 580 + return null; 581 + } 582 + } 583 + 584 + Future<dynamic> getJobStatus(String jobId) async { 585 + try { 586 + final response = await _bluesky.video.getJobStatus(jobId: jobId); 587 + return response.data.jobStatus; 588 + } catch (e, stackTrace) { 589 + log.e('Failed to get job status', error: e, stackTrace: stackTrace); 590 + return null; 591 + } 592 + } 593 + 594 + Future<({bool canUpload, String? message})?> getUploadLimits() async { 595 + try { 596 + final response = await _bluesky.video.getUploadLimits(); 597 + final d = response.data; 598 + return (canUpload: d.canUpload, message: d.message ?? d.error); 599 + } catch (e, stackTrace) { 600 + log.e('Failed to get upload limits', error: e, stackTrace: stackTrace); 601 + return null; 602 + } 603 + } 604 + 605 + Future<bool> createPost({ 606 + required String text, 607 + required List<Map<String, dynamic>> facets, 608 + Map<String, dynamic>? embed, 609 + Map<String, dynamic>? reply, 610 + required String repo, 611 + }) async { 612 + try { 613 + final record = <String, dynamic>{ 614 + '\$type': 'app.bsky.feed.post', 615 + 'text': text, 616 + 'createdAt': DateTime.now().toUtc().toIso8601String(), 617 + 'langs': ['en'], 618 + }; 619 + if (facets.isNotEmpty) record['facets'] = facets; 620 + if (embed != null) record['embed'] = embed; 621 + if (reply != null) record['reply'] = reply; 622 + 623 + await _bluesky.atproto.repo.createRecord(repo: repo, collection: 'app.bsky.feed.post', record: record); 624 + return true; 625 + } catch (e, stackTrace) { 626 + log.e('Failed to create post', error: e, stackTrace: stackTrace); 627 + return false; 628 + } 629 + } 630 + } 631 + 632 + /// Image dimension helper (used by compose screen when picking images). 633 + Future<({int width, int height})?> readImageDimensions(List<int> bytes) async { 634 + try { 635 + final completer = Completer<ui.Image>(); 636 + ui.decodeImageFromList(Uint8List.fromList(bytes), completer.complete); 637 + final image = await completer.future; 638 + final result = (width: image.width, height: image.height); 639 + image.dispose(); 640 + return result; 641 + } catch (_) { 642 + return null; 643 + } 644 + }
+133
lib/features/compose/bloc/compose_event.dart
··· 1 + part of 'compose_bloc.dart'; 2 + 3 + abstract class ComposeEvent extends Equatable { 4 + const ComposeEvent(); 5 + 6 + @override 7 + List<Object?> get props => []; 8 + } 9 + 10 + class TextChanged extends ComposeEvent { 11 + const TextChanged(this.text); 12 + 13 + final String text; 14 + 15 + @override 16 + List<Object?> get props => [text]; 17 + } 18 + 19 + class MediaAttached extends ComposeEvent { 20 + const MediaAttached(this.path, {this.width, this.height}); 21 + 22 + final String path; 23 + final int? width; 24 + final int? height; 25 + 26 + @override 27 + List<Object?> get props => [path, width, height]; 28 + } 29 + 30 + class MediaRemoved extends ComposeEvent { 31 + const MediaRemoved(this.index); 32 + 33 + final int index; 34 + 35 + @override 36 + List<Object?> get props => [index]; 37 + } 38 + 39 + class AltTextUpdated extends ComposeEvent { 40 + const AltTextUpdated({required this.index, required this.altText}); 41 + 42 + final int index; 43 + final String altText; 44 + 45 + @override 46 + List<Object?> get props => [index, altText]; 47 + } 48 + 49 + class DraftSaved extends ComposeEvent { 50 + const DraftSaved(); 51 + } 52 + 53 + class DraftLoaded extends ComposeEvent { 54 + const DraftLoaded(this.draftId); 55 + 56 + final int draftId; 57 + 58 + @override 59 + List<Object?> get props => [draftId]; 60 + } 61 + 62 + class DraftsRequested extends ComposeEvent { 63 + const DraftsRequested(); 64 + } 65 + 66 + class DraftDeleted extends ComposeEvent { 67 + const DraftDeleted(this.draftId); 68 + 69 + final int draftId; 70 + 71 + @override 72 + List<Object?> get props => [draftId]; 73 + } 74 + 75 + class PostScheduled extends ComposeEvent { 76 + const PostScheduled(this.scheduledAt); 77 + 78 + final DateTime scheduledAt; 79 + 80 + @override 81 + List<Object?> get props => [scheduledAt]; 82 + } 83 + 84 + class ScheduleCleared extends ComposeEvent { 85 + const ScheduleCleared(); 86 + } 87 + 88 + class VideoAttached extends ComposeEvent { 89 + const VideoAttached(this.path); 90 + 91 + final String path; 92 + 93 + @override 94 + List<Object?> get props => [path]; 95 + } 96 + 97 + class VideoRemoved extends ComposeEvent { 98 + const VideoRemoved(); 99 + } 100 + 101 + class VideoAltTextUpdated extends ComposeEvent { 102 + const VideoAltTextUpdated(this.altText); 103 + 104 + final String altText; 105 + 106 + @override 107 + List<Object?> get props => [altText]; 108 + } 109 + 110 + class PostSubmitted extends ComposeEvent { 111 + const PostSubmitted(); 112 + } 113 + 114 + class ReplyContextSet extends ComposeEvent { 115 + const ReplyContextSet({ 116 + required this.parentUri, 117 + required this.parentCid, 118 + required this.rootUri, 119 + required this.rootCid, 120 + }); 121 + 122 + final String parentUri; 123 + final String parentCid; 124 + final String rootUri; 125 + final String rootCid; 126 + 127 + @override 128 + List<Object?> get props => [parentUri, parentCid, rootUri, rootCid]; 129 + } 130 + 131 + class ReplyContextCleared extends ComposeEvent { 132 + const ReplyContextCleared(); 133 + }
+256
lib/features/compose/bloc/compose_state.dart
··· 1 + part of 'compose_bloc.dart'; 2 + 3 + /// Sentinel used by copyWith to distinguish "leave unchanged" from "set to null". 4 + class _Undefined { 5 + const _Undefined(); 6 + } 7 + 8 + enum ComposeStatus { initial, ready, submitting, success, error } 9 + 10 + enum VideoUploadStatus { idle, checkingLimits, uploading, processing, ready, error } 11 + 12 + class VideoAttachment extends Equatable { 13 + const VideoAttachment({ 14 + required this.localPath, 15 + this.status = VideoUploadStatus.idle, 16 + this.uploadProgress = 0, 17 + this.blob, 18 + this.altText = '', 19 + this.jobId, 20 + this.errorMessage, 21 + }); 22 + 23 + final String localPath; 24 + final VideoUploadStatus status; 25 + final int uploadProgress; 26 + final Blob? blob; 27 + final String altText; 28 + final String? jobId; 29 + final String? errorMessage; 30 + 31 + bool get isActive => 32 + status == VideoUploadStatus.checkingLimits || 33 + status == VideoUploadStatus.uploading || 34 + status == VideoUploadStatus.processing; 35 + bool get isReady => status == VideoUploadStatus.ready; 36 + bool get hasError => status == VideoUploadStatus.error; 37 + 38 + VideoAttachment copyWith({ 39 + String? localPath, 40 + VideoUploadStatus? status, 41 + int? uploadProgress, 42 + Object? blob = const _Undefined(), 43 + String? altText, 44 + Object? jobId = const _Undefined(), 45 + Object? errorMessage = const _Undefined(), 46 + }) { 47 + return VideoAttachment( 48 + localPath: localPath ?? this.localPath, 49 + status: status ?? this.status, 50 + uploadProgress: uploadProgress ?? this.uploadProgress, 51 + blob: blob is _Undefined ? this.blob : blob as Blob?, 52 + altText: altText ?? this.altText, 53 + jobId: jobId is _Undefined ? this.jobId : jobId as String?, 54 + errorMessage: errorMessage is _Undefined ? this.errorMessage : errorMessage as String?, 55 + ); 56 + } 57 + 58 + @override 59 + List<Object?> get props => [localPath, status, uploadProgress, blob, altText, jobId, errorMessage]; 60 + } 61 + 62 + class ComposeState extends Equatable { 63 + const ComposeState._({ 64 + required this.status, 65 + this.text = '', 66 + this.graphemeCount = 0, 67 + this.isOverLimit = false, 68 + this.isEmpty = true, 69 + this.mediaAttachments = const [], 70 + this.draftId, 71 + this.scheduledAt, 72 + this.replyParentUri, 73 + this.replyParentCid, 74 + this.replyRootUri, 75 + this.replyRootCid, 76 + this.errorMessage, 77 + this.drafts = const [], 78 + this.isSavingDraft = false, 79 + this.isLoadingDrafts = false, 80 + this.canSubmit = false, 81 + this.videoAttachment, 82 + }); 83 + 84 + const ComposeState.initial() : this._(status: ComposeStatus.initial); 85 + 86 + const ComposeState.ready({ 87 + String text = '', 88 + int graphemeCount = 0, 89 + bool isOverLimit = false, 90 + bool isEmpty = true, 91 + List<MediaAttachment> mediaAttachments = const [], 92 + int? draftId, 93 + DateTime? scheduledAt, 94 + String? replyParentUri, 95 + String? replyParentCid, 96 + String? replyRootUri, 97 + String? replyRootCid, 98 + VideoAttachment? videoAttachment, 99 + }) : this._( 100 + status: ComposeStatus.ready, 101 + text: text, 102 + graphemeCount: graphemeCount, 103 + isOverLimit: isOverLimit, 104 + isEmpty: isEmpty, 105 + mediaAttachments: mediaAttachments, 106 + draftId: draftId, 107 + scheduledAt: scheduledAt, 108 + replyParentUri: replyParentUri, 109 + replyParentCid: replyParentCid, 110 + replyRootUri: replyRootUri, 111 + replyRootCid: replyRootCid, 112 + videoAttachment: videoAttachment, 113 + canSubmit: !isOverLimit && !isEmpty, 114 + ); 115 + 116 + final ComposeStatus status; 117 + final String text; 118 + final int graphemeCount; 119 + final bool isOverLimit; 120 + final bool isEmpty; 121 + final List<MediaAttachment> mediaAttachments; 122 + final int? draftId; 123 + final DateTime? scheduledAt; 124 + final String? replyParentUri; 125 + final String? replyParentCid; 126 + final String? replyRootUri; 127 + final String? replyRootCid; 128 + final String? errorMessage; 129 + final List<DraftEntry> drafts; 130 + final bool isSavingDraft; 131 + final bool isLoadingDrafts; 132 + final bool canSubmit; 133 + final VideoAttachment? videoAttachment; 134 + 135 + bool get isSubmitting => status == ComposeStatus.submitting; 136 + bool get hasError => status == ComposeStatus.error; 137 + bool get isSuccess => status == ComposeStatus.success; 138 + bool get isReady => status == ComposeStatus.ready; 139 + bool get hasMedia => mediaAttachments.isNotEmpty; 140 + bool get hasVideo => videoAttachment != null; 141 + bool get canAddMoreMedia => mediaAttachments.length < 4 && videoAttachment == null; 142 + bool get canAddVideo => mediaAttachments.isEmpty && videoAttachment == null; 143 + bool get hasScheduledTime => scheduledAt != null; 144 + bool get isReply => replyParentUri != null; 145 + 146 + ComposeState copyWith({ 147 + ComposeStatus? status, 148 + String? text, 149 + int? graphemeCount, 150 + bool? isOverLimit, 151 + bool? isEmpty, 152 + List<MediaAttachment>? mediaAttachments, 153 + Object? draftId = const _Undefined(), 154 + Object? scheduledAt = const _Undefined(), 155 + Object? replyParentUri = const _Undefined(), 156 + Object? replyParentCid = const _Undefined(), 157 + Object? replyRootUri = const _Undefined(), 158 + Object? replyRootCid = const _Undefined(), 159 + Object? errorMessage = const _Undefined(), 160 + List<DraftEntry>? drafts, 161 + bool? isSavingDraft, 162 + bool? isLoadingDrafts, 163 + bool? canSubmit, 164 + Object? videoAttachment = const _Undefined(), 165 + }) { 166 + return ComposeState._( 167 + status: status ?? this.status, 168 + text: text ?? this.text, 169 + graphemeCount: graphemeCount ?? this.graphemeCount, 170 + isOverLimit: isOverLimit ?? this.isOverLimit, 171 + isEmpty: isEmpty ?? this.isEmpty, 172 + mediaAttachments: mediaAttachments ?? this.mediaAttachments, 173 + draftId: draftId is _Undefined ? this.draftId : draftId as int?, 174 + scheduledAt: scheduledAt is _Undefined ? this.scheduledAt : scheduledAt as DateTime?, 175 + replyParentUri: replyParentUri is _Undefined ? this.replyParentUri : replyParentUri as String?, 176 + replyParentCid: replyParentCid is _Undefined ? this.replyParentCid : replyParentCid as String?, 177 + replyRootUri: replyRootUri is _Undefined ? this.replyRootUri : replyRootUri as String?, 178 + replyRootCid: replyRootCid is _Undefined ? this.replyRootCid : replyRootCid as String?, 179 + errorMessage: errorMessage is _Undefined ? this.errorMessage : errorMessage as String?, 180 + drafts: drafts ?? this.drafts, 181 + isSavingDraft: isSavingDraft ?? this.isSavingDraft, 182 + isLoadingDrafts: isLoadingDrafts ?? this.isLoadingDrafts, 183 + canSubmit: canSubmit ?? this.canSubmit, 184 + videoAttachment: videoAttachment is _Undefined ? this.videoAttachment : videoAttachment as VideoAttachment?, 185 + ); 186 + } 187 + 188 + @override 189 + List<Object?> get props => [ 190 + status, 191 + text, 192 + graphemeCount, 193 + isOverLimit, 194 + isEmpty, 195 + mediaAttachments, 196 + draftId, 197 + scheduledAt, 198 + replyParentUri, 199 + replyParentCid, 200 + replyRootUri, 201 + replyRootCid, 202 + errorMessage, 203 + drafts, 204 + isSavingDraft, 205 + isLoadingDrafts, 206 + canSubmit, 207 + videoAttachment, 208 + ]; 209 + } 210 + 211 + class MediaAttachment extends Equatable { 212 + const MediaAttachment({ 213 + required this.localPath, 214 + this.blobRef, 215 + this.altText = '', 216 + this.width, 217 + this.height, 218 + this.isUploaded = false, 219 + this.isUploading = false, 220 + this.uploadError, 221 + }); 222 + 223 + final String localPath; 224 + final BlobRef? blobRef; 225 + final String altText; 226 + final int? width; 227 + final int? height; 228 + final bool isUploaded; 229 + final bool isUploading; 230 + final String? uploadError; 231 + 232 + MediaAttachment copyWith({ 233 + String? localPath, 234 + Object? blobRef = const _Undefined(), 235 + String? altText, 236 + Object? width = const _Undefined(), 237 + Object? height = const _Undefined(), 238 + bool? isUploaded, 239 + bool? isUploading, 240 + Object? uploadError = const _Undefined(), 241 + }) { 242 + return MediaAttachment( 243 + localPath: localPath ?? this.localPath, 244 + blobRef: blobRef is _Undefined ? this.blobRef : blobRef as BlobRef?, 245 + altText: altText ?? this.altText, 246 + width: width is _Undefined ? this.width : width as int?, 247 + height: height is _Undefined ? this.height : height as int?, 248 + isUploaded: isUploaded ?? this.isUploaded, 249 + isUploading: isUploading ?? this.isUploading, 250 + uploadError: uploadError is _Undefined ? this.uploadError : uploadError as String?, 251 + ); 252 + } 253 + 254 + @override 255 + List<Object?> get props => [localPath, blobRef, altText, width, height, isUploaded, isUploading, uploadError]; 256 + }
+17
lib/features/compose/presentation/compose_route_args.dart
··· 1 + class ComposeRouteArgs { 2 + const ComposeRouteArgs({ 3 + this.replyParentUri, 4 + this.replyParentCid, 5 + this.replyRootUri, 6 + this.replyRootCid, 7 + this.replyAuthorHandle, 8 + this.draftId, 9 + }); 10 + 11 + final String? replyParentUri; 12 + final String? replyParentCid; 13 + final String? replyRootUri; 14 + final String? replyRootCid; 15 + final String? replyAuthorHandle; 16 + final int? draftId; 17 + }
+947
lib/features/compose/presentation/compose_screen.dart
··· 1 + import 'dart:convert'; 2 + import 'dart:io'; 3 + import 'dart:ui' as ui; 4 + 5 + import 'package:bluesky_text/bluesky_text.dart'; 6 + import 'package:flutter/material.dart'; 7 + import 'package:flutter_bloc/flutter_bloc.dart'; 8 + import 'package:image_picker/image_picker.dart'; 9 + import 'package:intl/intl.dart'; 10 + import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 11 + 12 + class ComposeScreen extends StatefulWidget { 13 + const ComposeScreen({ 14 + super.key, 15 + this.replyParentUri, 16 + this.replyParentCid, 17 + this.replyRootUri, 18 + this.replyRootCid, 19 + this.replyAuthorHandle, 20 + this.draftId, 21 + }); 22 + 23 + final String? replyParentUri; 24 + final String? replyParentCid; 25 + final String? replyRootUri; 26 + final String? replyRootCid; 27 + final String? replyAuthorHandle; 28 + final int? draftId; 29 + 30 + @override 31 + State<ComposeScreen> createState() => _ComposeScreenState(); 32 + } 33 + 34 + class _ComposeScreenState extends State<ComposeScreen> { 35 + late final _FacetHighlightController _textController; 36 + final ImagePicker _imagePicker = ImagePicker(); 37 + 38 + @override 39 + void initState() { 40 + super.initState(); 41 + _textController = _FacetHighlightController(); 42 + 43 + if (widget.draftId != null) { 44 + context.read<ComposeBloc>().add(DraftLoaded(widget.draftId!)); 45 + } 46 + 47 + if (widget.replyParentUri != null && widget.replyParentCid != null) { 48 + context.read<ComposeBloc>().add( 49 + ReplyContextSet( 50 + parentUri: widget.replyParentUri!, 51 + parentCid: widget.replyParentCid!, 52 + rootUri: widget.replyRootUri ?? widget.replyParentUri!, 53 + rootCid: widget.replyRootCid ?? widget.replyParentCid!, 54 + ), 55 + ); 56 + } 57 + 58 + _textController.addListener(_onTextChanged); 59 + } 60 + 61 + @override 62 + void dispose() { 63 + _textController.removeListener(_onTextChanged); 64 + _textController.dispose(); 65 + super.dispose(); 66 + } 67 + 68 + void _onTextChanged() { 69 + context.read<ComposeBloc>().add(TextChanged(_textController.text)); 70 + } 71 + 72 + Future<void> _pickImage() async { 73 + final state = context.read<ComposeBloc>().state; 74 + if (!state.canAddMoreMedia) { 75 + if (mounted) { 76 + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Maximum 4 images allowed'))); 77 + } 78 + return; 79 + } 80 + 81 + try { 82 + final XFile? image = await _imagePicker.pickImage( 83 + source: ImageSource.gallery, 84 + maxWidth: 2048, 85 + maxHeight: 2048, 86 + imageQuality: 85, 87 + ); 88 + 89 + if (image != null && mounted) { 90 + final file = File(image.path); 91 + final fileSize = await file.length(); 92 + const maxSize = 1 * 1024 * 1024; 93 + if (fileSize > maxSize) { 94 + if (mounted) { 95 + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Image must be smaller than 1MB'))); 96 + } 97 + return; 98 + } 99 + 100 + final extension = image.path.toLowerCase().split('.').last; 101 + const validExtensions = ['jpg', 'jpeg', 'png', 'webp']; 102 + if (!validExtensions.contains(extension)) { 103 + if (mounted) { 104 + ScaffoldMessenger.of( 105 + context, 106 + ).showSnackBar(const SnackBar(content: Text('Image must be JPEG, PNG, or WebP'))); 107 + } 108 + return; 109 + } 110 + 111 + final bytes = await file.readAsBytes(); 112 + final ui.Codec codec = await ui.instantiateImageCodec(bytes); 113 + final ui.FrameInfo frameInfo = await codec.getNextFrame(); 114 + final int width = frameInfo.image.width; 115 + final int height = frameInfo.image.height; 116 + 117 + if (mounted) { 118 + context.read<ComposeBloc>().add(MediaAttached(image.path, width: width, height: height)); 119 + } 120 + } 121 + } catch (e) { 122 + if (mounted) { 123 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); 124 + } 125 + } 126 + } 127 + 128 + Future<void> _pickVideo() async { 129 + final state = context.read<ComposeBloc>().state; 130 + if (!state.canAddVideo) { 131 + if (mounted) { 132 + ScaffoldMessenger.of( 133 + context, 134 + ).showSnackBar(const SnackBar(content: Text('Remove existing media before adding a video'))); 135 + } 136 + return; 137 + } 138 + 139 + try { 140 + final XFile? video = await _imagePicker.pickVideo(source: ImageSource.gallery); 141 + if (video != null && mounted) { 142 + context.read<ComposeBloc>().add(VideoAttached(video.path)); 143 + } 144 + } catch (e) { 145 + if (mounted) { 146 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick video: $e'))); 147 + } 148 + } 149 + } 150 + 151 + Future<void> _showVideoAltTextDialog(String currentAltText) async { 152 + final TextEditingController altController = TextEditingController(text: currentAltText); 153 + 154 + final result = await showDialog<String>( 155 + context: context, 156 + builder: (context) => AlertDialog( 157 + title: const Text('Add video alt text'), 158 + content: TextField( 159 + controller: altController, 160 + maxLines: 3, 161 + maxLength: 1000, 162 + decoration: const InputDecoration( 163 + hintText: 'Describe the video for accessibility', 164 + border: OutlineInputBorder(), 165 + ), 166 + ), 167 + actions: [ 168 + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), 169 + TextButton(onPressed: () => Navigator.pop(context, altController.text), child: const Text('Save')), 170 + ], 171 + ), 172 + ); 173 + 174 + altController.dispose(); 175 + 176 + if (result != null && mounted) { 177 + context.read<ComposeBloc>().add(VideoAltTextUpdated(result)); 178 + } 179 + } 180 + 181 + Future<void> _showAltTextDialog(int index, String currentAltText) async { 182 + final TextEditingController altController = TextEditingController(text: currentAltText); 183 + 184 + final result = await showDialog<String>( 185 + context: context, 186 + builder: (context) => AlertDialog( 187 + title: const Text('Add alt text'), 188 + content: TextField( 189 + controller: altController, 190 + maxLines: 3, 191 + maxLength: 1000, 192 + decoration: const InputDecoration( 193 + hintText: 'Describe the image for visually impaired users', 194 + border: OutlineInputBorder(), 195 + ), 196 + ), 197 + actions: [ 198 + TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), 199 + TextButton(onPressed: () => Navigator.pop(context, altController.text), child: const Text('Save')), 200 + ], 201 + ), 202 + ); 203 + 204 + altController.dispose(); 205 + 206 + if (result != null && mounted) { 207 + context.read<ComposeBloc>().add(AltTextUpdated(index: index, altText: result)); 208 + } 209 + } 210 + 211 + Future<void> _showSchedulePicker() async { 212 + final now = DateTime.now(); 213 + final initialDate = now.add(const Duration(minutes: 5)); 214 + 215 + final DateTime? date = await showDatePicker( 216 + context: context, 217 + initialDate: initialDate, 218 + firstDate: now, 219 + lastDate: now.add(const Duration(days: 365)), 220 + ); 221 + 222 + if (date == null) return; 223 + 224 + if (!mounted) return; 225 + final TimeOfDay? time = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(initialDate)); 226 + 227 + if (time == null) return; 228 + 229 + if (!mounted) return; 230 + final scheduledDateTime = DateTime(date.year, date.month, date.day, time.hour, time.minute); 231 + 232 + if (scheduledDateTime.isAfter(now)) { 233 + context.read<ComposeBloc>().add(PostScheduled(scheduledDateTime)); 234 + } 235 + } 236 + 237 + Future<void> _showDraftsDialog() async { 238 + final bloc = context.read<ComposeBloc>(); 239 + bloc.add(const DraftsRequested()); 240 + 241 + await showModalBottomSheet<void>( 242 + context: context, 243 + isScrollControlled: true, 244 + builder: (context) => BlocProvider.value( 245 + value: bloc, 246 + child: BlocBuilder<ComposeBloc, ComposeState>( 247 + builder: (context, state) { 248 + if (state.isLoadingDrafts) { 249 + return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); 250 + } 251 + 252 + if (state.drafts.isEmpty) { 253 + return const SizedBox(height: 150, child: Center(child: Text('No drafts saved'))); 254 + } 255 + 256 + return DraggableScrollableSheet( 257 + initialChildSize: 0.6, 258 + minChildSize: 0.3, 259 + maxChildSize: 0.9, 260 + expand: false, 261 + builder: (context, scrollController) { 262 + return Column( 263 + children: [ 264 + Container( 265 + padding: const EdgeInsets.all(16), 266 + decoration: BoxDecoration( 267 + border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), 268 + ), 269 + child: Row( 270 + mainAxisAlignment: MainAxisAlignment.spaceBetween, 271 + children: [ 272 + Text('Drafts', style: Theme.of(context).textTheme.titleLarge), 273 + Text( 274 + '${state.drafts.length} draft${state.drafts.length != 1 ? 's' : ''}', 275 + style: Theme.of( 276 + context, 277 + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 278 + ), 279 + ], 280 + ), 281 + ), 282 + Expanded( 283 + child: ListView.builder( 284 + controller: scrollController, 285 + itemCount: state.drafts.length, 286 + itemBuilder: (context, index) { 287 + final draft = state.drafts[index]; 288 + return ListTile( 289 + title: Text( 290 + draft.content.isEmpty ? '(No text)' : draft.content, 291 + maxLines: 2, 292 + overflow: TextOverflow.ellipsis, 293 + ), 294 + subtitle: Row( 295 + children: [ 296 + Text(_formatDraftTime(draft.updatedAt), style: Theme.of(context).textTheme.bodySmall), 297 + if (draft.scheduledAt != null) ...[ 298 + const SizedBox(width: 8), 299 + Container( 300 + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 301 + decoration: BoxDecoration( 302 + color: Theme.of(context).colorScheme.primaryContainer, 303 + borderRadius: BorderRadius.circular(4), 304 + ), 305 + child: Text( 306 + 'Scheduled', 307 + style: Theme.of(context).textTheme.bodySmall?.copyWith( 308 + color: Theme.of(context).colorScheme.onPrimaryContainer, 309 + ), 310 + ), 311 + ), 312 + ], 313 + ], 314 + ), 315 + trailing: IconButton( 316 + icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), 317 + onPressed: () { 318 + final bloc = context.read<ComposeBloc>(); 319 + final theme = Theme.of(context); 320 + showDialog<bool>( 321 + context: context, 322 + builder: (dialogContext) => AlertDialog( 323 + title: const Text('Delete Draft?'), 324 + content: const Text('This action cannot be undone.'), 325 + actions: [ 326 + TextButton( 327 + onPressed: () => Navigator.of(dialogContext).pop(false), 328 + child: const Text('Cancel'), 329 + ), 330 + TextButton( 331 + onPressed: () => Navigator.of(dialogContext).pop(true), 332 + child: Text('Delete', style: TextStyle(color: theme.colorScheme.error)), 333 + ), 334 + ], 335 + ), 336 + ).then((confirmed) { 337 + if (confirmed == true && mounted) { 338 + bloc.add(DraftDeleted(draft.id)); 339 + } 340 + }); 341 + }, 342 + ), 343 + onTap: () { 344 + Navigator.pop(context); 345 + context.read<ComposeBloc>().add(DraftLoaded(draft.id)); 346 + }, 347 + ); 348 + }, 349 + ), 350 + ), 351 + ], 352 + ); 353 + }, 354 + ); 355 + }, 356 + ), 357 + ), 358 + ); 359 + } 360 + 361 + String _formatDraftTime(DateTime dateTime) { 362 + final now = DateTime.now(); 363 + final difference = now.difference(dateTime); 364 + 365 + if (difference.isNegative) { 366 + return 'Just now'; 367 + } 368 + 369 + if (difference.inMinutes < 1) { 370 + return 'Just now'; 371 + } else if (difference.inMinutes < 60) { 372 + return '${difference.inMinutes}m ago'; 373 + } else if (difference.inHours < 24) { 374 + return '${difference.inHours}h ago'; 375 + } else if (difference.inDays < 7) { 376 + return '${difference.inDays}d ago'; 377 + } else { 378 + return DateFormat('MMM d').format(dateTime); 379 + } 380 + } 381 + 382 + String _videoStatusLabel(VideoAttachment video) { 383 + return switch (video.status) { 384 + VideoUploadStatus.idle => 'Ready to upload', 385 + VideoUploadStatus.checkingLimits => 'Checking upload limits…', 386 + VideoUploadStatus.uploading => video.uploadProgress > 0 ? 'Uploading… ${video.uploadProgress}%' : 'Uploading…', 387 + VideoUploadStatus.processing => video.uploadProgress > 0 ? 'Processing… ${video.uploadProgress}%' : 'Processing…', 388 + VideoUploadStatus.ready => video.altText.isNotEmpty ? 'Ready · "${video.altText}"' : 'Ready', 389 + VideoUploadStatus.error => video.errorMessage ?? 'Upload failed', 390 + }; 391 + } 392 + 393 + void _submitPost() { 394 + context.read<ComposeBloc>().add(const PostSubmitted()); 395 + } 396 + 397 + void _saveDraft() { 398 + context.read<ComposeBloc>().add(const DraftSaved()); 399 + if (mounted) { 400 + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Draft saved'))); 401 + } 402 + } 403 + 404 + void _handleBackNavigation(BuildContext context) { 405 + final state = context.read<ComposeBloc>().state; 406 + final navigator = Navigator.of(context); 407 + 408 + final hasContent = state.text.trim().isNotEmpty || state.mediaAttachments.isNotEmpty; 409 + 410 + if (hasContent) { 411 + showDialog<bool>( 412 + context: context, 413 + builder: (dialogContext) => AlertDialog( 414 + title: const Text('Save Draft?'), 415 + content: const Text('You have unsaved content. Would you like to save it as a draft?'), 416 + actions: [ 417 + TextButton( 418 + onPressed: () { 419 + Navigator.of(dialogContext).pop(false); 420 + }, 421 + child: const Text('Discard'), 422 + ), 423 + TextButton( 424 + onPressed: () { 425 + Navigator.of(dialogContext).pop(true); 426 + }, 427 + child: const Text('Save'), 428 + ), 429 + ], 430 + ), 431 + ).then((shouldSave) { 432 + if (shouldSave == true) { 433 + _saveDraft(); 434 + } 435 + if (mounted) { 436 + navigator.pop(); 437 + } 438 + }); 439 + } else { 440 + navigator.pop(); 441 + } 442 + } 443 + 444 + @override 445 + Widget build(BuildContext context) { 446 + return BlocListener<ComposeBloc, ComposeState>( 447 + listener: (context, state) { 448 + if (state.text != _textController.text) { 449 + _textController.text = state.text; 450 + _textController.selection = TextSelection.collapsed(offset: state.text.length); 451 + } 452 + 453 + if (state.isSuccess) { 454 + Navigator.of(context).pop(); 455 + } 456 + 457 + if (state.hasError && state.errorMessage != null) { 458 + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.errorMessage!))); 459 + } 460 + }, 461 + child: PopScope( 462 + canPop: false, 463 + onPopInvokedWithResult: (bool didPop, dynamic result) { 464 + if (didPop) return; 465 + _handleBackNavigation(context); 466 + }, 467 + child: Scaffold( 468 + appBar: AppBar( 469 + leading: TextButton(onPressed: () => _handleBackNavigation(context), child: const Text('Cancel')), 470 + leadingWidth: 80, 471 + title: const Text('New Post'), 472 + centerTitle: true, 473 + actions: [ 474 + TextButton(onPressed: _saveDraft, child: const Text('Save Draft')), 475 + BlocBuilder<ComposeBloc, ComposeState>( 476 + builder: (context, state) { 477 + return Padding( 478 + padding: const EdgeInsets.only(right: 8), 479 + child: TextButton( 480 + onPressed: state.canSubmit && !state.isSubmitting ? _submitPost : null, 481 + child: state.isSubmitting 482 + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) 483 + : const Text('Post'), 484 + ), 485 + ); 486 + }, 487 + ), 488 + ], 489 + ), 490 + body: SafeArea( 491 + child: Column( 492 + children: [ 493 + BlocBuilder<ComposeBloc, ComposeState>( 494 + builder: (context, state) { 495 + if (!state.isReply || widget.replyAuthorHandle == null) { 496 + return const SizedBox.shrink(); 497 + } 498 + return Container( 499 + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 500 + decoration: BoxDecoration( 501 + color: Theme.of(context).colorScheme.surfaceContainerHighest, 502 + border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), 503 + ), 504 + child: Row( 505 + children: [ 506 + Icon(Icons.reply, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), 507 + const SizedBox(width: 8), 508 + Text( 509 + 'Replying to ', 510 + style: Theme.of( 511 + context, 512 + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 513 + ), 514 + Text( 515 + '@${widget.replyAuthorHandle}', 516 + style: Theme.of(context).textTheme.bodySmall?.copyWith( 517 + color: Theme.of(context).colorScheme.primary, 518 + fontWeight: FontWeight.w500, 519 + ), 520 + ), 521 + ], 522 + ), 523 + ); 524 + }, 525 + ), 526 + Expanded( 527 + child: Padding( 528 + padding: const EdgeInsets.all(16), 529 + child: TextField( 530 + controller: _textController, 531 + maxLines: null, 532 + expands: true, 533 + textAlignVertical: TextAlignVertical.top, 534 + decoration: const InputDecoration( 535 + hintText: "What's on your mind?", 536 + border: InputBorder.none, 537 + contentPadding: EdgeInsets.zero, 538 + ), 539 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.5), 540 + ), 541 + ), 542 + ), 543 + BlocBuilder<ComposeBloc, ComposeState>( 544 + builder: (context, state) { 545 + if (!state.hasScheduledTime) return const SizedBox.shrink(); 546 + 547 + return Container( 548 + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 549 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 550 + decoration: BoxDecoration( 551 + color: Theme.of(context).colorScheme.primaryContainer, 552 + borderRadius: BorderRadius.circular(20), 553 + ), 554 + child: Row( 555 + mainAxisSize: MainAxisSize.min, 556 + children: [ 557 + Icon(Icons.schedule, size: 16, color: Theme.of(context).colorScheme.onPrimaryContainer), 558 + const SizedBox(width: 8), 559 + Text( 560 + 'Scheduled for ${DateFormat('MMM d, h:mm a').format(state.scheduledAt!)}', 561 + style: Theme.of( 562 + context, 563 + ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer), 564 + ), 565 + const SizedBox(width: 8), 566 + GestureDetector( 567 + onTap: () { 568 + context.read<ComposeBloc>().add(const ScheduleCleared()); 569 + }, 570 + child: Icon(Icons.close, size: 16, color: Theme.of(context).colorScheme.onPrimaryContainer), 571 + ), 572 + ], 573 + ), 574 + ); 575 + }, 576 + ), 577 + 578 + BlocBuilder<ComposeBloc, ComposeState>( 579 + builder: (context, state) { 580 + if (state.mediaAttachments.isEmpty) return const SizedBox.shrink(); 581 + 582 + return Container( 583 + height: 120, 584 + padding: const EdgeInsets.symmetric(horizontal: 16), 585 + child: ListView.builder( 586 + scrollDirection: Axis.horizontal, 587 + itemCount: state.mediaAttachments.length, 588 + itemBuilder: (context, index) { 589 + final attachment = state.mediaAttachments[index]; 590 + return Padding( 591 + padding: const EdgeInsets.only(right: 8), 592 + child: Stack( 593 + children: [ 594 + ClipRRect( 595 + borderRadius: BorderRadius.circular(12), 596 + child: Image.file( 597 + File(attachment.localPath), 598 + width: 120, 599 + height: 120, 600 + fit: BoxFit.cover, 601 + ), 602 + ), 603 + Positioned( 604 + left: 8, 605 + bottom: 8, 606 + child: GestureDetector( 607 + onTap: () => _showAltTextDialog(index, attachment.altText), 608 + child: Container( 609 + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 610 + decoration: BoxDecoration( 611 + color: attachment.altText.isNotEmpty 612 + ? Theme.of(context).colorScheme.primary 613 + : Colors.black54, 614 + borderRadius: BorderRadius.circular(4), 615 + ), 616 + child: Text( 617 + 'ALT', 618 + style: Theme.of(context).textTheme.labelSmall?.copyWith( 619 + color: attachment.altText.isNotEmpty 620 + ? Theme.of(context).colorScheme.onPrimary 621 + : Colors.white, 622 + fontWeight: FontWeight.bold, 623 + ), 624 + ), 625 + ), 626 + ), 627 + ), 628 + Positioned( 629 + top: 4, 630 + right: 4, 631 + child: GestureDetector( 632 + onTap: () { 633 + context.read<ComposeBloc>().add(MediaRemoved(index)); 634 + }, 635 + child: Container( 636 + padding: const EdgeInsets.all(4), 637 + decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle), 638 + child: const Icon(Icons.close, size: 16, color: Colors.white), 639 + ), 640 + ), 641 + ), 642 + ], 643 + ), 644 + ); 645 + }, 646 + ), 647 + ); 648 + }, 649 + ), 650 + BlocBuilder<ComposeBloc, ComposeState>( 651 + builder: (context, state) { 652 + final video = state.videoAttachment; 653 + if (video == null) return const SizedBox.shrink(); 654 + 655 + return Container( 656 + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 657 + padding: const EdgeInsets.all(12), 658 + decoration: BoxDecoration( 659 + color: Theme.of(context).colorScheme.surfaceContainerHighest, 660 + borderRadius: BorderRadius.circular(12), 661 + border: Border.all( 662 + color: video.hasError ? Theme.of(context).colorScheme.error : Theme.of(context).dividerColor, 663 + ), 664 + ), 665 + child: Row( 666 + children: [ 667 + Container( 668 + width: 48, 669 + height: 48, 670 + decoration: BoxDecoration( 671 + color: Theme.of(context).colorScheme.primaryContainer, 672 + borderRadius: BorderRadius.circular(8), 673 + ), 674 + child: video.isActive 675 + ? Padding( 676 + padding: const EdgeInsets.all(12), 677 + child: CircularProgressIndicator( 678 + strokeWidth: 2, 679 + value: video.isActive && video.uploadProgress > 0 680 + ? video.uploadProgress / 100 681 + : null, 682 + ), 683 + ) 684 + : Icon( 685 + video.hasError ? Icons.error_outline : Icons.videocam_outlined, 686 + color: video.hasError 687 + ? Theme.of(context).colorScheme.error 688 + : Theme.of(context).colorScheme.onPrimaryContainer, 689 + ), 690 + ), 691 + const SizedBox(width: 12), 692 + Expanded( 693 + child: Column( 694 + crossAxisAlignment: CrossAxisAlignment.start, 695 + children: [ 696 + Text( 697 + video.localPath.split('/').last, 698 + style: Theme.of(context).textTheme.bodyMedium, 699 + maxLines: 1, 700 + overflow: TextOverflow.ellipsis, 701 + ), 702 + const SizedBox(height: 2), 703 + Text( 704 + _videoStatusLabel(video), 705 + style: Theme.of(context).textTheme.bodySmall?.copyWith( 706 + color: video.hasError 707 + ? Theme.of(context).colorScheme.error 708 + : Theme.of(context).colorScheme.onSurfaceVariant, 709 + ), 710 + ), 711 + if (video.isActive && video.uploadProgress > 0) ...[ 712 + const SizedBox(height: 4), 713 + LinearProgressIndicator( 714 + value: video.uploadProgress / 100, 715 + borderRadius: BorderRadius.circular(2), 716 + ), 717 + ], 718 + ], 719 + ), 720 + ), 721 + if (video.isReady) ...[ 722 + IconButton( 723 + icon: const Icon(Icons.subtitles_outlined), 724 + tooltip: 'Add alt text', 725 + onPressed: () => _showVideoAltTextDialog(video.altText), 726 + color: video.altText.isNotEmpty 727 + ? Theme.of(context).colorScheme.primary 728 + : Theme.of(context).colorScheme.onSurfaceVariant, 729 + ), 730 + ], 731 + IconButton( 732 + icon: const Icon(Icons.close), 733 + onPressed: () => context.read<ComposeBloc>().add(const VideoRemoved()), 734 + color: Theme.of(context).colorScheme.onSurfaceVariant, 735 + ), 736 + ], 737 + ), 738 + ); 739 + }, 740 + ), 741 + const SizedBox(height: 8), 742 + Container( 743 + decoration: BoxDecoration( 744 + border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), 745 + ), 746 + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), 747 + child: SafeArea( 748 + child: Row( 749 + children: [ 750 + BlocBuilder<ComposeBloc, ComposeState>( 751 + builder: (context, state) { 752 + return IconButton( 753 + onPressed: state.canAddMoreMedia ? _pickImage : null, 754 + icon: Icon( 755 + Icons.image_outlined, 756 + color: state.canAddMoreMedia 757 + ? Theme.of(context).colorScheme.primary 758 + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5), 759 + ), 760 + tooltip: 'Add image', 761 + ); 762 + }, 763 + ), 764 + BlocBuilder<ComposeBloc, ComposeState>( 765 + builder: (context, state) { 766 + return IconButton( 767 + onPressed: state.canAddVideo ? _pickVideo : null, 768 + icon: Icon( 769 + Icons.videocam_outlined, 770 + color: state.canAddVideo 771 + ? Theme.of(context).colorScheme.primary 772 + : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5), 773 + ), 774 + tooltip: 'Add video', 775 + ); 776 + }, 777 + ), 778 + IconButton( 779 + onPressed: _showDraftsDialog, 780 + icon: Icon(Icons.drive_file_rename_outline, color: Theme.of(context).colorScheme.primary), 781 + tooltip: 'Drafts', 782 + ), 783 + IconButton( 784 + onPressed: _showSchedulePicker, 785 + icon: Icon(Icons.schedule, color: Theme.of(context).colorScheme.primary), 786 + tooltip: 'Schedule', 787 + ), 788 + const Spacer(), 789 + BlocBuilder<ComposeBloc, ComposeState>( 790 + builder: (context, state) { 791 + return _CharCounter(count: state.graphemeCount, maxCount: kMaxGraphemes); 792 + }, 793 + ), 794 + ], 795 + ), 796 + ), 797 + ), 798 + ], 799 + ), 800 + ), 801 + ), 802 + ), 803 + ); 804 + } 805 + } 806 + 807 + /// A [TextEditingController] that highlights AT Protocol facets (mentions, 808 + /// links, hashtags) inline as the user types. 809 + /// 810 + /// Byte-offset → code-unit conversion is done via UTF-8 re-encode so that 811 + /// multi-byte characters (emoji, CJK, etc.) are handled correctly. 812 + class _FacetHighlightController extends TextEditingController { 813 + @override 814 + TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { 815 + final text = value.text; 816 + if (text.isEmpty) return TextSpan(style: style); 817 + 818 + final entities = BlueskyText(text).entities.where((e) => e.type != EntityType.markdownLink).toList(); 819 + if (entities.isEmpty) return TextSpan(style: style, text: text); 820 + 821 + final colorScheme = Theme.of(context).colorScheme; 822 + final textBytes = utf8.encode(text); 823 + final spans = <InlineSpan>[]; 824 + int lastCharEnd = 0; 825 + 826 + for (final entity in entities) { 827 + final charStart = _byteToCharOffset(textBytes, entity.indices.start); 828 + final charEnd = _byteToCharOffset(textBytes, entity.indices.end); 829 + 830 + if (charStart < lastCharEnd || charStart >= charEnd) continue; 831 + 832 + if (charStart > lastCharEnd) { 833 + spans.add(TextSpan(style: style, text: text.substring(lastCharEnd, charStart))); 834 + } 835 + 836 + final Color entityColor; 837 + if (entity.isHandle) { 838 + entityColor = colorScheme.primary; 839 + } else if (entity.isLink) { 840 + entityColor = colorScheme.tertiary; 841 + } else { 842 + entityColor = colorScheme.secondary; 843 + } 844 + 845 + spans.add( 846 + TextSpan( 847 + style: style?.copyWith(color: entityColor), 848 + text: text.substring(charStart, charEnd), 849 + ), 850 + ); 851 + lastCharEnd = charEnd; 852 + } 853 + 854 + if (lastCharEnd < text.length) { 855 + spans.add(TextSpan(style: style, text: text.substring(lastCharEnd))); 856 + } 857 + 858 + return TextSpan(children: spans); 859 + } 860 + 861 + /// Converts a UTF-8 byte offset into a Dart [String] code-unit offset. 862 + static int _byteToCharOffset(List<int> textBytes, int byteOffset) { 863 + if (byteOffset <= 0) return 0; 864 + if (byteOffset >= textBytes.length) return utf8.decode(textBytes, allowMalformed: true).length; 865 + return utf8.decode(textBytes.sublist(0, byteOffset), allowMalformed: true).length; 866 + } 867 + } 868 + 869 + class _CharCounter extends StatelessWidget { 870 + const _CharCounter({required this.count, required this.maxCount}); 871 + 872 + final int count; 873 + final int maxCount; 874 + 875 + @override 876 + Widget build(BuildContext context) { 877 + final remaining = maxCount - count; 878 + final progress = count / maxCount; 879 + 880 + Color color; 881 + if (progress < 0.8) { 882 + color = Theme.of(context).colorScheme.primary; 883 + } else if (progress < 0.95) { 884 + color = Theme.of(context).colorScheme.error.withValues(alpha: 0.7); 885 + } else { 886 + color = Theme.of(context).colorScheme.error; 887 + } 888 + 889 + return Row( 890 + mainAxisSize: MainAxisSize.min, 891 + children: [ 892 + if (count > 0) 893 + Text( 894 + '$remaining', 895 + style: Theme.of( 896 + context, 897 + ).textTheme.bodySmall?.copyWith(color: color, fontFeatures: const [FontFeature.tabularFigures()]), 898 + ), 899 + const SizedBox(width: 8), 900 + SizedBox( 901 + width: 28, 902 + height: 28, 903 + child: CustomPaint( 904 + painter: _ProgressRingPainter( 905 + progress: progress.clamp(0.0, 1.0), 906 + color: color, 907 + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, 908 + ), 909 + ), 910 + ), 911 + ], 912 + ); 913 + } 914 + } 915 + 916 + class _ProgressRingPainter extends CustomPainter { 917 + _ProgressRingPainter({required this.progress, required this.color, required this.backgroundColor}); 918 + 919 + final double progress; 920 + final Color color; 921 + final Color backgroundColor; 922 + 923 + @override 924 + void paint(Canvas canvas, Size size) { 925 + final center = Offset(size.width / 2, size.height / 2); 926 + final radius = (size.width - 4) / 2; 927 + 928 + final backgroundPaint = Paint() 929 + ..color = backgroundColor 930 + ..style = PaintingStyle.stroke 931 + ..strokeWidth = 2.5; 932 + 933 + canvas.drawCircle(center, radius, backgroundPaint); 934 + 935 + final progressPaint = Paint() 936 + ..color = color 937 + ..style = PaintingStyle.stroke 938 + ..strokeWidth = 2.5 939 + ..strokeCap = StrokeCap.round; 940 + 941 + final sweepAngle = 2 * 3.14159 * progress; 942 + canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -3.14159 / 2, sweepAngle, false, progressPaint); 943 + } 944 + 945 + @override 946 + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 947 + }
+4
lib/features/feed/presentation/home_feed_screen.dart
··· 111 111 ), 112 112 ], 113 113 ), 114 + floatingActionButton: FloatingActionButton( 115 + onPressed: () => context.push('/compose'), 116 + child: const Icon(Icons.add), 117 + ), 114 118 ); 115 119 }, 116 120 );
+5
lib/main.dart
··· 4 4 import 'package:go_router/go_router.dart'; 5 5 import 'package:lazurite/core/database/app_database.dart'; 6 6 import 'package:lazurite/core/logging/app_logger.dart'; 7 + import 'package:lazurite/core/scheduler/post_scheduler.dart'; 7 8 import 'package:lazurite/core/logging/logging_bloc_observer.dart'; 8 9 import 'package:lazurite/core/logging/logging_navigator_observer.dart'; 9 10 import 'package:lazurite/core/network/xrpc_client_factory.dart'; ··· 26 27 WidgetsFlutterBinding.ensureInitialized(); 27 28 28 29 await log.initialize(); 30 + await PostScheduler.initialize(); 29 31 Bloc.observer = LoggingBlocObserver(); 30 32 31 33 final database = AppDatabase(); ··· 141 143 ), 142 144 RepositoryProvider.value(value: feedRepository), 143 145 RepositoryProvider.value(value: searchRepository), 146 + RepositoryProvider.value(value: bluesky), 147 + RepositoryProvider.value(value: widget.database), 148 + RepositoryProvider.value(value: accountDid), 144 149 ], 145 150 child: appShell, 146 151 );
+114 -2
pubspec.lock
··· 178 178 source: hosted 179 179 version: "6.5.1" 180 180 characters: 181 - dependency: transitive 181 + dependency: "direct main" 182 182 description: 183 183 name: characters 184 184 sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 ··· 258 258 source: hosted 259 259 version: "1.15.0" 260 260 cross_file: 261 - dependency: transitive 261 + dependency: "direct main" 262 262 description: 263 263 name: cross_file 264 264 sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" ··· 361 361 url: "https://pub.dev" 362 362 source: hosted 363 363 version: "7.0.1" 364 + file_selector_linux: 365 + dependency: transitive 366 + description: 367 + name: file_selector_linux 368 + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" 369 + url: "https://pub.dev" 370 + source: hosted 371 + version: "0.9.4" 372 + file_selector_macos: 373 + dependency: transitive 374 + description: 375 + name: file_selector_macos 376 + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" 377 + url: "https://pub.dev" 378 + source: hosted 379 + version: "0.9.5" 380 + file_selector_platform_interface: 381 + dependency: transitive 382 + description: 383 + name: file_selector_platform_interface 384 + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" 385 + url: "https://pub.dev" 386 + source: hosted 387 + version: "2.7.0" 388 + file_selector_windows: 389 + dependency: transitive 390 + description: 391 + name: file_selector_windows 392 + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" 393 + url: "https://pub.dev" 394 + source: hosted 395 + version: "0.9.3+5" 364 396 fixnum: 365 397 dependency: transitive 366 398 description: ··· 390 422 url: "https://pub.dev" 391 423 source: hosted 392 424 version: "6.0.0" 425 + flutter_plugin_android_lifecycle: 426 + dependency: transitive 427 + description: 428 + name: flutter_plugin_android_lifecycle 429 + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 430 + url: "https://pub.dev" 431 + source: hosted 432 + version: "2.0.33" 393 433 flutter_svg: 394 434 dependency: "direct main" 395 435 description: ··· 496 536 url: "https://pub.dev" 497 537 source: hosted 498 538 version: "4.1.2" 539 + image_picker: 540 + dependency: "direct main" 541 + description: 542 + name: image_picker 543 + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" 544 + url: "https://pub.dev" 545 + source: hosted 546 + version: "1.2.1" 547 + image_picker_android: 548 + dependency: transitive 549 + description: 550 + name: image_picker_android 551 + sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156 552 + url: "https://pub.dev" 553 + source: hosted 554 + version: "0.8.13+14" 555 + image_picker_for_web: 556 + dependency: transitive 557 + description: 558 + name: image_picker_for_web 559 + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" 560 + url: "https://pub.dev" 561 + source: hosted 562 + version: "3.1.1" 563 + image_picker_ios: 564 + dependency: transitive 565 + description: 566 + name: image_picker_ios 567 + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 568 + url: "https://pub.dev" 569 + source: hosted 570 + version: "0.8.13+6" 571 + image_picker_linux: 572 + dependency: transitive 573 + description: 574 + name: image_picker_linux 575 + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" 576 + url: "https://pub.dev" 577 + source: hosted 578 + version: "0.2.2" 579 + image_picker_macos: 580 + dependency: transitive 581 + description: 582 + name: image_picker_macos 583 + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" 584 + url: "https://pub.dev" 585 + source: hosted 586 + version: "0.2.2+1" 587 + image_picker_platform_interface: 588 + dependency: transitive 589 + description: 590 + name: image_picker_platform_interface 591 + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" 592 + url: "https://pub.dev" 593 + source: hosted 594 + version: "2.11.1" 595 + image_picker_windows: 596 + dependency: transitive 597 + description: 598 + name: image_picker_windows 599 + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae 600 + url: "https://pub.dev" 601 + source: hosted 602 + version: "0.2.2" 499 603 intl: 500 604 dependency: "direct main" 501 605 description: ··· 1149 1253 url: "https://pub.dev" 1150 1254 source: hosted 1151 1255 version: "5.15.0" 1256 + workmanager: 1257 + dependency: "direct main" 1258 + description: 1259 + name: workmanager 1260 + sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00 1261 + url: "https://pub.dev" 1262 + source: hosted 1263 + version: "0.5.2" 1152 1264 xdg_directories: 1153 1265 dependency: transitive 1154 1266 description:
+4
pubspec.yaml
··· 33 33 http: ^1.2.2 34 34 uuid: ^4.5.1 35 35 flutter_svg: ^2.2.4 36 + image_picker: ^1.1.2 37 + cross_file: ^0.3.4+2 38 + characters: ^1.4.0 39 + workmanager: ^0.5.2 36 40 37 41 dev_dependencies: 38 42 flutter_test:
+672
test/features/compose/bloc/compose_bloc_test.dart
··· 1 + import 'package:bloc_test/bloc_test.dart'; 2 + import 'package:flutter_test/flutter_test.dart'; 3 + import 'package:lazurite/core/database/app_database.dart'; 4 + import 'package:lazurite/features/compose/bloc/compose_bloc.dart'; 5 + import 'package:mocktail/mocktail.dart'; 6 + 7 + class MockAppDatabase extends Mock implements AppDatabase {} 8 + 9 + class MockComposeRepository extends Mock implements ComposeRepository {} 10 + 11 + class FakeDraftsCompanion extends Fake implements DraftsCompanion {} 12 + 13 + DraftEntry _makeDraft({ 14 + int id = 1, 15 + String content = 'Draft', 16 + String? mediaPaths, 17 + String? embedJson, 18 + String? replyUri, 19 + String? replyCid, 20 + String? rootUri, 21 + String? rootCid, 22 + DateTime? scheduledAt, 23 + }) => DraftEntry( 24 + id: id, 25 + accountDid: 'did:plc:test', 26 + content: content, 27 + mediaPaths: mediaPaths, 28 + embedJson: embedJson, 29 + replyUri: replyUri, 30 + replyCid: replyCid, 31 + rootUri: rootUri, 32 + rootCid: rootCid, 33 + createdAt: DateTime(2025, 1, 1), 34 + updatedAt: DateTime(2025, 1, 1), 35 + scheduledAt: scheduledAt, 36 + ); 37 + 38 + void main() { 39 + group('ComposeBloc', () { 40 + late ComposeBloc composeBloc; 41 + late MockAppDatabase mockDatabase; 42 + late MockComposeRepository mockRepository; 43 + 44 + setUp(() { 45 + mockDatabase = MockAppDatabase(); 46 + mockRepository = MockComposeRepository(); 47 + composeBloc = ComposeBloc(composeRepository: mockRepository, database: mockDatabase, accountDid: 'did:plc:test'); 48 + registerFallbackValue(FakeDraftsCompanion()); 49 + }); 50 + 51 + tearDown(() { 52 + composeBloc.close(); 53 + }); 54 + 55 + test('initial state is correct', () { 56 + expect(composeBloc.state.status, ComposeStatus.ready); 57 + expect(composeBloc.state.isEmpty, true); 58 + expect(composeBloc.state.canSubmit, false); 59 + }); 60 + 61 + group('TextChanged', () { 62 + blocTest<ComposeBloc, ComposeState>( 63 + 'emits correct state when text changes', 64 + build: () => composeBloc, 65 + act: (bloc) => bloc.add(const TextChanged('Hello world')), 66 + expect: () => [ 67 + isA<ComposeState>() 68 + .having((s) => s.text, 'text', 'Hello world') 69 + .having((s) => s.graphemeCount, 'graphemeCount', 11) 70 + .having((s) => s.isEmpty, 'isEmpty', false) 71 + .having((s) => s.canSubmit, 'canSubmit', true), 72 + ], 73 + ); 74 + 75 + blocTest<ComposeBloc, ComposeState>( 76 + 'emits overLimit when text exceeds 300 graphemes', 77 + build: () => composeBloc, 78 + act: (bloc) => bloc.add(TextChanged('a' * 301)), 79 + expect: () => [ 80 + isA<ComposeState>() 81 + .having((s) => s.graphemeCount, 'graphemeCount', 301) 82 + .having((s) => s.isOverLimit, 'isOverLimit', true) 83 + .having((s) => s.canSubmit, 'canSubmit', false), 84 + ], 85 + ); 86 + 87 + blocTest<ComposeBloc, ComposeState>( 88 + 'isEmpty is true when text is empty and no media', 89 + build: () => composeBloc, 90 + act: (bloc) => bloc.add(const TextChanged('')), 91 + expect: () => [ 92 + isA<ComposeState>() 93 + .having((s) => s.text, 'text', '') 94 + .having((s) => s.isEmpty, 'isEmpty', true) 95 + .having((s) => s.canSubmit, 'canSubmit', false), 96 + ], 97 + ); 98 + }); 99 + 100 + group('MediaAttached', () { 101 + blocTest<ComposeBloc, ComposeState>( 102 + 'emits correct state when media is attached', 103 + build: () => composeBloc, 104 + act: (bloc) => bloc.add(const MediaAttached('/path/to/image.jpg')), 105 + expect: () => [ 106 + isA<ComposeState>() 107 + .having((s) => s.mediaAttachments.length, 'mediaAttachments.length', 1) 108 + .having((s) => s.canAddMoreMedia, 'canAddMoreMedia', true) 109 + .having((s) => s.isEmpty, 'isEmpty', false), 110 + ], 111 + ); 112 + 113 + blocTest<ComposeBloc, ComposeState>( 114 + 'stores width and height when provided', 115 + build: () => composeBloc, 116 + act: (bloc) => bloc.add(const MediaAttached('/path/to/image.jpg', width: 1920, height: 1080)), 117 + expect: () => [ 118 + isA<ComposeState>().having( 119 + (s) => s.mediaAttachments.first, 120 + 'attachment', 121 + isA<MediaAttachment>().having((a) => a.width, 'width', 1920).having((a) => a.height, 'height', 1080), 122 + ), 123 + ], 124 + ); 125 + 126 + blocTest<ComposeBloc, ComposeState>( 127 + 'does not add media when already at max', 128 + build: () => composeBloc, 129 + seed: () => const ComposeState.ready( 130 + mediaAttachments: [ 131 + MediaAttachment(localPath: '/1.jpg'), 132 + MediaAttachment(localPath: '/2.jpg'), 133 + MediaAttachment(localPath: '/3.jpg'), 134 + MediaAttachment(localPath: '/4.jpg'), 135 + ], 136 + ), 137 + act: (bloc) => bloc.add(const MediaAttached('/path/to/image.jpg')), 138 + expect: () => [], 139 + ); 140 + 141 + blocTest<ComposeBloc, ComposeState>( 142 + 'does not add media when video is attached', 143 + build: () => composeBloc, 144 + seed: () => const ComposeState.ready( 145 + videoAttachment: VideoAttachment(localPath: '/video.mp4', status: VideoUploadStatus.ready), 146 + ), 147 + act: (bloc) => bloc.add(const MediaAttached('/image.jpg')), 148 + expect: () => [], 149 + ); 150 + }); 151 + 152 + group('MediaRemoved', () { 153 + blocTest<ComposeBloc, ComposeState>( 154 + 'removes attachment at given index', 155 + build: () => composeBloc, 156 + seed: () => const ComposeState.ready(mediaAttachments: [MediaAttachment(localPath: '/path/to/image.jpg')]), 157 + act: (bloc) => bloc.add(const MediaRemoved(0)), 158 + expect: () => [ 159 + isA<ComposeState>() 160 + .having((s) => s.mediaAttachments.length, 'mediaAttachments.length', 0) 161 + .having((s) => s.isEmpty, 'isEmpty', true), 162 + ], 163 + ); 164 + 165 + blocTest<ComposeBloc, ComposeState>( 166 + 'preserves other attachments when removing from start', 167 + build: () => composeBloc, 168 + seed: () => const ComposeState.ready( 169 + mediaAttachments: [ 170 + MediaAttachment(localPath: '/1.jpg', altText: 'First'), 171 + MediaAttachment(localPath: '/2.jpg', altText: 'Second'), 172 + ], 173 + ), 174 + act: (bloc) => bloc.add(const MediaRemoved(0)), 175 + expect: () => [ 176 + isA<ComposeState>() 177 + .having((s) => s.mediaAttachments.length, 'mediaAttachments.length', 1) 178 + .having((s) => s.mediaAttachments.first.localPath, 'localPath', '/2.jpg') 179 + .having((s) => s.mediaAttachments.first.altText, 'altText', 'Second'), 180 + ], 181 + ); 182 + 183 + blocTest<ComposeBloc, ComposeState>( 184 + 'does nothing when index is out of bounds', 185 + build: () => composeBloc, 186 + seed: () => const ComposeState.ready(mediaAttachments: [MediaAttachment(localPath: '/path/to/image.jpg')]), 187 + act: (bloc) => bloc.add(const MediaRemoved(5)), 188 + expect: () => [], 189 + ); 190 + }); 191 + 192 + group('AltTextUpdated', () { 193 + blocTest<ComposeBloc, ComposeState>( 194 + 'updates alt text for media attachment', 195 + build: () => composeBloc, 196 + seed: () => const ComposeState.ready(mediaAttachments: [MediaAttachment(localPath: '/path/to/image.jpg')]), 197 + act: (bloc) => bloc.add(const AltTextUpdated(index: 0, altText: 'Description of image')), 198 + expect: () => [ 199 + isA<ComposeState>().having( 200 + (s) => s.mediaAttachments.first.altText, 201 + 'attachment.altText', 202 + 'Description of image', 203 + ), 204 + ], 205 + ); 206 + 207 + blocTest<ComposeBloc, ComposeState>( 208 + 'does nothing when index is out of bounds', 209 + build: () => composeBloc, 210 + seed: () => const ComposeState.ready(mediaAttachments: [MediaAttachment(localPath: '/path/to/image.jpg')]), 211 + act: (bloc) => bloc.add(const AltTextUpdated(index: 5, altText: 'Description')), 212 + expect: () => [], 213 + ); 214 + }); 215 + 216 + group('VideoRemoved', () { 217 + blocTest<ComposeBloc, ComposeState>( 218 + 'removes video attachment and allows media to be added again', 219 + build: () => composeBloc, 220 + seed: () => const ComposeState.ready( 221 + videoAttachment: VideoAttachment(localPath: '/video.mp4', status: VideoUploadStatus.ready), 222 + ), 223 + act: (bloc) => bloc.add(const VideoRemoved()), 224 + expect: () => [ 225 + isA<ComposeState>() 226 + .having((s) => s.videoAttachment, 'videoAttachment', isNull) 227 + .having((s) => s.canAddVideo, 'canAddVideo', true) 228 + .having((s) => s.canAddMoreMedia, 'canAddMoreMedia', true), 229 + ], 230 + ); 231 + 232 + blocTest<ComposeBloc, ComposeState>( 233 + 'isEmpty is true after video removed with no text', 234 + build: () => composeBloc, 235 + seed: () => const ComposeState.ready( 236 + videoAttachment: VideoAttachment(localPath: '/video.mp4', status: VideoUploadStatus.ready), 237 + ), 238 + act: (bloc) => bloc.add(const VideoRemoved()), 239 + expect: () => [isA<ComposeState>().having((s) => s.isEmpty, 'isEmpty', true)], 240 + ); 241 + }); 242 + 243 + group('VideoAltTextUpdated', () { 244 + blocTest<ComposeBloc, ComposeState>( 245 + 'updates video alt text', 246 + build: () => composeBloc, 247 + seed: () => const ComposeState.ready( 248 + videoAttachment: VideoAttachment(localPath: '/video.mp4', status: VideoUploadStatus.ready), 249 + ), 250 + act: (bloc) => bloc.add(const VideoAltTextUpdated('A cat playing piano')), 251 + expect: () => [ 252 + isA<ComposeState>().having( 253 + (s) => s.videoAttachment?.altText, 254 + 'videoAttachment.altText', 255 + 'A cat playing piano', 256 + ), 257 + ], 258 + ); 259 + 260 + blocTest<ComposeBloc, ComposeState>( 261 + 'does nothing when no video is attached', 262 + build: () => composeBloc, 263 + act: (bloc) => bloc.add(const VideoAltTextUpdated('Something')), 264 + expect: () => [], 265 + ); 266 + }); 267 + 268 + group('PostScheduled', () { 269 + blocTest<ComposeBloc, ComposeState>( 270 + 'sets scheduledAt', 271 + build: () => composeBloc, 272 + act: (bloc) => bloc.add(PostScheduled(DateTime(2025, 1, 1))), 273 + expect: () => [ 274 + isA<ComposeState>() 275 + .having((s) => s.hasScheduledTime, 'hasScheduledTime', true) 276 + .having((s) => s.scheduledAt, 'scheduledAt', DateTime(2025, 1, 1)), 277 + ], 278 + ); 279 + }); 280 + 281 + group('ScheduleCleared', () { 282 + blocTest<ComposeBloc, ComposeState>( 283 + 'clears scheduledAt', 284 + build: () => composeBloc, 285 + seed: () => ComposeState.ready(scheduledAt: DateTime(2025, 1, 1)), 286 + act: (bloc) => bloc.add(const ScheduleCleared()), 287 + expect: () => [ 288 + isA<ComposeState>() 289 + .having((s) => s.hasScheduledTime, 'hasScheduledTime', false) 290 + .having((s) => s.scheduledAt, 'scheduledAt', null), 291 + ], 292 + ); 293 + }); 294 + 295 + group('ReplyContextSet', () { 296 + blocTest<ComposeBloc, ComposeState>( 297 + 'sets reply context', 298 + build: () => composeBloc, 299 + act: (bloc) => bloc.add( 300 + const ReplyContextSet(parentUri: 'at://parent', parentCid: 'cid123', rootUri: 'at://root', rootCid: 'cid456'), 301 + ), 302 + expect: () => [ 303 + isA<ComposeState>() 304 + .having((s) => s.isReply, 'isReply', true) 305 + .having((s) => s.replyParentUri, 'replyParentUri', 'at://parent') 306 + .having((s) => s.replyParentCid, 'replyParentCid', 'cid123') 307 + .having((s) => s.replyRootUri, 'replyRootUri', 'at://root') 308 + .having((s) => s.replyRootCid, 'replyRootCid', 'cid456'), 309 + ], 310 + ); 311 + }); 312 + 313 + group('ReplyContextCleared', () { 314 + blocTest<ComposeBloc, ComposeState>( 315 + 'clears reply context', 316 + build: () => composeBloc, 317 + seed: () => const ComposeState.ready( 318 + replyParentUri: 'at://parent', 319 + replyParentCid: 'cid123', 320 + replyRootUri: 'at://root', 321 + replyRootCid: 'cid456', 322 + ), 323 + act: (bloc) => bloc.add(const ReplyContextCleared()), 324 + expect: () => [ 325 + isA<ComposeState>() 326 + .having((s) => s.isReply, 'isReply', false) 327 + .having((s) => s.replyParentUri, 'replyParentUri', null) 328 + .having((s) => s.replyParentCid, 'replyParentCid', null) 329 + .having((s) => s.replyRootUri, 'replyRootUri', null) 330 + .having((s) => s.replyRootCid, 'replyRootCid', null), 331 + ], 332 + ); 333 + }); 334 + 335 + group('DraftSaved', () { 336 + blocTest<ComposeBloc, ComposeState>( 337 + 'saves draft and updates draftId', 338 + build: () { 339 + when(() => mockDatabase.saveDraft(any())).thenAnswer((_) async => 42); 340 + return composeBloc; 341 + }, 342 + seed: () => const ComposeState.ready(text: 'Test content'), 343 + act: (bloc) => bloc.add(const DraftSaved()), 344 + expect: () => [ 345 + isA<ComposeState>().having((s) => s.isSavingDraft, 'isSavingDraft', true), 346 + isA<ComposeState>() 347 + .having((s) => s.draftId, 'draftId', 42) 348 + .having((s) => s.isSavingDraft, 'isSavingDraft', false), 349 + ], 350 + verify: (_) { 351 + verify(() => mockDatabase.saveDraft(any())).called(1); 352 + }, 353 + ); 354 + 355 + blocTest<ComposeBloc, ComposeState>( 356 + 'handles save error gracefully', 357 + build: () { 358 + when(() => mockDatabase.saveDraft(any())).thenThrow(Exception('DB error')); 359 + return composeBloc; 360 + }, 361 + act: (bloc) => bloc.add(const DraftSaved()), 362 + expect: () => [ 363 + isA<ComposeState>().having((s) => s.isSavingDraft, 'isSavingDraft', true), 364 + isA<ComposeState>().having((s) => s.isSavingDraft, 'isSavingDraft', false), 365 + ], 366 + ); 367 + }); 368 + 369 + group('DraftLoaded', () { 370 + blocTest<ComposeBloc, ComposeState>( 371 + 'loads draft text and id', 372 + build: () { 373 + when(() => mockDatabase.getDraft(1)).thenAnswer((_) async => _makeDraft(content: 'Loaded content')); 374 + return composeBloc; 375 + }, 376 + act: (bloc) => bloc.add(const DraftLoaded(1)), 377 + expect: () => [ 378 + isA<ComposeState>().having((s) => s.text, 'text', 'Loaded content').having((s) => s.draftId, 'draftId', 1), 379 + ], 380 + verify: (_) { 381 + verify(() => mockDatabase.getDraft(1)).called(1); 382 + }, 383 + ); 384 + 385 + blocTest<ComposeBloc, ComposeState>( 386 + 'loads scheduled time from draft', 387 + build: () { 388 + when( 389 + () => mockDatabase.getDraft(1), 390 + ).thenAnswer((_) async => _makeDraft(scheduledAt: DateTime(2026, 6, 1, 12))); 391 + return composeBloc; 392 + }, 393 + act: (bloc) => bloc.add(const DraftLoaded(1)), 394 + expect: () => [isA<ComposeState>().having((s) => s.scheduledAt, 'scheduledAt', DateTime(2026, 6, 1, 12))], 395 + ); 396 + 397 + blocTest<ComposeBloc, ComposeState>( 398 + 'loads reply context from draft', 399 + build: () { 400 + when(() => mockDatabase.getDraft(1)).thenAnswer( 401 + (_) async => _makeDraft(replyUri: 'at://parent', replyCid: 'cid1', rootUri: 'at://root', rootCid: 'cid2'), 402 + ); 403 + return composeBloc; 404 + }, 405 + act: (bloc) => bloc.add(const DraftLoaded(1)), 406 + expect: () => [ 407 + isA<ComposeState>() 408 + .having((s) => s.replyParentUri, 'replyParentUri', 'at://parent') 409 + .having((s) => s.isReply, 'isReply', true), 410 + ], 411 + ); 412 + 413 + blocTest<ComposeBloc, ComposeState>( 414 + 'parses embedJson with images format', 415 + build: () { 416 + when(() => mockDatabase.getDraft(1)).thenAnswer( 417 + (_) async => _makeDraft( 418 + content: 'Content', 419 + embedJson: '{"type":"images","paths":["/nonexistent.jpg"],"altTexts":["Alt"]}', 420 + ), 421 + ); 422 + return composeBloc; 423 + }, 424 + act: (bloc) => bloc.add(const DraftLoaded(1)), 425 + expect: () => [ 426 + isA<ComposeState>().having((s) => s.text, 'text', 'Content').having((s) => s.draftId, 'draftId', 1), 427 + ], 428 + ); 429 + 430 + blocTest<ComposeBloc, ComposeState>( 431 + 'does nothing when draft not found', 432 + build: () { 433 + when(() => mockDatabase.getDraft(1)).thenAnswer((_) async => null); 434 + return composeBloc; 435 + }, 436 + act: (bloc) => bloc.add(const DraftLoaded(1)), 437 + expect: () => [], 438 + ); 439 + }); 440 + 441 + group('DraftsRequested', () { 442 + blocTest<ComposeBloc, ComposeState>( 443 + 'loads list of drafts', 444 + build: () { 445 + when( 446 + () => mockDatabase.getDrafts('did:plc:test'), 447 + ).thenAnswer((_) async => [_makeDraft(id: 1, content: 'Draft 1')]); 448 + return composeBloc; 449 + }, 450 + act: (bloc) => bloc.add(const DraftsRequested()), 451 + expect: () => [ 452 + isA<ComposeState>().having((s) => s.isLoadingDrafts, 'isLoadingDrafts', true), 453 + isA<ComposeState>() 454 + .having((s) => s.drafts.length, 'drafts.length', 1) 455 + .having((s) => s.isLoadingDrafts, 'isLoadingDrafts', false), 456 + ], 457 + ); 458 + 459 + blocTest<ComposeBloc, ComposeState>( 460 + 'handles database error gracefully', 461 + build: () { 462 + when(() => mockDatabase.getDrafts(any())).thenThrow(Exception('DB error')); 463 + return composeBloc; 464 + }, 465 + act: (bloc) => bloc.add(const DraftsRequested()), 466 + expect: () => [ 467 + isA<ComposeState>().having((s) => s.isLoadingDrafts, 'isLoadingDrafts', true), 468 + isA<ComposeState>().having((s) => s.isLoadingDrafts, 'isLoadingDrafts', false), 469 + ], 470 + ); 471 + }); 472 + 473 + group('DraftDeleted', () { 474 + blocTest<ComposeBloc, ComposeState>( 475 + 'deletes draft from DB and removes from in-memory list', 476 + build: () { 477 + when(() => mockDatabase.deleteDraft(1)).thenAnswer((_) async => 1); 478 + return composeBloc; 479 + }, 480 + seed: () => const ComposeState.ready().copyWith( 481 + drafts: [ 482 + DraftEntry( 483 + id: 1, 484 + accountDid: 'did:plc:test', 485 + content: 'Draft 1', 486 + mediaPaths: null, 487 + embedJson: null, 488 + replyUri: null, 489 + replyCid: null, 490 + rootUri: null, 491 + rootCid: null, 492 + createdAt: DateTime(2025), 493 + updatedAt: DateTime(2025), 494 + scheduledAt: null, 495 + ), 496 + DraftEntry( 497 + id: 2, 498 + accountDid: 'did:plc:test', 499 + content: 'Draft 2', 500 + mediaPaths: null, 501 + embedJson: null, 502 + replyUri: null, 503 + replyCid: null, 504 + rootUri: null, 505 + rootCid: null, 506 + createdAt: DateTime(2025), 507 + updatedAt: DateTime(2025), 508 + scheduledAt: null, 509 + ), 510 + ], 511 + ), 512 + act: (bloc) => bloc.add(const DraftDeleted(1)), 513 + expect: () => [isA<ComposeState>().having((s) => s.drafts.length, 'drafts.length', 1)], 514 + verify: (_) { 515 + verify(() => mockDatabase.deleteDraft(1)).called(1); 516 + verifyNever(() => mockDatabase.getDrafts(any())); 517 + }, 518 + ); 519 + 520 + blocTest<ComposeBloc, ComposeState>( 521 + 'handles delete error gracefully', 522 + build: () { 523 + when(() => mockDatabase.deleteDraft(any())).thenThrow(Exception('DB error')); 524 + return composeBloc; 525 + }, 526 + seed: () => const ComposeState.ready().copyWith( 527 + drafts: [ 528 + DraftEntry( 529 + id: 1, 530 + accountDid: 'did:plc:test', 531 + content: 'Draft 1', 532 + mediaPaths: null, 533 + embedJson: null, 534 + replyUri: null, 535 + replyCid: null, 536 + rootUri: null, 537 + rootCid: null, 538 + createdAt: DateTime(2025), 539 + updatedAt: DateTime(2025), 540 + scheduledAt: null, 541 + ), 542 + ], 543 + ), 544 + act: (bloc) => bloc.add(const DraftDeleted(1)), 545 + 546 + expect: () => [], 547 + ); 548 + }); 549 + 550 + group('PostSubmitted', () { 551 + blocTest<ComposeBloc, ComposeState>( 552 + 'creates post and emits success on happy path', 553 + build: () { 554 + when( 555 + () => mockRepository.createPost( 556 + text: any(named: 'text'), 557 + facets: any(named: 'facets'), 558 + embed: any(named: 'embed'), 559 + reply: any(named: 'reply'), 560 + repo: any(named: 'repo'), 561 + ), 562 + ).thenAnswer((_) async => true); 563 + return composeBloc; 564 + }, 565 + seed: () => const ComposeState.ready(text: 'Hello', graphemeCount: 5, isEmpty: false), 566 + act: (bloc) => bloc.add(const PostSubmitted()), 567 + expect: () => [ 568 + isA<ComposeState>().having((s) => s.isSubmitting, 'isSubmitting', true), 569 + isA<ComposeState>().having((s) => s.isSuccess, 'isSuccess', true), 570 + ], 571 + verify: (_) { 572 + verify( 573 + () => mockRepository.createPost( 574 + text: 'Hello', 575 + facets: any(named: 'facets'), 576 + embed: any(named: 'embed'), 577 + reply: any(named: 'reply'), 578 + repo: 'did:plc:test', 579 + ), 580 + ).called(1); 581 + }, 582 + ); 583 + 584 + blocTest<ComposeBloc, ComposeState>( 585 + 'emits error and returns to ready when createPost returns false', 586 + build: () { 587 + when( 588 + () => mockRepository.createPost( 589 + text: any(named: 'text'), 590 + facets: any(named: 'facets'), 591 + embed: any(named: 'embed'), 592 + reply: any(named: 'reply'), 593 + repo: any(named: 'repo'), 594 + ), 595 + ).thenAnswer((_) async => false); 596 + return composeBloc; 597 + }, 598 + seed: () => const ComposeState.ready(text: 'Hello', graphemeCount: 5, isEmpty: false), 599 + act: (bloc) => bloc.add(const PostSubmitted()), 600 + expect: () => [ 601 + isA<ComposeState>().having((s) => s.isSubmitting, 'isSubmitting', true), 602 + isA<ComposeState>().having((s) => s.hasError, 'hasError', true), 603 + 604 + isA<ComposeState>().having((s) => s.isReady, 'isReady', true), 605 + ], 606 + ); 607 + 608 + blocTest<ComposeBloc, ComposeState>( 609 + 'saves draft on network failure and emits error', 610 + build: () { 611 + when( 612 + () => mockRepository.createPost( 613 + text: any(named: 'text'), 614 + facets: any(named: 'facets'), 615 + embed: any(named: 'embed'), 616 + reply: any(named: 'reply'), 617 + repo: any(named: 'repo'), 618 + ), 619 + ).thenThrow(Exception('Network error')); 620 + when(() => mockDatabase.saveDraft(any())).thenAnswer((_) async => 99); 621 + return composeBloc; 622 + }, 623 + seed: () => const ComposeState.ready(text: 'Hello', graphemeCount: 5, isEmpty: false), 624 + act: (bloc) => bloc.add(const PostSubmitted()), 625 + expect: () => [ 626 + isA<ComposeState>().having((s) => s.isSubmitting, 'isSubmitting', true), 627 + isA<ComposeState>() 628 + .having((s) => s.hasError, 'hasError', true) 629 + .having((s) => s.errorMessage, 'errorMessage', contains('draft')), 630 + isA<ComposeState>().having((s) => s.isReady, 'isReady', true), 631 + ], 632 + verify: (_) { 633 + verify(() => mockDatabase.saveDraft(any())).called(1); 634 + }, 635 + ); 636 + 637 + blocTest<ComposeBloc, ComposeState>( 638 + 'does nothing when canSubmit is false', 639 + build: () => composeBloc, 640 + 641 + act: (bloc) => bloc.add(const PostSubmitted()), 642 + expect: () => [], 643 + ); 644 + 645 + blocTest<ComposeBloc, ComposeState>( 646 + 'deletes associated draft on success', 647 + build: () { 648 + when( 649 + () => mockRepository.createPost( 650 + text: any(named: 'text'), 651 + facets: any(named: 'facets'), 652 + embed: any(named: 'embed'), 653 + reply: any(named: 'reply'), 654 + repo: any(named: 'repo'), 655 + ), 656 + ).thenAnswer((_) async => true); 657 + when(() => mockDatabase.deleteDraft(7)).thenAnswer((_) async => 1); 658 + return composeBloc; 659 + }, 660 + seed: () => const ComposeState.ready(text: 'Hello', graphemeCount: 5, isEmpty: false, draftId: 7), 661 + act: (bloc) => bloc.add(const PostSubmitted()), 662 + expect: () => [ 663 + isA<ComposeState>().having((s) => s.isSubmitting, 'isSubmitting', true), 664 + isA<ComposeState>().having((s) => s.isSuccess, 'isSuccess', true), 665 + ], 666 + verify: (_) { 667 + verify(() => mockDatabase.deleteDraft(7)).called(1); 668 + }, 669 + ); 670 + }); 671 + }); 672 + }