V5 (#1222)
* chore(flutter): change BoxFit of VLC player * chore(flutter): remove unused imports * chore(flutter sample): tweak breakpoints, ignore some lints * chore(flutter): minor code readability improvements * chore(flutter): extract out platform_widget_builder to its own "package" * fix(flutter): fix a theme bug with PlatformDialog * chore(persistence): test plugin on macOS * Generated the macos runner for the example * Re-generated the android runner because it was using the v1 embedding * chore(persistence): test plugin on web * chore(persistence): add Windows runner to example * chore(persistence): add linux runner to example * feat(flutter): branded context menus * chore(flutter): fix bug where lines would be shown in between menu items * chore(flutter): import/export fixes * chore(flutter): move file * chore(flutter): /src/channel Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code. chore(flutter): fix bad directory placement chore(flutter): fix bad directory placement * chore(flutter): more directory organization * chore(flutter): navigator improvements * chore(flutter): void async improvements * chore(flutter): fix imports/exports * docs(flutter): documentation updates for /attachment * docs(flutter): doc updates For: * /avatars * /bottom_sheets * /channel * feat(flutter): selectable message text on web & desktop * Squashed commit of the following: commit 50b249edb6b2ee2d744e4e00e97f81979026aab4 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:55:05 2022 -0400 chore(flutter): wrapping up docs commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:42:09 2022 -0400 chore(flutter): more docs & typedefs commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:22:34 2022 -0400 chore(flutter): more doc fixes commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc Merge: 59188f5d 88abf99f Author: GroovinChip <[email protected]> Date: Wed Mar 16 13:17:52 2022 -0400 Merge branch 'desk_web_dev' into utils commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:48:16 2022 -0400 chore(flutter): add todo commit 9b4a986ca5757f284677eb5335637d11ccb1d642 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:43:12 2022 -0400 chore(flutter): more docs and typedefs updates commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:14:14 2022 -0400 chore(flutter): more typedef and doc template updates commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21 Author: GroovinChip <[email protected]> Date: Wed Mar 16 10:46:04 2022 -0400 chore(flutter): create /uitils and move files Fixes subsequent imports/exports * chore(flutter): update file_selector plugin * chore(flutter): include utils/typedefs.dart in library export * Squashed commit of the following: commit 501228afa614b0d740858dec1ebedd3f671e2c96 Author: GroovinChip <[email protected]> Date: Thu Mar 17 16:18:12 2022 -0400 feat(flutter): ensure fullscreen media feature works on ALL platforms commit cd0aab92bc7d90af03258829a5aae66125578af5 Author: GroovinChip <[email protected]> Date: Thu Mar 17 14:26:48 2022 -0400 feat(flutter): working conditional import for initializing VLC * fix(flutter): remove stray import causing web to break * chore(flutter): ensure context menus can show on web in sample * feat(flutter) FullScreenMedia context menus for Web * feat(flutter): branded context menus * chore(flutter): no success dialog on file download (web) * chore(flutter): remove platform UI packages * chore(flutter): properly show/hide delete message menu item Also deletes deprecated PlatformDialog * feat(flutter): 'view info' channel preview context menu * feat(flutter): more menu actions for ChannelPreview Also new ConfirmationDialog and add missing docs * feat(flutter/localizations): translations related to channel mutes * chore(flutter): remove prints and fix super.didChangeDependencies order * chore(flutter): rename some things in ChannelBottomSheet * chore(flutter): more sensible name in TypingIndicator * chore(flutter): remove unused code in MessageWidget * feat(flutter): ChannelInfoDialog * chore(flutter): organize files more Also refactors out some internal MessageListView widgets * fix(flutter): fix render overflow issue * chore(flutter): move MediaListView * chore(flutter): remove unused imports * fix(flutter): fix weird share menu placement on macos * chore(flutter): clean up fullscreenmedia conditional imports * chore(flutter): adjust package exports * chore(flutter): update dart_vlc * chore(flutter): clean up platform checks * chore(flutter): run flutter format on lib * chore(flutter): minor sample cleanup * chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments * test(flutter): reformat existing tests * fix(flutter): pass onThreadReplyTap down to ThreadReplyButton * First round of new tests commit a90bed427521123fdaa4c5e276744a224fac2b68 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:34:19 2022 -0400 test(flutter): Username test commit d7bc179c20eae2713c9248191518bf1d955104a9 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:30:47 2022 -0400 chore(flutter): remove unused imports commit 09c9517de2917dc5ccd7de019b50a7875386991c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:29:58 2022 -0400 test(flutter): ThreadSeparator test commit 9422d1129211aaaac9a51ca4d29fe94bde06593c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:19:43 2022 -0400 chore(flutter): rename LoadingIndicator parameter commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7 Author: GroovinChip <[email protected]> Date: Thu Apr 7 11:47:46 2022 -0400 test(flutter): FloatingDateDivider test commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef Author: GroovinChip <[email protected]> Date: Tue Mar 29 15:19:39 2022 -0400 test(flutter): InputAttachment tests commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:41:40 2022 -0400 test(flutter): DmCheckbox tests commit 01567f235a2f00880bb530b232489e3790159b25 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:16:08 2022 -0400 chore(flutter): remove unused imports commit c9625858e840df6082506ae9374f1d2710053cbd Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:15:44 2022 -0400 test(flutter): CountdownButton tests commit d770474bb11ee8b36a3b0e18db31695fd812a6b3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:11:37 2022 -0400 test(flutter): CommandButton tests commit 95e63706d1dd971373d39d471894a38bb2a634f2 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:07:43 2022 -0400 test(flutter): ClearInputItem tests commit 4156586945c91408cb7943b6c57c2ae560a2857c Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:03:59 2022 -0400 test(flutter): AttachmentButton tests commit bba042806326999aabf338636c6e33900de86160 Author: GroovinChip <[email protected]> Date: Tue Mar 29 13:00:42 2022 -0400 test(flutter): golden test for SendButton commit e2cd0f00b9eeb8d81399f6316edb298234de12dc Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:52 2022 -0400 chore(flutter): deal with lints commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:00 2022 -0400 test(flutter): SendButton test commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:38:21 2022 -0400 test(flutter): KeyboardShortcutRunner tests commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:17:30 2022 -0400 test(flutter): UploadProgressIndicator commit ec50c8aea716bdc19e4308e367c8331211c61ea3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:47:53 2022 -0400 test(flutter): SendingIndicator tests commit e87bd1450fb5bfb713a867c7e831968f76428848 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:32:35 2022 -0400 test(flutter): more gallery tests commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:17:29 2022 -0400 test(flutter): remove bad mock commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38 Author: GroovinChip <[email protected]> Date: Tue Mar 29 10:45:45 2022 -0400 chore(flutter): minor refactor commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:49:07 2022 -0400 test(flutter): MessageDialog tests commit 220b3ae591e135d9606254666e04d4771945d0ae Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:42:51 2022 -0400 test(flutter): DeleteMessageDialog tests commit db508478bb96749971f563de87b6c2a49e1e4e57 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:46:41 2022 -0400 test(flutter): ConfirmationDialog tests commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:35:50 2022 -0400 test(flutter): ChannelInfoDialog test commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:57:02 2022 -0400 test(flutter): DownloadMenuItem tests commit afee1dace543e2fef8543f9b035f684b2ee00f3b Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:53:21 2022 -0400 test(flutter): StreamChatContextMenuItem tests commit 4616b1a973d0cf5224b5a71914c9908339c8462a Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:02:07 2022 -0400 test(flutter): golden test for ErrorAlertSheet commit 22fb8241ae333f57ade8e8550f87695929836b2b Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:59:01 2022 -0400 test(flutter): golden test for EditMessageSheet commit 0be79964827f344ba66e99cb9e5bd3d627aed01d Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:56:08 2022 -0400 test(flutter): golden test for AttachmentModalSheet commit 0ae842a98143478885bc211c788353c8bca57b40 Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:53:26 2022 -0400 test(flutter): GroupAvatar tests commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:30:25 2022 -0400 test(flutter): UserAvatar tests commit c03ba4124068c83d3a49c4db00ca18a659801c23 Author: GroovinChip <[email protected]> Date: Mon Mar 28 11:04:55 2022 -0400 test(flutter): ErrorAlertSheet test commit 3414f28530b0e22614b83af20361f21dd4967fae Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:45:37 2022 -0400 test(flutter): EditMessageSheet test commit b2962b641dfbf14c11d3da7f80a4be27f9980d90 Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:37:18 2022 -0400 test(flutter): AttachmentModalSheet test test(flutter): AttachmentModalSheet test chore(flutter): update exports commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408 Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:52:29 2022 -0400 test(flutter): more organization commit 4479b649835acc62d8a0553f9505a0af8077abfc Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:33:06 2022 -0400 test(flutter): tweak AttachmentHandler tests commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:24:00 2022 -0400 chore(flutter): streamline attachment handling * MobileAttachmentHandler for streamlining related code for mobile * Break out DesktopAttachmentHandler into its own file * Update minSdk version in Android runner commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb Merge: a44b3b39 15c83d03 Author: GroovinChip <[email protected]> Date: Fri Mar 25 13:00:27 2022 -0400 Merge branch 'desk_web_dev' into tests commit a44b3b390adc25a00d1597bb1460973e8555fa9e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:59:14 2022 -0400 chore(flutter): start cleaning up test dir commit de104a3491d942d12f72dd66152b62c6e8b10a5b Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:56:48 2022 -0400 test(flutter): VideoAttachment test commit 4748a1c081a3272f7fdbdeb69c501344e221e01e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:52:30 2022 -0400 test(flutter): UrlAttachment test commit 54408ef70f07c60adf8350553fdf3173e5edd7c7 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:48:24 2022 -0400 test(flutter): ImageGroup test commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:46:04 2022 -0400 test(flutter): ImageAttachment test commit a72677976c6524e76793432b0520e99cac4e7a62 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:34:06 2022 -0400 test(flutter): GiphyAttachment test commit 32ddac022e2c149726b4b6607f41850d556055b8 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:28:01 2022 -0400 test(flutter): FileAttachment test commit 6534958cab775be1fb9bda56e67caa4746aa1502 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:14:25 2022 -0400 test(flutter): AttachmentError test commit 718a83c10c4e883ce01c9c002ced24de671d30b9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:06:43 2022 -0400 test(flutter): AttachmentUploadStateBuilder test commit a05108c460e36b2e425e4ec06f8bf408b0c20e97 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:44:34 2022 -0400 chore(flutter): minor cleanup commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:23 2022 -0400 test(flutter): AttachmentTitle test commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:11 2022 -0400 test(flutter): minor cleanup commit 09845cd845aa8c546043f4ae04805a8e6f53f61c Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:42:52 2022 -0400 chore(flutter): improve device segmentation & vlc init check commit 172e6b25a064b5147b7641f29ebff3c920156f70 Author: GroovinChip <[email protected]> Date: Fri Mar 25 10:37:43 2022 -0400 test(flutter): attachment_handler tests commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:27 2022 -0400 test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder commit 5a457b7557cb4127b74ee022389a24e5e9150601 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:05 2022 -0400 chore(flutter): make base platform widgets testable Also fix environment check in stream_chat.dart commit 88fb16018103c581236aeebd978e705b223b2243 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:59:19 2022 -0400 chore(flutter): add readme to conditional_parent_widget library commit e9307f8a6a5639d59358c245f033f9d9b209fe20 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:54:54 2022 -0400 test(flutter): ConditionalParentBuilder tests * chore(flutter): add thumblr dependency * chore(flutter): fix typos * chore(flutter): properly order super calls * feat(flutter) video thumbnails on desktop * chore(flutter): fix incorrect file name * chore(flutter): fix docs for generateVideoThumbnail * chore(flutter): remove incorrect comment * chore(flutter): run flutter format * chore(flutter): ignore lint * chore(flutter): flutter format * chore(flutter): fix tests * fix(flutter): fix MessageListView bug caused by rebase * chore(flutter): migrate desktop stuff to v4/stream_message_input.dart * chore(flutter): migrate over desktop changes for message replies * chore(flutter): fix message replies on desktop * chore(flutter): address lints * chore(flutter): update dart_vlc * chore(flutter): melos clean & update photo_view * fix(ui): port #1118 * fix(ui): use attachmentButtonBuilder * fix(ui): fix file attachment * format * fix(ui): fix message bottom row * chore(flutter sample): use modern window look * chore(flutter): pass userAvatarBuilder through to MessageWidgetContent Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform * chore: fix MessageWidget menu actions * fix(flutter): fix "enter" sending empty messages in StreamMessageInput * fix(flutter): fix Giphys not loading in quoted messages * chore(flutter): update thumblr * feat(flutter): keyboard navigation for gallery items * fix(ui): fix double date divider * fix(ui): add scroll to bottom back * fix(ui): use mouse region * fix(ui): remove backbutton in desktop example * fix(ui): rename full screen media widget * feat(flutter): elevation prop for gallery header * chore(flutter): update dart_vlc * fix(flutter): fix "show in chat" showing when it shouldn't * fix(flutter): fix "show in chat" for mobile * docs(flutter): minor doc update * chore(flutter): add onMessageSwiped in example app * chore(flutter): pass usernameBuilder through to BottomRow * fix(flutter): hacky fix for chonky link previews * fix(flutter): fix chonky text messages * feat(flutter): press "esc" to close gallery Also refactors KeyboardShortcutRunner API * chore(flutter): fix innaccurate name * fix(ui): fix attachment + long text sizing * add key to channelpage * fix(ui): better example * fix format * fix example * chore(flutter): revert modern window look * improvement(flutter): improve `GalleryNavigationItem` * Make buttons more visible * Spacing from the sides of the screen * fix video thumbnail * fix split view * fix(ui): fix giphy in thread * feat(flutter): desktop message reactions commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a Author: Reuben Turner <[email protected]> Date: Thu Jun 9 12:25:21 2022 -0400 fix(flutter): fixes for desktop reactions * Fix reactions for reverse messages * Ensure reactions near the edges of MessageListView show properly commit f511513f8dbb272397e81d08bad33ee60108d738 Author: Salvatore Giordano <[email protected]> Date: Thu Jun 9 16:43:03 2022 +0200 fix(ui): fix DesktopReactionBuilder and Portal commit c4885648330552f5212c5ba6b771c0f07023f6ae Merge: 2b710a2b 72b34c21 Author: Reuben Turner <[email protected]> Date: Wed Jun 8 14:12:44 2022 -0400 Merge branch 'desk_web_dev' into desktop_reactions commit 2b710a2bb610326123d5b7f75c26caa705114c9b Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:19:12 2022 -0400 chore(flutter): add debug props commit e24dabc1de12da8dce697b6d4e1a3617e9835549 Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:00:03 2022 -0400 feat(flutter): first pass at desktop reactions popup on mouse hover * fix(flutter): potential fix for #1199 * fix(flutter): fix non-reversed reactions placement * refactor(flutter): extract bottom reaction, stacked reaction with debug props * fix(flutter): fixes for desktop reactions * Fix bottom row being rendered above reaction buttons * Fix padding issues related to avatars * chore(flutter): add back lost changes from 2b1fb6a Also removed commented code * chore(flutter): stacked reactions update * Show "You" if userId matches the reaction username (per design) * Ensure long names don't throw off alignment * chore(flutter): format * fix(ui): fix mime type generation * chore(flutter): tweak desktop video * chore(flutter): add some missing things from develop * merge develop * fix merge * fix merge * format * fix tests * increate test action timeout * remove test * refactor(ui,core): remove deprecated code and cleanup * run builder * update coverage threasholds * refactor(ui): remove arrow function from countdown button * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * update comment * fix(ui): fix attachment + link solution * fix(ui): fix attachment + link solution * chore(flutter) update changelog and version * chore(localizations): update changelog and versions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * fix example * fix deps * fix(ui): fix enter sending the message while in slowmode * fix(ui): fix reaction list * fix web * fix(ui): fix image download when using assetUrl * fix(ui): fix typo in changelog * fix(ui): fix gallery navigation * fix(ui): fix giphy sending size * fix(ui): add keyboard shorcuts to gallery view * fix(ui): fix gallery footer * fix(core): allow scroll notification bubbling in lazy load scroll view * fix(ui): fix lazy load scroll view * fix(ui): fix loading to unread position * fix(ui): fix last message preview * chore(ui): update changelog * fix tests * fix(llc): fix connecting while connecting and disconneting (#1237) * fix(llc): fix connecting while connecting and disconneting * chore(llc): update changelog * chore(llc): add test * chore(llc): improve test * fix(llc): _resetRequestFlags regardless of the connection status * feat(llc): add support for AttachmentFileUploaderProvider. (#1246) * feat(llc): add support for `AttachmentFileUploaderProvider`. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Remove deprecated attachmentFileUploader field. Signed-off-by: xsahil03x <[email protected]> * port no localization * fix(ui): fix jiffy locale initialization (#1255) * fix(ui): fix jiffy locale initialization * chore(ui): update changelog * fix(ui): cleanup * chore(llc,core,ui,persistence,localizations): align changelog * chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs * fix(ui): fix date divider not showing (#1259) * fix(ui): fix date divider not showing * chore(ui): update changelog * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix separator Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * chore(ui): add contextmenu requirements for web (#1261) * fix unread separator * feat(ui): `StreamChatConfiguration` class (#1128) * feat: StreamChatConfiguration class See #1125 for more details * chore(flutter): update changelog * fix analysis * cleanup * feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget * add tests * refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration * refactor(ui): rename StreamChat.config to streamChatConfigData * refactor(ui): make enforceUniqueReactions non nullable * chore(ui): fix analysis errors * refactor(ui): make config fields non nullable * chore(ui): format Co-authored-by: Salvatore Giordano <[email protected]> * feat(ui, core): add StreamAutocomplete (#1263) * feat(ui, core): add stream_autocomplete.dart Signed-off-by: xsahil03x <[email protected]> * refactor(core, ui): refactor message input controller Signed-off-by: xsahil03x <[email protected]> * fix(ui): minor fixes Signed-off-by: xsahil03x <[email protected]> * feat(ui): add StreamAutocompleteOptions widget Signed-off-by: xsahil03x <[email protected]> * Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart * fix(ui): cleanup * chore(core, ui): cleanup Signed-off-by: xsahil03x <[email protected]> * chore(ui): ignore no empty block * chore(ui): more cleanup Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix command button for bigger screens, other fixes. Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * fix(ui): increase default file limit to 100MB (#1279) * fix(ui): increase default file limit to 100MB * Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart * fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default * feat(ui): show dialog after clicking on the camera button and permission is denied (#1262) * feat(ui): show dialog after clicking on the camera button and permission is denied * chore(ui): update changelog * Remove permission_handler plugin and refactor * cleanup * cleanup * add tests * fix(ui): reset the permission state on resume * cleanup * fix concurrent calls * fix(ui): cleanup * fix(ui): add try catch * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285) * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes * fix(llc): do not save failed messages with attachments on web * fix(llc): replace every with any * chore(llc): add test * fix analysis * fix analysis * chore(llc,core,ui,persistence): align changelog with v4 * fix(ui): do not linkify system message mentions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289) * chore(ui): update deps * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(ui): publish beta.2 * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291) * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController * chore(core,ui): update changelog * fix(ui): fix exports * chore(ui): add tests * chore(ui): add StreamMemberGridTile typedef * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * fix(ui): do not send empty message * refactor(ui,localization): removed emoji picker and connected (#1292) * refactor(ui,localization): removed emoji picker and connected * chore(ui,localizations): update changelog * chore(repo): update ui coverage threashold * doc(ui): add autocomplete_triggers guide * chore(ui): add tests for isOnlyEmoji * chore(ui): add tests for isOnlyEmoji * feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace` * fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294) Signed-off-by: xsahil03x <[email protected]> * chore(ui): flutter format Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296) * feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes. Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * test(llc): add key_stroke_handler_test.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * fix(core): only handle the channel list event if the state is success. Signed-off-by: xsahil03x <[email protected]> * feat(llc): add `hide_history` flag in `channel.addMembers`. Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Convert positional argument to optional arguments Signed-off-by: xsahil03x <[email protected]> * test(llc): update tests Signed-off-by: xsahil03x <[email protected]> * perf: improve image resizing * fix(core): use copyWith while setting controller items. Signed-off-by: xsahil03x <[email protected]> * feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails. Signed-off-by: xsahil03x <[email protected]> * fix analysis * Release v4.5.0 (#1308) * chore(llc, ui, core): bump version to v4.5.0 Signed-off-by: xsahil03x <[email protected]> * chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> * third party video integration * fix llc * refactor: clean up and add docs * chore(llc): update changelog * chore(llc): add tests * chore(llc): add more tests * fix analysis * chore(llc): add more tests * chore(llc): add more tests * fix(llc): only start listening to event once connected. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * chore(llc): move files to correct folders. Signed-off-by: xsahil03x <[email protected]> * fix(ui): Fix message text hiding because of a flutter bug (#1327) * fix(ui): remove clip * chore(ui): update changelog * cleanup * fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line. Signed-off-by: xsahil03x <[email protected]> * feat(ui): add support for additional text field params in message input. Signed-off-by: xsahil03x <[email protected]> * fix analysis * remove print * chore(llc,core,ui): update pubspec and changelogs (#1333) * fix(ui): show placeholder for web video thumbs * fix file download on web * fix reply and show buttons * remove tests * treat svg as files * fix(ui): change default image resize to clip * solve some todos * polish v5 fixing analysis issues * feat(ui): stream attachment picker bottom sheet (#1336) * refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart Signed-off-by: xsahil03x <[email protected]> * feat(ui): add copyWith in command_button.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): remove permission_manager.dart Signed-off-by: xsahil03x <[email protected]> * fix(ui): some fixes Signed-off-by: xsahil03x <[email protected]> * fix(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): fix lints, add doc comments Signed-off-by: xsahil03x <[email protected]> * chore(ci): update flutter version Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ci): update melos and flutter version Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * remove todo * update changelog * update changelogs * revert * update coverage threasholds Signed-off-by: xsahil03x <[email protected]> Co-authored-by: GroovinChip <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Gordon Hayes <[email protected]> Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
co-authored by
Sahil Kumar
GroovinChip
Gordon Hayes
Sacha Arbonel
parent
3e1d6e91c7
commit
4f2542291c
@@ -1,3 +1,35 @@
|
||||
## 5.0.0
|
||||
|
||||
- Included the changes from version [4.5.0](#450).
|
||||
|
||||
🛑️ Breaking Changes from `5.0.0-beta.2`
|
||||
|
||||
- `Channel.addMembers`, `Channel.removeMembers`, `Channel.inviteMembers` and `Channel.update`
|
||||
positional params are now optional params.
|
||||
|
||||
```dart
|
||||
// previous
|
||||
channel.addMembers([...ids], message, hideHistory);
|
||||
channel.removeMembers([...ids], message);
|
||||
channel.inviteMembers([...ids], message);
|
||||
channel.update({...channelData}, updateMessage);
|
||||
|
||||
// new
|
||||
channel.addMembers([...ids], message: ..., hideHistory: ...);
|
||||
channel.removeMembers([...ids], message: ...);
|
||||
channel.inviteMembers([...ids], message: ...);
|
||||
channel.update({...channelData}, updateMessage: ...);
|
||||
```
|
||||
|
||||
## 5.0.0-beta.2
|
||||
|
||||
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
|
||||
|
||||
## 5.0.0-beta.1
|
||||
|
||||
- Minor fixes.
|
||||
- Removed deprecated code.
|
||||
|
||||
## 4.6.0
|
||||
|
||||
✅ Added
|
||||
|
||||
@@ -4,7 +4,10 @@ import 'package:stream_chat/stream_chat.dart';
|
||||
Future<void> main() async {
|
||||
/// Create a new instance of [StreamChatClient]
|
||||
/// by passing the apikey obtained from your project dashboard.
|
||||
final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO);
|
||||
final client = StreamChatClient(
|
||||
'b67pax5b2wdq',
|
||||
logLevel: Level.INFO,
|
||||
);
|
||||
|
||||
/// Set the current user. In a production scenario, this should be done using
|
||||
/// a backend to generate a user token using our server SDK.
|
||||
|
||||
@@ -1064,9 +1064,9 @@ class Channel {
|
||||
/// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart
|
||||
/// for more information.
|
||||
Future<UpdateChannelResponse> update(
|
||||
Map<String, Object?> channelData, [
|
||||
Map<String, Object?> channelData, {
|
||||
Message? updateMessage,
|
||||
]) async {
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
return _client.updateChannel(
|
||||
id!,
|
||||
@@ -1149,12 +1149,10 @@ class Channel {
|
||||
|
||||
/// Add members to the channel.
|
||||
Future<AddMembersResponse> addMembers(
|
||||
List<String> memberIds, [
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
// TODO: Convert to optional parameters in v5.0.0
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
bool hideHistory = false,
|
||||
]) async {
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
return _client.addChannelMembers(
|
||||
id!,
|
||||
@@ -1167,18 +1165,18 @@ class Channel {
|
||||
|
||||
/// Invite members to the channel.
|
||||
Future<InviteMembersResponse> inviteMembers(
|
||||
List<String> memberIds, [
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
]) async {
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
return _client.inviteChannelMembers(id!, type, memberIds, message: message);
|
||||
}
|
||||
|
||||
/// Remove members from the channel.
|
||||
Future<RemoveMembersResponse> removeMembers(
|
||||
List<String> memberIds, [
|
||||
List<String> memberIds, {
|
||||
Message? message,
|
||||
]) async {
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
return _client.removeChannelMembers(id!, type, memberIds, message: message);
|
||||
}
|
||||
@@ -2086,10 +2084,6 @@ class ChannelClientState {
|
||||
(m) => m.user?.id == _channel.client.state.currentUser?.id,
|
||||
);
|
||||
|
||||
/// User role for the current user.
|
||||
@Deprecated('Please use currentUserChannelRole')
|
||||
String? get currentUserRole => currentUserMember?.role;
|
||||
|
||||
/// Channel role for the current user
|
||||
String? get currentUserChannelRole => currentUserMember?.channelRole;
|
||||
|
||||
|
||||
@@ -70,15 +70,9 @@ class StreamChatClient {
|
||||
Duration receiveTimeout = const Duration(seconds: 6),
|
||||
StreamChatApi? chatApi,
|
||||
WebSocket? ws,
|
||||
@Deprecated('Use [attachmentFileUploaderProvider] instead')
|
||||
AttachmentFileUploader? attachmentFileUploader,
|
||||
AttachmentFileUploaderProvider? attachmentFileUploaderProvider,
|
||||
}) : assert(
|
||||
attachmentFileUploader == null ||
|
||||
attachmentFileUploaderProvider == null,
|
||||
'You can only use one of [attachmentFileUploader] '
|
||||
'or [attachmentFileUploaderProvider]',
|
||||
) {
|
||||
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||
StreamAttachmentFileUploader.new,
|
||||
}) {
|
||||
logger.info('Initiating new StreamChatClient');
|
||||
|
||||
final options = StreamHttpClientOptions(
|
||||
@@ -88,23 +82,13 @@ class StreamChatClient {
|
||||
headers: {'X-Stream-Client': defaultUserAgent},
|
||||
);
|
||||
|
||||
// TODO: simplify this once we remove the deprecated field.
|
||||
final AttachmentFileUploaderProvider fileUploaderProvider;
|
||||
if (attachmentFileUploaderProvider != null) {
|
||||
fileUploaderProvider = attachmentFileUploaderProvider;
|
||||
} else if (attachmentFileUploader != null) {
|
||||
fileUploaderProvider = (httpClient) => attachmentFileUploader;
|
||||
} else {
|
||||
fileUploaderProvider = StreamAttachmentFileUploader.new;
|
||||
}
|
||||
|
||||
_chatApi = chatApi ??
|
||||
StreamChatApi(
|
||||
apiKey,
|
||||
options: options,
|
||||
tokenManager: _tokenManager,
|
||||
connectionIdManager: _connectionIdManager,
|
||||
attachmentFileUploaderProvider: fileUploaderProvider,
|
||||
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
|
||||
logger: detachedLogger('🕸️'),
|
||||
);
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ class KeyStrokeHandler {
|
||||
// This is needed to prevent the user from being stuck in typing mode.
|
||||
if (_lastTypingEvent != null) {
|
||||
// We don't need to handle the error here
|
||||
// ignore: no-empty-block
|
||||
_stopTyping(_currentParentId).catchError((_) {});
|
||||
}
|
||||
_cancelKeyStrokeTimer();
|
||||
|
||||
@@ -62,8 +62,6 @@ class PaginationParams extends Equatable {
|
||||
/// ```
|
||||
const PaginationParams({
|
||||
this.limit = 10,
|
||||
this.before = 10,
|
||||
this.after = 10,
|
||||
this.offset,
|
||||
this.next,
|
||||
this.idAround,
|
||||
@@ -88,14 +86,6 @@ class PaginationParams extends Equatable {
|
||||
/// The amount of items requested from the APIs.
|
||||
final int limit;
|
||||
|
||||
/// The amount of items requested before message ID from the APIs.
|
||||
@Deprecated('before is deprecated, use limit instead')
|
||||
final int before;
|
||||
|
||||
/// The amount of items requested after message ID from the APIs.
|
||||
@Deprecated('after is deprecated, use limit instead')
|
||||
final int after;
|
||||
|
||||
/// The offset of requesting items.
|
||||
final int? offset;
|
||||
|
||||
@@ -165,8 +155,6 @@ class PaginationParams extends Equatable {
|
||||
}) =>
|
||||
PaginationParams(
|
||||
limit: limit ?? this.limit,
|
||||
before: before ?? this.before,
|
||||
after: limit ?? this.after,
|
||||
offset: offset ?? this.offset,
|
||||
idAround: idAround ?? this.idAround,
|
||||
next: next ?? this.next,
|
||||
@@ -186,8 +174,6 @@ class PaginationParams extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
limit,
|
||||
before,
|
||||
after,
|
||||
offset,
|
||||
next,
|
||||
idAround,
|
||||
|
||||
@@ -21,8 +21,6 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
||||
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
||||
PaginationParams(
|
||||
limit: json['limit'] as int? ?? 10,
|
||||
before: json['before'] as int? ?? 10,
|
||||
after: json['after'] as int? ?? 10,
|
||||
offset: json['offset'] as int?,
|
||||
next: json['next'] as String?,
|
||||
idAround: json['id_around'] as String?,
|
||||
@@ -50,8 +48,6 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||
final val = <String, dynamic>{
|
||||
'limit': instance.limit,
|
||||
'before': instance.before,
|
||||
'after': instance.after,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
|
||||
@@ -23,10 +23,10 @@ class StreamChatApi {
|
||||
StreamHttpClientOptions? options,
|
||||
TokenManager? tokenManager,
|
||||
ConnectionIdManager? connectionIdManager,
|
||||
AttachmentFileUploaderProvider? attachmentFileUploaderProvider,
|
||||
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||
StreamAttachmentFileUploader.new,
|
||||
Logger? logger,
|
||||
}) : _fileUploaderProvider =
|
||||
attachmentFileUploaderProvider ?? StreamAttachmentFileUploader.new,
|
||||
}) : _fileUploaderProvider = attachmentFileUploaderProvider,
|
||||
_client = client ??
|
||||
StreamHttpClient(
|
||||
apiKey,
|
||||
|
||||
@@ -49,7 +49,7 @@ class Attachment extends Equatable {
|
||||
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
|
||||
} {
|
||||
this.uploadState = uploadState ??
|
||||
((assetUrl != null || imageUrl != null)
|
||||
((assetUrl != null || imageUrl != null || thumbUrl != null)
|
||||
? const UploadState.success()
|
||||
: const UploadState.preparing());
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ class AttachmentFile {
|
||||
final String? _name;
|
||||
|
||||
/// File name including its extension.
|
||||
String? get name => _name ?? path?.split('/').last;
|
||||
String? get name =>
|
||||
_name ?? path?.split(CurrentPlatform.isWindows ? r'\' : '/').last;
|
||||
|
||||
/// Byte data for this file. Particularly useful if you want to manipulate
|
||||
/// its data or easily upload to somewhere else.
|
||||
@@ -85,6 +86,22 @@ class AttachmentFile {
|
||||
}
|
||||
return multiPartFile;
|
||||
}
|
||||
|
||||
/// Creates a copy of this [AttachmentFile] but with the given fields
|
||||
/// replaced with the new values.
|
||||
AttachmentFile copyWith({
|
||||
String? path,
|
||||
String? name,
|
||||
Uint8List? bytes,
|
||||
int? size,
|
||||
}) {
|
||||
return AttachmentFile(
|
||||
path: path ?? this.path,
|
||||
name: name ?? this.name,
|
||||
bytes: bytes ?? this.bytes,
|
||||
size: size ?? this.size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Union class to hold various [UploadState] of a attachment.
|
||||
|
||||
@@ -39,7 +39,6 @@ class ChannelModel {
|
||||
createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now(),
|
||||
|
||||
// TODO: Make them top-level fields in v5
|
||||
// For backwards compatibility, set 'disabled', 'hidden'
|
||||
// and 'truncated_at' in [extraData].
|
||||
extraData = {
|
||||
|
||||
@@ -16,7 +16,6 @@ class Member extends Equatable {
|
||||
this.inviteAcceptedAt,
|
||||
this.inviteRejectedAt,
|
||||
this.invited = false,
|
||||
this.role,
|
||||
this.channelRole,
|
||||
this.userId,
|
||||
this.isModerator = false,
|
||||
@@ -48,10 +47,6 @@ class Member extends Equatable {
|
||||
/// True if the user has been invited to the channel
|
||||
final bool invited;
|
||||
|
||||
/// The role of the user in the channel
|
||||
@Deprecated('Please use channelRole')
|
||||
final String? role;
|
||||
|
||||
/// The role of this member in the channel
|
||||
final String? channelRole;
|
||||
|
||||
@@ -100,7 +95,6 @@ class Member extends Equatable {
|
||||
banned: banned ?? this.banned,
|
||||
banExpires: banExpires ?? this.banExpires,
|
||||
shadowBanned: shadowBanned ?? this.shadowBanned,
|
||||
role: role ?? this.role,
|
||||
channelRole: channelRole ?? this.channelRole,
|
||||
userId: userId ?? this.userId,
|
||||
isModerator: isModerator ?? this.isModerator,
|
||||
@@ -117,7 +111,6 @@ class Member extends Equatable {
|
||||
inviteAcceptedAt,
|
||||
inviteRejectedAt,
|
||||
invited,
|
||||
role,
|
||||
channelRole,
|
||||
userId,
|
||||
isModerator,
|
||||
|
||||
@@ -17,7 +17,6 @@ Member _$MemberFromJson(Map<String, dynamic> json) => Member(
|
||||
? null
|
||||
: DateTime.parse(json['invite_rejected_at'] as String),
|
||||
invited: json['invited'] as bool? ?? false,
|
||||
role: json['role'] as String?,
|
||||
channelRole: json['channel_role'] as String?,
|
||||
userId: json['user_id'] as String?,
|
||||
isModerator: json['is_moderator'] as bool? ?? false,
|
||||
@@ -39,7 +38,6 @@ Map<String, dynamic> _$MemberToJson(Member instance) => <String, dynamic>{
|
||||
'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(),
|
||||
'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(),
|
||||
'invited': instance.invited,
|
||||
'role': instance.role,
|
||||
'channel_role': instance.channelRole,
|
||||
'user_id': instance.userId,
|
||||
'is_moderator': instance.isModerator,
|
||||
|
||||
@@ -46,7 +46,6 @@ class User extends Equatable {
|
||||
this.language,
|
||||
}) : createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now(),
|
||||
// TODO: Make them top-level fields in v5
|
||||
// For backwards compatibility, set 'name', 'image' in [extraData].
|
||||
extraData = {
|
||||
...extraData,
|
||||
|
||||
@@ -4,25 +4,25 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.da
|
||||
|
||||
/// Possible platforms
|
||||
enum PlatformType {
|
||||
///
|
||||
/// Android: <https://www.android.com/>
|
||||
android,
|
||||
|
||||
///
|
||||
/// iOS: <https://www.apple.com/ios/>
|
||||
ios,
|
||||
|
||||
///
|
||||
/// web: <https://en.wikipedia.org/wiki/World_Wide_Web>
|
||||
web,
|
||||
|
||||
///
|
||||
/// macOS: <https://www.apple.com/macos>
|
||||
macOS,
|
||||
|
||||
///
|
||||
/// Windows: <https://www.windows.com>
|
||||
windows,
|
||||
|
||||
///
|
||||
/// Linux: <https://www.linux.org>
|
||||
linux,
|
||||
|
||||
///
|
||||
/// Fuchsia: <https://fuchsia.dev/fuchsia-src/concepts>
|
||||
fuchsia,
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ class CurrentPlatform {
|
||||
/// True if the app is running on fuchsia
|
||||
static bool get isFuchsia => type == PlatformType.fuchsia;
|
||||
|
||||
/// True if the app is running in test environment
|
||||
static bool get isFlutterTest => isFlutterTestEnvironment;
|
||||
|
||||
/// Returns a string version of the platform
|
||||
static String get name {
|
||||
switch (type) {
|
||||
@@ -68,8 +71,6 @@ class CurrentPlatform {
|
||||
return 'linux';
|
||||
case PlatformType.fuchsia:
|
||||
return 'fuchsia';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,8 @@ PlatformType get currentPlatform {
|
||||
if (Platform.isIOS) return PlatformType.ios;
|
||||
return PlatformType.android;
|
||||
}
|
||||
|
||||
/// True if the app is running in test environment.
|
||||
bool get isFlutterTestEnvironment {
|
||||
return Platform.environment.containsKey('FLUTTER_TEST');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Stub implementation
|
||||
PlatformType get currentPlatform {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
PlatformType get currentPlatform => throw UnimplementedError();
|
||||
|
||||
/// Stub implementation
|
||||
bool get isFlutterTestEnvironment => throw UnimplementedError();
|
||||
|
||||
@@ -2,3 +2,8 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Version running on web
|
||||
PlatformType get currentPlatform => PlatformType.web;
|
||||
|
||||
/// True if the app is running in test environment.
|
||||
///
|
||||
/// Always returns false as we don't have environment variables on web.
|
||||
bool get isFlutterTestEnvironment => false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:stream_chat/src/core/api/requests.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/core/models/event.dart';
|
||||
@@ -8,6 +9,7 @@ import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/models/reaction.dart';
|
||||
import 'package:stream_chat/src/core/models/read.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/util/extension.dart';
|
||||
|
||||
/// A simple client used for persisting chat data locally.
|
||||
@@ -133,7 +135,7 @@ abstract class ChatPersistenceClient {
|
||||
/// Remove a pinned message by message [cids]
|
||||
Future<void> deletePinnedMessageByCids(List<String> cids);
|
||||
|
||||
/// Remove a channel by [cid]
|
||||
/// Remove a channel by [channelId]
|
||||
Future<void> deleteChannels(List<String> cids);
|
||||
|
||||
/// Updates the message data of a particular channel [cid] with
|
||||
@@ -243,7 +245,15 @@ abstract class ChatPersistenceClient {
|
||||
final cid = channel.cid;
|
||||
final reads = state.read;
|
||||
final members = state.members;
|
||||
final messages = state.messages;
|
||||
final Iterable<Message>? messages;
|
||||
if (CurrentPlatform.isWeb) {
|
||||
messages = state.messages?.where((it) => !it.attachments.any(
|
||||
(attachment) =>
|
||||
attachment.uploadState != const UploadState.success(),
|
||||
));
|
||||
} else {
|
||||
messages = state.messages;
|
||||
}
|
||||
final pinnedMessages = state.pinnedMessages;
|
||||
|
||||
// Preparing deletion data
|
||||
@@ -255,7 +265,7 @@ abstract class ChatPersistenceClient {
|
||||
// preparing addition data
|
||||
channelWithReads[cid] = reads;
|
||||
channelWithMembers[cid] = members;
|
||||
channelWithMessages[cid] = messages;
|
||||
channelWithMessages[cid] = messages?.toList();
|
||||
channelWithPinnedMessages[cid] = pinnedMessages;
|
||||
|
||||
reactions.addAll(messages?.expand(_expandReactions) ?? []);
|
||||
|
||||
@@ -5,12 +5,14 @@ export 'package:dio/src/cancel_token.dart';
|
||||
export 'package:dio/src/dio_error.dart';
|
||||
export 'package:dio/src/multipart_file.dart';
|
||||
export 'package:dio/src/options.dart';
|
||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||
export 'package:logging/logging.dart' show Logger, Level, LogRecord;
|
||||
export 'package:rate_limiter/rate_limiter.dart';
|
||||
export 'package:uuid/uuid.dart';
|
||||
|
||||
export './src/core/api/attachment_file_uploader.dart';
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/responses.dart';
|
||||
export './src/core/api/stream_chat_api.dart' show PushProvider;
|
||||
export './src/core/error/error.dart';
|
||||
@@ -41,6 +43,7 @@ export 'src/client/client.dart';
|
||||
export 'src/client/key_stroke_handler.dart';
|
||||
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
|
||||
export 'src/core/api/requests.dart';
|
||||
export 'src/core/api/requests.dart';
|
||||
export 'src/core/api/responses.dart';
|
||||
export 'src/core/api/stream_chat_api.dart';
|
||||
export 'src/core/error/error.dart';
|
||||
@@ -61,6 +64,7 @@ export 'src/core/models/own_user.dart';
|
||||
export 'src/core/models/reaction.dart';
|
||||
export 'src/core/models/read.dart';
|
||||
export 'src/core/models/user.dart';
|
||||
export 'src/core/platform_detector/platform_detector.dart';
|
||||
export 'src/core/util/extension.dart';
|
||||
export 'src/db/chat_persistence_client.dart';
|
||||
export 'src/event_type.dart';
|
||||
|
||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
||||
/// Current package version
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '4.6.0';
|
||||
const PACKAGE_VERSION = '5.0.0';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||
version: 4.6.0
|
||||
version: 5.0.0
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"size": 12,
|
||||
"path": "/me/user/test.jpg",
|
||||
"name": "test.jpg"
|
||||
}
|
||||
@@ -108,7 +108,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO : test all persistence related logic in this group
|
||||
group('Initialized Channel with Persistence', () {
|
||||
late final client = MockStreamChatClientWithPersistence();
|
||||
const channelId = 'test-channel-id';
|
||||
@@ -1832,7 +1831,10 @@ void main() {
|
||||
..message = updateMessage,
|
||||
);
|
||||
|
||||
final res = await channel.update(channelData, updateMessage);
|
||||
final res = await channel.update(
|
||||
channelData,
|
||||
updateMessage: updateMessage,
|
||||
);
|
||||
|
||||
expect(res, isNotNull);
|
||||
expect(res.channel.cid, channelModel.cid);
|
||||
@@ -2028,7 +2030,7 @@ void main() {
|
||||
..message = message,
|
||||
);
|
||||
|
||||
final res = await channel.addMembers(memberIds, message);
|
||||
final res = await channel.addMembers(memberIds, message: message);
|
||||
|
||||
expect(res, isNotNull);
|
||||
expect(res.channel.cid, channelModel.cid);
|
||||
@@ -2060,7 +2062,7 @@ void main() {
|
||||
..message = message,
|
||||
);
|
||||
|
||||
final res = await channel.inviteMembers(memberIds, message);
|
||||
final res = await channel.inviteMembers(memberIds, message: message);
|
||||
|
||||
expect(res, isNotNull);
|
||||
expect(res.channel.cid, channelModel.cid);
|
||||
@@ -2093,7 +2095,7 @@ void main() {
|
||||
..message = message,
|
||||
);
|
||||
|
||||
final res = await channel.removeMembers(memberIds, message);
|
||||
final res = await channel.removeMembers(memberIds, message: message);
|
||||
|
||||
expect(res, isNotNull);
|
||||
expect(res.channel.cid, channelModel.cid);
|
||||
|
||||
@@ -68,6 +68,4 @@ void main() {
|
||||
expect(retryQueue.hasMessages, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add more tests once macbook is fixed :(
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../utils.dart';
|
||||
|
||||
void main() {
|
||||
group('src/models/attachment_file', () {
|
||||
test('should parse json correctly', () {
|
||||
final attachment =
|
||||
AttachmentFile.fromJson(jsonFixture('attachment_file.json'));
|
||||
expect(attachment.name, 'test.jpg');
|
||||
expect(attachment.size, 12);
|
||||
expect(
|
||||
attachment.path,
|
||||
'/me/user/test.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
test('should serialize to json correctly', () {
|
||||
final attachment = AttachmentFile(
|
||||
size: 12,
|
||||
bytes: Uint8List.fromList([1, 2, 3]),
|
||||
name: 'test.jpg',
|
||||
path: '/me/user/test.jpg',
|
||||
);
|
||||
|
||||
expect(
|
||||
attachment.toJson(),
|
||||
{
|
||||
'size': 12,
|
||||
'name': 'test.jpg',
|
||||
'path': '/me/user/test.jpg',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,116 @@
|
||||
## 5.0.0
|
||||
|
||||
- Included the changes from version [4.5.0](#450).
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#1326]](https://github.com/GetStream/stream-chat-flutter/issues/1326) Fixed hitting "enter" on
|
||||
the android keyboard sends the message instead of going to a new line.
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `StreamMemberGridView` and `StreamMemberListView`.
|
||||
- Added support for additional text field params in `StreamMessageInput`
|
||||
* `maxLines`
|
||||
* `minLines`
|
||||
* `textInputAction`
|
||||
* `keyboardType`
|
||||
* `textCapitalization`
|
||||
- Added `showStreamAttachmentPickerModalBottomSheet` to show the attachment picker modal bottom sheet.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- Removed Emoji picker from `StreamMessageInput`.
|
||||
|
||||
## 5.0.0-beta.2
|
||||
|
||||
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed the unread message header in the message list view.
|
||||
- Show dialog after clicking on the camera button and permission is denied.
|
||||
- Fix Jiffy initialization.
|
||||
- Fix loading to unread position in `StreamMessageListView`.
|
||||
- Minor fixes and improvements.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- [[#1125]](https://github.com/GetStream/stream-chat-flutter/issues/1125) `defaultUserImage`
|
||||
, `placeholderUserImage`, `reactionIcons`, and `enforceUniqueReactions` have been refactored out
|
||||
of `StreamChatThemeData` and into the new `StreamChatConfigurationData` class.
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `StreamAutocomplete` widget for autocomplete triggers in `StreamMessageInput`.
|
||||
- Added `StreamMessageInput.customAutocompleteTriggers` to allow users to define their custom
|
||||
triggers.
|
||||
|
||||
## 5.0.0-beta.1
|
||||
|
||||
- 🎉 Initial support for desktop 🖥️ and web 🧑💻
|
||||
- Right-click context menus for messages and full-screen attachments
|
||||
- Upload and download attachments using the native desktop file system
|
||||
- Press the "enter" key to send a message
|
||||
- If you are quoting a message and have not yet typed any text, you can press the "esc" key to
|
||||
remove the quoted message.
|
||||
- A dedicated "X" button for removing a quoted message with your mouse
|
||||
- Drag and drop attachment files to `StreamMessageInput`
|
||||
- New `StreamMessageInput.draggingBorder` property to customize the border color of the
|
||||
message input when dropping a file.
|
||||
- Message reactions bubbles
|
||||
- Hovering over a message reaction will show the users that have reacted to the message
|
||||
- Desktop attachment sharing UI
|
||||
- Selectable message text
|
||||
- Gallery navigation controls with keyboard shortcuts (left and right arrow keys)
|
||||
- Appropriate message sizing for large screens
|
||||
- Right-click context menu for `StreamMessageListView` items
|
||||
- `StreamMessageListView` items not swipeable on desktop & web
|
||||
- Video support for Windows & Linux through `dart_vlc`
|
||||
- Video support for macOS through `video_player_macos`
|
||||
- Replace bottom sheets with dialogs where appropriate
|
||||
- Other Additions ✅
|
||||
- `onQuotedMessageCleared` to `StreamMessageInput`
|
||||
- `selected` and `selectedTileColor` to `StreamChannelListTile`
|
||||
- `AttachmentUploadStateBuilder.inProgressBuilder` to `AttachmentUploadStateBuilder`
|
||||
- `AttachmentUploadStateBuilder.successBuilder` to `AttachmentUploadStateBuilder`
|
||||
- `AttachmentUploadStateBuilder.failedBuilder` to `AttachmentUploadStateBuilder`
|
||||
- Translations:
|
||||
- `couldNotReadBytesFromFileError`
|
||||
- `downloadLabel`
|
||||
- `toggleMuteUnmuteAction`
|
||||
- `toggleMuteUnmuteGroupQuestion`
|
||||
- `toggleMuteUnmuteGroupText`
|
||||
- `toggleMuteUnmuteUserQuestion`
|
||||
- `toggleMuteUnmuteUserText`
|
||||
- Deprecated `showConfirmationDialog` in favor of `showConfirmationBottomSheet`
|
||||
- Deprecated `showInfoDialog` in favor of `showInfoBottomSheet`
|
||||
- Deprecated `wrapAttachmentWidget` in favor of the `WrapAttachmentWidget` class
|
||||
- Breaking changes 🚧
|
||||
- `StreamImageAttachment.size` has been converted from type `Size` to type `BoxConstraints`
|
||||
- `StreamFileAttachment.size` has been converted from type `Size` to type `BoxConstraints`
|
||||
- `StreamGiphyAttachment.size` has been converted from type `Size` to type `BoxConstraints`
|
||||
- `StreamVideoAttachment.size` has been converted from type `Size` to type `BoxConstraints`
|
||||
- `StreamVideoThumbnailImage.width` and `StreamVideoThumbnailImage.height` have been removed in
|
||||
favor of
|
||||
`StreamVideoThumbnailImage.constraints`
|
||||
- Dependency updates ⬆️
|
||||
- `chewie: ^1.3.0` -> `chewie: ^1.3.4`
|
||||
- `path_provider: ^2.0.1` -> `path_provider: ^2.0.9`
|
||||
- `video_player: ^2.1.0` -> `video_player: ^2.4.5`
|
||||
- Code Improvements 🔧
|
||||
- Extracted many widgets to classes to improve readability, maintainability, and devtools usage.
|
||||
- Organized internal directory structure
|
||||
- Extracted typedefs to their own file
|
||||
- Updated dartdoc documentation
|
||||
- Various code readability improvements
|
||||
|
||||
## 4.6.0
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding because of a [flutter bug](https://github.com/flutter/flutter/issues/110628).
|
||||
- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding
|
||||
because of a [flutter bug](https://github.com/flutter/flutter/issues/110628).
|
||||
|
||||
## 4.5.0
|
||||
|
||||
|
||||
@@ -60,6 +60,26 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v
|
||||
To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin.
|
||||
Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements.
|
||||
|
||||
### Web
|
||||
|
||||
For the web, edit your `index.html` and add the following in the `<body>` tag in order to allow the SDK to override the right-click behaviour:
|
||||
|
||||
```html
|
||||
<body oncontextmenu="return false;">
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
For MacOS use the [file_selector](https://pub.dev/packages/file_selector#macos) package.
|
||||
Follow [these instructions](https://pub.dev/packages/file_selector#macos) to check the requirements.
|
||||
|
||||
You also need to add the following [entitlement](https://docs.flutter.dev/development/platform-integration/desktop#entitlements-and-the-app-sandbox):
|
||||
|
||||
```xml
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
It may happen that you have some problems building the app.
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@@ -40,7 +40,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.example"
|
||||
minSdkVersion 22
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="example"
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
/// Create a new instance of [StreamChatClient] passing the apikey obtained
|
||||
/// from your project dashboard.
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
logLevel: Level.OFF,
|
||||
);
|
||||
|
||||
/// Set the current user and connect the websocket. In a production
|
||||
@@ -63,50 +66,272 @@ class MyApp extends StatelessWidget {
|
||||
final Channel channel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
],
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
builder: (context, widget) => StreamChat(
|
||||
client: client,
|
||||
child: widget,
|
||||
),
|
||||
home: StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
// themeMode: ThemeMode.dark,
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
Locale('es'),
|
||||
],
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
builder: (context, widget) => StreamChat(
|
||||
client: client,
|
||||
child: widget,
|
||||
),
|
||||
home: StreamChannel(
|
||||
channel: channel,
|
||||
child: const ResponsiveChat(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of messages sent in the current channel.
|
||||
///
|
||||
/// This is implemented using [StreamMessageListView],
|
||||
/// a widget that provides query
|
||||
/// functionalities fetching the messages from the api and showing them in a
|
||||
/// listView.
|
||||
class ChannelPage extends StatelessWidget {
|
||||
/// Creates the page that shows the list of messages
|
||||
const ChannelPage({
|
||||
class ResponsiveChat extends StatelessWidget {
|
||||
const ResponsiveChat({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInformation) {
|
||||
if (sizingInformation.isMobile) {
|
||||
return ChannelListPage(
|
||||
onTap: (c) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: c,
|
||||
child: ChannelPage(
|
||||
onBackPressed: (context) {
|
||||
Navigator.of(
|
||||
context,
|
||||
rootNavigator: true,
|
||||
).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const SplitView();
|
||||
},
|
||||
breakpoints: const ScreenBreakpoints(
|
||||
desktop: 550,
|
||||
tablet: 550,
|
||||
watch: 300,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplitView extends StatefulWidget {
|
||||
const SplitView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
_SplitViewState createState() => _SplitViewState();
|
||||
}
|
||||
|
||||
class _SplitViewState extends State<SplitView> {
|
||||
Channel? selectedChannel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelListPage(
|
||||
onTap: (channel) {
|
||||
setState(() {
|
||||
selectedChannel = channel;
|
||||
});
|
||||
},
|
||||
selectedChannel: selectedChannel,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: ClipPath(
|
||||
child: Scaffold(
|
||||
body: selectedChannel != null
|
||||
? StreamChannel(
|
||||
key: ValueKey(selectedChannel!.cid),
|
||||
channel: selectedChannel!,
|
||||
child: const ChannelPage(showBackButton: false),
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
'Pick a channel to show the messages 💬',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamMessageInput(attachmentLimit: 3),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.selectedChannel,
|
||||
});
|
||||
|
||||
final void Function(Channel)? onTap;
|
||||
final Channel? selectedChannel;
|
||||
|
||||
@override
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
onChannelTap: widget.onTap,
|
||||
controller: _listController,
|
||||
itemBuilder: (context, channels, index, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
selected: channels[index] == widget.selectedChannel,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelPage extends StatefulWidget {
|
||||
const ChannelPage({
|
||||
super.key,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
});
|
||||
|
||||
final bool showBackButton;
|
||||
final void Function(BuildContext)? onBackPressed;
|
||||
|
||||
@override
|
||||
State<ChannelPage> createState() => _ChannelPageState();
|
||||
}
|
||||
|
||||
class _ChannelPageState extends State<ChannelPage> {
|
||||
late final messageInputController = StreamMessageInputController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Navigator(
|
||||
onGenerateRoute: (settings) => MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: StreamChannelHeader(
|
||||
onBackPressed: widget.onBackPressed != null
|
||||
? () {
|
||||
widget.onBackPressed!(context);
|
||||
}
|
||||
: null,
|
||||
showBackButton: widget.showBackButton,
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
onMessageSwiped:
|
||||
(CurrentPlatform.isAndroid || CurrentPlatform.isIos)
|
||||
? reply
|
||||
: null,
|
||||
threadBuilder: (context, parent) {
|
||||
return ThreadPage(
|
||||
parent: parent!,
|
||||
);
|
||||
},
|
||||
messageBuilder:
|
||||
(context, details, messages, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
onReplyTap: reply,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
StreamMessageInput(
|
||||
onQuotedMessageCleared:
|
||||
messageInputController.clearQuotedMessage,
|
||||
focusNode: focusNode,
|
||||
messageInputController: messageInputController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void reply(Message message) {
|
||||
messageInputController.quotedMessage = message;
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
const ThreadPage({
|
||||
super.key,
|
||||
required this.parent,
|
||||
});
|
||||
|
||||
final Message parent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: StreamThreadHeader(
|
||||
parent: parent,
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
parentMessage: parent,
|
||||
),
|
||||
),
|
||||
StreamMessageInput(
|
||||
messageInputController: StreamMessageInputController(
|
||||
message: Message(parentId: parent.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
@@ -29,13 +29,15 @@ class MyApp extends StatelessWidget {
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
client: client,
|
||||
child: child,
|
||||
),
|
||||
home: const SplitView(),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
client: client,
|
||||
child: child,
|
||||
),
|
||||
home: const SplitView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplitView extends StatefulWidget {
|
||||
@@ -51,37 +53,39 @@ class _SplitViewState extends State<SplitView> {
|
||||
Channel? selectedChannel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelListPage(
|
||||
onTap: (channel) {
|
||||
setState(() {
|
||||
selectedChannel = channel;
|
||||
});
|
||||
},
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelListPage(
|
||||
onTap: (channel) {
|
||||
setState(() {
|
||||
selectedChannel = channel;
|
||||
});
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Scaffold(
|
||||
body: selectedChannel != null
|
||||
? StreamChannel(
|
||||
key: ValueKey(selectedChannel!.cid),
|
||||
channel: selectedChannel!,
|
||||
child: const ChannelPage(),
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
'Pick a channel to show the messages 💬',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Scaffold(
|
||||
body: selectedChannel != null
|
||||
? StreamChannel(
|
||||
key: ValueKey(selectedChannel!.cid),
|
||||
channel: selectedChannel!,
|
||||
child: const ChannelPage(),
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
'Pick a channel to show the messages 💬',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -32,7 +31,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// and [StreamMessageInput]
|
||||
///
|
||||
/// If you now run the simulator you will see a single channel UI.
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
@@ -90,15 +89,18 @@ class ChannelPage extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -31,7 +29,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// [StreamChannelListView] handles pagination
|
||||
/// and updates automatically when new channels are created or when a new
|
||||
/// message is added to a channel.
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
@@ -100,23 +98,25 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _controller.refresh,
|
||||
child: StreamChannelListView(
|
||||
controller: _controller,
|
||||
onChannelTap: (channel) => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _controller.refresh,
|
||||
child: StreamChannelListView(
|
||||
controller: _controller,
|
||||
onChannelTap: (channel) => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -125,15 +125,17 @@ class ChannelPage extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -106,21 +104,23 @@ class ChannelPage extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
),
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -86,21 +85,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -126,21 +124,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -149,21 +149,23 @@ class ChannelPage extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
),
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
@@ -0,0 +1,116 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
set(BINARY_NAME "example")
|
||||
set(APPLICATION_ID "com.example.example")
|
||||
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Configure build options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Application build
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
@@ -0,0 +1,87 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <dart_vlc/dart_vlc_plugin.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin");
|
||||
dart_vlc_plugin_register_with_registrar(dart_vlc_registrar);
|
||||
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -0,0 +1,29 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dart_vlc
|
||||
desktop_drop
|
||||
screen_retriever
|
||||
sqlite3_flutter_libs
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "example");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "example");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
nullptr));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
@@ -10,5 +10,9 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -28,5 +28,7 @@
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>In order to access your photo library</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,12 +4,12 @@ import FlutterMacOS
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController.init()
|
||||
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
self.setFrame(windowFrame, display: true)
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,10 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
collection: ^1.15.0
|
||||
cupertino_icons: ^1.0.3
|
||||
cupertino_icons: ^1.0.4
|
||||
flutter:
|
||||
sdk: flutter
|
||||
responsive_builder: ^0.4.2
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
stream_chat_localizations:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -11,7 +11,6 @@
|
||||
Fore more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
-->
|
||||
<base href="/">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
@@ -31,7 +30,7 @@
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<body oncontextmenu="return false;">
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
flutter/ephemeral/
|
||||
|
||||
# Visual Studio user-specific files.
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Visual Studio build-related files.
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
@@ -0,0 +1,95 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(example LANGUAGES CXX)
|
||||
|
||||
set(BINARY_NAME "example")
|
||||
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Configure build options.
|
||||
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(IS_MULTICONFIG)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||
CACHE STRING "" FORCE)
|
||||
else()
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
|
||||
# Use Unicode for all projects.
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||
endfunction()
|
||||
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# Application build
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# Support files are copied into place next to the executable, so that it can
|
||||
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||
# so that building and running from within Visual Studio will work.
|
||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
# Make the "install" step default, as it's required to run.
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
CONFIGURATIONS Profile;Release
|
||||
COMPONENT Runtime)
|
||||
@@ -0,0 +1,103 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||
|
||||
# === Flutter Library ===
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"flutter_export.h"
|
||||
"flutter_windows.h"
|
||||
"flutter_messenger.h"
|
||||
"flutter_plugin_registrar.h"
|
||||
"flutter_texture_registrar.h"
|
||||
)
|
||||
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Wrapper ===
|
||||
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||
"core_implementations.cc"
|
||||
"standard_codec.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||
"plugin_registrar.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||
"flutter_engine.cc"
|
||||
"flutter_view_controller.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||
|
||||
# Wrapper sources needed for a plugin.
|
||||
add_library(flutter_wrapper_plugin STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_plugin)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||
|
||||
# Wrapper sources needed for the runner.
|
||||
add_library(flutter_wrapper_app STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_app)
|
||||
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_app PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
${PHONY_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||
windows-x64 $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
|
||||
#include <dart_vlc/dart_vlc_plugin.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_native_view/flutter_native_view_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <thumblr_windows/thumblr_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DartVlcPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DartVlcPlugin"));
|
||||
DesktopDropPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterNativeViewPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterNativeViewPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
ThumblrWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ThumblrWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus_windows
|
||||
dart_vlc
|
||||
desktop_drop
|
||||
file_selector_windows
|
||||
flutter_native_view
|
||||
screen_retriever
|
||||
sqlite3_flutter_libs
|
||||
thumblr_windows
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
@@ -0,0 +1,121 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#pragma code_page(65001)
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
#ifdef FLUTTER_BUILD_NUMBER
|
||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0
|
||||
#endif
|
||||
|
||||
#ifdef FLUTTER_BUILD_NAME
|
||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VERSION_AS_NUMBER
|
||||
PRODUCTVERSION VERSION_AS_NUMBER
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "FileDescription", "example" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "example" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "example.exe" "\0"
|
||||
VALUE "ProductName", "example" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
FlutterWindow::~FlutterWindow() {}
|
||||
|
||||
bool FlutterWindow::OnCreate() {
|
||||
if (!Win32Window::OnCreate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
// The size here must match the window dimensions to avoid unnecessary surface
|
||||
// creation / destruction in the startup path.
|
||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||
// Ensure that basic setup of the controller was successful.
|
||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlutterWindow::OnDestroy() {
|
||||
if (flutter_controller_) {
|
||||
flutter_controller_ = nullptr;
|
||||
}
|
||||
|
||||
Win32Window::OnDestroy();
|
||||
}
|
||||
|
||||
LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||
if (flutter_controller_) {
|
||||
std::optional<LRESULT> result =
|
||||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||
lparam);
|
||||
if (result) {
|
||||
return *result;
|
||||
}
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_FONTCHANGE:
|
||||
flutter_controller_->engine()->ReloadSystemFonts();
|
||||
break;
|
||||
}
|
||||
|
||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||
#define RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "win32_window.h"
|
||||
|
||||
// A window that does nothing but host a Flutter view.
|
||||
class FlutterWindow : public Win32Window {
|
||||
public:
|
||||
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||
explicit FlutterWindow(const flutter::DartProject& project);
|
||||
virtual ~FlutterWindow();
|
||||
|
||||
protected:
|
||||
// Win32Window:
|
||||
bool OnCreate() override;
|
||||
void OnDestroy() override;
|
||||
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept override;
|
||||
|
||||
private:
|
||||
// The project to run.
|
||||
flutter::DartProject project_;
|
||||
|
||||
// The Flutter instance hosted by this window.
|
||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||
@@ -0,0 +1,43 @@
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
// Initialize COM, so that it is available for use in the library and/or
|
||||
// plugins.
|
||||
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
flutter::DartProject project(L"data");
|
||||
|
||||
std::vector<std::string> command_line_arguments =
|
||||
GetCommandLineArguments();
|
||||
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.CreateAndShow(L"example", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by Runner.rc
|
||||
//
|
||||
#define IDI_APP_ICON 101
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void CreateAndAttachConsole() {
|
||||
if (::AllocConsole()) {
|
||||
FILE *unused;
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||
_dup2(_fileno(stdout), 1);
|
||||
}
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||
_dup2(_fileno(stdout), 2);
|
||||
}
|
||||
std::ios::sync_with_stdio();
|
||||
FlutterDesktopResyncOutputStreams();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> GetCommandLineArguments() {
|
||||
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||
int argc;
|
||||
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||
if (argv == nullptr) {
|
||||
return std::vector<std::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> command_line_arguments;
|
||||
|
||||
// Skip the first argument as it's the binary name.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||
}
|
||||
|
||||
::LocalFree(argv);
|
||||
|
||||
return command_line_arguments;
|
||||
}
|
||||
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||
if (utf16_string == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
int target_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
-1, nullptr, 0, nullptr, nullptr);
|
||||
if (target_length == 0) {
|
||||
return std::string();
|
||||
}
|
||||
std::string utf8_string;
|
||||
utf8_string.resize(target_length);
|
||||
int converted_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
-1, utf8_string.data(),
|
||||
target_length, nullptr, nullptr);
|
||||
if (converted_length == 0) {
|
||||
return std::string();
|
||||
}
|
||||
return utf8_string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef RUNNER_UTILS_H_
|
||||
#define RUNNER_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Creates a console for the process, and redirects stdout and stderr to
|
||||
// it for both the runner and the Flutter library.
|
||||
void CreateAndAttachConsole();
|
||||
|
||||
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||
|
||||
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
||||
@@ -0,0 +1,245 @@
|
||||
#include "win32_window.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::CreateAndShow(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates and shows a win32 window with |title| and position and size using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size to will treat the width height passed in to this function
|
||||
// as logical pixels and scale to appropriate for the default monitor. Returns
|
||||
// true if the window was created successfully.
|
||||
bool CreateAndShow(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size);
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responsponds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
@@ -0,0 +1,25 @@
|
||||
# conditional_parent_builder
|
||||
|
||||
A widget that allows developers to conditionally wrap a child widget with a parent widget.
|
||||
This is useful in situations where a child widget should always be built, but should be wrapped
|
||||
by another widget if certain conditions are met.
|
||||
|
||||
In the following real-world example, we conditionally wrap the child widget
|
||||
tree with a context menu if the message is not deleted:
|
||||
```dart
|
||||
ConditionalParentBuilder(
|
||||
builder: (context, child) {
|
||||
if (!widget.message.isDeleted) {
|
||||
return ContextMenuArea(
|
||||
builder: (context) => buildContextMenu(),
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
},
|
||||
child: Material(...),
|
||||
),
|
||||
```
|
||||
This example can be found in the `stream_chat_flutter` source code under
|
||||
`src/message_widget/message_widget.dart`.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
library conditional_parent_builder;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@template parentBuilder}
|
||||
/// A function that provides the [BuildContext] and the [child] widget.
|
||||
/// {@endtemplate}
|
||||
typedef ParentBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
);
|
||||
|
||||
/// {@template conditionalParentBuilder}
|
||||
/// A widget that allows developers to conditionally wrap the [child] widget
|
||||
/// with a parent widget.
|
||||
///
|
||||
/// In the following real-world example, we conditionally wrap the child widget
|
||||
/// tree with a context menu if the message is not deleted:
|
||||
/// ```dart
|
||||
/// ConditionalParentBuilder(
|
||||
/// builder: (context, child) {
|
||||
/// if (!widget.message.isDeleted) {
|
||||
/// return ContextMenuArea(
|
||||
/// builder: (context) => buildContextMenu(),
|
||||
/// child: child,
|
||||
/// );
|
||||
/// } else {
|
||||
/// return child;
|
||||
/// }
|
||||
/// },
|
||||
/// child: Material(...),
|
||||
/// ),
|
||||
/// ```
|
||||
/// This example can be found in the `stream_chat_flutter` source code under
|
||||
/// `src/message_widget/message_widget.dart`.
|
||||
/// {@endtemplate}
|
||||
class ConditionalParentBuilder extends StatelessWidget {
|
||||
/// {@macro conditionalParentBuilder}
|
||||
const ConditionalParentBuilder({
|
||||
super.key,
|
||||
required this.builder,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// {@macro parentBuilder}
|
||||
final ParentBuilder builder;
|
||||
|
||||
/// The child widget to build.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return builder.call(context, child);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# platform_widget_builder
|
||||
|
||||
This package is a more specialized version of the [flutter_platform_widgets](https://pub.dev/packages/flutter_platform_widgets) package.
|
||||
It provides two specialized widget builders:
|
||||
* `PlatformWidgetBuilder`
|
||||
* `DesktopWidgetBuilder`
|
||||
|
||||
### `PlatformWidgetBuilder`
|
||||
This widget differs from the `PlatformWidgetBuilder` found in the `flutter_platform_widgets` package in that it
|
||||
provides three builders:
|
||||
* `mobile`
|
||||
* `desktop`
|
||||
* `web`
|
||||
|
||||
This allows developers to build different widgets for their generalized platform targets
|
||||
(Android + iOS = mobile, macOS + Windows + Linux = desktop). This is advantageous when requirements call for one set
|
||||
of widgets that are identical across mobile, a different set of widgets that are identical across desktop, and another
|
||||
that are required for web.
|
||||
|
||||
### `DesktopWidgetBuilder`
|
||||
This widget is more specialized than `PlatformWidgetBuilder` in that it allows for more targeted widget-building for
|
||||
desktop platforms. It provides three builders:
|
||||
* `macOS`
|
||||
* `windows`
|
||||
* `linux`
|
||||
|
||||
This allows developers to build different widgets for the various desktop platforms. This is advantageous when building
|
||||
native-looking desktop applications using the `macos_ui`, `fluent_ui`, and `yaru_widgets` packages.
|
||||
@@ -0,0 +1,4 @@
|
||||
library platform_widget_builder;
|
||||
|
||||
export 'src/desktop_widget_builder.dart';
|
||||
export 'src/platform_widget_builder.dart';
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget_base.dart';
|
||||
|
||||
/// A widget that will only be built for the specified desktop Platforms.
|
||||
///
|
||||
/// See [DesktopWidgetBuilder] and [DesktopWidgetBase] for more.
|
||||
///
|
||||
/// Also see: [PlatformWidget] and [PlatformWidgetBase].
|
||||
class DesktopWidget extends DesktopWidgetBase<Widget, Widget, Widget> {
|
||||
/// Builds a [DesktopWidget].
|
||||
const DesktopWidget({
|
||||
super.key,
|
||||
this.macOS,
|
||||
this.windows,
|
||||
this.linux,
|
||||
});
|
||||
|
||||
/// The widget to build for macOS.
|
||||
final PlatformBuilder<Widget?>? macOS;
|
||||
|
||||
/// The widget to build for Windows.
|
||||
final PlatformBuilder<Widget?>? windows;
|
||||
|
||||
/// The widget to build for Linux.
|
||||
final PlatformBuilder<Widget?>? linux;
|
||||
|
||||
@override
|
||||
Widget createMacosWidget(BuildContext context) =>
|
||||
macOS?.call(context) ?? const SizedBox.shrink();
|
||||
|
||||
@override
|
||||
Widget createWindowsWidget(BuildContext context) =>
|
||||
windows?.call(context) ?? const SizedBox.shrink();
|
||||
|
||||
@override
|
||||
Widget createLinuxWidget(BuildContext context) =>
|
||||
linux?.call(context) ?? const SizedBox.shrink();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show Theme;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A generic widget builder function.
|
||||
typedef PlatformBuilder<T> = T Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// An abstract class used as a building block for creating
|
||||
/// [DesktopPlatformWidget]s.
|
||||
///
|
||||
/// This class is similar to [PlatformWidgetBase], which broadly covers
|
||||
/// the platform categories; it combines the desktop platforms into a single
|
||||
/// "desktop" target, for the purpose of returning the same widget one time
|
||||
/// for all platforms in that category. [DesktopWidgetBase] differs in that
|
||||
/// it more specifically targets each platform in the "desktop" category.
|
||||
///
|
||||
/// This class utilizes generics to define the types of widgets it expects to
|
||||
/// build:
|
||||
/// * M = macOS
|
||||
/// * W = Windows
|
||||
/// * L = Linux
|
||||
abstract class DesktopWidgetBase<M extends Widget, W extends Widget,
|
||||
L extends Widget> extends StatelessWidget {
|
||||
/// Builds a [DesktopWidgetBase].
|
||||
const DesktopWidgetBase({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final platform = Theme.of(context).platform;
|
||||
if (platform == TargetPlatform.macOS) {
|
||||
return createMacosWidget(context);
|
||||
} else if (platform == TargetPlatform.windows) {
|
||||
return createWindowsWidget(context);
|
||||
} else if (platform == TargetPlatform.linux) {
|
||||
return createLinuxWidget(context);
|
||||
}
|
||||
|
||||
return throw UnsupportedError(
|
||||
'This platform is not supported: $defaultTargetPlatform',
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a `M` macOS widget.
|
||||
M createMacosWidget(BuildContext context);
|
||||
|
||||
/// Builds a `W` Windows widget.
|
||||
W createWindowsWidget(BuildContext context);
|
||||
|
||||
/// Builds a `L` Linux widget.
|
||||
L createLinuxWidget(BuildContext context);
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget.dart';
|
||||
|
||||
/// A widget-building function that includes the child widget.
|
||||
typedef DesktopTargetBuilder = Widget? Function(
|
||||
BuildContext context,
|
||||
Widget? child,
|
||||
)?;
|
||||
|
||||
/// A widget that utilizes [DesktopWidgetBuilder]s to build different widgets
|
||||
/// for each specified desktop platform.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```
|
||||
/// DesktopWidgetBuilder(
|
||||
/// macOS: (context, child) => MacosWidget(),
|
||||
/// windows: (context, child) => WindowsWidget(),
|
||||
/// linux: (context, child) => LinuxWidget(),
|
||||
/// ),
|
||||
/// ```
|
||||
class DesktopWidgetBuilder extends StatelessWidget {
|
||||
/// Builds a [DesktopWidgetBuilder].
|
||||
const DesktopWidgetBuilder({
|
||||
super.key,
|
||||
this.child,
|
||||
this.macOS,
|
||||
this.windows,
|
||||
this.linux,
|
||||
});
|
||||
|
||||
/// The child widget.
|
||||
final Widget? child;
|
||||
|
||||
/// The widget to build for macOS.
|
||||
final DesktopTargetBuilder? macOS;
|
||||
|
||||
/// The widget to build for windows.
|
||||
final DesktopTargetBuilder? windows;
|
||||
|
||||
/// The widget to build for linux.
|
||||
final DesktopTargetBuilder? linux;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopWidget(
|
||||
macOS: (context) => macOS?.call(context, child),
|
||||
windows: (context) => windows?.call(context, child),
|
||||
linux: (context) => linux?.call(context, child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_base.dart';
|
||||
|
||||
/// A widget that will only be built for the specific Platforms:
|
||||
///
|
||||
/// See [PlatformWidgetBuilder] and [PlatformWidgetBase] for more.
|
||||
///
|
||||
/// Also see: [DesktopWidget] and [DesktopWidgetBase].
|
||||
class PlatformWidget extends PlatformWidgetBase<Widget, Widget, Widget> {
|
||||
/// Builds a [PlatformWidget].
|
||||
const PlatformWidget({
|
||||
super.key,
|
||||
this.desktop,
|
||||
this.mobile,
|
||||
this.web,
|
||||
});
|
||||
|
||||
/// The mobile widget to build.
|
||||
final PlatformBuilder<Widget?>? mobile;
|
||||
|
||||
/// The desktop widget to build.
|
||||
final PlatformBuilder<Widget?>? desktop;
|
||||
|
||||
/// The web widget to build.
|
||||
final PlatformBuilder<Widget?>? web;
|
||||
|
||||
@override
|
||||
Widget createDesktopWidget(BuildContext context) =>
|
||||
desktop?.call(context) ?? const SizedBox.shrink();
|
||||
|
||||
@override
|
||||
Widget createMobileWidget(BuildContext context) =>
|
||||
mobile?.call(context) ?? const SizedBox.shrink();
|
||||
|
||||
@override
|
||||
Widget createWebWidget(BuildContext context) =>
|
||||
web?.call(context) ?? const SizedBox.shrink();
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart' show Theme;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A generic widget builder function.
|
||||
typedef PlatformBuilder<T> = T Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// An abstract class used as a building block for creating [PlatformWidget]s.
|
||||
///
|
||||
/// This class broadly covers the platforms by combining Android and iOS
|
||||
/// together as a "mobile" category, macOS, Windows, and Linux together as a
|
||||
/// "desktop" category. This is useful is cases where a widget is expected to
|
||||
/// be the same for the various mobile and desktop categories, and would
|
||||
/// therefore be tedious to return the same widget more than once for the
|
||||
/// specified category. This is unlike [DesktopWidgetBase], which more
|
||||
/// specifically targets the various platforms in the "desktop" category.
|
||||
///
|
||||
/// This class utilizes generics to define the types of widgets it expects to
|
||||
/// build:
|
||||
/// * M = Mobile
|
||||
/// * D = Desktop
|
||||
/// * W = Web
|
||||
abstract class PlatformWidgetBase<M extends Widget, D extends Widget,
|
||||
W extends Widget> extends StatelessWidget {
|
||||
/// Builds a [PlatformWidgetBase].
|
||||
const PlatformWidgetBase({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final platform = Theme.of(context).platform;
|
||||
if (platform == TargetPlatform.android || platform == TargetPlatform.iOS) {
|
||||
return createMobileWidget(context);
|
||||
} else if (platform == TargetPlatform.macOS ||
|
||||
platform == TargetPlatform.windows ||
|
||||
platform == TargetPlatform.linux) {
|
||||
return createDesktopWidget(context);
|
||||
} else {
|
||||
return createWebWidget(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `M` mobile widget.
|
||||
M createMobileWidget(BuildContext context);
|
||||
|
||||
/// Builds a `D` desktop widget.
|
||||
D createDesktopWidget(BuildContext context);
|
||||
|
||||
/// Builds a `W` web widget.
|
||||
W createWebWidget(BuildContext context);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
|
||||
|
||||
/// A widget-building function that includes the child widget.
|
||||
typedef PlatformTargetBuilder = Widget? Function(
|
||||
BuildContext context,
|
||||
Widget? child,
|
||||
)?;
|
||||
|
||||
/// A widget that utilizes [PlatformTargetBuilder]s to build different widgets
|
||||
/// for each specified platform.
|
||||
///
|
||||
/// In the following real-world example, only the `mobile` builder is used
|
||||
/// to ensure the child widget is only built for Android and iOS:
|
||||
/// ```
|
||||
/// PlatformWidgetBuilder(
|
||||
/// mobile: (context, child) => _buildFilePickerSection(),
|
||||
/// ),
|
||||
/// ```
|
||||
class PlatformWidgetBuilder extends StatelessWidget {
|
||||
/// Builds a [PlatformWidgetBuilder].
|
||||
const PlatformWidgetBuilder({
|
||||
super.key,
|
||||
this.child,
|
||||
this.mobile,
|
||||
this.desktop,
|
||||
this.web,
|
||||
});
|
||||
|
||||
/// The child widget.
|
||||
final Widget? child;
|
||||
|
||||
/// The widget to build for mobile platforms.
|
||||
final PlatformTargetBuilder? mobile;
|
||||
|
||||
/// The widget to build for desktop platforms.
|
||||
final PlatformTargetBuilder? desktop;
|
||||
|
||||
/// The widget to build for web platforms.
|
||||
final PlatformTargetBuilder? web;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PlatformWidget(
|
||||
desktop: (context) => desktop?.call(context, child),
|
||||
mobile: (context) => mobile?.call(context, child),
|
||||
web: (context) => web?.call(context, child),
|
||||
);
|
||||
}
|
||||
}
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scrollable_positioned_list.dart';
|
||||
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
|
||||
|
||||
/// Internal implementation of [ItemPositionsListener].
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/element_registry.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart';
|
||||
|
||||
/// {@template custom_scroll_view}
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ import 'dart:math';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
|
||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export 'attachment_error.dart';
|
||||
export 'attachment_error.dart';
|
||||
export 'attachment_upload_state_builder.dart';
|
||||
export 'attachment_widget.dart' show AttachmentError, AttachmentSource;
|
||||
export 'attachment_widget.dart' show AttachmentSource;
|
||||
export 'file_attachment.dart';
|
||||
export 'giphy_attachment.dart';
|
||||
export 'image_attachment.dart';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template attachmentError}
|
||||
/// Widget for building in case of error
|
||||
/// {@endtemplate}
|
||||
class AttachmentError extends StatelessWidget {
|
||||
/// {@macro attachmentError}
|
||||
const AttachmentError({
|
||||
super.key,
|
||||
this.constraints,
|
||||
});
|
||||
|
||||
/// constraints of error
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: constraints ?? const BoxConstraints.expand(),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.accentError.withOpacity(0.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro attachment_title}
|
||||
@Deprecated("Use 'StreamAttachmentTitle' instead")
|
||||
typedef AttachmentTitle = StreamAttachmentTitle;
|
||||
|
||||
/// {@template attachment_title}
|
||||
/// {@template attachmentTitle}
|
||||
/// Title for attachments
|
||||
/// {@endtemplate}
|
||||
class StreamAttachmentTitle extends StatelessWidget {
|
||||
/// Supply attachment and theme for constructing title
|
||||
/// {@macro attachmentTitle}
|
||||
const StreamAttachmentTitle({
|
||||
super.key,
|
||||
required this.attachment,
|
||||
|
||||
+58
-61
@@ -1,50 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget to build in progress
|
||||
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||
|
||||
/// Widget to build on failure
|
||||
typedef FailedBuilder = Widget Function(BuildContext, String);
|
||||
|
||||
/// {@macro attachment_upload_state_builder}
|
||||
@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead")
|
||||
typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder;
|
||||
|
||||
/// {@template attachment_upload_state_builder}
|
||||
/// {@template streamAttachmentUploadStateBuilder}
|
||||
/// Widget to display attachment upload state
|
||||
/// {@endtemplate}
|
||||
class StreamAttachmentUploadStateBuilder extends StatelessWidget {
|
||||
/// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget
|
||||
/// {@macro streamAttachmentUploadStateBuilder}
|
||||
const StreamAttachmentUploadStateBuilder({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.attachment,
|
||||
this.failedBuilder,
|
||||
this.successBuilder,
|
||||
this.inProgressBuilder,
|
||||
this.preparingBuilder,
|
||||
this.inProgressBuilder,
|
||||
this.successBuilder,
|
||||
this.failedBuilder,
|
||||
});
|
||||
|
||||
/// Message which attachment is added to
|
||||
/// The message that [attachment] is associated with
|
||||
final Message message;
|
||||
|
||||
/// Attachment in concern
|
||||
/// The attachment currently being handled
|
||||
final Attachment attachment;
|
||||
|
||||
/// Widget to display when failed
|
||||
final FailedBuilder? failedBuilder;
|
||||
/// Widget to display when preparing to upload the [attachment]
|
||||
final PreparingBuilder? preparingBuilder;
|
||||
|
||||
/// Widget to display when succeeded
|
||||
final WidgetBuilder? successBuilder;
|
||||
|
||||
/// Widget to display when in progress
|
||||
/// {@macro inProgressBuilder}
|
||||
final InProgressBuilder? inProgressBuilder;
|
||||
|
||||
/// Widget to display when in prep
|
||||
final WidgetBuilder? preparingBuilder;
|
||||
/// {@macro successBuilder}
|
||||
final SuccessBuilder? successBuilder;
|
||||
|
||||
/// {@macro failedBuilder}
|
||||
final FailedBuilder? failedBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -56,18 +45,22 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget {
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
final inProgress = inProgressBuilder ??
|
||||
(context, int sent, int total) => _InProgressState(
|
||||
sent: sent,
|
||||
total: total,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
(context, int sent, int total) {
|
||||
return _InProgressState(
|
||||
sent: sent,
|
||||
total: total,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
final failed = failedBuilder ??
|
||||
(context, error) => _FailedState(
|
||||
error: error,
|
||||
messageId: messageId,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
(context, error) {
|
||||
return _FailedState(
|
||||
error: error,
|
||||
messageId: messageId,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
final success = successBuilder ?? (context) => _SuccessState();
|
||||
|
||||
@@ -93,22 +86,24 @@ class _IconButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: icon,
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
);
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreparingState extends StatelessWidget {
|
||||
@@ -237,14 +232,16 @@ class _FailedState extends StatelessWidget {
|
||||
|
||||
class _SuccessState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
maxRadius: 12,
|
||||
child: StreamSvgIcon.check(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
maxRadius: 12,
|
||||
child: StreamSvgIcon.check(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,31 +24,28 @@ enum AttachmentSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro attachment_widget}
|
||||
@Deprecated("Use 'StreamAttachmentWidget' instead")
|
||||
typedef AttachmentWidget = StreamAttachmentWidget;
|
||||
|
||||
/// {@template attachment_widget}
|
||||
/// {@template streamAttachmentWidget}
|
||||
/// Abstract class for deriving attachment types
|
||||
/// {@endtemplate}
|
||||
abstract class StreamAttachmentWidget extends StatelessWidget {
|
||||
/// Constructor for creating attachment widget
|
||||
/// {@macro streamAttachmentWidget}
|
||||
const StreamAttachmentWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.attachment,
|
||||
this.size,
|
||||
this.constraints,
|
||||
AttachmentSource? source,
|
||||
}) : _source = source;
|
||||
|
||||
/// Size of attachments
|
||||
final Size? size;
|
||||
/// Contraints of attachments
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
final AttachmentSource? _source;
|
||||
|
||||
/// Message which attachment is attached to
|
||||
/// The message that [attachment] is associated with
|
||||
final Message message;
|
||||
|
||||
/// Attachment to display
|
||||
/// The [Attachment] to display
|
||||
final Attachment attachment;
|
||||
|
||||
/// Getter for source of attachment
|
||||
@@ -58,33 +55,3 @@ abstract class StreamAttachmentWidget extends StatelessWidget {
|
||||
? AttachmentSource.local
|
||||
: AttachmentSource.network);
|
||||
}
|
||||
|
||||
/// Widget for building in case of error
|
||||
class AttachmentError extends StatelessWidget {
|
||||
/// Constructor for creating AttachmentError
|
||||
const AttachmentError({
|
||||
super.key,
|
||||
this.size,
|
||||
});
|
||||
|
||||
/// Size of error
|
||||
final Size? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentError
|
||||
.withOpacity(0.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,46 +2,45 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart';
|
||||
import 'package:stream_chat_flutter/src/indicators/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// {@macro file_attachment}
|
||||
@Deprecated("Use 'StreamFileAttachment' instead")
|
||||
typedef FileAttachment = StreamFileAttachment;
|
||||
|
||||
/// {@template file_attachment}
|
||||
/// Widget for displaying file attachments
|
||||
/// {@template streamFileAttachment}
|
||||
/// Displays file attachments that have been sent in a chat.
|
||||
///
|
||||
/// Used in [MessageWidget].
|
||||
/// {@endtemplate}
|
||||
class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a widget when attachment is of type 'file'
|
||||
/// {@macro streamFileAttachment}
|
||||
const StreamFileAttachment({
|
||||
super.key,
|
||||
required super.message,
|
||||
required super.attachment,
|
||||
super.size,
|
||||
super.constraints,
|
||||
this.title,
|
||||
this.trailing,
|
||||
this.onAttachmentTap,
|
||||
});
|
||||
|
||||
/// Title for attachment
|
||||
/// Title for the attachment
|
||||
final Widget? title;
|
||||
|
||||
/// Widget for displaying at the end of attachment (such as a download button)
|
||||
/// Widget for displaying at the end of the attachment
|
||||
/// (such as a download button)
|
||||
final Widget? trailing;
|
||||
|
||||
/// Callback called when attachment widget is tapped
|
||||
final VoidCallback? onAttachmentTap;
|
||||
/// {@macro onAttachmentTap}
|
||||
final OnAttachmentTap? onAttachmentTap;
|
||||
|
||||
/// Check if attachment is a video
|
||||
/// Checks if the attachment is a video
|
||||
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
|
||||
|
||||
/// Check if attachment is an image
|
||||
/// Checks if the attachment is an image
|
||||
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
|
||||
|
||||
@override
|
||||
@@ -51,7 +50,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
child: GestureDetector(
|
||||
onTap: onAttachmentTap,
|
||||
child: Container(
|
||||
width: size?.width ?? 100,
|
||||
constraints: constraints ?? const BoxConstraints.tightFor(width: 100),
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: colorTheme.barsBg,
|
||||
@@ -67,7 +66,12 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
height: 40,
|
||||
width: 33.33,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: _getFileTypeImage(context),
|
||||
child: _FileTypeImage(
|
||||
isImageAttachment: isImageAttachment,
|
||||
isVideoAttachment: isVideoAttachment,
|
||||
source: source,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@@ -82,27 +86,51 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
_buildSubtitle(context),
|
||||
_FileAttachmentSubtitle(attachment: attachment),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildTrailing(context),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: trailing ??
|
||||
_Trailing(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
||||
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
class _FileTypeImage extends StatelessWidget {
|
||||
const _FileTypeImage({
|
||||
required this.isImageAttachment,
|
||||
required this.isVideoAttachment,
|
||||
required this.source,
|
||||
required this.attachment,
|
||||
});
|
||||
|
||||
// TODO: Improve image memory. This is using the full image instead of a
|
||||
// smaller version (thumbnail)
|
||||
Widget _getFileTypeImage(BuildContext context) {
|
||||
final bool isImageAttachment;
|
||||
final bool isVideoAttachment;
|
||||
final AttachmentSource source;
|
||||
final Attachment attachment;
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||
return RoundedRectangleBorder(
|
||||
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Improve image memory.
|
||||
// This is using the full image instead of a smaller version (thumbnail)
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isImageAttachment) {
|
||||
return Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
@@ -111,13 +139,16 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
child: source.when(
|
||||
local: () {
|
||||
if (attachment.file?.bytes == null) {
|
||||
return getFileTypeImage(attachment.extraData['other'] as String?);
|
||||
return getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
);
|
||||
}
|
||||
return Image.memory(
|
||||
attachment.file!.bytes!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) =>
|
||||
getFileTypeImage(attachment.extraData['other'] as String?),
|
||||
errorBuilder: (_, obj, trace) => getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
),
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
@@ -125,15 +156,18 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl) ==
|
||||
null) {
|
||||
return getFileTypeImage(attachment.extraData['other'] as String?);
|
||||
return getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) =>
|
||||
getFileTypeImage(attachment.extraData['other'] as String?),
|
||||
errorWidget: (_, obj, trace) => getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
),
|
||||
placeholder: (_, __) {
|
||||
final image = Image.asset(
|
||||
'images/placeholder.png',
|
||||
@@ -187,38 +221,47 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
}
|
||||
return getFileTypeImage(attachment.extraData['mime_type'] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildButton({
|
||||
Widget? icon,
|
||||
double iconSize = 24.0,
|
||||
VoidCallback? onPressed,
|
||||
Color? fillColor,
|
||||
}) =>
|
||||
SizedBox(
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: fillColor,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
class _Trailing extends StatelessWidget {
|
||||
const _Trailing({
|
||||
required this.attachment,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
final Attachment attachment;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final attachmentId = attachment.id;
|
||||
var trailingWidget = trailing;
|
||||
trailingWidget ??= attachment.uploadState.when(
|
||||
|
||||
if (message.status == MessageSendingStatus.sent) {
|
||||
return IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: theme.colorTheme.textHighEmphasis,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () async {
|
||||
final assetUrl = attachment.assetUrl;
|
||||
if (assetUrl != null) {
|
||||
if (isMobileDeviceOrWeb) {
|
||||
launchURL(context, assetUrl);
|
||||
} else {
|
||||
StreamAttachmentHandler.instance.downloadAttachment(attachment);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return attachment.uploadState.when(
|
||||
preparing: () => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _buildButton(
|
||||
child: _TrailingButton(
|
||||
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
@@ -226,7 +269,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
),
|
||||
inProgress: (_, __) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _buildButton(
|
||||
child: _TrailingButton(
|
||||
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
@@ -242,7 +285,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
),
|
||||
failed: (_) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _buildButton(
|
||||
child: _TrailingButton(
|
||||
icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.retryAttachmentUpload(
|
||||
@@ -252,28 +295,48 @@ class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.status == MessageSendingStatus.sent) {
|
||||
trailingWidget = IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: theme.colorTheme.textHighEmphasis,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
final assetUrl = attachment.assetUrl;
|
||||
if (assetUrl != null) launchURL(context, assetUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
class _TrailingButton extends StatelessWidget {
|
||||
const _TrailingButton({
|
||||
this.onPressed,
|
||||
this.fillColor,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: trailingWidget,
|
||||
final VoidCallback? onPressed;
|
||||
final Color? fillColor;
|
||||
final Widget? icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: fillColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(BuildContext context) {
|
||||
class _FileAttachmentSubtitle extends StatelessWidget {
|
||||
const _FileAttachmentSubtitle({
|
||||
required this.attachment,
|
||||
});
|
||||
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final size = attachment.file?.size ?? attachment.extraData['file_size'];
|
||||
final textStyle = theme.textTheme.footnote.copyWith(
|
||||
|
||||
@@ -2,36 +2,32 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro giphy_attachment}
|
||||
@Deprecated("Use 'StreamGiphyAttachment' instead")
|
||||
typedef GiphyAttachment = StreamGiphyAttachment;
|
||||
|
||||
/// {@template giphy_attachment}
|
||||
/// Widget for showing a GIF attachment
|
||||
/// {@template streamGiphyAttachment}
|
||||
/// Shows a GIF attachment in a [StreamMessageWidget].
|
||||
/// {@endtemplate}
|
||||
class StreamGiphyAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a [StreamGiphyAttachment] widget
|
||||
/// {@macro streamGiphyAttachment}
|
||||
const StreamGiphyAttachment({
|
||||
super.key,
|
||||
required super.message,
|
||||
required super.attachment,
|
||||
super.size,
|
||||
super.constraints,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
this.onReplyMessage,
|
||||
this.onAttachmentTap,
|
||||
});
|
||||
|
||||
/// Callback when show message is tapped
|
||||
/// {@macro showMessageCallback}
|
||||
final ShowMessageCallback? onShowMessage;
|
||||
|
||||
/// Callback when attachment is returned to from other screens
|
||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||
/// {@macro replyMessageCallback}
|
||||
final ReplyMessageCallback? onReplyMessage;
|
||||
|
||||
/// Callback when attachment is tapped
|
||||
final VoidCallback? onAttachmentTap;
|
||||
/// {@macro onAttachmentTap}
|
||||
final OnAttachmentTap? onAttachmentTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -48,287 +44,301 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
|
||||
|
||||
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
return ConstrainedBox(
|
||||
constraints: constraints?.copyWith(
|
||||
maxHeight: double.infinity,
|
||||
) ??
|
||||
const BoxConstraints.expand(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.giphyIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.translations.giphyLabel,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (attachment.title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
attachment.title!,
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.giphyIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.translations.giphyLabel,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (attachment.title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
attachment.title!,
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
height: constraints?.maxHeight,
|
||||
width: constraints?.maxWidth,
|
||||
placeholder: (_, __) => SizedBox(
|
||||
width: constraints?.maxHeight,
|
||||
height: constraints?.maxWidth,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
constraints: constraints,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
width: double.infinity,
|
||||
height: 0.5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(
|
||||
message,
|
||||
{
|
||||
'image_action': 'cancel',
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.translations.cancelLabel
|
||||
.toLowerCase()
|
||||
.capitalize(),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
height: 50,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(
|
||||
message,
|
||||
{
|
||||
'image_action': 'shuffle',
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.translations.shuffleLabel,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
height: 50,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(
|
||||
message,
|
||||
{
|
||||
'image_action': 'send',
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.translations.sendLabel,
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) => SizedBox(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) =>
|
||||
AttachmentError(size: size),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
width: double.infinity,
|
||||
height: 0.5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'cancel',
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
context.translations.cancelLabel
|
||||
.toLowerCase()
|
||||
.capitalize(),
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
height: 50,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'shuffle',
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
context.translations.shuffleLabel,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.2),
|
||||
height: 50,
|
||||
),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'send',
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
context.translations.sendLabel,
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: StreamVisibleFootnote(),
|
||||
const SizedBox(height: 4),
|
||||
const Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: StreamVisibleFootnote(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onImageTap(BuildContext context) async {
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
Future<void> _onImageTap(BuildContext context) async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: StreamFullScreenMedia(
|
||||
child: StreamFullScreenMediaBuilder(
|
||||
mediaAttachmentPackages: message.getAttachmentPackageList(),
|
||||
startIndex: message.attachments.indexOf(attachment),
|
||||
userName: message.user?.name,
|
||||
userName: message.user!.name,
|
||||
onShowMessage: onShowMessage,
|
||||
onReplyMessage: onReplyMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction?.call(res);
|
||||
}
|
||||
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
|
||||
SizedBox(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
final image = Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
return Shimmer.fromColors(
|
||||
baseColor: colorTheme.disabled,
|
||||
highlightColor: colorTheme.inputBg,
|
||||
child: image,
|
||||
);
|
||||
},
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) =>
|
||||
AttachmentError(size: size),
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
height: constraints?.maxHeight,
|
||||
width: constraints?.maxWidth,
|
||||
placeholder: (_, __) {
|
||||
final image = Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
return Shimmer.fromColors(
|
||||
baseColor: colorTheme.disabled,
|
||||
highlightColor: colorTheme.inputBg,
|
||||
child: image,
|
||||
);
|
||||
},
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
constraints: constraints,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
size: 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
context.translations.giphyLabel.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
context.translations.giphyLabel.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.barsBg,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Downloads the [attachment] to the device and returns
|
||||
/// the path to the file.
|
||||
Future<String?> downloadWebOrDesktopAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
}) async {
|
||||
final type = attachment.type;
|
||||
|
||||
String? downloadUrl;
|
||||
String? fileName;
|
||||
/* ---IMAGES/GIFS--- */
|
||||
if (type == 'image') {
|
||||
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
|
||||
fileName = attachment.title;
|
||||
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
|
||||
}
|
||||
/* ---GIPHY's--- */
|
||||
else if (type == 'giphy') {
|
||||
downloadUrl = attachment.thumbUrl;
|
||||
fileName = '${attachment.title}.gif';
|
||||
}
|
||||
/* ---FILES AND VIDEOS--- */
|
||||
else if (type == 'file' || type == 'video') {
|
||||
downloadUrl = attachment.assetUrl;
|
||||
fileName = attachment.title;
|
||||
}
|
||||
|
||||
assert(
|
||||
downloadUrl != null,
|
||||
'Attachment must have an assetUrl or imageUrl or thumbUrl',
|
||||
);
|
||||
|
||||
final response = await Dio().get<List<int>>(
|
||||
downloadUrl!,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
// set responseType to `bytes`
|
||||
options: options?.copyWith(responseType: ResponseType.bytes) ??
|
||||
Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
|
||||
// Open the native file browser so the user can select the download path.
|
||||
final path = await getSavePath(suggestedName: fileName);
|
||||
|
||||
if (path == null) {
|
||||
// Operation was canceled by the user.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create an XFile for proper file saving
|
||||
final file = XFile.fromData(
|
||||
Uint8List.fromList(response.data!),
|
||||
mimeType: attachment.mimeType,
|
||||
name: fileName,
|
||||
path: path,
|
||||
);
|
||||
|
||||
// Save the file to the user's selected path.
|
||||
await file.saveTo(path);
|
||||
return path;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export 'stream_attachment_handler_base.dart'
|
||||
if (dart.library.html) 'stream_attachment_handler_html.dart'
|
||||
if (dart.library.io) 'stream_attachment_handler_io.dart'
|
||||
show StreamAttachmentHandler;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Base class for handling attachment related functionality.
|
||||
abstract class StreamAttachmentHandlerBase {
|
||||
/// Pick an image from the device.
|
||||
Future<Attachment?> pickImage({
|
||||
required ImageSource source,
|
||||
double? maxWidth,
|
||||
double? maxHeight,
|
||||
int? imageQuality,
|
||||
CameraDevice preferredCameraDevice = CameraDevice.rear,
|
||||
}) {
|
||||
throw UnimplementedError('pickImage is not implemented');
|
||||
}
|
||||
|
||||
/// Pick a video from the device.
|
||||
Future<Attachment?> pickVideo({
|
||||
required ImageSource source,
|
||||
CameraDevice preferredCameraDevice = CameraDevice.rear,
|
||||
Duration? maxDuration,
|
||||
}) {
|
||||
throw UnimplementedError('pickVideo is not implemented');
|
||||
}
|
||||
|
||||
/// Pick a file from the device.
|
||||
Future<Attachment?> pickFile({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(FilePickerStatus)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool withData = true,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = true,
|
||||
}) {
|
||||
throw UnimplementedError('pickFile is not implemented');
|
||||
}
|
||||
|
||||
/// Pick an audio from the device.
|
||||
Future<Attachment?> pickAudio() {
|
||||
throw UnimplementedError('pickAudio is not implemented');
|
||||
}
|
||||
|
||||
/// Saves the [attachmentFile] to the temporary directory.
|
||||
Future<String> saveAttachmentFile({
|
||||
required AttachmentFile attachmentFile,
|
||||
}) {
|
||||
throw UnimplementedError('saveFile is not implemented');
|
||||
}
|
||||
|
||||
/// Deletes the [attachmentFile] from the temporary directory.
|
||||
Future<void> deleteAttachmentFile({
|
||||
required AttachmentFile attachmentFile,
|
||||
}) {
|
||||
throw UnimplementedError('deleteAttachmentFile is not implemented');
|
||||
}
|
||||
|
||||
/// Downloads the [attachment] to the device and returns
|
||||
/// the path to the file.
|
||||
Future<String?> downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
}) {
|
||||
throw UnimplementedError('downloadAttachment is not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub implementation of [StreamAttachmentHandlerBase].
|
||||
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
|
||||
/// Returns an instance of [StreamAttachmentHandler].
|
||||
static StreamAttachmentHandler get instance {
|
||||
throw UnimplementedError('instance is not implemented');
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/common.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// StreamAttachmentHandler implementation for html.
|
||||
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
|
||||
StreamAttachmentHandler._();
|
||||
|
||||
static StreamAttachmentHandler? _instance;
|
||||
|
||||
/// Returns the singleton instance of [StreamAttachmentHandler].
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static StreamAttachmentHandler get instance =>
|
||||
_instance ??= StreamAttachmentHandler._();
|
||||
|
||||
late final _filePicker = FilePicker.platform;
|
||||
|
||||
@override
|
||||
Future<Attachment?> pickFile({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(FilePickerStatus)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool withData = true,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = true,
|
||||
}) async {
|
||||
final result = await _filePicker.pickFiles(
|
||||
dialogTitle: dialogTitle,
|
||||
initialDirectory: initialDirectory,
|
||||
type: type,
|
||||
allowedExtensions: allowedExtensions,
|
||||
onFileLoading: onFileLoading,
|
||||
allowCompression: allowCompression,
|
||||
withData: withData,
|
||||
withReadStream: withReadStream,
|
||||
lockParentWindow: lockParentWindow,
|
||||
);
|
||||
|
||||
return result?.files.first.toAttachment(type: type.toAttachmentType());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
}) {
|
||||
return downloadWebOrDesktopAttachment(
|
||||
attachment,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/common.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/src/utils/device_segmentation.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// StreamAttachmentHandler implementation for desktop.
|
||||
class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler {
|
||||
/// Returns the singleton instance of [StreamAttachmentHandler].
|
||||
StreamAttachmentHandlerDesktop() : super.__();
|
||||
|
||||
@override
|
||||
Future<String?> downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
}) {
|
||||
return downloadWebOrDesktopAttachment(
|
||||
attachment,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// StreamAttachmentHandler implementation for io.
|
||||
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
|
||||
StreamAttachmentHandler.__();
|
||||
|
||||
factory StreamAttachmentHandler._() {
|
||||
if (isDesktopDevice) {
|
||||
return StreamAttachmentHandlerDesktop();
|
||||
}
|
||||
return StreamAttachmentHandler.__();
|
||||
}
|
||||
|
||||
static StreamAttachmentHandler? _instance;
|
||||
|
||||
/// Returns the singleton instance of [StreamAttachmentHandler].
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static StreamAttachmentHandler get instance =>
|
||||
_instance ??= StreamAttachmentHandler._();
|
||||
|
||||
late final _imagePicker = ImagePicker();
|
||||
late final _filePicker = FilePicker.platform;
|
||||
|
||||
@override
|
||||
Future<Attachment?> pickImage({
|
||||
required ImageSource source,
|
||||
double? maxWidth,
|
||||
double? maxHeight,
|
||||
int? imageQuality,
|
||||
CameraDevice preferredCameraDevice = CameraDevice.rear,
|
||||
}) async {
|
||||
final image = await _imagePicker.pickImage(
|
||||
source: source,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: imageQuality,
|
||||
preferredCameraDevice: preferredCameraDevice,
|
||||
);
|
||||
|
||||
return image?.toAttachment(type: 'image');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Attachment?> pickVideo({
|
||||
required ImageSource source,
|
||||
CameraDevice preferredCameraDevice = CameraDevice.rear,
|
||||
Duration? maxDuration,
|
||||
}) async {
|
||||
final video = await _imagePicker.pickVideo(
|
||||
source: source,
|
||||
preferredCameraDevice: preferredCameraDevice,
|
||||
maxDuration: maxDuration,
|
||||
);
|
||||
|
||||
return video?.toAttachment(type: 'video');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Attachment?> pickFile({
|
||||
String? dialogTitle,
|
||||
String? initialDirectory,
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
Function(FilePickerStatus)? onFileLoading,
|
||||
bool allowCompression = true,
|
||||
bool withData = true,
|
||||
bool withReadStream = false,
|
||||
bool lockParentWindow = true,
|
||||
}) async {
|
||||
final result = await _filePicker.pickFiles(
|
||||
dialogTitle: dialogTitle,
|
||||
initialDirectory: initialDirectory,
|
||||
type: type,
|
||||
allowedExtensions: allowedExtensions,
|
||||
onFileLoading: onFileLoading,
|
||||
allowCompression: allowCompression,
|
||||
withData: withData,
|
||||
withReadStream: withReadStream,
|
||||
lockParentWindow: lockParentWindow,
|
||||
);
|
||||
|
||||
return result?.files.first.toAttachment(type: type.toAttachmentType());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> saveAttachmentFile({
|
||||
required AttachmentFile attachmentFile,
|
||||
}) async {
|
||||
final fileName = attachmentFile.name;
|
||||
assert(fileName != null, 'Attachment file name is required');
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows);
|
||||
final tempFilePath = tempPath.resolve(fileName!).path;
|
||||
print(tempFilePath);
|
||||
|
||||
final attachmentFileBytes = attachmentFile.bytes;
|
||||
if (attachmentFileBytes == null) {
|
||||
final attachmentFilePath = attachmentFile.path!;
|
||||
final file = File(attachmentFilePath);
|
||||
return file.copy(tempFilePath).then((it) => it.path);
|
||||
} else {
|
||||
final file = File(tempFilePath);
|
||||
return file.writeAsBytes(attachmentFileBytes).then((it) => it.path);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAttachmentFile({
|
||||
required AttachmentFile attachmentFile,
|
||||
}) async {
|
||||
final attachmentFilePath = attachmentFile.path;
|
||||
if (attachmentFilePath != null) {
|
||||
final file = File(attachmentFilePath);
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
}) async {
|
||||
final type = attachment.type;
|
||||
|
||||
String? downloadUrl;
|
||||
String? fileName;
|
||||
/* ---IMAGES/GIFS--- */
|
||||
if (type == 'image') {
|
||||
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
|
||||
fileName = attachment.title;
|
||||
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
|
||||
}
|
||||
/* ---GIPHY's--- */
|
||||
else if (type == 'giphy') {
|
||||
downloadUrl = attachment.thumbUrl;
|
||||
fileName = '${attachment.title}.gif';
|
||||
}
|
||||
/* ---FILES AND VIDEOS--- */
|
||||
else if (type == 'file' || type == 'video') {
|
||||
downloadUrl = attachment.assetUrl;
|
||||
fileName = attachment.title;
|
||||
}
|
||||
|
||||
assert(
|
||||
downloadUrl != null,
|
||||
'Attachment must have an assetUrl or imageUrl or thumbUrl',
|
||||
);
|
||||
|
||||
final response = await Dio().get<List<int>>(
|
||||
downloadUrl!,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
// set responseType to `bytes`
|
||||
options: options?.copyWith(responseType: ResponseType.bytes) ??
|
||||
Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
|
||||
final appDir = await getTemporaryDirectory();
|
||||
final ext = Uri.parse(downloadUrl).pathSegments.last;
|
||||
final path = '${appDir.path}/${attachment.id}.$ext';
|
||||
|
||||
// Create an XFile for proper file saving
|
||||
final file = XFile.fromData(
|
||||
Uint8List.fromList(response.data!),
|
||||
mimeType: attachment.mimeType,
|
||||
name: fileName,
|
||||
path: path,
|
||||
);
|
||||
|
||||
// Save the file to the user's selected path.
|
||||
await file.saveTo(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user