Compare commits

..
Author SHA1 Message Date
Mathias Mogensen 6a14ba8047 feat: initial date text input 2024-03-05 13:18:48 +01:00
Mathias Mogensen 5daf9d23f5 fix: update view names in page reference menu (#4802) 2024-03-04 18:24:49 +01:00
Mathias Mogensen b087a9aeb3 fix: filter keydownevents in actions handler (#4817) 2024-03-04 13:42:42 +01:00
Mathias Mogensen 107e3cea4f feat: duplicate calendar event (#4816)
* feat: duplicate calendar event

* test: add simple test
2024-03-04 12:42:00 +01:00
Mathias Mogensen ba965caa8f fix: stop hover when scrolling (#4801) 2024-03-04 12:41:34 +01:00
Zack 2af93a9bcb feat: add support for renaming and updating the icon for workspaces (#4806) 2024-03-04 16:05:16 +08:00
Nathan.fooo f8962edfd7 chore: bump client api (#4804)
* chore: bump client api

* chore: bump client api

* chore: bump client api
2024-03-04 14:05:04 +08:00
Lucas.Xu b236afd0c3 chore: update version to 0.5.1 (#4811) 2024-03-04 12:28:28 +08:00
Lucas.Xu 8944edf75f feat: support clearing caches and fix unable to load image (#4809)
* feat: support clearing caches

* fix: try to clear the image cache when loading failed.

* feat: clear cache on mobile

* chore: add error log
2024-03-04 12:24:25 +08:00
Richard Shiue 6b05be2362 fix: remove dup translation token and rearrange (#4810) 2024-03-04 12:01:31 +08:00
Lucas.Xu 90146148b6 feat: show the workspace list on top-left corner (#4799) 2024-03-04 10:43:00 +08:00
Mathias Mogensen 63464cbf2e fix: launch url on all platforms (#4797)
* fix: error handled launchUrl

* chore: update links to use new subdomain

* feat: show toast if onFailure is not provided
2024-03-03 22:04:59 +01:00
Daniil 1fadb15351 chore: update russian translations (#4796) 2024-03-03 09:41:33 +08:00
Lucas.Xu 8732c3c28b feat: members settings (#4788)
* feat: add member settings

* feat: fetch workspace members from server

* feat: add translations

* feat: implement invite feature

* feat: support inviting people via email

* feat: support updating member role

* feat: add feature flag to control the visibilty of members settings
2024-03-03 09:36:12 +08:00
YiHeCN f7233f6949 chore: update zh-CN translations (#4795) 2024-03-03 00:05:43 +08:00
Sore 8c7ed808a7 chore: rename ckb-KUR.json to ckb-KU.json (#4786) 2024-03-02 22:51:20 +08:00
Richard Shiue b38fc43300 feat: checklist cell editor a11y improvement (#4784) 2024-03-02 19:14:00 +08:00
Richard Shiue f4170755fa fix: reorder grid field updating all views (#4791) 2024-03-02 18:27:34 +08:00
Richard Shiue 79585d0835 fix: flutter_ci (#4793) 2024-03-02 17:30:27 +08:00
Kilu.He 835563f81b feat: support image block (#4783) 2024-03-01 10:48:07 +08:00
Richard Shiue 37bc5b3fbf chore: type option handler cleanup (#4787) 2024-03-01 10:26:38 +08:00
Richard Shiue 682bf19838 chore: type option cell data trait cleanup (#4782)
* chore: remove dead code

* chore: remove legacy FromCellStringTrait
2024-02-29 15:48:06 +08:00
Richard Shiue f4ca3ef782 feat: add basic relation field (#4397)
* feat: add basic relation field

* fix: clippy

* fix: tauri build 🤞

* chore: merge changes

* fix: merge main

* chore: initial code review pass

* fix: rust-lib test

* chore: code cleanup

* fix: unwrap or default
2024-02-29 14:38:18 +08:00
Richard Shiue f826d05f03 fix: mobile ui fixes (#4780)
* fix: adjust mobile grid padding

* fix: reorder field not working
2024-02-29 09:44:48 +08:00
Richard Shiue 01f2b15f70 fix: type option transform for checklists (#4779) 2024-02-28 22:04:32 +08:00
Mathias Mogensen 2d6856a23d fix: edit column sheet uniform ui (#4774) 2024-02-28 22:04:16 +08:00
Mohammad Zolfaghari 2286a1c726 feat: rtl toolbar item setting (#3958) 2024-02-28 21:56:18 +08:00
domenico code ef90c35175 chore: update translations (#4753) 2024-02-28 21:55:52 +08:00
Mathias Mogensen af12beb1c3 fix: remove empty space if no email (#4777) 2024-02-28 13:23:06 +01:00
Mathias Mogensen 0d68c42aab fix: symmetric padding in mobile database header (#4773) 2024-02-28 13:21:23 +01:00
Mathias Mogensen 0cffe42d80 fix: add time to view meta info (#4769) 2024-02-28 12:34:02 +01:00
Lucas.Xu abd08e5e53 feat: support customizing font and color before typing (#4765)
* fix: bius buttons highlight status

* feat: support customizing font family before typing

* feat: support customizing text color before typing
2024-02-28 15:51:50 +08:00
Kilu.He e250f780a4 feat: support to show condition in filter chip label (#4751)
* feat: support to show condition in filter chip label

* fix: improve the performance of inline formula with selection
2024-02-28 14:26:08 +08:00
Lucas.XuandMathias Mogensen 980bebf86a fix: mobile UI regression (#4754)
* fix: aa menu item selected status

* feat: support selecting word on iOS throught long pressing

* chore: update editor version

* feat: update add block menu colors

* feat: add scrollbar in bottom sheet

* fix: aa menu should not dimiss after changing block type

* fix: add link menu ui issues

* feat: sync bius status

* Update frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: update editor version

* fix: flutter analyze

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-02-28 13:32:24 +08:00
Mathias Mogensen 26f8397624 feat: duplicate field with cell data (#4737)
* feat: duplicate field with cell data

* chore: code review remove extra option
2024-02-27 14:42:16 +01:00
Lucas.Xu d67e904f56 chore: cleanup disk space before running mobile ci (#4744)
* chore: use macos instead of linux in ci to avoid no spaces error

* chore: optimize disk space
2024-02-27 21:41:47 +08:00
Lucas.Xu f36ca3777b fix: unable to resize the image (#4750) 2024-02-27 21:41:14 +08:00
Richard Shiue 74233d2606 fix: mobile regression (#4747) 2024-02-27 10:51:32 +08:00
Nathan.fooo faea0fa532 chore: Update CHANGELOG.md 2024-02-27 03:37:56 +08:00
Lucas.Xu 3454af1caa chore: update privacy and terms link (#4742) 2024-02-26 15:29:55 +08:00
Nathan.fooo 8d50ad2936 chore: fix log (#4740)
* chore: fix log

* chore: fix log

* chore: bump client api
2024-02-26 15:27:12 +08:00
Richard Shiue daedf95c95 fix: some mobile database bugs (#4738)
* chore: no autofocus on field name

* refactor: one, two, many

* chore: reduce spacing between tabbar header and grid header

* fix: update field type

* fix: cannot edit view name

* fix: title for select field type bottom sheet

* fix: forget checks, just do it
2024-02-26 15:13:38 +08:00
Lucas.Xu 95bfda9dc6 chore: update changelog (#4743) 2024-02-26 15:02:04 +08:00
Lucas.Xu 93664167e0 fix: unable to redo/undo in lists and data loss issues (#4739) 2024-02-26 14:21:36 +08:00
Lucas.Xu dfe3b0a929 fix: the button sizes in math equation input field are not same (#4741) 2024-02-26 14:21:27 +08:00
Kilu.He e2028ac5a0 fix: database bugs (#4632)
* fix: database bugs

* fix: calendar picker

* fix: the position of collapse menu button

* fix: modified some style

* fix: slash command

* fix: selection style

* fix: support toggle inline formula

* fix: block color effect grid block

* fix: if isRange and date is greater than endDate, swap date and endDate

* fix: remove sorting before insert row

* fix: toggle property visible status

* fix: modified tauri window size

* fix: placeholder should be hidden when composing

* fix: support href shortcut

* fix: prevent submit when the formula has error

* fix: modified layout selection

* fix: add padding for record edit

* fix: remove sorts before drag row

* fix: modified chip style

* fix: if previous node is an embed, merge the current node to another node which is not an embed

* fix: modified emoji picker
2024-02-26 10:19:21 +08:00
Mathias Mogensen c9dc24a13c fix: launch review 0.5.0 (#4735)
* fix: top bar buttons slow respond

* fix: fav button responsive

* fix: add favorite button to database views

* fix: add overflow to calculation

* fix: minor workaround for showing number format prefix

* fix: clear calculation on empty field cells

* fix: average calculation should not have def value

* fix: cargo fmt
2024-02-25 21:32:44 +01:00
Mathias Mogensen 609557c357 feat: word and char count (#4705)
* feat: word and char count

* chore: lints after merge

* feat: add create time of view in more action

* fix: missing comma lint

* feat: add duplicate/delete + database view more

* fix: dispose logic

* feat: code cleanup

* fix: add false for isDocument in databases

* feat: register view info bloc on plugin basis

* fix: accidental removal

* chore: clean up

* fix: add ViewInfoBloc above Row Document Editor

* chore: final clean up
2024-02-25 16:46:13 +01:00
Lucas.Xu 93af6e69a1 feat: convert link preview block to url text (#4734) 2024-02-25 22:59:23 +08:00
Lucas.Xu b75947b630 fix: unable to click the items in a long list (#4733) 2024-02-25 21:36:46 +08:00
Lucas.Xu b60d25a0e3 fix: unable to upload image from clipboard (#4728)
* fix: unable to upload image from clipboard

* chore: apply suggestion
2024-02-25 18:50:35 +08:00
Richard Shiue 537fa52e77 chore: move database domain code to its own folder (#4556) 2024-02-25 18:38:18 +08:00
Richard Shiue d861ce6227 fix: mobile view and field list bottom sheets (#4727) 2024-02-25 11:51:18 +08:00
Nathan.fooo c3e5aa29fc fix: doc open sync (#4732)
* chore: bump client api

* fix: open document and folder

* chore: bump collab rev

* chore: fix wasm build

* chore: fix warnings
2024-02-25 07:49:44 +08:00
Richard Shiue 4ca3ba8e08 fix: cjk input in calendar and row detail (#4726) 2024-02-24 22:55:35 +08:00
Richard Shiue f5cc6521fb chore: clean up db sort blocs (#4725) 2024-02-24 22:55:22 +08:00
Lucas.Xu cea1c17b76 feat: improve outline block (#4722) 2024-02-24 21:54:21 +08:00
Lucas.Xu 2abb396467 feat: use result instead of either (#4724)
* feat: use result instead of either

* chore: remove dartz
2024-02-24 21:54:10 +08:00
Lucas.Xu 236b5bfe90 feat: error message should persist on the image block (#4710) 2024-02-24 11:05:20 +08:00
Dev-for-fun fa3bc97698 fix: remove duplicated button labels for done, cancel and ok (#4718) 2024-02-23 23:01:01 +08:00
Richard Shiue c0796e8ae5 feat: mobile sort editor (#4714) 2024-02-23 20:47:19 +08:00
Richard Shiue acd75befbc fix: disable onTap when quick action button is disabled (#4713) 2024-02-23 16:56:31 +08:00
Richard Shiue 746f0817bc fix: show database view options for inline as disabled (#4711) 2024-02-23 10:49:34 +08:00
Lucas.Xu c0b667b4ea chore: optimize image block actions menu (#4707) 2024-02-23 09:46:21 +08:00
Lucas.XuandMathias Mogensen ef83688907 fix: the keyboard blocked the content of bottom sheet (#4706)
* fix: the keyboard blocked the content of bottom sheet

* chore: apply suggestion

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2024-02-22 22:54:56 +08:00
Lucas.Xu c06a0346d8 chore: bump version 0.5.0 (#4700) 2024-02-22 10:22:33 +08:00
Richard Shiue 4a0c48f419 feat: bottom sheets with transition (#4608)
* feat: shiny new bottom sheets

* chore: code review
2024-02-22 09:54:54 +08:00
Lucas.Xu 7802c75d7c feat: support scaling text on mobile (#4690) 2024-02-22 09:50:24 +08:00
Richard Shiueandnathan f1831d07af chore: re-implement get_filter_by_field_id function (#4678)
* chore: remove inaccurate database view function

* chore: update collab rev

* chore: fix clippy

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-22 08:07:59 +08:00
Richard ShiueandNathan.fooo 6636731487 feat: checklist sort (#4659)
* refactor: use BoxAny for dynamically-typed cell changesets

* fix: rust-lib tests and clippy

* feat: enable sorting by checklist type option

* test: checklist sort rust-lib tests

* chore: update related tests

* fix: clippy

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2024-02-22 08:00:59 +08:00
Richard Shiue a4a2a4088b refactor: use BoxAny for dynamically-typed cell changesets (#4655)
* refactor: use BoxAny for dynamically-typed cell changesets

* fix: rust-lib tests and clippy
2024-02-22 07:12:52 +08:00
Richard Shiue c500cd1287 refactor: improve type option filter logic (#4652) 2024-02-22 07:09:12 +08:00
Richard Shiue 6f173c2ada chore: adjust show mobile bottom sheet (#4689) 2024-02-21 21:06:46 +08:00
Richard Shiue 34fb1bcfa4 fix: disappearing sorts (#4691)
* chore: copy the field type

* chore: don't store field type in Sort

* fix: sorts disappearing
2024-02-21 20:01:57 +08:00
HY 58aa73b5be fix: resolve lexical ambiguity between "row" and "col" in zh-TW translation (#4683)
ref: https://terms.naer.edu.tw/detail/5e94c72b55cc73ebf81d71ca2b5d57e6/?seq=1
2024-02-20 22:40:03 +08:00
Richard Shiue 334aedd6c6 refactor: checkbox field type (#4682) 2024-02-20 14:02:32 +08:00
Richard Shiue a6c6aa819c fix: newly-created rows aren't being automatically sorted (#4661)
* fix: newly-created rows aren't being automatically sorted

* chore: new database view collab notification and add tests
2024-02-20 13:17:46 +08:00
Jayaprakash 15c9a67028 feat: implement outline block component depth control (#4642)
* feat: add support to control the depth of outline block component

* feat: update localization keys

* feat: add depth option to `BlockOptionButton`

* feat: retrive outline block heading components upto the depth level

* feat: add depth option config to editor configuration

* test: outline block depth control

* feat: add outline block placeholder

* refactor: remove redundant codes

* ci: trigger github actions

* chore: refactor `OptionDepthType` enum

* fix: flutter ci error

* refactor: removed `finalHeadingLevel` from outline keys

* fix: flutter ci error
2024-02-20 10:52:06 +08:00
Lucas.Xu 42cb032bca fix: flutter analyze (#4680) 2024-02-19 21:48:06 +08:00
Mathias Mogensen a3a5709b70 fix: dispose of previous applaunch tasks (#4631) 2024-02-19 21:32:01 +08:00
Ansah Mohammad 3a4247c304 feat: convert time format from HHmm to HH:mm in time field (#4641)
* feat: add a HHmm to HH:mm function

* fix: Replaced string.parse() with string.parseStrict() to address time input bug

* feat: add TextFormatter for formatting the time input
2024-02-19 21:30:28 +08:00
Sore 8eaadccda0 chore: add ckb-KUR translations (#4658)
* Create ckb-KUR.json

Add Translate (Kurdish-ckb Language)

* Update language.dart

i added ckb lang(kurdish).

* Update app_widget.dart
2024-02-19 17:43:01 +08:00
ArcesilasandLucas.Xu 2b6b10788d chore: update fr-FR and fr-CA translations (#4676)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-02-19 17:25:00 +08:00
Lucas.Xu 26f8bbf7c6 feat: optimize image upload process and display an error message if upload fails (#4679)
* chore: optimize image upload

* feat: show upload image status

* chore: upload the ai image to cloud server
2024-02-19 17:24:47 +08:00
Lucas.Xu 252699d249 chore: upgrade to flutter 3.19.0 (#4677)
* chore: upgrade to flutter 3.19.0

* chore: adjust to the latest flutter convention

* chore: update CI flutter version

* fix: flutter analyze

* fix: flutter analyze

* fix: flutter analyze

* chore: fix docker build
2024-02-19 13:49:55 +08:00
Nathan.fooo b2e55c4e2e Fix: init state (#4672)
* chore: fix login state

* chore: change log

* chore: fix flutter version
2024-02-18 16:40:18 +08:00
Bastian Leichtandnathan d5ee372131 chore: update translations (#4615)
Co-authored-by: nathan <nathan@appflowy.io>
2024-02-18 07:36:23 +08:00
Arcesilasandnathan 39588a4731 chore: update translations (#4664)
Co-authored-by: nathan <nathan@appflowy.io>
2024-02-18 00:44:39 +08:00
HYandnathan 211559ed6e chore: Update translations for Traditional Chinese (#4662)
* chore: update zh-TW translations

* inlang: update translations

* inlang: update translations

* chore: update zh tw

* chore: update zh tw

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-18 00:40:22 +08:00
kosei a637517413 feat: update ja-JP translations (#4627) 2024-02-18 00:25:52 +08:00
Mathias Mogensen ddbe0fa698 fix: tauri build (#4669)
* ci: update workflows

* chore: add back translations used on web
2024-02-17 14:38:12 +01:00
TeddyFlink ee61ef3f12 feat: update translations (#4654) 2024-02-17 16:31:25 +08:00
PWandnathan 54bfc65664 feat: translate to traditional Chinese (#4505)
* inlang: update translations

* inlang: update translations

* inlang: update translations

* inlang: update translations

* inlang: update translations

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-17 16:28:10 +08:00
Richard Shiue c159a5e42b chore: enable number filter (#4653)
* chore: enable filtering by number field type

* chore: code cleanup

* fix: integration test

* chore: remove unnecessary async from event handler
2024-02-17 15:28:49 +08:00
Richard Shiue 1311e2d379 fix: build regression (#4667) 2024-02-17 14:42:01 +08:00
Hassam Sulehriaandjazima e6aa57275a feat: open emoji selection menu with keyboard short (#4133)
* feat: Implement emoji shortcut event (#2038)

* test: emoji shortcut integration test (#2038)

* test: Fixed testcase for emoji_shortcut_test (#2038)

* fix: remove _showEmojiPickerMenu (#4133)

* fix: change local variables with underscore

---------

Co-authored-by: jazima <jazim.a.29@gmail.com>
2024-02-16 18:47:23 +01:00
Danilo Barion Nogueiraandnathan d690ca2751 chore: update translations (#4270)
Co-authored-by: nathan <nathan@appflowy.io>
2024-02-17 01:44:05 +08:00
Richard Shiue 3e0a003872 fix: deleting a sort doesn't allow creating a new sort on that field (#4660) 2024-02-16 13:39:49 +08:00
Nathan.fooo e81a2ff577 fix: token refresh on local (#4650)
* fix: refresh user token on local

* chore: add test
2024-02-14 09:38:05 +08:00
Mathias Mogensen b356927e60 feat: improve more action in document (#4639) 2024-02-09 15:54:05 +01:00
Mathias Mogensen 1894409dc3 ci: resolve mobile build failing due to missing space (#4640)
* chore: remove additionally unused tools

* chore: test

* chore: bunch cleanup commands
2024-02-09 14:40:06 +01:00
nathan f29f1bae32 chore: update error message 2024-02-09 10:18:52 +08:00
Nathan.fooo 45054e7639 chore: bump version 047 (#4635) 2024-02-09 00:18:51 +08:00
Nathan.fooo 3b51a6e6be chore: reload workspace when fail to load at the first time (#4633)
* chore: reload workspace when fail to load at the first time

* fix: clippy

* chore: update client api

* chore: fix wasm build

* chore: fix test
2024-02-08 23:53:05 +08:00
Ansah Mohammad 747fe40648 feat: toggle favourites from the documents top bar. (#4382)
* feat: init star button

* feat: implemented star button on the right bar

* feat: Implemented toggle logic

* chore: Added ToolTip for favorite button

* refactor: renamed star to favorite

* refactor: moved appplication logic into favorite_cubit

* chore: arranged imports in alphabetical order

* refactor: reused favorite_bloc for toggle

* fix: fixed the icon no rebuild

* refactor: moved the FavoriteBloc higher in the widget tree

* refactor: moved FavoriteBloc inside multiBlocProvider of desktop_home_screen

* chore: specified bloc type

* chore: cleaned code

* refactor: use any instead of map
2024-02-08 10:45:54 +01:00
Kilu.He 60fc5bb2c8 fix: tauri folder bugs (#4589) 2024-02-08 14:22:44 +08:00
Mathias Mogensen 9d71464f1a fix: select name by default (#4630) 2024-02-07 21:49:17 +01:00
Mathias Mogensen 92ceb2cd7d fix: dispose of resources in time (#4625)
* fix: limit length when renaming views

* chore: clean up database listeners

* fix: dispose of controllers properly

* fix: dispose of resources properly

* fix: deleting filters with same name

* chore: extend DatabaseTabBarItemBuilder

* fix: null check on null value
2024-02-07 18:38:53 +01:00
Richard Shiue 0db21b91a3 fix: unequal grid header and body widths (#4626) 2024-02-07 17:49:53 +01:00
Richard Shiue 88e9d63a13 fix: show newChecklistTask on hover over entire list item in row detail page (#4622) 2024-02-07 23:20:26 +08:00
Mathias Mogensen 3129fa6cc1 fix: use cmd+. to toggle sidebar on macOS (#4621)
* fix: use cmd+. to hide sidebar on macOS

* fix: update missed tooltip
2024-02-07 12:18:13 +08:00
Mathias Mogensen db5372c18e feat: hotkey to open settings dialog (#4620)
* feat: hotkey to open settings dialog

* chore: change to cmd/ctrl+,
2024-02-07 12:17:48 +08:00
Richard Shiue b781c9aa0f fix: deleting a sorting field doesn't delete existing sorts (#4611) 2024-02-07 10:47:17 +08:00
Richard Shiue a9c6b80f4a chore: add spacing between field buttons in setting popover (#4612) 2024-02-07 10:46:36 +08:00
Lucas.Xu 271f781cce fix: application will raise an error when selecting unsplash image in local mode (#4604) 2024-02-05 21:53:14 +07:00
Richard Shiue ef4bce25d8 feat: sort by last modified time or created time (#4601) 2024-02-05 21:54:42 +08:00
ansizheshi 95c4cb241a chore: update zh-CN translations (#4599) 2024-02-05 17:39:45 +08:00
Dorian Péron e20d938ce3 chore: update translations (#4581) 2024-02-05 16:53:18 +08:00
Richard Shiue a515715543 feat: reorder sort precedence (#4592)
* feat: reorder sorts

* chore: add tests, fix tests, fix tauri build and fix clippy

* fix: add missing import
2024-02-05 13:52:59 +08:00
Nathan.fooo fda70ff560 feat: folder web (#4580)
* chore: folder wasm

* chore: folder wasm

* chore: resolve deps

* chore: fix trait

* chore: try localset

* chore: fix

* chore: fix

* chore: fix

* chore: async init sdk

* chore: fix test

* chore: fix test
2024-02-04 05:50:23 +08:00
Zackandnathan 08938b8c70 feat: cloud workspace api (#4469)
* feat: workspace api

* feat: added cloud apis for add and delete workspace

* feat: add and delete workspace event handlers

* chore: rust fmt

* chore: save user workspace

* test: add test

* test: add test

* chore: add to gitignore

* feat: update api add name to workspace

* chore: cargo clippy and rename to create

* chore: add envrc and direnv to gitignore

* chore: change name to create workspace instead of add workspace

* chore: update client api rev

* feat: add create workspace impl

* chore: restore gitignore to original

* test: fix create workspace event test

* fix: change delete workspace input

* fix: compile

* fix: create workspace test

* feat: add error code for request payload too large

* chore: remove cargo backup files

* feat: add is async option for upload file handler

* chore: update client api version

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 05:49:45 +08:00
Richard Shiueandnathan 250f29f325 chore: clean up sort and filter code (#4585)
* refactor: port away from extra SortType struct

* refactor: add validator to flowy_database and clean up unused structs

* refactor: port away from extra FilterType struct

* chore: analysis options

* fix: clippy and dart/ts compile

* fix: tauri build

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 05:49:28 +08:00
Yijing Huang d117cd6b5b chore: delete placeholder code and clean code (#4503) 2024-02-04 00:56:50 +08:00
Richard Shiue ca93cb20ec chore: revamp mobile database toolbar (#4579)
* chore: revamp mobile database toolbar

* chore: code cleanup
2024-02-04 00:55:44 +08:00
Richard Shiue 8c1d0106dd chore: inkwell mobile quick action sheets (#4576)
* chore: inkwell mobile quick action sheets

* chore: more subtle tap effect

* chore: improve code style
2024-02-04 00:55:05 +08:00
Mathias Mogensenandnathan 5cbc8b1e18 feat: calculations (#4473)
* feat: initial calculation controller

* fix: entities

* feat: calculations

* fix: review comments and support floats

* fix: abstract business logic into calculations service

* fix: clean calculation entities after merge

* feat: react to changes to row/cell/field_type

* chore: changes after merging main

* feat: handle delete field

* test: add grid calculations tests

* fix: add validation + format numbers

* refactor: get cell number

* chore: bump collab

* chore: fix clippy

* chore: update docs

* chore: update docs

* chore: fmt

* chore: fix flutter

* chore: collab rev

* fix: cleanup and hover to show

* fix: localization

* test: add basic rust test

* fix: clippy

* fix: support updating calculation on duplicate row

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 00:52:38 +08:00
Nathan.fooo 274742e334 chore: bump version v0.4.6 (#4595)
* chore: bump version

* chore: bump client api version
2024-02-03 19:45:46 +08:00
Lucas.Xu 792f56db79 chore: remove unused build and add build bot (#4586) 2024-02-02 17:42:23 +08:00
nathan 458d3f971b chore: bump client api 2024-02-02 09:35:55 +08:00
Nathan.fooo 2ffd6bf7ea chore: bump version (#4583) 2024-02-02 09:30:27 +08:00
Nathan.fooo 722691c922 chore: fix ws conn (#4582)
* chore: fix ws conn

* chore: flutter analyzer

* chore: update client api
2024-02-02 09:27:11 +08:00
Kilu.He 9746852b5f fix: fixed editor bugs (#4552)
* fix: fixed editor bugs

* fix: add error boundary
2024-02-02 05:47:41 +08:00
Lucas.Xu 5b030303a6 fix: appimage build (#4578)
* chore: force exec flutter package pub get

* feat: support building appimage on github ci
2024-02-01 20:58:58 +08:00
XiaoFans 1e4f342fc3 chore: update translations (#4577) 2024-02-01 20:53:49 +08:00
Richard Shiue 4f0379bdd7 fix: size of hitbox in app bar button (#4575) 2024-02-01 20:53:21 +08:00
1163 changed files with 32030 additions and 13479 deletions

No files matched your search

-99
View File
@@ -1,99 +0,0 @@
name: Build AppFlowy Release APK
on: workflow_dispatch
env:
FLUTTER_VERSION: "3.18.0-0.2.pre"
RUST_TOOLCHAIN: "1.75"
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: aarch64-linux-android
runs-on: ${{ matrix.os }}
steps:
# the following step is required to avoid running out of space
- name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: ${{ matrix.target }}
override: true
profile: minimal
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: "r24"
add-to-path: true
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
- uses: davidB/rust-cargo-make@v1
with:
version: "0.36.6"
- name: Install prerequisites
working-directory: frontend
run: |
rustup target install aarch64-linux-android
rustup target install x86_64-linux-android
cargo install --force duckscript_cli
cargo install cargo-ndk
if [ "$RUNNER_OS" == "Linux" ]; then
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0 libnotify-dev
sudo apt-get install gcc-multilib
elif [ "$RUNNER_OS" == "Windows" ]; then
vcpkg integrate install
elif [ "$RUNNER_OS" == "macOS" ]; then
echo 'do nothing'
fi
cargo make appflowy-flutter-deps-tools
shell: bash
- name: Build AppFlowy
working-directory: frontend
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
cargo make --profile production-android appflowy-android
- name: Upload APK
uses: actions/upload-artifact@v2
with:
name: app-arm64-v8a-release.apk
path: frontend/appflowy_flutter/build/app/outputs/flutter-apk/
+28
View File
@@ -0,0 +1,28 @@
name: Build Bot
on:
issue_comment:
types: [created]
jobs:
dispatch_slash_command:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
# get build name from pubspec.yaml
- name: Get build version
working-directory: frontend/appflowy_flutter
id: get_build_name
run: |
echo "fetching version from pubspec.yaml..."
echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT
- uses: peter-evans/slash-command-dispatch@v4
with:
token: ${{ secrets.PAT }}
commands: build
static-args: |
ref=refs/pull/${{ github.event.issue.number }}/head
build_name=${{ steps.get_build_name.outputs.build_name }}
+42
View File
@@ -0,0 +1,42 @@
name: build
on:
repository_dispatch:
types: [build-command]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: notify appflowy_builder
run: |
platform=${{ github.event.client_payload.slash_command.args.unnamed.arg1 }}
build_name=${{ github.event.client_payload.slash_command.args.named.build_name }}
branch=${{ github.event.client_payload.slash_command.args.named.ref }}
build_type=""
arch=""
if [ "$platform" = "android" ]; then
build_type="apk"
elif [ "$platform" = "macos" ]; then
arch="universal"
fi
params=$(jq -n \
--arg ref "main" \
--arg repo "LucasXu0/AppFlowy" \
--arg branch "$branch" \
--arg build_name "$build_name" \
--arg build_type "$build_type" \
--arg arch "$arch" \
'{ref: $ref, inputs: {repo: $repo, branch: $branch, build_name: $build_name, build_type: $build_type, arch: $arch}} | del(.inputs | .. | select(. == ""))')
echo "params: $params"
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/AppFlowy-IO/AppFlowy-Builder/actions/workflows/$platform.yaml/dispatches \
-d "$params"
+1 -2
View File
@@ -5,8 +5,7 @@ jobs:
commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v4
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build the app
shell: bash
+33 -20
View File
@@ -9,6 +9,7 @@ on:
- ".github/workflows/flutter_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
pull_request:
branches:
@@ -18,10 +19,11 @@ on:
- ".github/workflows/flutter_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.18.0-0.2.pre"
FLUTTER_VERSION: "3.19.0"
RUST_TOOLCHAIN: "1.75"
CARGO_MAKE_VERSION: "0.36.6"
@@ -59,7 +61,7 @@ jobs:
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -70,12 +72,20 @@ jobs:
override: true
profile: minimal
- name: Export pub environment variables and add to PATH
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
fi
shell: bash
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: Swatinem/rust-cache@v2
with:
@@ -120,7 +130,7 @@ jobs:
run: |
tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
path: appflowy_flutter.tar.gz
@@ -140,7 +150,7 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -155,8 +165,9 @@ jobs:
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: Swatinem/rust-cache@v2
with:
@@ -192,7 +203,7 @@ jobs:
fi
shell: bash
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
@@ -216,11 +227,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [ prepare ]
needs: [prepare]
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -229,11 +240,10 @@ jobs:
steps:
- name: Checkout appflowy cloud code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
depth: 1
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
@@ -253,14 +263,15 @@ jobs:
sleep 10
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: taiki-e/install-action@v2
with:
@@ -280,7 +291,7 @@ jobs:
flutter config --enable-linux-desktop
shell: bash
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
@@ -318,7 +329,7 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -333,8 +344,9 @@ jobs:
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: taiki-e/install-action@v2
with:
@@ -363,7 +375,7 @@ jobs:
fi
shell: bash
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
@@ -409,7 +421,7 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -424,8 +436,9 @@ jobs:
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: taiki-e/install-action@v2
with:
@@ -454,7 +467,7 @@ jobs:
fi
shell: bash
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
+24 -24
View File
@@ -18,8 +18,7 @@ on:
- "!frontend/appflowy_tauri/**"
env:
FLUTTER_VERSION: "3.18.0-0.2.pre"
RUST_TOOLCHAIN: "1.75"
FLUTTER_VERSION: "3.19.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -38,7 +37,17 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
# the following step is required to avoid running out of space
- name: Check storage space
run: df -h
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 2048
swap-size-mb: 1024
remove-dotnet: 'true'
# the following step is required to avoid running out of space
- name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: |
@@ -46,43 +55,34 @@ jobs:
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: Check storage space
run: df -h
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: ${{ matrix.target }}
override: true
profile: minimal
uses: actions/checkout@v4
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: "r24"
add-to-path: true
ndk-version: "r24"
add-to-path: true
- uses: gradle/gradle-build-action@v2
- uses: gradle/gradle-build-action@v3
with:
gradle-version: 7.6.3
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.os }}
workspaces: |
frontend/rust-lib
- uses: davidB/rust-cargo-make@v1
with:
version: "0.36.6"
+34 -17
View File
@@ -6,7 +6,7 @@ on:
- "*"
env:
FLUTTER_VERSION: "3.18.0-0.2.pre"
FLUTTER_VERSION: "3.19.0"
RUST_TOOLCHAIN: "1.75"
jobs:
@@ -18,7 +18,7 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Build release notes
run: |
@@ -52,12 +52,12 @@ jobs:
- { target: x86_64-pc-windows-msvc, os: windows-2019 }
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
@@ -138,12 +138,12 @@ jobs:
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" }
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
@@ -238,12 +238,12 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
@@ -327,6 +327,8 @@ jobs:
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
strategy:
fail-fast: false
@@ -341,12 +343,12 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
@@ -395,11 +397,15 @@ jobs:
- name: Build Linux package (.rpm)
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
run: |
ls -a
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
echo '------------'
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
ls -a
- name: Build Linux package (.AppImage)
working-directory: frontend
continue-on-error: true
run: |
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
- name: Upload Asset
id: upload-release-asset
@@ -434,23 +440,34 @@ jobs:
asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}
asset_content_type: application/octet-stream
- name: Upload AppImage package
id: upload-release-asset-install-package-appimage
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
asset_content_type: application/octet-stream
build-for-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
with:
context: .
file: ./frontend/scripts/docker-buildfiles/Dockerfile
+3 -5
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -45,16 +45,15 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
prefix-key: 'ubuntu-latest'
prefix-key: "ubuntu-latest"
workspaces: |
frontend/rust-lib
- name: Checkout appflowy cloud code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
depth: 1
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
@@ -80,7 +79,6 @@ jobs:
af_cloud_test_gotrue_url: http://localhost/gotrue
run: cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
working-directory: frontend/rust-lib/
+3 -3
View File
@@ -10,7 +10,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.18.0-0.2.pre"
FLUTTER_VERSION: "3.19.0"
RUST_TOOLCHAIN: "1.75"
jobs:
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
@@ -33,7 +33,7 @@ jobs:
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "beta"
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
+2 -2
View File
@@ -5,6 +5,7 @@ on:
- ".github/workflows/tauri_ci.yaml"
- "frontend/rust-lib/**"
- "frontend/appflowy_tauri/**"
- "frontend/resources/**"
env:
NODE_VERSION: "18.16.0"
@@ -21,8 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# platform: [macos-latest, ubuntu-latest, windows-latest]
platform: [ubuntu-latest]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
+6 -4
View File
@@ -26,9 +26,9 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
@@ -40,14 +40,16 @@ jobs:
frontend/rust-lib
frontend/appflowy_web/appflowy_wasm
# TODO: Can combine caching deps and node_modules in one
# See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob
- name: Cache Node.js dependencies
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}
- name: Cache node_modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: frontend/appflowy_web/node_modules
key: node-modules-${{ runner.os }}
+35
View File
@@ -1,4 +1,39 @@
# Release Notes
## Version 0.5.0 - 02/26/2024
### New Features
- Added support for scaling text on mobile platforms for better readability.
- Introduced a toggle for favorites directly from the documents' top bar.
- Optimized the image upload process and added error messaging for failed uploads.
- Implemented depth control for outline block components.
- New checklist task creation is now more intuitive, with prompts appearing on hover over list items in the row detail page.
- Enhanced sorting capabilities, allowing reordering and addition of multiple sorts.
- Expanded sorting and filtering options to include more field types like checklist, creation time, and modification time.
- Added support for field calculations within databases.
### Bug Fixes
- Fixed an issue where inserting an image from Unsplash in local mode was not possible.
- Fixed undo/redo functionality in lists.
- Fixed data loss issues when converting between block types.
- Fixed a bug where newly created rows were not being automatically sorted.
- Fixed issues related to deleting a sorting field or sort not removing existing sorts properly.
### Notes
- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.19.0.
## Version 0.4.9 - 02/17/2024
### Bug Fixes
- Resolved the issue that caused users to be redirected to the Sign In page
## Version 0.4.8 - 02/13/2024
### Bug Fixes
- Fixed a possible error when loading workspaces
## Version 0.4.6 - 02/03/2024
### Bug Fixes
- Fixed refresh token bug
## Version 0.4.5 - 02/01/2024
### Bug Fixes
- Fixed WebSocket connection issue
## Version 0.4.4 - 01/31/2024
### New Features
- Added functionality for uploading images to cloud storage.
+1 -1
View File
@@ -12,7 +12,7 @@
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "trace",
"RUST_LOG": "debug",
"RUST_BACKTRACE": "1"
},
// uncomment the following line to testing performance.
+2 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.4.4"
APPFLOWY_VERSION = "0.5.1"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -50,6 +50,7 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm"
# Test default config
TEST_CRATE_TYPE = "cdylib"
TEST_LIB_EXT = "dylib"
@@ -3,7 +3,7 @@
#include <stdint.h>
#include <stdlib.h>
int64_t init_sdk(char *data);
int64_t init_sdk(int64_t port, char *data);
void async_event(int64_t port, const uint8_t *input, uintptr_t len);
@@ -13,6 +13,6 @@ int32_t set_stream_port(int64_t port);
void link_me_please(void);
void backend_log(int64_t level, const char *data);
void rust_log(int64_t level, const char *data);
void set_env(const char *data);
@@ -40,19 +40,19 @@ void main() {
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userAvatarFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserAvatar),
);
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
// Open icon picker dialog and select emoji
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
await tester.tapEmoji('😁');
await tester.pumpAndSettle();
final UserAvatar userAvatar =
tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, '😁');
// await tester.tap(userAvatarFinder);
// await tester.pumpAndSettle();
// await tester.tapEmoji('😁');
// await tester.pumpAndSettle();
// final UserAvatar userAvatar =
// tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// enter user name
final userNameFinder = find.descendant(
@@ -81,12 +81,12 @@ void main() {
await tester.openSettingsPage(SettingsPage.user);
// verify icon
final userAvatarFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserAvatar),
);
final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, '😁');
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
// final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// verify name
final userNameFinder = find.descendant(
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -147,6 +149,49 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
});
testWidgets('create and duplicate calendar event', (tester) async {
const customTitle = "EventTitleCustom";
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create the calendar view
await tester.createNewPageWithNameUnderParent(
layout: ViewLayoutPB.Calendar,
);
// Scroll until today's date cell is visible
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell(
// Tap on create new event button
onHover: () async => tester.tapAddCalendarEventButton(),
);
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
tester.assertNumberOfEventsInCalendar(1);
// Change the title of the event
await tester.editEventTitle(customTitle);
// Duplicate event
final duplicateBtnFinder = find
.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byType(
FlowyIconButton,
),
)
.first;
await tester.tap(duplicateBtnFinder);
await tester.pumpAndSettle();
tester.assertNumberOfEventsInCalendar(2, title: customTitle);
});
testWidgets('rescheduling events', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -39,12 +39,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditFieldButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Checkbox);
await tester.dismissFieldEditor();
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox);
await tester.assertFieldTypeWithFieldName(
'Type',
@@ -9,7 +9,7 @@ import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('database filter', () {
group('grid filter:', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();
@@ -86,7 +86,7 @@ void main() {
testWidgets('add checkbox sort', (tester) async {
await tester.openV020database();
// create a filter
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -136,7 +136,7 @@ void main() {
testWidgets('add number sort', (tester) async {
await tester.openV020database();
// create a filter
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
@@ -188,7 +188,7 @@ void main() {
testWidgets('add checkbox and number sort', (tester) async {
await tester.openV020database();
// create a filter
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@@ -264,5 +264,111 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('reorder sort', (tester) async {
await tester.openV020database();
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
// open the sort menu and sort checkbox by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapSortByDescending();
// add another sort, this time by number descending
await tester.tapSortMenuInSettingBar();
await tester.tapCreateSortByFieldTypeInSortMenu(
FieldType.Number,
'number',
);
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
// check checkbox cell order
for (final (index, content) in <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check number cell order
for (final (index, content) in <String>[
'1',
'0.2',
'0.1',
'-1',
'-2',
'12',
'11',
'10',
'2',
'',
].indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// reorder sort
await tester.tapSortMenuInSettingBar();
await tester.reorderSort(
(FieldType.Number, 'number'),
(FieldType.Checkbox, 'Done'),
);
// check checkbox cell order
for (final (index, content) in <bool>[
false,
false,
false,
false,
true,
true,
true,
true,
true,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check the number cell order
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
});
});
}
@@ -1,4 +1,3 @@
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -10,7 +9,7 @@ void main() {
group('text direction', () {
testWidgets(
'''no text direction items will be displayed in the default/LTR mode,and three text direction items will be displayed in the RTL mode.''',
'''no text direction items will be displayed in the default/LTR mode, and three text direction items will be displayed when toggle is enabled.''',
(tester) async {
// combine the two tests into one to avoid the time-consuming process of initializing the app
await tester.initializeAppFlowy();
@@ -32,7 +31,7 @@ void main() {
'toolbar/text_direction_ltr',
'toolbar/text_direction_rtl',
];
// no text direction items in default/LTR mode
// no text direction items by default
var button = find.byWidgetPredicate(
(widget) =>
widget is SVGIconItemWidget &&
@@ -41,7 +40,7 @@ void main() {
expect(button, findsNothing);
// switch to the RTL mode
await tester.switchLayoutDirectionMode(LayoutDirection.rtlLayout);
await tester.toggleEnableRTLToolbarItems();
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.updateSelection(selection);
@@ -1,11 +1,18 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
const String heading1 = "Heading 1";
const String heading2 = "Heading 2";
const String heading3 = "Heading 3";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -33,12 +40,8 @@ void main() {
await tester.createNewPageWithNameUnderParent(
name: 'outline_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# Heading 1\n');
await tester.ime.insertText('## Heading 2\n');
await tester.ime.insertText('### Heading 3\n');
await insertHeadingComponent(tester);
/* Results in:
* # Heading 1
* ## Heading 2
@@ -51,7 +54,7 @@ void main() {
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1'),
matching: find.text(heading1),
),
findsOneWidget,
);
@@ -60,7 +63,7 @@ void main() {
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 2'),
matching: find.text(heading2),
),
findsOneWidget,
);
@@ -69,7 +72,7 @@ void main() {
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 3'),
matching: find.text(heading3),
),
findsOneWidget,
);
@@ -80,11 +83,86 @@ void main() {
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text('Heading 1Hello world'),
matching: find.text('${heading1}Hello world'),
),
findsOneWidget,
);
});
testWidgets("control the depth of outline block", (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(
name: 'outline_test',
);
await insertHeadingComponent(tester);
/* Results in:
* # Heading 1
* ## Heading 2
* ### Heading 3
*/
await tester.editor.tapLineOfEditorAt(3);
await insertOutlineInDocument(tester);
// expect to find only the `heading1` widget under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 1);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsNothing,
);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsNothing,
);
//////
/// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 2);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsNothing,
);
//////
// expect to find all the headings under the [OutlineBlockWidget]
await hoverAndClickDepthOptionAction(tester, [3], 3);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading1),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading2),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byType(OutlineBlockWidget),
matching: find.text(heading3),
),
findsOneWidget,
);
//////
});
});
}
@@ -97,3 +175,25 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
);
await tester.pumpAndSettle();
}
Future<void> hoverAndClickDepthOptionAction(
WidgetTester tester,
List<int> path,
int level,
) async {
await tester.editor.hoverAndClickOptionMenuButton([3]);
await tester.tap(find.byType(AppFlowyPopover).hitTestable().last);
await tester.pumpAndSettle();
// Find a total of 4 HoverButtons under the [BlockOptionButton],
// in addition to 3 HoverButtons under the [DepthOptionAction] - (child of BlockOptionButton)
await tester.tap(find.byType(HoverButton).hitTestable().at(3 + level));
await tester.pumpAndSettle();
}
Future<void> insertHeadingComponent(WidgetTester tester) async {
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('# $heading1\n');
await tester.ime.insertText('## $heading2\n');
await tester.ime.insertText('### $heading3\n');
}
@@ -0,0 +1,41 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/keyboard.dart';
import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// May be better to move this to an existing test but unsure what it fits with
group('Keyboard shortcuts related to emojis', () {
testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
expect(find.byType(EmojiSelectionMenu), findsNothing);
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.alt,
LogicalKeyboardKey.keyE,
],
tester: tester,
);
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
});
});
}
@@ -0,0 +1,107 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Grid Calculations', () {
testWidgets('add calculation and update cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Change one Field to Number
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
expect(find.text('Calculate'), findsOneWidget);
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
// Enter values in cells
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.Number,
input: '100',
);
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.Number,
input: '100',
);
// Dismiss edit cell
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('200'), findsOneWidget);
});
testWidgets('add calculations and remove row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Change two Fields to Number
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number);
expect(find.text('Calculate'), findsNWidgets(2));
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
await tester.changeCalculateAtIndex(2, CalculationType.Min);
// Enter values in cells
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.Number,
input: '100',
);
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.Number,
input: '150',
);
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.Number,
input: '50',
cellIndex: 1,
);
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.Number,
input: '100',
cellIndex: 1,
);
await tester.pumpAndSettle();
// Dismiss edit cell
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(find.text('250'), findsOneWidget);
expect(find.text('50'), findsNWidgets(2));
// Delete 1st row
await tester.hoverOnFirstRowOfGrid();
await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu();
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text('150'), findsNWidgets(2));
expect(find.text('100'), findsNWidgets(2));
});
});
}
@@ -23,6 +23,7 @@ import 'share_markdown_test.dart' as share_markdown_test;
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'switch_folder_test.dart' as switch_folder_test;
import 'tabs_test.dart' as tabs_test;
import 'emoji_shortcut_test.dart' as emoji_shortcut_test;
// import 'auth/supabase_auth_test.dart' as supabase_auth_test_runner;
/// The main task runner for all integration tests in AppFlowy.
@@ -69,6 +70,7 @@ Future<void> main() async {
// Others
hotkeys_test.main();
emoji_shortcut_test.main();
// Appearance integration test
appearance_test_runner.main();
@@ -306,7 +306,7 @@ extension CommonOperations on WidgetTester {
KVKeys.showRenameDialogWhenCreatingNewFile,
(value) => bool.parse(value),
);
final showRenameDialog = settingsOrFailure.fold(() => false, (r) => r);
final showRenameDialog = settingsOrFailure ?? false;
if (showRenameDialog) {
await tapOKButton();
}
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
@@ -720,6 +723,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> changeFieldTypeOfFieldWithName(
String name,
FieldType type,
) async {
await tapGridFieldWithName(name);
await tapEditFieldButton();
await tapSwitchFieldTypeButton();
await selectFieldType(type);
await dismissFieldEditor();
}
Future<void> changeCalculateAtIndex(int index, CalculationType type) async {
await tap(find.byType(CalculateCell).at(index));
await pumpAndSettle();
await tap(
find.descendant(
of: find.byType(CalculationTypeItem),
matching: find.text(type.label),
),
);
await pumpAndSettle();
}
/// Should call [tapGridFieldWithName] first.
Future<void> tapEditFieldButton() async {
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
@@ -1042,6 +1070,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findSortItem);
}
/// Must call [tapSortMenuInSettingBar] first.
Future<void> reorderSort(
(FieldType, String) from,
(FieldType, String) to,
) async {
final fromSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == from.$1 &&
widget.sortInfo.fieldInfo.name == from.$2,
);
final toSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == to.$1 &&
widget.sortInfo.fieldInfo.name == to.$2,
);
final dragElement = find.descendant(
of: fromSortItem,
matching: find.byType(ReorderableDragStartListener),
);
await drag(
dragElement,
getCenter(toSortItem) - getCenter(fromSortItem),
);
await pumpAndSettle(const Duration(milliseconds: 200));
}
/// Must call [tapSortButtonByName] first.
Future<void> tapSortByDescending() async {
await tapButton(
@@ -1060,7 +1116,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Must call [tapSortMenuInSettingBar] first.
Future<void> tapAllSortButton() async {
await tapButton(find.byType(DatabaseDeleteSortButton));
await tapButton(find.byType(DeleteAllSortsButton));
}
Future<void> scrollOptionFilterListByOffset(Offset offset) async {
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
@@ -241,4 +242,25 @@ class EditorOperations {
},
);
}
/// hover and click on the option menu button beside the block component.
Future<void> hoverAndClickOptionMenuButton(Path path) async {
final optionMenuButton = find.byWidgetPredicate(
(widget) =>
widget is BlockComponentActionWrapper &&
widget.node.path.equals(path),
);
await tester.hoverOnWidget(
optionMenuButton,
onHover: () async {
await tester.tapButton(
find.byWidgetPredicate(
(widget) =>
widget is BlockOptionButton &&
widget.blockComponentContext.node.path.equals(path),
),
);
},
);
}
}
@@ -1,12 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
@@ -76,31 +75,15 @@ extension AppFlowySettings on WidgetTester {
await pumpAndSettle();
}
// go to settings page and switch the layout direction
Future<void> switchLayoutDirectionMode(
LayoutDirection layoutDirection,
) async {
// go to settings page and toggle enable RTL toolbar items
Future<void> toggleEnableRTLToolbarItems() async {
await openSettings();
await openSettingsPage(SettingsPage.appearance);
final button = find.byKey(const ValueKey('layout_direction_option_button'));
expect(button, findsOneWidget);
await tapButton(button);
switch (layoutDirection) {
case LayoutDirection.ltrLayout:
final ltrButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_ltr.tr(),
);
await tapButton(ltrButton);
break;
case LayoutDirection.rtlLayout:
final rtlButton = find.text(
LocaleKeys.settings_appearance_layoutDirection_rtl.tr(),
);
await tapButton(rtlButton);
break;
}
final switchButton =
find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
expect(switchButton, findsOneWidget);
await tapButton(switchButton);
// tap anywhere to close the settings page
await tapAt(Offset.zero);
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
+1 -1
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+4 -4
View File
@@ -169,8 +169,8 @@ SPEC CHECKSUMS:
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: 13825b8a9334a850581300559b8839134b124670
@@ -189,6 +189,6 @@ SPEC CHECKSUMS:
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac
PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127
COCOAPODS: 1.12.1
COCOAPODS: 1.15.2
@@ -143,7 +143,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -330,7 +330,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -419,7 +419,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -468,7 +468,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,10 +1,9 @@
import 'package:dartz/dartz.dart';
import 'package:shared_preferences/shared_preferences.dart';
abstract class KeyValueStorage {
Future<void> set(String key, String value);
Future<Option<String>> get(String key);
Future<Option<T>> getWithFormat<T>(
Future<String?> get(String key);
Future<T?> getWithFormat<T>(
String key,
T Function(String value) formatter,
);
@@ -17,26 +16,26 @@ class DartKeyValue implements KeyValueStorage {
SharedPreferences get sharedPreferences => _sharedPreferences!;
@override
Future<Option<String>> get(String key) async {
Future<String?> get(String key) async {
await _initSharedPreferencesIfNeeded();
final value = sharedPreferences.getString(key);
if (value != null) {
return Some(value);
return value;
}
return none();
return null;
}
@override
Future<Option<T>> getWithFormat<T>(
Future<T?> getWithFormat<T>(
String key,
T Function(String value) formatter,
) async {
final value = await get(key);
return value.fold(
() => none(),
(s) => Some(formatter(s)),
);
if (value == null) {
return null;
}
return formatter(value);
}
@override
@@ -49,6 +49,13 @@ class KVKeys {
static const String kCloudType = 'kCloudType';
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
static const String kSupabaseURL = 'kSupbaseURL';
static const String kSupabaseURL = 'kSupabaseURL';
static const String kSupabaseAnonKey = 'kSupabaseAnonKey';
/// The key for saving the text scale factor.
///
/// The value is a double string.
/// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause
/// the text to be too large and not aligned with the icon
static const String textScaleFactor = 'textScaleFactor';
}
@@ -1,2 +1 @@
export 'target_platform.dart';
export 'url_validator.dart';
@@ -0,0 +1,50 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;
typedef OnFailureCallback = void Function(Uri uri);
Future<bool> afLaunchUrl(
Uri uri, {
BuildContext? context,
OnFailureCallback? onFailure,
launcher.LaunchMode mode = launcher.LaunchMode.platformDefault,
String? webOnlyWindowName,
}) async {
try {
return await launcher.launchUrl(
uri,
mode: mode,
webOnlyWindowName: webOnlyWindowName,
);
} on PlatformException catch (e) {
Log.error("Failed to open uri: $e");
if (onFailure != null) {
onFailure(uri);
} else {
showMessageToast(
LocaleKeys.failedToOpenUrl.tr(args: [e.message ?? "PlatformException"]),
context: context,
);
}
}
return false;
}
Future<void> afLaunchUrlString(String url) async {
try {
final uri = Uri.parse(url);
await launcher.launchUrl(uri);
} on PlatformException catch (e) {
Log.error("Failed to open uri: $e");
} on FormatException catch (e) {
Log.error("Failed to parse url: $e");
}
}
@@ -1,21 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'url_validator.freezed.dart';
Either<UriFailure, Uri> parseValidUrl(String url) {
try {
final uri = Uri.parse(url);
if (uri.scheme.isEmpty || uri.host.isEmpty) {
return left(const UriFailure.invalidSchemeHost());
}
return right(uri);
} on FormatException {
return left(const UriFailure.invalidUriFormat());
}
}
@freezed
class UriFailure with _$UriFailure {
const factory UriFailure.invalidSchemeHost() = _InvalidSchemeHost;
const factory UriFailure.invalidUriFormat() = _InvalidUriFormat;
}
@@ -3,11 +3,11 @@ import 'dart:typed_data';
import 'package:appflowy/core/notification/notification_helper.dart';
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_result/appflowy_result.dart';
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
Either<Uint8List, FlowyError>,
FlowyResult<Uint8List, FlowyError>,
);
class DocumentNotificationParser
@@ -1,17 +1,18 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// Folder
typedef FolderNotificationCallback = void Function(
FolderNotification,
Either<Uint8List, FlowyError>,
FlowyResult<Uint8List, FlowyError>,
);
class FolderNotificationParser
@@ -27,7 +28,7 @@ class FolderNotificationParser
typedef FolderNotificationHandler = Function(
FolderNotification ty,
Either<Uint8List, FlowyError> result,
FlowyResult<Uint8List, FlowyError> result,
);
class FolderNotificationListener {
@@ -1,17 +1,18 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// DatabasePB
typedef DatabaseNotificationCallback = void Function(
DatabaseNotification,
Either<Uint8List, FlowyError>,
FlowyResult<Uint8List, FlowyError>,
);
class DatabaseNotificationParser
@@ -27,7 +28,7 @@ class DatabaseNotificationParser
typedef DatabaseNotificationHandler = Function(
DatabaseNotification ty,
Either<Uint8List, FlowyError> result,
FlowyResult<Uint8List, FlowyError> result,
);
class DatabaseNotificationListener {
@@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_result/appflowy_result.dart';
class NotificationParser<T, E> {
NotificationParser({
@@ -11,7 +12,7 @@ class NotificationParser<T, E> {
});
String? id;
void Function(T, Either<Uint8List, E>) callback;
void Function(T, FlowyResult<Uint8List, E>) callback;
E Function(Uint8List) errorParser;
T? Function(int) tyParser;
@@ -30,10 +31,10 @@ class NotificationParser<T, E> {
if (subject.hasError()) {
final bytes = Uint8List.fromList(subject.error);
final error = errorParser(bytes);
callback(ty, right(error));
callback(ty, FlowyResult.failure(error));
} else {
final bytes = Uint8List.fromList(subject.payload);
callback(ty, left(bytes));
callback(ty, FlowyResult.success(bytes));
}
}
}
@@ -1,17 +1,18 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// User
typedef UserNotificationCallback = void Function(
UserNotification,
Either<Uint8List, FlowyError>,
FlowyResult<Uint8List, FlowyError>,
);
class UserNotificationParser
@@ -27,7 +28,7 @@ class UserNotificationParser
typedef UserNotificationHandler = Function(
UserNotification ty,
Either<Uint8List, FlowyError> result,
FlowyResult<Uint8List, FlowyError> result,
);
class UserNotificationListener {
@@ -1,19 +0,0 @@
import 'package:flutter/services.dart';
extension RawKeyboardExtension on RawKeyboard {
bool get isAltPressed => keysPressed.any(
(key) => [
LogicalKeyboardKey.alt,
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
].contains(key),
);
bool get isControlPressed => keysPressed.any(
(key) => [
LogicalKeyboardKey.control,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
].contains(key),
);
}
@@ -1,19 +1,25 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-date/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_result/appflowy_result.dart';
class DateService {
static Future<Either<FlowyError, DateTime>> queryDate(String search) async {
static Future<FlowyResult<DateTime, FlowyError>> queryDate(
String search,
) async {
final query = DateQueryPB.create()..query = search;
final result = (await DateEventQueryDate(query).send()).swap();
return result.fold((l) => left(l), (r) {
final date = DateTime.tryParse(r.date);
if (date != null) {
return right(date);
}
return left(FlowyError(msg: 'Could not parse Date (NLP) from String'));
});
final result = await DateEventQueryDate(query).send();
return result.fold(
(s) {
final date = DateTime.tryParse(s.date);
if (date != null) {
return FlowyResult.success(date);
}
return FlowyResult.failure(
FlowyError(msg: 'Could not parse Date (NLP) from String'),
);
},
(e) => FlowyResult.failure(e),
);
}
}
+40 -41
View File
@@ -4,7 +4,6 @@ import 'package:appflowy/env/backend_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:dartz/dartz.dart';
/// Sets the cloud type for the application.
///
@@ -52,13 +51,16 @@ const String kAppflowyCloudUrl = "https://beta.appflowy.cloud";
///
Future<AuthenticatorType> getAuthenticatorType() async {
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
if (value.isNone() && !integrationMode().isUnitTest) {
if (value == null && !integrationMode().isUnitTest) {
// if the cloud type is not set, then set it to AppFlowy Cloud as default.
await useAppFlowyBetaCloudWithURL(kAppflowyCloudUrl);
await useAppFlowyBetaCloudWithURL(
kAppflowyCloudUrl,
AuthenticatorType.appflowyCloud,
);
return AuthenticatorType.appflowyCloud;
}
switch (value.getOrElse(() => "0")) {
switch (value ?? "0") {
case "0":
return AuthenticatorType.local;
case "1":
@@ -70,7 +72,10 @@ Future<AuthenticatorType> getAuthenticatorType() async {
case "4":
return AuthenticatorType.appflowyCloudDevelop;
default:
await useAppFlowyBetaCloudWithURL(kAppflowyCloudUrl);
await useAppFlowyBetaCloudWithURL(
kAppflowyCloudUrl,
AuthenticatorType.appflowyCloud,
);
return AuthenticatorType.appflowyCloud;
}
}
@@ -171,21 +176,21 @@ AuthenticatorType currentCloudType() {
return getIt<AppFlowyCloudSharedEnv>().authenticatorType;
}
Future<void> _setAppFlowyCloudUrl(Option<String> url) async {
await url.fold(
() => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, ""),
(s) => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, s),
);
Future<void> _setAppFlowyCloudUrl(String? url) async {
await getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, url ?? '');
}
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
await _setAppFlowyCloudUrl(Some(url));
await _setAppFlowyCloudUrl(url);
}
Future<void> useAppFlowyBetaCloudWithURL(String url) async {
await _setAuthenticatorType(AuthenticatorType.appflowyCloud);
await _setAppFlowyCloudUrl(Some(url));
Future<void> useAppFlowyBetaCloudWithURL(
String url,
AuthenticatorType authenticatorType,
) async {
await _setAuthenticatorType(authenticatorType);
await _setAppFlowyCloudUrl(url);
}
Future<void> useLocalServer() async {
@@ -197,7 +202,7 @@ Future<void> useSupabaseCloud({
required String anonKey,
}) async {
await _setAuthenticatorType(AuthenticatorType.supabase);
await setSupbaseServer(Some(url), Some(anonKey));
await setSupabaseServer(url, anonKey);
}
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
@@ -305,10 +310,7 @@ Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig(
Future<String> getAppFlowyCloudUrl() async {
final result =
await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);
return result.fold(
() => kAppflowyCloudUrl,
(url) => url,
);
return result ?? kAppflowyCloudUrl;
}
Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
@@ -330,27 +332,30 @@ Future<String> _getAppFlowyCloudGotrueUrl(String baseURL) async {
return "$baseURL/gotrue";
}
Future<void> setSupbaseServer(
Option<String> url,
Option<String> anonKey,
Future<void> setSupabaseServer(
String? url,
String? anonKey,
) async {
assert(
(url.isSome() && anonKey.isSome()) || (url.isNone() && anonKey.isNone()),
(url != null && anonKey != null) || (url == null && anonKey == null),
"Either both Supabase URL and anon key must be set, or both should be unset",
);
await url.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL),
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, s),
);
await anonKey.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey),
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, s),
);
if (url == null) {
await getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL);
} else {
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, url);
}
if (anonKey == null) {
await getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey);
} else {
await getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, anonKey);
}
}
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
final url = await _getSupbaseUrl();
final url = await _getSupabaseUrl();
final anonKey = await _getSupabaseAnonKey();
return SupabaseConfiguration(
url: url,
@@ -358,18 +363,12 @@ Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
);
}
Future<String> _getSupbaseUrl() async {
Future<String> _getSupabaseUrl() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
return result.fold(
() => "",
(url) => url,
);
return result ?? '';
}
Future<String> _getSupabaseAnonKey() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
return result.fold(
() => "",
(url) => url,
);
return result ?? '';
}
@@ -31,8 +31,8 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
);
final userProfile = userOrFailure.fold(
(error) => null,
(userProfilePB) => userProfilePB,
(error) => null,
);
if (workspaceSetting == null || userProfile == null) {
@@ -8,14 +8,17 @@ class AppBarBackButton extends StatelessWidget {
const AppBarBackButton({
super.key,
this.onTap,
this.padding,
});
final VoidCallback? onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => Navigator.pop(context),
padding: padding,
child: const FlowySvg(
FlowySvgs.m_app_bar_back_s,
),
@@ -73,10 +76,10 @@ class AppBarDoneButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
isActionButton: true,
onTap: onTap,
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
child: FlowyText(
LocaleKeys.button_Done.tr(),
LocaleKeys.button_done.tr(),
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
textAlign: TextAlign.right,
@@ -85,6 +88,39 @@ class AppBarDoneButton extends StatelessWidget {
}
}
class AppBarSaveButton extends StatelessWidget {
const AppBarSaveButton({
super.key,
required this.onTap,
this.enable = true,
this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12),
});
final VoidCallback onTap;
final bool enable;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: () {
if (enable) {
onTap();
}
},
padding: padding,
child: FlowyText(
LocaleKeys.button_save.tr(),
color: enable
? Theme.of(context).colorScheme.primary
: Theme.of(context).disabledColor,
fontWeight: FontWeight.w500,
textAlign: TextAlign.right,
),
);
}
}
class AppBarFilledDoneButton extends StatelessWidget {
const AppBarFilledDoneButton({super.key, required this.onTap});
@@ -129,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
isActionButton: true,
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
onTap: () => onTap(context),
child: const FlowySvg(FlowySvgs.three_dots_s),
);
@@ -139,28 +175,22 @@ class AppBarMoreButton extends StatelessWidget {
class AppBarButton extends StatelessWidget {
const AppBarButton({
super.key,
this.isActionButton = false,
required this.onTap,
required this.child,
this.padding,
});
static const defaultWidth = 40.0;
final VoidCallback onTap;
final Widget child;
final bool isActionButton;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: EdgeInsets.only(
top: 12.0,
bottom: 12.0,
left: 12.0,
right: isActionButton ? 12.0 : 8.0,
),
padding: padding ?? const EdgeInsets.all(12),
child: child,
),
);
@@ -13,7 +13,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:dartz/dartz.dart' hide State;
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -40,7 +40,7 @@ class MobileViewPage extends StatefulWidget {
}
class _MobileViewPageState extends State<MobileViewPage> {
late final Future<Either<ViewPB, FlowyError>> future;
late final Future<FlowyResult<ViewPB, FlowyError>> future;
@override
void initState() {
@@ -71,10 +71,9 @@ class _MobileViewPageState extends State<MobileViewPage> {
body = state.data!.fold((view) {
viewPB = view;
actions.add(_buildAppBarMoreButton(view));
return view
.plugin(arguments: widget.arguments ?? const {})
.widgetBuilder
.buildWidget(shrinkWrap: false);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
}, (error) {
return FlowyMobileStateContainer.error(
emoji: '😔',
@@ -7,3 +7,4 @@ export 'bottom_sheet_view_item_body.dart';
export 'bottom_sheet_view_page.dart';
export 'default_mobile_action_pane.dart';
export 'show_mobile_bottom_sheet.dart';
export 'show_transition_bottom_sheet.dart';
@@ -44,7 +44,6 @@ class _MobileBottomSheetEditLinkWidgetState
void dispose() {
textController.dispose();
hrefController.dispose();
super.dispose();
}
@@ -49,7 +49,7 @@ class BottomSheetHeader extends StatelessWidget {
color: Color(0xFF00BCF0),
),
text: FlowyText.medium(
LocaleKeys.button_Done.tr(),
LocaleKeys.button_done.tr(),
color: Colors.white,
fontSize: 16.0,
),
@@ -27,14 +27,16 @@ class _MobileBottomSheetRenameWidgetState
@override
void initState() {
super.initState();
controller = TextEditingController(text: widget.name);
controller = TextEditingController(text: widget.name)
..selection = TextSelection(
baseOffset: 0,
extentOffset: widget.name.length,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@@ -1,7 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/separated_flex.dart';
import 'package:flutter/material.dart';
enum MobileViewItemBottomSheetBodyAction {
@@ -25,55 +26,46 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
return SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.stretch,
separatorBuilder: () => const Divider(
height: 8.5,
thickness: 0.5,
),
children: [
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_rename.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_rename_s,
),
showTopBorder: false,
icon: FlowySvgs.m_rename_s,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.rename,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: isFavorite
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
leftIcon: FlowySvg(
size: const Size(20, 20),
isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
color: isFavorite ? Colors.yellow : null,
),
showTopBorder: false,
icon: isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
iconColor: isFavorite ? Colors.yellow : null,
onTap: () => onAction(
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_duplicate_s,
),
showTopBorder: false,
icon: FlowySvgs.m_duplicate_s,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.duplicate,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_delete_s,
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
icon: FlowySvgs.m_delete_s,
iconColor: Theme.of(context).colorScheme.error,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.delete,
),
@@ -1,9 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
enum MobileViewBottomSheetBodyAction {
@@ -84,55 +85,46 @@ class MobileViewBottomSheetBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isFavorite = view.isFavorite;
return Column(
return SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.stretch,
separatorBuilder: () => const Divider(
height: 8.5,
thickness: 0.5,
),
children: [
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_rename.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_rename_s,
),
showTopBorder: false,
icon: FlowySvgs.m_rename_s,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.rename,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: isFavorite
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
leftIcon: FlowySvg(
size: const Size(20, 20),
isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
color: isFavorite ? Colors.yellow : null,
),
showTopBorder: false,
icon: isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
iconColor: isFavorite ? Colors.yellow : null,
onTap: () => onAction(
isFavorite
? MobileViewBottomSheetBodyAction.removeFromFavorites
: MobileViewBottomSheetBodyAction.addToFavorites,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(
FlowySvgs.m_duplicate_s,
),
showTopBorder: false,
icon: FlowySvgs.m_duplicate_s,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.duplicate,
),
),
FlowyOptionTile.text(
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_delete_s,
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
icon: FlowySvgs.m_delete_s,
iconColor: Theme.of(context).colorScheme.error,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.delete,
),
@@ -1,9 +1,27 @@
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
extension BottomSheetPaddingExtension on BuildContext {
/// Calculates the total amount of space that should be added to the bottom of
/// a bottom sheet
double bottomSheetPadding({
bool ignoreViewPadding = true,
}) {
final viewPadding = MediaQuery.viewPaddingOf(this);
final viewInsets = MediaQuery.viewInsetsOf(this);
double bottom = 0.0;
if (!ignoreViewPadding) {
bottom += viewPadding.bottom;
}
// for screens with 0 view padding, add some even more space
bottom += viewPadding.bottom == 0 ? 28.0 : 16.0;
bottom += viewInsets.bottom;
return bottom;
}
}
Future<T?> showMobileBottomSheet<T>(
BuildContext context, {
required WidgetBuilder builder,
@@ -16,7 +34,6 @@ Future<T?> showMobileBottomSheet<T>(
bool showCloseButton = false,
// this field is only used if showHeader is true
String title = '',
bool resizeToAvoidBottomInset = true,
bool isScrollControlled = true,
bool showDivider = true,
bool useRootNavigator = false,
@@ -42,7 +59,7 @@ Future<T?> showMobileBottomSheet<T>(
shape ??= const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Corners.s12Radius,
top: Radius.circular(16),
),
);
@@ -68,23 +85,20 @@ Future<T?> showMobileBottomSheet<T>(
final Widget child = builder(context);
// if the children is only one, we don't need to wrap it with a column
if (!showDragHandle &&
!showHeader &&
!showDivider &&
!resizeToAvoidBottomInset) {
if (!showDragHandle && !showHeader && !showDivider) {
return child;
}
// ----- header area -----
if (showDragHandle) {
children.add(
const DragHandler(),
const DragHandle(),
);
}
if (showHeader) {
children.add(
_Header(
BottomSheetHeader(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
@@ -113,9 +127,11 @@ Future<T?> showMobileBottomSheet<T>(
children: [
...children,
Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: child,
child: Scrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
),
],
@@ -125,32 +141,20 @@ Future<T?> showMobileBottomSheet<T>(
}
// ----- content area -----
if (resizeToAvoidBottomInset) {
children.add(
Padding(
padding: EdgeInsets.only(
top: padding.top,
left: padding.left,
right: padding.right,
bottom: padding.bottom + MediaQuery.of(context).viewInsets.bottom,
),
child: child,
),
);
} else {
children.add(child);
}
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
// ----- content area -----
if (children.length == 1) {
return children.first;
}
// add default padding
children.add(
VSpace(MediaQuery.of(context).padding.bottom == 0 ? 28.0 : 16.0),
);
return useSafeArea
? SafeArea(
child: Column(
@@ -166,8 +170,9 @@ Future<T?> showMobileBottomSheet<T>(
);
}
class _Header extends StatelessWidget {
const _Header({
class BottomSheetHeader extends StatelessWidget {
const BottomSheetHeader({
super.key,
required this.showBackButton,
required this.showCloseButton,
required this.title,
@@ -0,0 +1,345 @@
import 'dart:math' as math;
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sheet/route.dart';
import 'package:sheet/sheet.dart';
import 'show_mobile_bottom_sheet.dart';
Future<T?> showTransitionMobileBottomSheet<T>(
BuildContext context, {
required WidgetBuilder builder,
bool useRootNavigator = false,
EdgeInsets contentPadding = EdgeInsets.zero,
Color? backgroundColor,
// drag handle
bool showDragHandle = false,
// header
bool showHeader = false,
String title = '',
bool showBackButton = false,
bool showCloseButton = false,
bool showDoneButton = false,
bool showDivider = true,
// stops
double initialStop = 1.0,
List<double>? stops,
}) {
assert(
showHeader ||
title.isEmpty &&
!showCloseButton &&
!showBackButton &&
!showDoneButton &&
!showDivider,
);
assert(!(showCloseButton && showBackButton));
backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB)
: const Color(0xFF626364);
return Navigator.of(
context,
rootNavigator: useRootNavigator,
).push<T>(
TransitionSheetRoute<T>(
backgroundColor: backgroundColor,
initialStop: initialStop,
stops: stops,
builder: (context) {
final Widget child = builder(context);
// if the children is only one, we don't need to wrap it with a column
if (!showDragHandle && !showHeader) {
return child;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showDragHandle) const DragHandle(),
if (showHeader) ...[
BottomSheetHeader(
showCloseButton: showCloseButton,
showBackButton: showBackButton,
showDoneButton: showDoneButton,
title: title,
),
if (showDivider)
const Divider(
height: 0.5,
thickness: 0.5,
),
],
Expanded(
child: Padding(
padding: contentPadding,
child: child,
),
),
],
);
},
),
);
}
/// The top offset that will be displayed from the bottom route
const double _kPreviousRouteVisibleOffset = 10.0;
/// Minimal distance from the top of the screen to the top of the previous route
/// It will be used ff the top safe area is less than this value.
/// In iPhones the top SafeArea is more or equal to this distance.
const double _kSheetMinimalOffset = 10;
const Curve _kCupertinoSheetCurve = Curves.easeOutExpo;
const Curve _kCupertinoTransitionCurve = Curves.linear;
/// Wraps the child into a cupertino modal sheet appearance. This is used to
/// create a [SheetRoute].
///
/// Clip the child widget to rectangle with top rounded corners and adds
/// top padding and top safe area.
class _CupertinoSheetDecorationBuilder extends StatelessWidget {
const _CupertinoSheetDecorationBuilder({
required this.child,
required this.topRadius,
this.backgroundColor,
});
/// The child contained by the modal sheet
final Widget child;
/// The color to paint behind the child
final Color? backgroundColor;
/// The top corners of this modal sheet are rounded by this Radius
final Radius topRadius;
@override
Widget build(BuildContext context) {
return Builder(
builder: (BuildContext context) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: topRadius),
color: backgroundColor,
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: child,
),
);
},
);
}
}
/// Customized CupertinoSheetRoute from the sheets package
///
/// A modal route that overlays a widget over the current route and animates
/// it from the bottom with a cupertino modal sheet appearance
///
/// Clip the child widget to rectangle with top rounded corners and adds
/// top padding and top safe area.
class TransitionSheetRoute<T> extends SheetRoute<T> {
TransitionSheetRoute({
required WidgetBuilder builder,
super.stops,
double initialStop = 1.0,
super.settings,
Color? backgroundColor,
super.maintainState = true,
super.fit,
}) : super(
builder: (BuildContext context) {
return _CupertinoSheetDecorationBuilder(
backgroundColor: backgroundColor,
topRadius: const Radius.circular(16),
child: Builder(builder: builder),
);
},
animationCurve: _kCupertinoSheetCurve,
initialExtent: initialStop,
);
@override
bool get draggable => true;
final SheetController _sheetController = SheetController();
@override
SheetController createSheetController() => _sheetController;
@override
Color? get barrierColor => Colors.transparent;
@override
bool get barrierDismissible => true;
@override
Widget buildSheet(BuildContext context, Widget child) {
final effectivePhysics = draggable
? BouncingSheetPhysics(
parent: SnapSheetPhysics(
stops: stops ?? <double>[0, 1],
parent: physics,
),
)
: const NeverDraggableSheetPhysics();
final MediaQueryData mediaQuery = MediaQuery.of(context);
final double topMargin =
math.max(_kSheetMinimalOffset, mediaQuery.padding.top) +
_kPreviousRouteVisibleOffset;
return Sheet.raw(
initialExtent: initialExtent,
decorationBuilder: decorationBuilder,
fit: fit,
maxExtent: mediaQuery.size.height - topMargin,
physics: effectivePhysics,
controller: sheetController,
child: child,
);
}
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final double topPadding = MediaQuery.of(context).padding.top;
final double topOffset = math.max(_kSheetMinimalOffset, topPadding);
return AnimatedBuilder(
animation: secondaryAnimation,
child: child,
builder: (BuildContext context, Widget? child) {
final double progress = secondaryAnimation.value;
final double scale = 1 - progress / 10;
final double distanceWithScale =
(topOffset + _kPreviousRouteVisibleOffset) * 0.9;
final Offset offset =
Offset(0, progress * (topOffset - distanceWithScale));
return Transform.translate(
offset: offset,
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: child,
),
);
},
);
}
@override
bool canDriveSecondaryTransitionForPreviousRoute(
Route<dynamic> previousRoute,
) =>
true;
@override
Widget buildSecondaryTransitionForPreviousRoute(
BuildContext context,
Animation<double> secondaryAnimation,
Widget child,
) {
final Animation<double> delayAnimation = CurvedAnimation(
parent: _sheetController.animation,
curve: Interval(
initialExtent == 1 ? 0 : initialExtent,
1,
),
);
final Animation<double> secondaryAnimation = CurvedAnimation(
parent: _sheetController.animation,
curve: Interval(
0,
initialExtent,
),
);
return CupertinoSheetBottomRouteTransition(
body: child,
sheetAnimation: delayAnimation,
secondaryAnimation: secondaryAnimation,
);
}
}
/// Animation for previous route when a [TransitionSheetRoute] enters/exits
@visibleForTesting
class CupertinoSheetBottomRouteTransition extends StatelessWidget {
const CupertinoSheetBottomRouteTransition({
super.key,
required this.sheetAnimation,
required this.secondaryAnimation,
required this.body,
});
final Widget body;
final Animation<double> sheetAnimation;
final Animation<double> secondaryAnimation;
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top;
final double topOffset = math.max(_kSheetMinimalOffset, topPadding);
final CurvedAnimation curvedAnimation = CurvedAnimation(
parent: sheetAnimation,
curve: _kCupertinoTransitionCurve,
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: AnimatedBuilder(
animation: secondaryAnimation,
child: body,
builder: (BuildContext context, Widget? child) {
final double progress = curvedAnimation.value;
final double scale = 1 - progress / 10;
return Stack(
children: <Widget>[
Container(color: Colors.black),
Transform.translate(
offset: Offset(0, progress * topOffset),
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.lerp(
Radius.zero,
const Radius.circular(16.0),
progress,
)!,
),
child: ColorFiltered(
colorFilter: ColorFilter.mode(
(Theme.of(context).brightness == Brightness.dark
? Colors.grey
: Colors.black)
.withOpacity(secondaryAnimation.value * 0.1),
BlendMode.srcOver,
),
child: child,
),
),
),
),
],
);
},
),
);
}
}
@@ -38,6 +38,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
scrollController = ScrollController();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
@@ -8,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -107,46 +109,43 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
splashRadius: 5,
onPressed: () => showMobileBottomSheet(
context,
title: LocaleKeys.board_column_groupActions.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
return Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.edit_s,
text: LocaleKeys.board_column_renameColumn.tr(),
onTap: () {
context.read<BoardBloc>().add(
BoardEvent.startEditingHeader(
widget.groupData.id,
),
);
context.pop();
},
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.hide_s,
text: LocaleKeys.board_column_hideColumn.tr(),
onTap: () {
context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
widget.groupData.customData.group
as GroupPB,
false,
),
);
context.pop();
},
),
),
],
);
},
showDragHandle: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) => SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.stretch,
separatorBuilder: () => const Divider(
height: 8.5,
thickness: 0.5,
),
children: [
MobileQuickActionButton(
text: LocaleKeys.board_column_renameColumn.tr(),
icon: FlowySvgs.edit_s,
onTap: () {
context.read<BoardBloc>().add(
BoardEvent.startEditingHeader(
widget.groupData.id,
),
);
context.pop();
},
),
MobileQuickActionButton(
text: LocaleKeys.board_column_hideColumn.tr(),
icon: FlowySvgs.hide_s,
onTap: () {
context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
widget.groupData.customData.group
as GroupPB,
false,
),
);
context.pop();
},
),
],
),
),
),
IconButton(
@@ -19,6 +19,12 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
bool isEditing = false;
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
@@ -81,6 +81,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
child: Scaffold(
appBar: FlowyAppBar(
leadingType: FlowyAppBarLeadingType.close,
showDivider: false,
actions: [
AppBarMoreButton(
onTap: (_) => _showCardActions(context),
@@ -135,54 +136,44 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MobileQuickActionButton(
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
return;
}
RowBackendService.duplicateRow(viewId, rowId);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDuplicated.tr(),
gravity: ToastGravity.BOTTOM,
);
},
icon: FlowySvgs.copy_s,
text: LocaleKeys.button_duplicate.tr(),
),
MobileQuickActionButton(
onTap: () =>
_performAction(viewId, _bloc.state.currentRowId, false),
icon: FlowySvgs.copy_s,
text: LocaleKeys.button_duplicate.tr(),
),
const Divider(height: 9),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MobileQuickActionButton(
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
return;
}
RowBackendService.deleteRow(viewId, rowId);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDeleted.tr(),
gravity: ToastGravity.BOTTOM,
);
},
icon: FlowySvgs.m_delete_m,
text: LocaleKeys.button_delete.tr(),
color: Theme.of(context).colorScheme.error,
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.m_delete_m,
iconColor: Theme.of(context).colorScheme.error,
),
],
),
);
}
void _performAction(String viewId, String? rowId, bool deleteRow) {
if (rowId == null) {
return;
}
deleteRow
? RowBackendService.deleteRow(viewId, rowId)
: RowBackendService.duplicateRow(viewId, rowId);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: deleteRow
? LocaleKeys.board_cardDeleted.tr()
: LocaleKeys.board_cardDuplicated.tr(),
gravity: ToastGravity.BOTTOM,
);
}
}
class RowDetailFab extends StatelessWidget {
@@ -40,7 +40,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
LocaleKeys.grid_field_newProperty.tr(),
fontSize: 15,
),
onPressed: () => showCreateFieldBottomSheet(context, viewId),
onPressed: () => mobileCreateFieldWorkflow(context, viewId),
icon: const FlowySvg(FlowySvgs.add_m),
),
);
@@ -1,7 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class OptionTextField extends StatelessWidget {
@@ -10,31 +13,43 @@ class OptionTextField extends StatelessWidget {
required this.controller,
required this.type,
required this.onTextChanged,
required this.onFieldTypeChanged,
});
final TextEditingController controller;
final FieldType type;
final void Function(String value) onTextChanged;
final void Function(FieldType value) onFieldTypeChanged;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: controller,
autofocus: true,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: onTextChanged,
leftIcon: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: type.mobileIconBackgroundColor,
),
child: Center(
child: FlowySvg(
type.svgData,
blendMode: null,
size: const Size.square(22),
leftIcon: GestureDetector(
onTap: () async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_editProperty.tr(),
);
if (fieldType != null) {
onFieldTypeChanged(fieldType);
}
},
child: Container(
height: 38,
width: 38,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: type.mobileIconBackgroundColor,
),
child: Center(
child: FlowySvg(
type.svgData,
blendMode: null,
size: const Size.square(22),
),
),
),
),
@@ -61,7 +61,7 @@ class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
children: [
ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: const Center(child: DragHandler()),
child: const Center(child: DragHandle()),
),
const MobileDateHeader(),
_buildDatePicker(),
@@ -1,9 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/plugins/database/application/field/field_backend_service.dart';
import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -61,7 +61,7 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
body: MobileFieldEditor(
mode: FieldOptionMode.edit,
isPrimary: widget.field.isPrimary,
defaultValues: _fieldOptionValues,
defaultValues: FieldOptionValues.fromField(field: widget.field.field),
actions: [
widget.field.fieldSettings?.visibility.isVisibleState() ?? true
? FieldOptionAction.hide
@@ -69,9 +69,25 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
FieldOptionAction.duplicate,
FieldOptionAction.delete,
],
onOptionValuesChanged: (newFieldOptionValues) {
onOptionValuesChanged: (fieldOptionValues) async {
await fieldService.updateField(name: fieldOptionValues.name);
await FieldBackendService.updateFieldType(
viewId: widget.viewId,
fieldId: widget.field.id,
fieldType: fieldOptionValues.type,
);
final data = fieldOptionValues.getTypeOptionData();
if (data != null) {
await FieldBackendService.updateFieldTypeOption(
viewId: widget.viewId,
fieldId: widget.field.id,
typeOptionData: data,
);
}
setState(() {
_fieldOptionValues = newFieldOptionValues;
_fieldOptionValues = fieldOptionValues;
});
},
onAction: (action) {
@@ -29,23 +29,18 @@ const mobileSupportedFieldTypes = [
FieldType.Checklist,
];
/// Shows the field type grid and upon selection, allow users to edit the
/// field's properties and saving it when the user clicks save.
void showCreateFieldBottomSheet(
BuildContext context,
String viewId, {
OrderObjectPositionPB? position,
Future<FieldType?> showFieldTypeGridBottomSheet(
BuildContext context, {
required String title,
}) {
showMobileBottomSheet(
return showMobileBottomSheet<FieldType>(
context,
padding: EdgeInsets.zero,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
elevation: 20,
title: LocaleKeys.grid_field_newProperty.tr(),
title: title,
backgroundColor: Theme.of(context).colorScheme.surface,
barrierColor: Colors.transparent,
enableDraggableScrollable: true,
builder: (context) {
final typeOptionMenuItemValue = mobileSupportedFieldTypes
@@ -55,24 +50,8 @@ void showCreateFieldBottomSheet(
backgroundColor: fieldType.mobileIconBackgroundColor,
text: fieldType.i18n,
icon: fieldType.svgData,
onTap: (_, fieldType) async {
final optionValues = await context.push<FieldOptionValues>(
Uri(
path: MobileNewPropertyScreen.routeName,
queryParameters: {
MobileNewPropertyScreen.argViewId: viewId,
MobileNewPropertyScreen.argFieldTypeId:
fieldType.value.toString(),
},
).toString(),
);
if (optionValues != null) {
await optionValues.create(viewId: viewId, position: position);
if (context.mounted) {
context.pop();
}
}
},
onTap: (context, fieldType) =>
Navigator.of(context).pop(fieldType),
),
)
.toList();
@@ -87,6 +66,37 @@ void showCreateFieldBottomSheet(
);
}
/// Shows the field type grid and upon selection, allow users to edit the
/// field's properties and saving it when the user clicks save.
void mobileCreateFieldWorkflow(
BuildContext context,
String viewId, {
OrderObjectPositionPB? position,
}) async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
title: LocaleKeys.grid_field_newProperty.tr(),
);
if (fieldType == null || !context.mounted) {
return;
}
final optionValues = await context.push<FieldOptionValues>(
Uri(
path: MobileNewPropertyScreen.routeName,
queryParameters: {
MobileNewPropertyScreen.argViewId: viewId,
MobileNewPropertyScreen.argFieldTypeId: fieldType.value.toString(),
},
).toString(),
);
if (optionValues != null) {
await optionValues.create(viewId: viewId, position: position);
}
if (context.mounted) {
context.pop();
}
}
/// Used to edit a field.
Future<FieldOptionValues?> showEditFieldScreen(
BuildContext context,
@@ -106,18 +116,18 @@ Future<FieldOptionValues?> showEditFieldScreen(
void showQuickEditField(
BuildContext context,
String viewId,
FieldController fieldController,
FieldInfo fieldInfo,
) {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
resizeToAvoidBottomInset: true,
showDragHandle: true,
builder: (context) {
return SingleChildScrollView(
child: QuickEditField(
viewId: viewId,
fieldController: fieldController,
fieldInfo: fieldInfo,
),
);
@@ -47,7 +47,7 @@ class _MobileFieldPickerListState extends State<MobileFieldPickerList> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const DragHandler(),
const DragHandle(),
_Header(
title: widget.title,
onDone: (context) => context.pop(newFieldId),
@@ -5,16 +5,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/base/option_color_list.dart';
import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
@@ -196,7 +194,6 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
@override
void initState() {
super.initState();
values = widget.defaultValues;
controller.text = values.name;
}
@@ -204,7 +201,6 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
@override
void dispose() {
controller.dispose();
super.dispose();
}
@@ -225,6 +221,18 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
isFieldNameChanged = true;
_updateOptionValues(name: value);
},
onFieldTypeChanged: (type) {
setState(
() {
if (widget.mode == FieldOptionMode.add &&
!isFieldNameChanged) {
controller.text = type.i18n;
_updateOptionValues(name: type.i18n);
}
_updateOptionValues(type: type);
},
);
},
),
const _Divider(),
if (!widget.isPrimary) ...[
@@ -446,42 +454,14 @@ class _PropertyType extends StatelessWidget {
),
],
),
onTap: () {
showMobileBottomSheet(
onTap: () async {
final fieldType = await showFieldTypeGridBottomSheet(
context,
padding: EdgeInsets.zero,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
elevation: 20,
title: LocaleKeys.grid_field_editProperty.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
barrierColor: Colors.transparent,
enableDraggableScrollable: true,
builder: (context) {
final typeOptionMenuItemValue = mobileSupportedFieldTypes
.map(
(fieldType) => TypeOptionMenuItemValue(
value: fieldType,
backgroundColor: fieldType.mobileIconBackgroundColor,
text: fieldType.i18n,
icon: fieldType.svgData,
onTap: (_, fieldType) {
onSelected(fieldType);
context.pop();
},
),
)
.toList();
return Padding(
padding: EdgeInsets.all(16 * context.scale),
child: TypeOptionMenu<FieldType>(
values: typeOptionMenuItemValue,
scaleFactor: context.scale,
),
);
},
);
if (fieldType != null) {
onSelected(fieldType);
}
},
);
}
@@ -713,7 +693,7 @@ class _NumberFormatListState extends State<_NumberFormatList> {
controller: widget.scrollController,
children: [
const Center(
child: DragHandler(),
child: DragHandle(),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
@@ -2,27 +2,29 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/field/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';
import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
class QuickEditField extends StatefulWidget {
const QuickEditField({
super.key,
required this.viewId,
required this.fieldController,
required this.fieldInfo,
});
final String viewId;
final FieldController fieldController;
final FieldInfo fieldInfo;
@override
@@ -38,14 +40,10 @@ class _QuickEditFieldState extends State<QuickEditField> {
);
late FieldVisibility fieldVisibility;
late FieldOptionValues _fieldOptionValues;
@override
void initState() {
super.initState();
_fieldOptionValues =
FieldOptionValues.fromField(field: widget.fieldInfo.field);
fieldVisibility = widget.fieldInfo.fieldSettings?.visibility ??
FieldVisibility.AlwaysShown;
controller.text = widget.fieldInfo.field.name;
@@ -59,140 +57,125 @@ class _QuickEditFieldState extends State<QuickEditField> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
OptionTextField(
controller: controller,
type: _fieldOptionValues.type,
onTextChanged: (text) async {
await service.updateName(text);
},
),
const _Divider(),
FlowyOptionTile.text(
text: LocaleKeys.grid_field_editProperty.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s),
onTap: () async {
widget.fieldInfo.field.freeze();
final field = widget.fieldInfo.field.rebuild((field) {
field.name = controller.text;
field.fieldType = _fieldOptionValues.type;
field.typeOptionData =
_fieldOptionValues.getTypeOptionData() ?? [];
});
final fieldOptionValues = await showEditFieldScreen(
context,
widget.viewId,
widget.fieldInfo.copyWith(field: field),
);
if (fieldOptionValues != null) {
if (fieldOptionValues.name != _fieldOptionValues.name) {
await service.updateName(fieldOptionValues.name);
}
if (fieldOptionValues.type != _fieldOptionValues.type) {
await FieldBackendService.updateFieldType(
viewId: widget.viewId,
fieldId: widget.fieldInfo.id,
fieldType: fieldOptionValues.type,
);
}
final data = fieldOptionValues.getTypeOptionData();
if (data != null) {
await FieldBackendService.updateFieldTypeOption(
viewId: widget.viewId,
fieldId: widget.fieldInfo.id,
typeOptionData: data,
);
}
setState(() {
_fieldOptionValues = fieldOptionValues;
controller.text = fieldOptionValues.name;
});
} else {
if (mounted) {
context.pop();
}
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: fieldVisibility.isVisibleState()
? LocaleKeys.grid_field_hide.tr()
: LocaleKeys.grid_field_show.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),
onTap: () async {
context.pop();
if (fieldVisibility.isVisibleState()) {
await service.hide();
} else {
await service.hide();
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s),
onTap: () async {
context.pop();
showCreateFieldBottomSheet(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: widget.fieldInfo.id,
),
);
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertRight.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s),
onTap: () async {
context.pop();
showCreateFieldBottomSheet(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: widget.fieldInfo.id,
return BlocProvider(
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
field: widget.fieldInfo.field,
)..add(const FieldEditorEvent.initial()),
child: BlocConsumer<FieldEditorBloc, FieldEditorState>(
listenWhen: (previous, current) =>
previous.field.name != current.field.name,
listener: (context, state) => controller.text = state.field.name,
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(16),
OptionTextField(
controller: controller,
type: state.field.fieldType,
onTextChanged: (text) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.renameField(text));
},
onFieldTypeChanged: (fieldType) {
context
.read<FieldEditorBloc>()
.add(FieldEditorEvent.switchFieldType(fieldType));
},
),
);
},
),
if (!widget.fieldInfo.isPrimary) ...[
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
onTap: () async {
context.pop();
await service.duplicate();
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_field_delete_s,
color: Theme.of(context).colorScheme.error,
),
onTap: () async {
context.pop();
await service.delete();
},
),
],
],
const _Divider(),
FlowyOptionTile.text(
text: LocaleKeys.grid_field_editProperty.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s),
onTap: () {
showEditFieldScreen(
context,
widget.viewId,
state.field,
);
context.pop();
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: fieldVisibility.isVisibleState()
? LocaleKeys.grid_field_hide.tr()
: LocaleKeys.grid_field_show.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),
onTap: () async {
context.pop();
if (fieldVisibility.isVisibleState()) {
await service.hide();
} else {
await service.hide();
}
},
),
if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s),
onTap: () {
context.pop();
mobileCreateFieldWorkflow(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: widget.fieldInfo.id,
),
);
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertRight.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s),
onTap: () {
context.pop();
mobileCreateFieldWorkflow(
context,
widget.viewId,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: widget.fieldInfo.id,
),
);
},
),
if (!widget.fieldInfo.isPrimary) ...[
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),
onTap: () {
context.pop();
service.duplicate();
},
),
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.m_field_delete_s,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
context.pop();
service.delete();
},
),
],
],
);
},
),
);
}
}
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@@ -22,15 +23,18 @@ class MobileDatabaseFieldList extends StatelessWidget {
const MobileDatabaseFieldList({
super.key,
required this.databaseController,
required this.canCreate,
});
final DatabaseController databaseController;
final bool canCreate;
@override
Widget build(BuildContext context) {
return _MobileDatabaseFieldListBody(
databaseController: databaseController,
viewId: context.read<ViewBloc>().state.view.id,
canCreate: canCreate,
);
}
}
@@ -39,10 +43,12 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
const _MobileDatabaseFieldListBody({
required this.databaseController,
required this.viewId,
required this.canCreate,
});
final DatabaseController databaseController;
final String viewId;
final bool canCreate;
@override
Widget build(BuildContext context) {
@@ -56,6 +62,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
if (state.fieldContexts.isEmpty) {
return const SizedBox.shrink();
}
final fields = [...state.fieldContexts];
final firstField = fields.removeAt(0);
final firstCell = DatabaseFieldListTile(
@@ -63,7 +70,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
viewId: viewId,
fieldController: databaseController.fieldController,
fieldInfo: firstField,
showTopBorder: true,
showTopBorder: false,
);
final cells = fields
.mapIndexed(
@@ -113,13 +120,22 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
.add(DatabasePropertyEvent.moveField(from, to));
},
header: firstCell,
footer: Column(
mainAxisSize: MainAxisSize.min,
children: [
_divider(),
_NewDatabaseFieldTile(viewId: viewId),
],
),
footer: canCreate
? Column(
mainAxisSize: MainAxisSize.min,
children: [
_divider(),
_NewDatabaseFieldTile(viewId: viewId),
VSpace(
context.bottomSheetPadding(
ignoreViewPadding: false,
),
),
],
)
: VSpace(
context.bottomSheetPadding(ignoreViewPadding: false),
),
itemCount: cells.length,
itemBuilder: (context, index) => cells[index],
);
@@ -197,7 +213,7 @@ class _NewDatabaseFieldTile extends StatelessWidget {
color: Theme.of(context).hintColor,
),
textColor: Theme.of(context).hintColor,
onTap: () => showCreateFieldBottomSheet(context, viewId),
onTap: () => mobileCreateFieldWorkflow(context, viewId),
);
}
}
@@ -0,0 +1,553 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'database_sort_bottom_sheet_cubit.dart';
class MobileSortEditor extends StatefulWidget {
const MobileSortEditor({
super.key,
});
@override
State<MobileSortEditor> createState() => _MobileSortEditorState();
}
class _MobileSortEditorState extends State<MobileSortEditor> {
final PageController _pageController = PageController();
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => MobileSortEditorCubit(
pageController: _pageController,
),
child: Column(
children: [
const _Header(),
SizedBox(
height: 400, //314,
child: PageView.builder(
controller: _pageController,
itemCount: 2,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return index == 0
? Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,
),
child: const _Overview(),
)
: const _SortDetail();
},
),
),
],
),
);
}
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return BlocBuilder<MobileSortEditorCubit, MobileSortEditorState>(
builder: (context, state) {
return SizedBox(
height: 44.0,
child: Stack(
children: [
if (state.showBackButton)
Align(
alignment: Alignment.centerLeft,
child: AppBarBackButton(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
onTap: () => context
.read<MobileSortEditorCubit>()
.returnToOverview(),
),
),
Align(
child: FlowyText.medium(
LocaleKeys.grid_settings_sort.tr(),
fontSize: 16.0,
),
),
if (state.isCreatingNewSort)
Align(
alignment: Alignment.centerRight,
child: AppBarSaveButton(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
enable: state.newSortFieldId != null,
onTap: () {
_tryCreateSort(context, state);
context.read<MobileSortEditorCubit>().returnToOverview();
},
),
),
],
),
);
},
);
}
void _tryCreateSort(BuildContext context, MobileSortEditorState state) {
if (state.newSortFieldId != null && state.newSortCondition != null) {
context.read<SortEditorBloc>().add(
SortEditorEvent.createSort(
fieldId: state.newSortFieldId!,
condition: state.newSortCondition!,
),
);
}
}
}
class _Overview extends StatelessWidget {
const _Overview();
@override
Widget build(BuildContext context) {
return BlocBuilder<SortEditorBloc, SortEditorState>(
builder: (context, state) {
return Column(
children: [
Expanded(
child: state.sortInfos.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowySvg(
FlowySvgs.sort_descending_s,
size: const Size.square(60),
color: Theme.of(context).hintColor,
),
FlowyText(
LocaleKeys.grid_sort_empty.tr(),
color: Theme.of(context).hintColor,
),
],
),
)
: ReorderableListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: child,
),
onReorder: (oldIndex, newIndex) => context
.read<SortEditorBloc>()
.add(SortEditorEvent.reorderSort(oldIndex, newIndex)),
itemCount: state.sortInfos.length,
itemBuilder: (context, index) => _SortItem(
key: ValueKey("sort_item_$index"),
sort: state.sortInfos[index],
),
),
),
Container(
height: 44,
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
width: 0.5,
color: Theme.of(context).dividerColor,
),
),
borderRadius: Corners.s10Border,
),
child: InkWell(
onTap: () {
final firstField = context
.read<SortEditorBloc>()
.state
.creatableFields
.firstOrNull;
if (firstField == null) {
Fluttertoast.showToast(
msg: LocaleKeys.grid_sort_cannotFindCreatableField.tr(),
gravity: ToastGravity.BOTTOM,
);
} else {
context.read<MobileSortEditorCubit>().startCreatingSort();
}
},
borderRadius: Corners.s10Border,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.add_s,
size: Size.square(16),
),
const HSpace(6.0),
FlowyText(
LocaleKeys.grid_sort_addSort.tr(),
fontSize: 15,
),
],
),
),
),
),
],
);
},
);
}
}
class _SortItem extends StatelessWidget {
const _SortItem({super.key, required this.sort});
final SortInfo sort;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
vertical: 4.0,
),
decoration: BoxDecoration(
color: Theme.of(context).hoverColor,
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context
.read<MobileSortEditorCubit>()
.startEditingSort(sort.sortId),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Expanded(
child: FlowyText.medium(
LocaleKeys.grid_sort_by.tr(),
fontSize: 15,
),
),
),
const VSpace(10),
Row(
children: [
Expanded(
child: Container(
height: 44,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
width: 0.5,
color: Theme.of(context).dividerColor,
),
),
borderRadius: Corners.s10Border,
color: Theme.of(context).colorScheme.surface,
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Center(
child: Row(
children: [
Expanded(
child: FlowyText(
sort.fieldInfo.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(6.0),
FlowySvg(
FlowySvgs.icon_right_small_ccm_outlined_s,
size: const Size.square(14),
color: Theme.of(context).hintColor,
),
],
),
),
),
),
const HSpace(6),
Expanded(
child: Container(
height: 44,
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
width: 0.5,
color: Theme.of(context).dividerColor,
),
),
borderRadius: Corners.s10Border,
color: Theme.of(context).colorScheme.surface,
),
padding: const EdgeInsetsDirectional.only(
start: 12,
end: 10,
),
child: Center(
child: Row(
children: [
Expanded(
child: FlowyText(
sort.sortPB.condition.name,
),
),
const HSpace(6.0),
FlowySvg(
FlowySvgs.icon_right_small_ccm_outlined_s,
size: const Size.square(14),
color: Theme.of(context).hintColor,
),
],
),
),
),
),
],
),
],
),
),
),
Positioned(
right: 8,
top: 9,
child: InkWell(
onTap: () => context
.read<SortEditorBloc>()
.add(SortEditorEvent.deleteSort(sort)),
// steal from the container LongClickReorderWidget thing
onLongPress: () {},
borderRadius: BorderRadius.circular(10),
child: SizedBox.square(
dimension: 34,
child: Center(
child: FlowySvg(
FlowySvgs.trash_m,
size: const Size.square(18),
color: Theme.of(context).hintColor,
),
),
),
),
),
],
),
);
}
}
class _SortDetail extends StatelessWidget {
const _SortDetail();
@override
Widget build(BuildContext context) {
final isCreatingNewSort =
context.read<MobileSortEditorCubit>().state.isCreatingNewSort;
return isCreatingNewSort
? const _SortDetailContent()
: BlocSelector<SortEditorBloc, SortEditorState, SortInfo>(
selector: (state) => state.sortInfos.firstWhere(
(sortInfo) =>
sortInfo.sortId ==
context.read<MobileSortEditorCubit>().state.editingSortId,
),
builder: (context, sortInfo) {
return _SortDetailContent(sortInfo: sortInfo);
},
);
}
}
class _SortDetailContent extends StatelessWidget {
const _SortDetailContent({
this.sortInfo,
});
final SortInfo? sortInfo;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DefaultTabController(
length: 2,
initialIndex: sortInfo == null
? 0
: sortInfo!.sortPB.condition == SortConditionPB.Ascending
? 0
: 1,
child: Container(
padding: const EdgeInsets.all(3.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).hoverColor,
),
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.zero,
padding: EdgeInsets.zero,
indicatorWeight: 0,
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.surface,
),
splashFactory: NoSplash.splashFactory,
overlayColor: const MaterialStatePropertyAll(
Colors.transparent,
),
onTap: (index) {
final newCondition = index == 0
? SortConditionPB.Ascending
: SortConditionPB.Descending;
_changeCondition(context, newCondition);
},
tabs: [
Tab(
height: 34,
child: Center(
child: FlowyText(
LocaleKeys.grid_sort_ascending.tr(),
fontSize: 14,
),
),
),
Tab(
height: 34,
child: Center(
child: FlowyText(
LocaleKeys.grid_sort_descending.tr(),
fontSize: 14,
),
),
),
],
),
),
),
),
const VSpace(20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: FlowyText(
LocaleKeys.grid_settings_sortBy.tr().toUpperCase(),
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
const VSpace(4.0),
const Divider(
height: 0.5,
thickness: 0.5,
),
Expanded(
child: BlocBuilder<SortEditorBloc, SortEditorState>(
builder: (context, state) {
final fields = state.allFields
.where(
(field) =>
field.canCreateSort ||
sortInfo != null && sortInfo!.fieldId == field.id,
)
.toList();
return ListView.builder(
itemCount: fields.length,
itemBuilder: (context, index) {
final fieldInfo = fields[index];
final isSelected = sortInfo == null
? context
.watch<MobileSortEditorCubit>()
.state
.newSortFieldId ==
fieldInfo.id
: sortInfo!.fieldId == fieldInfo.id;
return FlowyOptionTile.checkbox(
text: fieldInfo.field.name,
isSelected: isSelected,
showTopBorder: false,
onTap: () {
if (!isSelected) {
_changeFieldId(context, fieldInfo.id);
}
},
);
},
);
},
),
),
],
);
}
void _changeCondition(BuildContext context, SortConditionPB newCondition) {
if (sortInfo == null) {
context.read<MobileSortEditorCubit>().changeSortCondition(newCondition);
} else {
context.read<SortEditorBloc>().add(
SortEditorEvent.editSort(
sortId: sortInfo!.sortId,
condition: newCondition,
),
);
}
}
void _changeFieldId(BuildContext context, String newFieldId) {
if (sortInfo == null) {
context.read<MobileSortEditorCubit>().changeFieldId(newFieldId);
} else {
context.read<SortEditorBloc>().add(
SortEditorEvent.editSort(
sortId: sortInfo!.sortId,
fieldId: newFieldId,
),
);
}
}
}
@@ -0,0 +1,77 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'database_sort_bottom_sheet_cubit.freezed.dart';
class MobileSortEditorCubit extends Cubit<MobileSortEditorState> {
MobileSortEditorCubit({
required this.pageController,
}) : super(MobileSortEditorState.initial());
final PageController pageController;
void returnToOverview() {
_animateToPage(0);
emit(MobileSortEditorState.initial());
}
void startCreatingSort() {
_animateToPage(1);
emit(
state.copyWith(
showBackButton: true,
isCreatingNewSort: true,
newSortCondition: SortConditionPB.Ascending,
),
);
}
void startEditingSort(String sortId) {
_animateToPage(1);
emit(
state.copyWith(
showBackButton: true,
editingSortId: sortId,
),
);
}
/// only used when creating a new sort
void changeFieldId(String fieldId) {
emit(state.copyWith(newSortFieldId: fieldId));
}
/// only used when creating a new sort
void changeSortCondition(SortConditionPB condition) {
emit(state.copyWith(newSortCondition: condition));
}
Future<void> _animateToPage(int page) async {
return pageController.animateToPage(
page,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
);
}
}
@freezed
class MobileSortEditorState with _$MobileSortEditorState {
factory MobileSortEditorState({
required bool showBackButton,
required String? editingSortId,
required bool isCreatingNewSort,
required String? newSortFieldId,
required SortConditionPB? newSortCondition,
}) = _MobileSortEditorState;
factory MobileSortEditorState.initial() => MobileSortEditorState(
showBackButton: false,
editingSortId: null,
isCreatingNewSort: false,
newSortFieldId: null,
newSortCondition: null,
);
}
@@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -27,50 +26,44 @@ class MobileDatabaseViewList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
snap: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (context, scrollController) {
return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final views = [state.view, ...state.view.childViews];
return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final views = [state.view, ...state.view.childViews];
return Column(
children: [
const DragHandler(),
_Header(
title: LocaleKeys.grid_settings_viewList.plural(
context.watch<DatabaseTabBarBloc>().state.tabBars.length,
namedArgs: {
'count':
'${context.watch<DatabaseTabBarBloc>().state.tabBars.length}',
},
return Column(
children: [
_Header(
title: LocaleKeys.grid_settings_viewList.plural(
context.watch<DatabaseTabBarBloc>().state.tabBars.length,
namedArgs: {
'count':
'${context.watch<DatabaseTabBarBloc>().state.tabBars.length}',
},
),
showBackButton: false,
useFilledDoneButton: false,
onDone: (context) => Navigator.pop(context),
),
Expanded(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: [
...views.mapIndexed(
(index, view) => MobileDatabaseViewListButton(
view: view,
showTopBorder: index == 0,
),
),
showBackButton: false,
useFilledDoneButton: false,
onDone: (context) => Navigator.pop(context),
),
SingleChildScrollView(
controller: scrollController,
child: Column(
children: [
...views.mapIndexed(
(index, view) => MobileDatabaseViewListButton(
view: view,
showTopBorder: index == 0,
),
),
const VSpace(20),
const MobileNewDatabaseViewButton(),
],
const VSpace(20),
const MobileNewDatabaseViewButton(),
VSpace(
context.bottomSheetPadding(ignoreViewPadding: false),
),
),
],
);
},
],
),
),
],
);
},
);
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -30,18 +30,13 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionButton(context, _Action.edit, () {
_actionButton(context, _Action.edit, () async {
final bloc = context.read<ViewBloc>();
context.pop();
showMobileBottomSheet(
await showTransitionMobileBottomSheet(
context,
showHeader: true,
showDoneButton: true,
title: LocaleKeys.grid_settings_editView.tr(),
enableDraggableScrollable: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (_) => BlocProvider.value(
value: bloc,
child: MobileEditDatabaseViewScreen(
@@ -49,20 +44,31 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
),
),
);
if (context.mounted) {
context.pop();
}
}),
if (!isInline) ...[
_divider(),
_actionButton(context, _Action.duplicate, () {
_divider(),
_actionButton(
context,
_Action.duplicate,
() {
context.read<ViewBloc>().add(const ViewEvent.duplicate());
context.pop();
}),
_divider(),
_actionButton(context, _Action.delete, () {
},
!isInline,
),
_divider(),
_actionButton(
context,
_Action.delete,
() {
context.read<ViewBloc>().add(const ViewEvent.delete());
context.pop();
}),
_divider(),
],
},
!isInline,
),
_divider(),
],
);
}
@@ -70,20 +76,20 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
Widget _actionButton(
BuildContext context,
_Action action,
VoidCallback onTap,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MobileQuickActionButton(
icon: action.icon,
text: action.label,
color: action.color(context),
onTap: onTap,
),
VoidCallback onTap, [
bool enable = true,
]) {
return MobileQuickActionButton(
icon: action.icon,
text: action.label,
textColor: action.color(context),
iconColor: action.color(context),
onTap: onTap,
enable: enable,
);
}
Widget _divider() => const Divider(height: 9);
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
}
enum _Action {
@@ -3,8 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/database_view_service.dart';
import 'package:appflowy/plugins/database/application/layout/layout_service.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
import 'package:appflowy/plugins/database/domain/layout_service.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -33,8 +33,7 @@ class MobileEditDatabaseViewScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return ListView(
shrinkWrap: true,
return Column(
children: [
_NameAndIcon(view: state.view),
_divider(),
@@ -83,15 +82,23 @@ class _NameAndIconState extends State<_NameAndIcon> {
textEditingController.text = widget.view.name;
}
@override
void dispose() {
textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
autofocus: true,
showTopBorder: false,
controller: textEditingController,
onTextChanged: (text) {
context.read<ViewBloc>().add(ViewEvent.rename(text));
},
return Material(
child: FlowyOptionTile.textField(
autofocus: true,
showTopBorder: false,
controller: textEditingController,
onTextChanged: (text) {
context.read<ViewBloc>().add(ViewEvent.rename(text));
},
),
);
}
}
@@ -222,24 +229,18 @@ class DatabaseViewSettingTile extends StatelessWidget {
}
if (setting == DatabaseViewSettings.fields) {
await showMobileBottomSheet(
await showTransitionMobileBottomSheet(
context,
useSafeArea: false,
resizeToAvoidBottomInset: false,
showDragHandle: true,
showHeader: true,
showBackButton: true,
title: LocaleKeys.grid_settings_properties.tr(),
showDivider: true,
enableDraggableScrollable: true,
initialChildSize: 0.98,
minChildSize: 0.98,
maxChildSize: 0.98,
builder: (_) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
child: MobileDatabaseFieldList(
databaseController: databaseController,
canCreate: true,
),
);
},
@@ -250,7 +251,6 @@ class DatabaseViewSettingTile extends StatelessWidget {
if (setting == DatabaseViewSettings.board) {
await showMobileBottomSheet<DatabaseLayoutPB>(
context,
resizeToAvoidBottomInset: false,
builder: (context) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 46),
@@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar.
/// For demonstration purposes
class DetailsPlaceholderScreen extends StatefulWidget {
/// Constructs a [DetailsScreen].
const DetailsPlaceholderScreen({
required this.label,
this.param,
this.extra,
this.withScaffold = true,
super.key,
});
/// The label to display in the center of the screen.
final String label;
/// Optional param
final String? param;
/// Optional extra object
final Object? extra;
/// Wrap in scaffold
final bool withScaffold;
@override
State<StatefulWidget> createState() => DetailsPlaceholderScreenState();
}
/// The state for DetailsScreen
class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> {
int _counter = 0;
@override
Widget build(BuildContext context) {
if (widget.withScaffold) {
return Scaffold(
appBar: AppBar(
title: Text('Details Screen - ${widget.label}'),
),
body: _build(context),
);
} else {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: _build(context),
);
}
}
Widget _build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Details for ${widget.label} - Counter: $_counter',
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.all(4)),
TextButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: const Text('Increment counter'),
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.param != null)
Text(
'Parameter: ${widget.param!}',
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.all(8)),
if (widget.extra != null)
Text(
'Extra: ${widget.extra!}',
style: Theme.of(context).textTheme.titleMedium,
),
if (!widget.withScaffold) ...<Widget>[
const Padding(padding: EdgeInsets.all(16)),
TextButton(
onPressed: () {
GoRouter.of(context).pop();
},
child: const Text(
'< Back',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
],
],
),
);
}
}
@@ -35,10 +35,12 @@ class MobileFavoriteScreen extends StatelessWidget {
},
(error) => null,
);
final userProfile =
snapshots.data?[1].fold((error) => null, (userProfilePB) {
return userProfilePB as UserProfilePB?;
});
final userProfile = snapshots.data?[1].fold(
(userProfilePB) {
return userProfilePB as UserProfilePB?;
},
(error) => null,
);
// In the unlikely case either of the above is null, eg.
// when a workspace is already open this can happen.
@@ -41,10 +41,12 @@ class MobileHomeScreen extends StatelessWidget {
},
(error) => null,
);
final userProfile =
snapshots.data?[1].fold((error) => null, (userProfilePB) {
return userProfilePB as UserProfilePB?;
});
final userProfile = snapshots.data?[1].fold(
(userProfilePB) {
return userProfilePB as UserProfilePB?;
},
(error) => null,
);
// In the unlikely case either of the above is null, eg.
// when a workspace is already open this can happen.
@@ -35,12 +35,15 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
return const Center(child: CircularProgressIndicator.adaptive());
}
final userProfile = snapshot.data?.fold((error) {
errorMsg = error.msg;
return null;
}, (userProfile) {
return userProfile;
});
final userProfile = snapshot.data?.fold(
(userProfile) {
return userProfile;
},
(error) {
errorMsg = error.msg;
return null;
},
);
return Scaffold(
appBar: FlowyAppBar(
@@ -164,23 +164,23 @@ class _DeletedFilesListView extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
itemBuilder: (context, index) {
final object = state.objects[index];
final deletedFile = state.objects[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
// TODO(Yijing): implement file type after TrashPB has file type
// TODO: show different file type icon, implement this feature after TrashPB has file type field
leading: FlowySvg(
FlowySvgs.document_s,
size: const Size.square(24),
color: theme.colorScheme.onSurface,
),
title: Text(
object.name,
deletedFile.name,
style: theme.textTheme.labelMedium
?.copyWith(color: theme.colorScheme.onBackground),
),
horizontalTitleGap: 0,
// TODO(yiing): needs improve by container/surface theme color
tileColor: theme.colorScheme.onSurface.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@@ -188,7 +188,6 @@ class _DeletedFilesListView extends StatelessWidget {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO(yijing): extract icon button
IconButton(
splashRadius: 20,
icon: FlowySvg(
@@ -199,10 +198,10 @@ class _DeletedFilesListView extends StatelessWidget {
onPressed: () {
context
.read<TrashBloc>()
.add(TrashEvent.putback(object.id));
.add(TrashEvent.putback(deletedFile.id));
Fluttertoast.showToast(
msg:
'${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
'${deletedFile.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
gravity: ToastGravity.BOTTOM,
);
},
@@ -215,10 +214,12 @@ class _DeletedFilesListView extends StatelessWidget {
color: theme.colorScheme.onSurface,
),
onPressed: () {
context.read<TrashBloc>().add(TrashEvent.delete(object));
context
.read<TrashBloc>()
.add(TrashEvent.delete(deletedFile));
Fluttertoast.showToast(
msg:
'${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
'${deletedFile.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
gravity: ToastGravity.BOTTOM,
);
},
@@ -1,4 +1,3 @@
export 'details_placeholder_page.dart';
export 'editor/mobile_editor_screen.dart';
export 'home/home.dart';
export 'mobile_bottom_navigation_bar.dart';
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import '../widgets/widgets.dart';
@@ -22,14 +23,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => safeLaunchUrl('https://appflowy.io/privacy/mobile'),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => safeLaunchUrl('https://appflowy.io/terms'),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_version.tr(),
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/appearance/rtl_setting.dart';
import 'package:appflowy/mobile/presentation/setting/appearance/text_scale_setting.dart';
import 'package:appflowy/mobile/presentation/setting/appearance/theme_setting.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -18,6 +19,7 @@ class AppearanceSettingGroup extends StatelessWidget {
settingItemList: const [
ThemeSetting(),
FontSetting(),
TextScaleSetting(),
RTLSetting(),
],
);
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../setting.dart';
const int _divisions = 4;
class TextScaleSetting extends StatelessWidget {
const TextScaleSetting({
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textScaleFactor =
context.watch<AppearanceSettingsCubit>().state.textScaleFactor;
return MobileSettingItem(
name: LocaleKeys.settings_appearance_fontScaleFactor.tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
// map the text scale factor to the 0-1
// 0.8 - 0.0
// 0.9 - 0.5
// 1.0 - 1.0
((_divisions + 1) * textScaleFactor - _divisions)
.toStringAsFixed(2),
color: theme.colorScheme.onSurface,
),
const Icon(Icons.chevron_right),
],
),
onTap: () {
showMobileBottomSheet(
context,
showHeader: true,
showDragHandle: true,
showDivider: false,
showCloseButton: false,
title: LocaleKeys.settings_appearance_fontScaleFactor.tr(),
builder: (context) {
return FontSizeStepper(
value: textScaleFactor,
minimumValue: 0.8,
maximumValue: 1.0,
divisions: _divisions,
onChanged: (newTextScaleFactor) {
context
.read<AppearanceSettingsCubit>()
.setTextScaleFactor(newTextScaleFactor);
},
);
},
);
},
);
}
}
@@ -1,12 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -48,7 +48,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
final newLocale =
await context.push<Locale>(LanguagePickerScreen.routeName);
if (newLocale != null && newLocale != locale) {
if (mounted) {
if (context.mounted) {
context
.read<AppearanceSettingsCubit>()
.setLocale(context, newLocale);
@@ -15,7 +15,7 @@ class NotificationsSettingGroup extends StatefulWidget {
}
class _NotificationsSettingGroupState extends State<NotificationsSettingGroup> {
// TODO(yijing):remove this after notification page is implemented
// TODO:remove this after notification page is implemented
bool isPushNotificationOn = false;
@override
@@ -23,6 +23,7 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {
late TextEditingController _textFieldController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -5,10 +7,10 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../widgets/widgets.dart';
import 'personal_info.dart';
class PersonalInfoSettingGroup extends StatelessWidget {
@@ -34,7 +36,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
settingItemList: [
MobileSettingItem(
name: userName,
subtitle: isAuthEnabled
subtitle: isAuthEnabled && userProfile.email.isNotEmpty
? Text(
userProfile.email,
style: theme.textTheme.bodyMedium?.copyWith(
@@ -1,11 +1,14 @@
import 'dart:io';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/shared/appflowy_cache_manager.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/share_log_files.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -28,7 +31,7 @@ class SupportSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'),
),
MobileSettingItem(
name: LocaleKeys.workspace_errorActions_reportIssue.tr(),
@@ -50,6 +53,31 @@ class SupportSettingGroup extends StatelessWidget {
);
},
),
MobileSettingItem(
name: LocaleKeys.settings_files_clearCache.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () async {
await showFlowyMobileConfirmDialog(
context,
title: FlowyText(
LocaleKeys.settings_files_areYouSureToClearCache.tr(),
maxLines: 2,
),
content: FlowyText(
LocaleKeys.settings_files_clearCacheDesc.tr(),
fontSize: 12,
maxLines: 4,
),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.error,
onActionButtonPressed: () async {
await getIt<FlowyCacheManager>().clearAllCache();
},
);
},
),
],
),
);
@@ -73,7 +101,7 @@ class _ReportIssuesWidget extends StatelessWidget {
text: LocaleKeys.workspace_errorActions_reportIssueOnGithub.tr(),
onTap: () {
final String os = Platform.operatingSystem;
safeLaunchUrl(
afLaunchUrlString(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os',
);
},
@@ -5,6 +5,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -27,12 +28,9 @@ class UserSessionSettingGroup extends StatelessWidget {
create: (context) => getIt<SignInBloc>(),
child: BlocConsumer<SignInBloc, SignInState>(
listener: (context, state) {
state.successOrFail.fold(
() => null,
(result) => result.fold(
(l) {},
(r) async => runAppFlowy(),
),
state.successOrFail?.fold(
(result) => runAppFlowy(),
(e) => Log.error(e),
);
},
builder: (context, state) {
@@ -8,28 +8,52 @@ class MobileQuickActionButton extends StatelessWidget {
required this.onTap,
required this.icon,
required this.text,
this.color,
this.textColor,
this.iconColor,
this.enable = true,
});
final VoidCallback onTap;
final FlowySvgData icon;
final String text;
final Color? color;
final Color? textColor;
final Color? iconColor;
final bool enable;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
FlowySvg(icon, size: const Size.square(20), color: color),
const HSpace(8),
FlowyText(text, fontSize: 15, color: color),
],
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: () {
if (enable) {
onTap();
}
},
borderRadius: BorderRadius.circular(12),
overlayColor:
enable ? null : const MaterialStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FlowySvg(
icon,
size: const Size.square(20),
color: enable ? iconColor : Theme.of(context).disabledColor,
),
const HSpace(12),
Expanded(
child: FlowyText(
text,
fontSize: 15,
color: enable ? textColor : Theme.of(context).disabledColor,
),
),
],
),
),
),
);
@@ -1,9 +1,10 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:package_info_plus/package_info_plus.dart';
enum _FlowyMobileStateContainerType {
@@ -80,7 +81,7 @@ class FlowyMobileStateContainer extends StatelessWidget {
onPressed: () {
final String? version = snapshot.data?.version;
final String os = Platform.operatingSystem;
safeLaunchUrl(
afLaunchUrlString(
'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os&context=Error%20log:%20$errorMsg',
);
},
@@ -90,7 +91,7 @@ class FlowyMobileStateContainer extends StatelessWidget {
),
OutlinedButton(
onPressed: () =>
safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
afLaunchUrlString('https://discord.gg/JucBXeU2FE'),
child: Text(
LocaleKeys.workspace_errorActions_reachOut.tr(),
),
@@ -218,7 +218,7 @@ class FlowyOptionTile extends StatelessWidget {
final padding = EdgeInsets.symmetric(
horizontal: leading == null ? 0.0 : 12.0,
vertical: 16.0,
vertical: 14.0,
);
return Expanded(
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
class DragHandler extends StatelessWidget {
const DragHandler({
class DragHandle extends StatelessWidget {
const DragHandle({
super.key,
});
@@ -0,0 +1,18 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
extension CalcTypeLabel on CalculationType {
String get label => switch (this) {
CalculationType.Average =>
LocaleKeys.grid_calculationTypeLabel_average.tr(),
CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(),
CalculationType.Median =>
LocaleKeys.grid_calculationTypeLabel_median.tr(),
CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),
CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),
_ => throw UnimplementedError(
'Label for $this has not been implemented',
),
};
}
@@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef UpdateCalculationValue
= FlowyResult<CalculationChangesetNotificationPB, FlowyError>;
class CalculationsListener {
CalculationsListener({required this.viewId});
final String viewId;
PublishNotifier<UpdateCalculationValue>? _calculationNotifier =
PublishNotifier();
DatabaseNotificationListener? _listener;
void start({
required void Function(UpdateCalculationValue) onCalculationChanged,
}) {
_calculationNotifier?.addPublishListener(onCalculationChanged);
_listener = DatabaseNotificationListener(
objectId: viewId,
handler: _handler,
);
}
void _handler(
DatabaseNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidUpdateCalculation:
_calculationNotifier?.value = result.fold(
(payload) => FlowyResult.success(
CalculationChangesetNotificationPB.fromBuffer(payload),
),
(err) => FlowyResult.failure(err),
);
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_calculationNotifier?.dispose();
_calculationNotifier = null;
}
}
@@ -0,0 +1,48 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
class CalculationsBackendService {
const CalculationsBackendService({required this.viewId});
final String viewId;
// Get Calculations (initial fetch)
Future<FlowyResult<RepeatedCalculationsPB, FlowyError>>
getCalculations() async {
final payload = DatabaseViewIdPB()..value = viewId;
return DatabaseEventGetAllCalculations(payload).send();
}
Future<void> updateCalculation(
String fieldId,
CalculationType type, {
String? calculationId,
}) async {
final payload = UpdateCalculationChangesetPB()
..viewId = viewId
..fieldId = fieldId
..calculationType = type;
if (calculationId != null) {
payload.calculationId = calculationId;
}
await DatabaseEventUpdateCalculation(payload).send();
}
Future<void> removeCalculation(
String fieldId,
String calculationId,
) async {
final payload = RemoveCalculationChangesetPB()
..viewId = viewId
..fieldId = fieldId
..calculationId = calculationId;
await DatabaseEventRemoveCalculation(payload).send();
}
}
Loaded 100 of 1163 files, more files were not shown because too many files have changed in this diff. Show more