Compare commits

...
98 Commits
Author SHA1 Message Date
Lucas.Xu fe5ce75ea8 fix: minor ui issues (#4102)
* fix: minor ui issues

* feat: support using emoji as icon

* chore: update langauges

* fix: missing reminder bloc in detail page

* fix: integration test
2023-12-06 13:49:30 +08:00
Richard Shiue 99b2b2712b test: date group_id generation logic (#4045) 2023-12-06 12:06:49 +08:00
Richard Shiue 96af012471 fix: grid width update (#4101)
* chore: enable checklist on mobile

* fix: width of grid not updating properly

* chore: fix sign-in button color and font
2023-12-06 11:39:25 +08:00
Nathan.fooo 7ad05feb5b fix: nav title of cloud setting on mobile (#4098) 2023-12-06 06:02:45 +08:00
nathan 69d3181a64 fix: icon url 2023-12-06 03:05:07 +08:00
Nathan.fooo 109ebaf989 fix: url icon (#4097) 2023-12-06 02:38:50 +08:00
Nathan.fooo 1a13de2ee9 chore: bump collab rev (#4096) 2023-12-06 02:04:23 +08:00
Lucas.XuandRichard Shiue 48d6967d3b fix: double click to select / unselect options (#4094)
Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2023-12-05 20:23:20 +08:00
Lucas.Xu 7d55153475 feat: add quick edit panel (#4089)
* feat: add quick edit panel

* feat: improve datepicker color

* fix: quick edit field editor overflow

* chore: try to fix mobile ci

* Revert "chore: try to fix mobile ci"

This reverts commit 68f0ccecd6.
2023-12-05 17:34:42 +08:00
Richard Shiue d25830aece feat: mobile grid fab (#4093) 2023-12-05 17:24:38 +08:00
Richard Shiue 04eea26a55 fix: hidden board card cannot open as page (#4090)
* fix: card open in board

* style: super.key and remove unused variables

* chore: more code cleanup
2023-12-05 17:24:21 +08:00
Richard Shiue 25e94da7e7 feat: mobile checklist (#4088)
* fix: grid header new property font size on desktop

* feat: checklist cell and editor on mobile
2023-12-05 13:39:51 +08:00
Nathan.fooo 2d7a373d77 chore: fix migrate empty document (#4087)
* chore: fix migrate empty document

* chore: update collab rev
2023-12-05 12:25:15 +08:00
Lucas.Xu 1491fbfb0d fix: 0.3.9 launch review (#4076)
* fix: cursor away doesn’t save the change. This behavior is inconsistent with the rest

* fix: emoji doesn’t show fully

* chore: set the default header text style to bold

* fix: add missing divider in time option

* fix: update placeholder cover color

* fix: background color of the change cover button

* fix: remove redundant padding

* fix: use done action instead of return

* fix: incorrect font size used in board and editor

* feat: update appflowy-editor

* fix: document freeze and the toolbar was invisible when hovering the align icon that in the toolbar

* fix: snackbar text is invisiable

* feat: padding of add icon button should be aligned the block

* chore: update icon

* fix: ci issues

* feat: add time picker
2023-12-05 10:46:17 +08:00
Nathan.fooo 0d776a9294 chore: bump up yrs to 0.17.1 (#4078)
* chore: bump yrs

* chore: update rev
2023-12-05 06:20:07 +08:00
Richard Shiue d4cef2e866 chore: various database UI fixes (#4077)
* chore: only activate row detail from primary field

* chore: fix create filter and sort list

* chore: allow reordering rows when active filter

* chore: reduce animations in tab bar

* chore: url accessory tooltip

* feat: use number keyboard for number cell inputs

* chore: mobile grid padding adjustments

* chore: field cell redesign and first field cannot be reordered

* fix: deal with empty fields

* chore: improve appearance of card action sheet
2023-12-04 22:58:02 +08:00
Kilu.HeandRichard Shiue a070ed2441 feat: support all fields type and filters in grid of tauri
* feat: support the single select field actions in grid of tauri

* feat: support multiselect

* feat: support number field and number filter

* feat: support url field

* fix: eslint error

* feat: support checkbox filter

* feat: support checklist field

* fix: adjusting keydown event

* fix: edit record ui

* feat: support date field

* fix: url field bugs

* fix: the bug of the type option wasn't update

* chore: make plural tokens compatible with tauri

* fix: plural key

* fix: optimize get cell performance

* fix: update ts error

* fix: update select option bugs

* fix: grid calculate css

* fix: add DidUpdateFieldSettings

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2023-12-04 10:33:31 +08:00
Richard Shiue 5fa441cbf5 fix: launch review issues (#4075)
* chore: add a tooltip for fields in row detail page

* fix: grouping by field makes cell contents disappear

* chore: code cleanup

* chore: env var values in launch.json should be string

* fix: group orders not being saved

* test: fix test

* chore: more code cleanup

* fix: field settings not found

* chore: ellide cell text

* fix: alignment issues in row detail page
2023-12-04 10:22:26 +08:00
Lucas.Xu 4ae679128f fix: mobile ui issues (#4074)
* fix: edit field ui issues

* fix: set desktop font weight to w500
2023-12-03 22:12:07 +08:00
Richard Shiue 4d71b42a3d chore: misc mobile UI improvements (#4072)
* chore: adjust colors

* chore: adjust card detail padding and text weight

* fix: recent view horizontal padding

* chore: entire row opens row detail page
2023-12-03 12:19:43 +08:00
Richard Shiue ff3ff9f888 feat: integrate new field editor components (#4070)
* chore: use the fancy new create field UI

* chore: adjust select option tag text weight

* chore: use new field editor

* chore: remove old field editor

* chore: code cleanup
2023-12-02 22:23:19 +08:00
Lucas.Xu 147080ad27 chore: bump version 0.3.9 (#4069) 2023-12-02 21:34:42 +08:00
Richard Shiue bb4b987681 chore: require always two lines (#4068) 2023-12-02 21:33:15 +08:00
Lucas.Xu 415f121720 feat: edit property (#4065)
* feat: edit property

* feat: disable actions if field is primary

* feat: remove include time

* chore: set title to medium

* fix: flutter analyze

* chore: update built in text style

* chore: update text style

* fix: unable to click the calendar day
2023-12-02 20:39:03 +08:00
Yijing Huang 9824d5980a fix: mobile bugs fix (#4059)
* fix: add missing tap gesture

* fix: unify bottom sheet shape and padding

* fix: fail to pop the detail page when add new card

* fix: show card title placeholder when it is empty

* refactor: ThirdPartySignInButtons

* chore: add resizeToAvoidBottomInset in showFlowyMobileBottomSheet

* chore: refactor code
2023-12-02 19:46:02 +08:00
Richard Shiue 5ff6405f7f chore: optimize recent page views section UI (#4067)
* chore: adjust text sizing in recent views section
2023-12-02 18:07:07 +08:00
Nathan.fooo a4e0c5754f ci: cloud integration test (#4062) 2023-12-02 06:47:10 +08:00
Richard Shiue 5f94ba129e feat: mobile detail page UI revamp (#4057)
* feat: card detail page ui revamp

* feat: redesign the cells

* chore: code cleanup

* chore: code review

* fix: merge issues

* chore: adjust cell ui size and padding

* fix: dart analyzer

* fix: integration test

* fix: test
2023-12-02 02:03:30 +08:00
Lucas.Xu af07b53484 feat: new property ui revamp (#4063)
* feat: implement new property page

* feat: implement date option

* feat: add include time

* feat: add field header

* feat: implement new property page

* feat: add icons

* feat: add color list

* feat: add color list

* feat: integrate new property page

* feat: support creating property with values

* fix: select option doesn't work

* feat: set textinputaction to done
2023-12-01 20:16:43 +08:00
Nathan.fooo b43b522d97 chore: enable cloud setting on mobile (#4060)
* chore: enable cloud setting on mobile

* chore: disable ansi

* chore: bump up client api

* chore: bump up client api
2023-12-01 11:17:49 +08:00
Lucas.Xu 0683483fd4 fix: editor page issues (#4055)
* fix: disable editor in card detail page

* fix: mobile toolbar disappears after editing link

* fix: favorite icon shows incorrectly

* fix: inkWell when pressing on the Trash is different from the rest of our list tiles in the app

* fix: recent view didn't update after deleting view

* fix: trash button too small

* feat: use new bottom sheet style in cover plugin

* feat: use new bottom sheet style in add new page

* feat: use new bottom sheet style in view page option

* feat: use new bottom sheet style in image block

* feat: use new bottom sheet style in settings block and edit link menu

* fix: data picker doesn't show

* fix: flutter analyze
2023-12-01 09:58:36 +08:00
Nathan.fooo afab3d5374 chore: enable debug with localhost (#4058) 2023-12-01 05:47:12 +08:00
Mohammad Zolfaghari 066bba95f5 fix: update auto-update fields on change group (#3833)
* fix: update auto-update fields on change group

auto-update fields should get updated when on moving row from one
group to another. They where getting updated only on changing row
properties directly.
Also added integration test for changing row groupd related to #3010.

* style: rust format

* fix: add board group test to runner

* fix: update auto updated fiels only when group has changed

* fix: apply group changeset before move_row

* fix: get from_row before move_row

* fix: groups method changed to get_all_groups

* fix: pass from group id in move group payload

We can't know the from group id from the row itself. Because a row
can exist in multiple groups (group by multiselect).

* fix: MoveGroupRowPayloadPB from_group_id index
2023-12-01 00:24:10 +08:00
Nathan.fooo d004c94a2f chore: bump up tauri version (#4056) 2023-12-01 00:11:43 +08:00
Lucas.Xu c0ae803280 feat: hide tabbar when pushing to view page (#4050) 2023-11-30 16:56:59 +08:00
Lucas.Xu d7a67c0efb feat: support editing database icon (#4052) 2023-11-30 16:56:44 +08:00
Nathan.fooo b307dd30d8 chore: fix macos open crash (#4051)
* chore: disable native cpu

* chore: update client api
2023-11-30 13:46:10 +08:00
Yijing Huang 068245eec9 chore: improve UI in MobileSignInScreen (#4048) 2023-11-30 10:03:25 +08:00
Yijing Huang 6b4d4fef15 feat: mobile board view (#4030) 2023-11-30 10:01:29 +08:00
Nathan.fooo 64aa2ba7e4 feat: open appflowy from admin web (#4047)
* feat: open appflowy from admin web

* feat: add loading indicator
2023-11-30 04:55:13 +08:00
Lucas.Xu 8665a25b39 feat: date picker ui revamp (#4044)
* feat: date picker editor

* feat: refactor date picker on mobile

* chore: update background color

* feat: date picker UI revamp

* feat: optimize the scroll behavior

* chore: remove unused code
2023-11-29 21:02:04 +08:00
Yijing Huang 551e012147 feat: improve UI in MobileFieldEditor (#4042) 2023-11-29 10:38:11 +08:00
Lucas.Xu 7da759c662 feat: support select / multi select option (#4039)
* feat: implement select option cell

* feat: support adding new select option

* feat: support selecting / deselecting option

* feat: clear search field after adding new property

* feat: support updating option name
2023-11-29 09:38:53 +08:00
Nathan.fooo 8036d070ad feat: create the default workspace on cloud when using appflowy cloud (#4040)
* chore: sync strategy for collab type

* chore: update rev id

* chore: update rev id

* ci: fix test

* ci: fix test
2023-11-29 07:49:47 +08:00
Mathias Mogensen a5ed145aa3 feat: mobile no date optimization (#4038)
* feat: mobile no date optimization

* fix: code review
2023-11-29 07:19:40 +08:00
Richard Shiueandnathan 20b485bcfe feat: insert a new field to the left or right of an existing one (#4022)
* feat: allow inserting fields before or after a certain field

* fix: tauri build

* chore: implement frontend

* test: rust-lib tests

* test: integration test

* chore: point to temp collab rev

* chore: bump collab rev

* chore: fix tauri build

* chore: fix the tauri build, for real this time

* fix: new field editor show detail not general

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-11-29 04:42:53 +08:00
Lucas.Xu 38e3947b2d chore: disable codecov (#4037) 2023-11-28 17:43:05 +08:00
Lucas.Xu 94f9349aba fix: sv.json missing (#4035) 2023-11-28 11:46:11 +08:00
Mathias Mogensen a9ea8fd159 fix: changing visibility of property (#4032)
* fix: changing visibility of property

* fix: null aware ternary
2023-11-28 11:45:50 +08:00
Mathias Mogensen 1e6762b424 fix: url cell mobile (#4029) 2023-11-28 11:45:02 +08:00
Richard Shiue 7c54b6d599 fix: mobile grid issues (#4026)
* chore: some mobile grid issues

* fix: grid right padding
2023-11-28 11:44:45 +08:00
Nathan.fooo 3e17613f54 tests: AppFlowy Cloud integration test (#4015)
* chore: save cloud ofnig

* chore: fix .a link warnings

* chore: add cloud test runner

* refactor: test folder

* ci: add test

* ci: add test

* ci: fix

* ci: fix
2023-11-28 10:54:31 +08:00
Richard ShiueandMathias Mogensen 9d61ca0278 feat: delete kanban board groups (#3925)
* feat: hide/unhide ui

* chore: implement collapsible side bar and adjust group header (#2)

* refactor: hidden columns into own file

* chore: adjust new group button position

* fix: flowy icon buton secondary color bleed

* chore: some UI adjustments

* fix: some regressions

* chore: proper group is_visible fetching

* chore: use a bloc to manage hidden groups

* fix: hiding groups not working

* chore: implement hidden group popups

* chore: proper ungrouped item column management

* chore: remove ungrouped items button

* chore: flowy hover build

* fix: clean up code

* test: integration tests

* fix: not null promise on null value

* fix: hide and unhide multiple groups

* chore: i18n and code review

* chore: missed review

* fix: rust-lib-test

* fix: dont completely remove flowyiconhovercolor

* chore: apply suggest

* fix: number of rows inside hidden groups not updating properly

* fix: hidden groups disappearing after collapse

* fix: hidden group title alignment

* fix: insert newly unhidden groups into the correct position

* chore: adjust padding all around

* feat: reorder hidden groups

* chore: adjust padding

* chore: collapse hidden groups section persist

* chore: no status group at beginning

* fix: hiding groups when grouping with other types

* chore: disable rename groups that arent supported

* chore: update appflowy board ref

* chore: better naming

* feat: delete kanban groups

* chore: forgot to save

* chore: fix build and small ui adjustments

* chore: add a confirm dialog when deleting a column

* fix: flutter lint

* test: add integration test

* chore: fix some design review issues

* chore: apply suggestions from Nathan

* fix: write lock on group controller

---------

Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
2023-11-28 10:43:22 +08:00
Lucas.Xu 3595de5e12 fix: v0.3.8.2 known issues (#4025) 2023-11-28 10:29:05 +08:00
Mathias Mogensen 32ca864bf7 fix: open slash menu on adding block above/below (#4028) 2023-11-28 02:41:30 +01:00
Mathias Mogensen bfa9233bd4 fix: mobile improvements for calendar (#4027) 2023-11-28 02:13:22 +01:00
Lucas.Xu c659cf4ff2 fix: mobile UI issues (#4020)
* fix: color picker overflow

* fix: image picker overflow

* feat: expand list item to 48.0
2023-11-27 13:51:05 +08:00
Richard Shiue cac3acd553 feat: open a mobile grid row as a page (#4000)
* chore: restore text cursor color

* chore: open row as a card in mobile

* refactor: clean up code

* chore: code review

* chore: restore c++ shared library
2023-11-27 12:37:38 +08:00
Mathias Mogensen 771dd9979f feat: mobile database settings rework (#4016)
* feat: mobile database settings rework

* chore: clean
2023-11-27 12:36:10 +08:00
Lucas.Xu 929508df16 chore: set minimum macos target development target supported to 10.14 (#4011) 2023-11-27 10:29:39 +08:00
Lucas.Xu 79b1515c3a fix: memory leaks (#3995)
* feat: add memory leak monitor

* fix: memory leaks

* feat: dump call stack in memory leak detector

* chore: disable memory leak detector
2023-11-26 15:10:48 +08:00
Mathias Mogensen 7fb1b4f43f feat: optimize calendar for mobile (#3979)
* feat: calendar mobile ui

- Resolves double border on Calendar Cells
- Adds Jump to year quick action for Mobile
- Reduces Cell height in Calendar
- Change out EventList with EventIndicator in Cell

* chore: push card details screen

* fix: navigation to card details

* feat: day events screen update on new event

* fix: changes after merging main

* fix: missing argument

* fix: amend test and remove stack
2023-11-25 15:31:54 +01:00
Nathan.fooo b3dd5fb8bd fix: windows callback (#4006)
* tests: fix supabase integration test

* ci: fix

* chore: fix windows callback
2023-11-25 17:18:31 +08:00
Richard Shiue 29a6eab1ce fix: select option mouse-click event conflict (#4003) 2023-11-24 23:13:52 +08:00
Richard ShiueandLucas.Xu c475a5d960 ci: use linux vm for dart unit test (#3991)
* chore: use the faster linux vm

* chore: download artifact

* chore: make it work

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-24 16:20:25 +08:00
Lucas.Xu 414bb26e56 feat: support changing block background color (#3998) 2023-11-24 15:30:20 +08:00
Nathan.fooo 1fad713477 feat: custom server url in application (#3996)
* chore:test

* chore: update ui

* feat: set appflowy cloud url

* chore: add self host docs

* fix: save user

* fix: sign out when authenticator not match

* fix: sign out when authenticator not match

* fix: db lock

* chore: remove unuse env file

* test: disable supabase cloud test

* test: disable supabase cloud test

* chore: fix save
2023-11-24 11:54:47 +08:00
Richard ShiueandMathias Mogensen e18e031710 chore: adjust board card UI (#3954)
* chore: adjust board card UI

* chore: fix accessory popover and add title placeholder

* chore: add pubspec.lock

* chore: fix integration test

* chore: apply suggestions from code review

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

* chore: apply suggestions from Mathias

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
2023-11-24 00:25:12 +08:00
Lucas.Xu bddaac05ae fix: unable to login on android when building in release mode (#3994) 2023-11-23 23:32:52 +08:00
Richard Shiue 3e5fae002b chore: remove the flutter submodule from contributor (#3993) 2023-11-23 22:32:27 +08:00
Richard Shiue 66835a5409 feat: field editing bloc refactor and add mobile field editor (#3981) 2023-11-23 16:43:29 +08:00
Lucas.Xu 8afbf28430 feat: improve editing link user experience (#3990) 2023-11-23 16:42:36 +08:00
Kilu.He 0427402ba7 fix: adjusting sort UI and support filter and settings (#3971)
* fix: adjusting sort UI and support filter and settings

* fix: code review
2023-11-23 15:57:08 +08:00
Emil 6973d66263 chore: update Russian translations (#3933) 2023-11-23 12:40:27 +08:00
watchakorn-18kandLucas.Xu 5fea801a88 chore: add Thai language (#3985)
* Added Thai language

* Complete Thai translation

* feat: Added Thai translation

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-23 12:40:09 +08:00
MarekJAM 8b81889bcf chore: updated polish translations (#3959) 2023-11-23 10:54:34 +08:00
slhmy 800094587b feat: update simple Chinese translation (#3963) 2023-11-23 10:54:13 +08:00
Kaya Zeren de75e028df chore: turkish translation update (#3983)
Existing translation strings are reviewed and missing ones  are completed.
2023-11-23 10:39:18 +08:00
Lucas.Xu 9b3c1921e8 fix: login page hint text overflow (#3987) 2023-11-23 09:39:40 +08:00
Lucas.Xu 412f34c72a feat: add ... button in mobile toolbar (#3970)
* feat: add ... button in mobile toolbar

* fix: the title state should be reset when canceling

* fix: reset hover status after picking emoji

* fix: some emojis missing on linux and android

* fix: unable to press enter key to rename page
2023-11-22 10:49:22 +08:00
Yijing Huang a7364e1f4a fix: set cell style nullable (#3984) 2023-11-22 10:49:01 +08:00
Richard ShiueandLucas.Xu 16467e9c13 chore: improve mobile grid page (#3939)
* chore: more typo

* chore: improve appearance of mobile grid page

* fix: focus problems with editable grid cells

* chore: apply suggestions from Mathias

* revert: the dragged header looks ugly

* chore: more suggestions from Mathias

* chore: more tarbars

* fix: scrollbar padding is a bit off

* chore: add launch tasks and fix android debug

* chore: more mobile grid improvement

* fix: initial attempt to fix cell focus bug

* chore: fix grid cell lazy loading

* chore: fix cell focus problems

* chore: update same changes to desktop

* fix: revert accessory changes due to regression

* chore: new database view name i18n

* chore: add mobile tab bar header

* fix: fiz zuh eentuhgrashun tastes

* chore: rudimentary grid header

* style: code style stuffz

* chore: restore android standard lib

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-21 22:54:09 +08:00
Richard Shiue b00d29d0cd test: fix integration tests (#3977) 2023-11-21 21:50:31 +08:00
Yijing HuangandLucas.Xu acc951c5eb feat: mobile card detail screen (#3935)
* feat: add CardDetailScreen and CardPropertyEditScreen

- add basic UI layout for these two screens
- add MobileTextCell as the GridCellWidget adapts to mobile

* feat: add MobileNumberCell and MobileTimestampCell

* feat: Add MobileDateCell and MobileCheckboxCell

- Add MobileDateCellEditScreen
- Add dateStr and endDateStr in DateCellCalendarState

* feat:  add MobileFieldTypeOptionEditor

- Add placeholder for different TypeOptionMobileWidgetBuilders
- Add _MobileSwitchFieldButton

* feat: add property delete feature in CardPropertyEditScreen

* fix: fix VisibilitySwitch didn't update

* feat: add MobileCreateRowFieldScreen

- Refactor MobileFieldEditor to used in CardPropertyEditScreen and MobileCreateRowFieldScreen
- Add MobileCreateRowFieldScreen

* chore: localization and improve spacing

* feat: add TimestampTypeOptionMobileWidget

- Refactor  TimeFormatListTile to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody
- Add IncludeTimeSwitch to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody

* feat: add checkbox and DateTypeOptionMobileWidget

* chore: improve UI

* chore: improve spacing

* fix: fix end time shown issue

* fix: minor issues

* fix: flutter analyze

* chore: delete unused TextEditingController

* fix: prevent group field from deleting

* feat: add NumberTypeOptionMobileWidget

* chore: rename and clean code

* chore: clean code

* chore: extract class

* chore: refactor reorder cells

* chore: improve super.key

* chore: refactor MobileFieldTypeList

* chore: reorginize code

* chore: remove unused import file

* chore: clean code

* chore: add commas due to flutter upgrade

* feat: add MobileURLCell

* fix: close keyboard when user tap outside of textfield

* chore: update go_router version

* fix: add missing GridCellStyle

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-11-20 22:56:21 -07:00
Nathan.fooo b9ecc7ceb6 chore: add custom folder prompt (#3961)
* chore: add custom folder prompt

* chore: zip collab db

* chore: fix test

* chore: add test

* chore: fmt

* chore: fmt

* chore: fmt
2023-11-20 20:54:47 +08:00
Lucas.Xu 6f83f41c2d refactor: use dependency injection to adapt the theme on mobile. (#3957) 2023-11-20 10:35:13 +08:00
Nathan.fooo 8179419f8b feat: show server url (#3956)
* chore: data folder for cloud

* chore: display server url

* chore: fix test
2023-11-17 15:38:56 +08:00
Mathias Mogensen 4a1a143a66 fix: checkbox power toggle missing configuration (#3941)
* fix: add trigger for checkbox children toggle

* fix: localization of block placeholders
2023-11-17 13:55:25 +08:00
Emil 21bbf87089 chore: update Russian, fix naming conventions and typos (#3946)
* chore: typos fix

* chore: compliance with naming conventions
2023-11-17 13:53:21 +08:00
Richard Shiue 19d9e7717c chore: use page icon instead of document icon (#3952) 2023-11-17 13:52:33 +08:00
Lucas.Xu d190850f03 feat: keep the toolbar the same height as the keyboard to optimize the editing experience (#3947) 2023-11-17 13:51:26 +08:00
Kilu.He 68de83c611 feat: support modified cell in modal (#3948)
* feat: support drag fields in modal

* fix: wrong draggable position
2023-11-17 10:56:25 +08:00
Richard Shiue 729b8571b5 chore: fix ungroupedsection being shown (#3953)
* chore: fix ungroupedsection being shown

* chore: board ui adjustment
2023-11-17 09:27:10 +08:00
Lucas.Xu 31e9a0b4ea feat: adjust code block, outline block on mobile (#3940)
* fix: missing delete cover button on mobile

* fix: ensure last node is an empty paragraph

* feat: adjust code block

* feat: adjust code block on mobile

* feat: adjust outline on mobile

* fix: appimage builder issues

* fix: view title issues

* fix: integration tests
2023-11-15 16:04:18 +08:00
Lucas.Xu 6a9866b9d2 chore: upgrade flutter to 3.13.9 (#3936) 2023-11-14 22:33:07 +08:00
Nathan.fooo 1c500fbfc5 feat: add lru cache for document/database instance (#3938) 2023-11-14 17:21:09 +08:00
Richard Shiue 4992f9c281 chore: fix-typo (#3934) 2023-11-14 15:41:44 +08:00
Nathan.fooo 75c26c807c feat: User profile notify (#3937)
* chore: collab rev

* feat: recv user change via websocket
2023-11-14 14:01:46 +08:00
900 changed files with 35996 additions and 10036 deletions

No files matched your search

+154 -54
View File
@@ -23,7 +23,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.10.4"
FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
CARGO_MAKE_VERSION: "0.36.6"
@@ -108,21 +108,19 @@ jobs:
- name: Build AppFlowy
working-directory: frontend
run: |
cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev
run: cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev
- name: Run code generation
working-directory: frontend
run: |
cargo make code_generation
run: cargo make code_generation
- name: Flutter Analyzer
working-directory: frontend/appflowy_flutter
run: |
flutter analyze .
run: flutter analyze .
- name: Compress appflowy_flutter
run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
run: |
tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
- uses: actions/upload-artifact@v3
with:
@@ -135,11 +133,11 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-latest]
os: [ubuntu-latest]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
target: x86_64-apple-darwin
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
@@ -176,36 +174,63 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
if [ "$RUNNER_OS" == "macOS" ]; then
cargo make appflowy-flutter-deps-tools
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 keybinder-3.0 libnotify-dev
fi
shell: bash
- name: Enable Flutter Desktop
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
flutter config --enable-linux-desktop
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter config --enable-macos-desktop
elif [ "$RUNNER_OS" == "Windows" ]; then
git config --system core.longpaths true
flutter config --enable-windows-desktop
fi
shell: bash
- uses: actions/download-artifact@v3
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompress appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
- name: Run flutter pub get
working-directory: frontend
run: cargo make pub_get
- name: Run Flutter unit tests
working-directory: frontend
run: |
if [ "$RUNNER_OS" == "macOS" ]; then
flutter config --enable-macos-desktop
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
cargo make dart_unit_test
else
elif [ "$RUNNER_OS" == "Linux" ]; then
cargo make dart_unit_test_no_build
elif [ "$RUNNER_OS" == "Windows" ]; then
cargo make dart_unit_test_no_build
fi
shell: bash
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
with:
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_unit_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
token: ${{ secrets.CODECOV_TOKEN }}
attempt_limit: 20
attempt_delay: 10000
# disable coverage for now as it's not working
# - name: Upload coverage to Codecov
# uses: Wandalen/wretry.action@v1.0.36
# with:
# action: codecov/codecov-action@v3
# with: |
# name: appflowy
# flags: appflowy_flutter_unit_test
# fail_ci_if_error: true
# verbose: true
# os: ${{ matrix.os }}
# token: ${{ secrets.CODECOV_TOKEN }}
# attempt_limit: 20
# attempt_delay: 10000
integration_test:
needs: [prepare]
@@ -213,7 +238,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest,windows-latest]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -274,13 +299,12 @@ jobs:
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompress appflowy_flutter
- name: Uncompressed appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
- name: Run code generation
- name: Run flutter pub get
working-directory: frontend
run: |
cargo make code_generation
run: cargo make pub_get
- name: Run Flutter integration tests
working-directory: frontend/appflowy_flutter
@@ -297,19 +321,101 @@ jobs:
fi
shell: bash
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
cloud_integration_test:
needs: [prepare]
# if: github.event_name != 'pull_request'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- name: Checkout appflowy cloud code
uses: actions/checkout@v3
with:
action: codecov/codecov-action@v3
with: |
name: appflowy
flags: appflowy_flutter_integrateion_test
fail_ci_if_error: true
verbose: true
os: ${{ matrix.os }}
token: ${{ secrets.CODECOV_TOKEN }}
attempt_limit: 20
attempt_delay: 10000
repository: AppFlowy-IO/AppFlowy-Cloud
path: AppFlowy-Cloud
depth: 1
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
run: |
# log level
cp dev.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_USER }}/' .env
sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_PASS }}/' .env
sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_ADMIN_EMAIL }}/' .env
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
cat .env
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
run: |
docker compose up -d
- name: Checkout source code
uses: actions/checkout@v2
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev
shell: bash
- name: Enable Flutter Desktop
run: |
flutter config --enable-linux-desktop
shell: bash
- uses: actions/download-artifact@v3
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompressed appflowy_flutter
run: |
tar -xf appflowy_flutter.tar.gz
ls -al
- name: Enable localhost env
working-directory: frontend/appflowy_flutter
run: |
echo 'APPFLOWY_CLOUD_URL=http://localhost' >> .env
dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
- name: Run flutter pub get
working-directory: frontend
run: cargo make pub_get
- name: Run Flutter integration tests
working-directory: frontend/appflowy_flutter
run: |
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test integration_test/cloud_runner.dart -d Linux --coverage
shell: bash
build:
needs: [prepare]
@@ -381,15 +487,9 @@ jobs:
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompress appflowy_flutter
- name: Uncompressed appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
- name: Run code generation
if: matrix.os == 'windows-latest'
working-directory: frontend
run: |
cargo make code_generation
- name: Build flutter product
working-directory: frontend
run: |
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- "!frontend/appflowy_tauri/**"
env:
FLUTTER_VERSION: "3.10.1"
FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
concurrency:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
- "*"
env:
FLUTTER_VERSION: "3.10.1"
FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
jobs:
+1 -1
View File
@@ -11,7 +11,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.10.1"
FLUTTER_VERSION: "3.13.9"
RUST_TOOLCHAIN: "1.70"
jobs:
+44 -1
View File
@@ -25,7 +25,7 @@
"preLaunchTask": "AF: Build Appflowy Core",
"env": {
"RUST_LOG": "trace",
"RUST_BACKTRACE": 1
"RUST_BACKTRACE": "1"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
@@ -44,6 +44,17 @@
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS: Clean + Rebuild All",
"request": "launch",
@@ -55,6 +66,27 @@
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build Dart only",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-iOS-Simulator: Clean + Rebuild All",
"request": "launch",
@@ -66,6 +98,17 @@
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Build All",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Build Appflowy Core For Android",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/appflowy_flutter"
},
{
"name": "AF-Android: Clean + Rebuild All",
"request": "launch",
+1 -5
View File
@@ -14,7 +14,6 @@
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Dart Clean",
"AF: Flutter Clean",
"AF: Build Appflowy Core",
"AF: Flutter Pub Get",
@@ -33,7 +32,6 @@
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Dart Clean",
"AF: Flutter Clean",
"AF: Build Appflowy Core For iOS",
"AF: Flutter Pub Get",
@@ -52,7 +50,6 @@
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Dart Clean",
"AF: Flutter Clean",
"AF: Build Appflowy Core For iOS Simulator",
"AF: Flutter Pub Get",
@@ -89,7 +86,6 @@
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Dart Clean",
"AF: Flutter Clean",
"AF: Build Appflowy Core For Android",
"AF: Flutter Pub Get",
@@ -309,7 +305,7 @@
{
"label": "AF: Generate Env File",
"type": "shell",
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ",
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter"
}
+2 -1
View File
@@ -25,9 +25,10 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.3.8"
CURRENT_APP_VERSION = "0.3.9"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
# If you update the macOS's CRATE_TYPE, don't forget to update the
# appflowy_backend.podspec
+2 -2
View File
@@ -1,7 +1,7 @@
<h1 align="center" style="margin:0"> AppFlowy_Flutter</h1>
<div align="center">
<img src="https://img.shields.io/badge/Flutter-v3.10.1-blue"/>
<img src="https://img.shields.io/badge/Rust-v1.65-orange"/>
<img src="https://img.shields.io/badge/Flutter-v3.13.19-blue"/>
<img src="https://img.shields.io/badge/Rust-v1.70-orange"/>
</div>
> Documentation for Contributors
@@ -3,7 +3,7 @@
android:name="${applicationName}">
<activity android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
@@ -45,4 +45,6 @@
-->
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -3,7 +3,7 @@
#include <stdint.h>
#include <stdlib.h>
int64_t init_sdk(char *path);
int64_t init_sdk(char *data);
void async_event(int64_t port, const uint8_t *input, uintptr_t len);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+1 -41
View File
@@ -1,41 +1 @@
# Initial Setup
# 1. Copy the dev.env file to .env:
# cp dev.env .env
# Update the environment parameters as needed.
# 2. Generate the env.dart from this .env file:
# You can use the "Generate Env File" task in VSCode.
# Alternatively, execute the following commands:
# cd appflowy_flutter
# dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
# Cloud Type Configuration
# Use this configuration file to specify the cloud type and its associated settings. The available cloud types are:
# Local: 0
# Supabase: 1
# AppFlowy Cloud: 2
# By default, it's set to Local.
CLOUD_TYPE=0
# Supabase Configuration
# If using Supabase (CLOUD_TYPE=1), provide the following details:
SUPABASE_URL=
SUPABASE_ANON_KEY=
# AppFlowy Cloud Configuration
# If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details:
# For instance:
# APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx
# APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx
# APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx
#
# Local host machine(For local develop)
# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000
# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws
# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998
APPFLOWY_CLOUD_BASE_URL=
APPFLOWY_CLOUD_WS_BASE_URL=
APPFLOWY_CLOUD_GOTRUE_URL=
APPFLOWY_CLOUD_URL=
+5
View File
@@ -0,0 +1,5 @@
SUPABASE_URL=
SUPABASE_ANON_KEY=
APPFLOWY_CLOUD_URL=
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_appear
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/util.dart';
void main() {
@@ -0,0 +1,133 @@
// ignore_for_file: unused_import
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import '../util/mock/mock_file_picker.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('appflowy cloud auth', () {
testWidgets('sign in', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloud,
);
await tester.tapGoogleLoginInButton();
tester.expectToSeeHomePage();
});
testWidgets('sign out', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloud,
);
await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.tapButton(find.byType(SettingLogoutButton));
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
tester.expectToSeeGoogleLoginButton();
});
testWidgets('sign in as annoymous', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloud,
);
await tester.tapSignInAsGuest();
// should not see the sync setting page when sign in as annoymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
tester.expectToSeeGoogleLoginButton();
});
testWidgets('enable sync', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloud,
);
await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.cloud);
// the switch should be on by default
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
// the switch should be off
tester.assertAppFlowyCloudEnableSyncSwitchValue(false);
// the switch should be on after toggling
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
});
// testWidgets('custom folder sign in', (tester) async {
// const userA = 'UserA';
// final userAEmail = "${uuid()}@appflowy.io";
// final initialPath = p.join(userA, appFlowyDataFolder);
// final context = await tester.initializeAppFlowy(
// cloudType: AuthenticatorType.appflowyCloud,
// pathExtension: initialPath,
// );
// getIt.registerFactory<AuthService>(
// () => AppFlowyCloudMockAuthService(
// email: userAEmail,
// ),
// );
// // remove the last extension
// final rootPath = context.applicationDataDirectory.replaceFirst(
// initialPath,
// '',
// );
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.user);
// await tester.enterUserName(userA);
// await tester.openSettingsPage(SettingsPage.files);
// await tester.pumpAndSettle();
// // mock the file_picker result
// await mockGetDirectoryPath(
// p.join(rootPath, "random_folder"),
// );
// // after selecting the folder, an annoymous user should be signed in
// await tester.tapCustomLocationButton();
// tester.expectToSeeHomePage();
// await tester.pumpAndSettle();
// // Login as userA in custom folder
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.user);
// await tester.tapGoogleLoginInButton();
// await tester.pumpAndSettle(const Duration(seconds: 1));
// tester.expectToSeeHomePage();
// // UserA should be displayed
// tester.expectToSeeUserName(userA);
// });
});
}
@@ -1,5 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -9,15 +11,15 @@ import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('auth', () {
group('supabase auth', () {
testWidgets('sign in with supabase', (tester) async {
await tester.initializeAppFlowy();
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
tester.expectToSeeHomePage();
});
testWidgets('sign out with supabase', (tester) async {
await tester.initializeAppFlowy();
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
@@ -34,52 +36,53 @@ void main() {
});
testWidgets('sign in as annoymous', (tester) async {
await tester.initializeAppFlowy();
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapSignInAsGuest();
// should not see the sync setting page when sign in as annoymous
await tester.openSettings();
await tester.expectNoSettingsPage(SettingsPage.syncSetting);
await tester.openSettingsPage(SettingsPage.user);
tester.expectToSeeGoogleLoginButton();
});
testWidgets('enable encryption', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoogleLoginInButton();
// testWidgets('enable encryption', (tester) async {
// await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.syncSetting);
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.cloud);
// the switch should be off by default
tester.assertEnableEncryptSwitchValue(false);
await tester.toggleEnableEncrypt();
// // the switch should be off by default
// tester.assertEnableEncryptSwitchValue(false);
// await tester.toggleEnableEncrypt();
// the switch should be on after toggling
tester.assertEnableEncryptSwitchValue(true);
// // the switch should be on after toggling
// tester.assertEnableEncryptSwitchValue(true);
// the switch can not be toggled back to off
await tester.toggleEnableEncrypt();
tester.assertEnableEncryptSwitchValue(true);
});
// // the switch can not be toggled back to off
// await tester.toggleEnableEncrypt();
// tester.assertEnableEncryptSwitchValue(true);
// });
testWidgets('enable sync', (tester) async {
await tester.initializeAppFlowy();
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
await tester.tapGoogleLoginInButton();
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.syncSetting);
await tester.openSettingsPage(SettingsPage.cloud);
// the switch should be on by default
tester.assertEnableSyncSwitchValue(true);
await tester.toggleEnableSync();
tester.assertSupabaseEnableSyncSwitchValue(true);
await tester.toggleEnableSync(SupabaseEnableSync);
// the switch should be off
tester.assertEnableSyncSwitchValue(false);
tester.assertSupabaseEnableSyncSwitchValue(false);
// the switch should be on after toggling
await tester.toggleEnableSync();
tester.assertEnableSyncSwitchValue(true);
await tester.toggleEnableSync(SupabaseEnableSync);
tester.assertSupabaseEnableSyncSwitchValue(true);
});
});
}
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database_view/widgets/card/container/card_container.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -15,8 +16,8 @@ const defaultLastCardName = 'Card 3';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board add row test', () {
testWidgets('Add card from header', (tester) async {
group('board add row test:', () {
testWidgets('from header', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -45,7 +46,7 @@ void main() {
const newCardName = 'Card 4';
await tester.enterText(
find.descendant(
of: find.byType(IntrinsicHeight),
of: find.byType(RowCardContainer),
matching: find.byType(TextField),
),
newCardName,
@@ -59,7 +60,7 @@ void main() {
expect(firstCardText.text, newCardName);
});
testWidgets('Add card from footer', (tester) async {
testWidgets('from footer', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -87,7 +88,7 @@ void main() {
const newCardName = 'Card 4';
await tester.enterText(
find.descendant(
of: find.byType(IntrinsicHeight),
of: find.byType(RowCardContainer),
matching: find.byType(TextField),
),
newCardName,
@@ -0,0 +1,50 @@
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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 'package:appflowy_board/appflowy_board.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('board group test', () {
testWidgets('move row to another group', (tester) async {
const card1Name = 'Card 1';
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
final card1 = find.ancestor(
of: find.findTextInFlowyText(card1Name),
matching: find.byType(AppFlowyGroupCard),
);
final doingGroup = find.findTextInFlowyText('Doing');
final doingGroupCenter = tester.getCenter(doingGroup);
final card1Center = tester.getCenter(card1);
await tester.timedDrag(
card1,
doingGroupCenter.translate(-card1Center.dx, -card1Center.dy),
const Duration(seconds: 1),
);
await tester.pumpAndSettle();
await tester.tap(card1);
await tester.pumpAndSettle();
final card1StatusFinder = find.descendant(
of: find.byType(RowPropertyList),
matching: find.descendant(
of: find.byType(SelectOptionTag),
matching: find.byType(FlowyText),
),
);
expect(card1StatusFinder, findsNWidgets(1));
final card1StatusText = tester.widget<FlowyText>(card1StatusFinder).text;
expect(card1StatusText, 'Doing');
});
});
}
@@ -3,15 +3,17 @@ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.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('board hide groups test', () {
group('board group options:', () {
testWidgets('expand/collapse hidden groups', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -76,6 +78,47 @@ void main() {
expect(shownGroups, 4);
});
});
testWidgets('delete a group', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
// tap group option button for the first group. Delete shouldn't show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.first,
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
// dismiss the popup
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// tap group option button for the first group. Delete should show up
await tester.tapButton(
find
.descendant(
of: find.byType(BoardColumnHeader),
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
)
.at(1),
);
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
// Tap the delete button and confirm
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
await tester.tapDialogOkButton();
// Expect number of groups to decrease by one
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
});
}
extension FlowySvgFinder on CommonFinders {
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
import 'board_row_test.dart' as board_row_test;
import 'board_add_row_test.dart' as board_add_row_test;
import 'board_group_test.dart' as board_group_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -9,4 +10,5 @@ void startTesting() {
// Board integration tests
board_row_test.main();
board_add_row_test.main();
board_group_test.main();
}
@@ -0,0 +1,13 @@
import 'package:integration_test/integration_test.dart';
import 'auth/appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'empty_test.dart' as first_test;
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
appflowy_cloud_auth_test.main();
}
@@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -74,10 +74,10 @@ void main() {
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell();
// Tap on create new event button
await tester.tapAddCalendarEventButton();
await tester.hoverOnTodayCalendarCell(
// Tap on create new event button
onHover: () async => await tester.tapAddCalendarEventButton(),
);
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
@@ -90,15 +90,9 @@ void main() {
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(2);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertEventEditorOpen();
@@ -112,7 +106,7 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Click on the event
await tester.openCalendarEvent(index: 1);
await tester.openCalendarEvent(index: 0);
tester.assertEventEditorOpen();
// Click on the open icon
@@ -137,7 +131,7 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Delete event from row detail page
await tester.openCalendarEvent(index: 1);
await tester.openCalendarEvent(index: 0);
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
@@ -163,7 +157,7 @@ void main() {
await tester.dismissEventEditor();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
await tester.dragDropRescheduleCalendarEvent();
// Make sure that the event has been rescheduled to the new date
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
@@ -4,14 +4,14 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'util/database_test_op.dart';
import 'util/util.dart';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid cell', () {
testWidgets('edit text cell', (tester) async {
group('edit grid cell:', () {
testWidgets('text', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -32,9 +32,9 @@ void main() {
await tester.pumpAndSettle();
});
// Makesure the text cells are filled with the right content when there are
// Make sure the text cells are filled with the right content when there are
// multiple text cell
testWidgets('edit multiple text cells', (tester) async {
testWidgets('multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(
@@ -73,7 +73,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit number cell', (tester) async {
testWidgets('number', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -131,7 +131,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit checkbox cell', (tester) async {
testWidgets('checkbox', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -149,7 +149,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit create time cell', (tester) async {
testWidgets('created time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -167,7 +167,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit last time cell', (tester) async {
testWidgets('last modified time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -185,7 +185,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit date time cell', (tester) async {
testWidgets('date time', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -275,7 +275,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit single select cell', (tester) async {
testWidgets('single select', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -347,7 +347,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('edit multi select cell', (tester) async {
testWidgets('multi select', (tester) async {
final tags = [
'tag 1',
'tag 2',
@@ -436,107 +436,107 @@ void main() {
await tester.pumpAndSettle();
});
});
testWidgets('edit checklist cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
testWidgets('checklist', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType, fieldType.name);
const fieldType = FieldType.Checklist;
await tester.createField(fieldType, fieldType.name);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// tap on the first checklist cell
await tester.tapChecklistCellInGrid(rowIndex: 0);
// tap on the first checklist cell
await tester.tapChecklistCellInGrid(rowIndex: 0);
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// assert that the checklist editor is shown
tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
isChecked: false,
);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
isChecked: false,
);
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
isChecked: false,
);
// dismiss new task editor
await tester.dismissCellEditor();
// dismiss new task editor
await tester.dismissCellEditor();
// dismiss checklist cell editor
await tester.dismissCellEditor();
// dismiss checklist cell editor
await tester.dismissCellEditor();
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// assert that progress bar is shown in grid at 0%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
// start editing the first checklist cell again
await tester.tapChecklistCellInGrid(rowIndex: 0);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
// create another task with the create button
await tester.createNewChecklistTask(name: "task 2", button: true);
// assert that the task was inserted
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
// assert that the task was inserted
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: false,
);
// mark it as complete
await tester.checkChecklistTask(index: 1);
// mark it as complete
await tester.checkChecklistTask(index: 1);
// assert that the task was checked in the editor
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: true,
);
// assert that the task was checked in the editor
tester.assertChecklistTaskInEditor(
index: 1,
name: "task 2",
isChecked: true,
);
// dismiss checklist editor
await tester.dismissCellEditor();
await tester.dismissCellEditor();
// dismiss checklist editor
await tester.dismissCellEditor();
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 50%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
// assert that progressbar is shown in grid at 50%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
// re-open the cell editor
await tester.tapChecklistCellInGrid(rowIndex: 0);
// re-open the cell editor
await tester.tapChecklistCellInGrid(rowIndex: 0);
// hover over first task and delete it
await tester.deleteChecklistTask(index: 0);
// hover over first task and delete it
await tester.deleteChecklistTask(index: 0);
// dismiss cell editor
await tester.dismissCellEditor();
// dismiss cell editor
await tester.dismissCellEditor();
// assert that progressbar is shown in grid at 100%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
// assert that progressbar is shown in grid at 100%
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
// re-open the cell edior
await tester.tapChecklistCellInGrid(rowIndex: 0);
// re-open the cell edior
await tester.tapChecklistCellInGrid(rowIndex: 0);
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// delete the remaining task
await tester.deleteChecklistTask(index: 0);
// dismiss the cell editor
await tester.dismissCellEditor();
// dismiss the cell editor
await tester.dismissCellEditor();
// check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
// check that the progress bar is not viisble
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
});
});
}
@@ -1,11 +1,11 @@
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
@@ -34,7 +34,7 @@ void main() {
await tester.findFieldWithName('New field 1');
// go back to linked database view, expect field to be hidden
await tester.tapTabBarLinkedViewByViewName('grid');
await tester.tapTabBarLinkedViewByViewName('Grid');
await tester.noFieldWithName('New field 1');
// use the settings button to show the field
@@ -6,13 +6,13 @@ import 'package:flutter/material.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid page', () {
group('grid field editor:', () {
testWidgets('rename existing field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -98,7 +98,7 @@ void main() {
await tester.renameField('New field 1');
await tester.dismissFieldEditor();
// Delete the field
// duplicate the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapDuplicatePropertyButton();
@@ -106,7 +106,30 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('create checklist field ', (tester) async {
testWidgets('insert field on either side of a field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
// insert new field to the right
await tester.tapGridFieldWithName('Type');
await tester.tapInsertFieldButton(left: false, name: 'Right');
await tester.dismissFieldEditor();
await tester.findFieldWithName('Right');
// insert new field to the right
await tester.tapGridFieldWithName('Type');
await tester.tapInsertFieldButton(left: true, name: "Left");
await tester.dismissFieldEditor();
await tester.findFieldWithName('Left');
await tester.pumpAndSettle();
});
testWidgets('create checklist field', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -7,9 +7,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import 'util/emoji.dart';
import 'util/util.dart';
import '../util/database_test_op.dart';
import '../util/emoji.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -123,7 +123,6 @@ void main() {
await tester.tapTypeOptionButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// After update the field type, the cells should be updated
await tester.findCellByFieldType(fieldType);
@@ -2,8 +2,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -34,7 +34,7 @@ void main() {
false,
false,
false,
false
false,
];
for (final (index, content) in checkboxCells.indexed) {
await tester.assertCheckboxCell(
@@ -54,7 +54,7 @@ void main() {
'10',
'11',
'12',
''
'',
];
for (final (index, content) in numberCells.indexed) {
await tester.assertCellContent(
@@ -152,7 +152,7 @@ void main() {
'Jun 16, 2023',
'',
'',
''
'',
];
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
@@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/database_test_op.dart';
import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -1,11 +1,10 @@
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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';
import '../util/database_test_op.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -18,15 +17,15 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// Create grid view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
// Create calendar view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
await tester.pumpAndSettle();
@@ -39,7 +38,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// rename board view
@@ -64,7 +63,7 @@ void main() {
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
// delete the board
@@ -219,7 +219,7 @@ void main() {
expect(node.delta!.toJson(), [
{
'insert': text,
'attributes': {'href': url}
'attributes': {'href': url},
}
]);
},
@@ -2,8 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -21,7 +21,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
name: 'math equation',
layout: ViewLayoutPB.Document,
);
@@ -67,7 +67,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
name: 'math equation',
layout: ViewLayoutPB.Document,
);
@@ -1,13 +1,14 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'util/mock/mock_openai_repository.dart';
import 'util/util.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:appflowy/startup/startup.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -1,20 +1,19 @@
import 'package:appflowy/env/env.dart';
import 'package:integration_test/integration_test.dart';
import 'appearance_settings_test.dart' as appearance_test_runner;
import 'auth/auth_test.dart' as auth_test_runner;
import 'board/board_test_runner.dart' as board_test_runner;
import 'database_calendar_test.dart' as database_calendar_test;
import 'database_cell_test.dart' as database_cell_test;
import 'database_field_settings_test.dart' as database_field_settings_test;
import 'database_field_test.dart' as database_field_test;
import 'database_filter_test.dart' as database_filter_test;
import 'database_row_page_test.dart' as database_row_page_test;
import 'database_row_test.dart' as database_row_test;
import 'database_setting_test.dart' as database_setting_test;
import 'database_share_test.dart' as database_share_test;
import 'database_sort_test.dart' as database_sort_test;
import 'database_view_test.dart' as database_view_test;
import 'database/database_calendar_test.dart' as database_calendar_test;
import 'database/database_cell_test.dart' as database_cell_test;
import 'database/database_field_settings_test.dart'
as database_field_settings_test;
import 'database/database_field_test.dart' as database_field_test;
import 'database/database_filter_test.dart' as database_filter_test;
import 'database/database_row_page_test.dart' as database_row_page_test;
import 'database/database_row_test.dart' as database_row_test;
import 'database/database_setting_test.dart' as database_setting_test;
import 'database/database_share_test.dart' as database_share_test;
import 'database/database_sort_test.dart' as database_sort_test;
import 'database/database_view_test.dart' as database_view_test;
import 'document/document_test_runner.dart' as document_test_runner;
import 'empty_test.dart' as first_test;
import 'hotkeys_test.dart' as hotkeys_test;
@@ -24,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 'auth/supabase_auth_test.dart' as supabase_auth_test_runner;
/// The main task runner for all integration tests in AppFlowy.
///
@@ -32,7 +32,7 @@ import 'tabs_test.dart' as tabs_test;
/// If flutter/flutter#101031 is resolved, this file can be removed completely.
/// Once removed, the integration_test.yaml must be updated to exclude this as
/// as the test target.
void main() {
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
@@ -76,10 +76,6 @@ void main() {
// User settings
settings_test_runner.main();
if (isCloudEnabled) {
auth_test_runner.main();
}
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();
@@ -3,6 +3,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/emoji.dart';
import '../util/util.dart';
void main() {
@@ -28,27 +30,12 @@ void main() {
await tester.pumpAndSettle();
// Select first option that isn't default
await tester.tap(find.byType(IconOption).first);
await tester.tapEmoji('😁');
await tester.pumpAndSettle();
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isNotEmpty);
// Open icon picker dialog again
await tester.tap(userAvatarFinder);
await tester.pumpAndSettle();
// Tap the default option
await tester.tap(
find.descendant(
of: find.byType(IconGallery),
matching: find.byType(UserAvatar),
),
);
await tester.pumpAndSettle();
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, isEmpty);
final UserAvatar userAvatar =
tester.widget(userAvatarFinder) as UserAvatar;
expect(userAvatar.iconUrl, '😁');
});
});
}
@@ -1,5 +1,6 @@
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -34,10 +35,20 @@ extension AppFlowyAuthTest on WidgetTester {
);
}
void assertEnableSyncSwitchValue(bool value) {
void assertSupabaseEnableSyncSwitchValue(bool value) {
assertSwitchValue(
find.descendant(
of: find.byType(EnableSync),
of: find.byType(SupabaseEnableSync),
matching: find.byWidgetPredicate((widget) => widget is Switch),
),
value,
);
}
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
assertSwitchValue(
find.descendant(
of: find.byType(AppFlowyCloudEnableSync),
matching: find.byWidgetPredicate((widget) => widget is Switch),
),
value,
@@ -53,9 +64,9 @@ extension AppFlowyAuthTest on WidgetTester {
await tapButton(finder);
}
Future<void> toggleEnableSync() async {
Future<void> toggleEnableSync(Type syncButton) async {
final finder = find.descendant(
of: find.byType(EnableSync),
of: find.byType(syncButton),
matching: find.byWidgetPredicate((widget) => widget is Switch),
);
@@ -1,12 +1,17 @@
import 'dart:async';
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
@@ -29,6 +34,8 @@ extension AppFlowyTestBase on WidgetTester {
// use to append after the application data directory
String? pathExtension,
Size windowsSize = const Size(1600, 1200),
AuthenticatorType? cloudType,
String? userEmail,
}) async {
binding.setSurfaceSize(windowsSize);
@@ -38,13 +45,54 @@ extension AppFlowyTestBase on WidgetTester {
);
WidgetsFlutterBinding.ensureInitialized();
await FlowyRunner.run(
FlowyApp(),
IntegrationMode.integrationTest,
rustEnvsBuilder: () {
final rustEnvs = <String, String>{};
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
break;
case AuthenticatorType.supabase:
break;
case AuthenticatorType.appflowyCloud:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
break;
}
}
return rustEnvs;
},
didInitGetItCallback: () {
return Future(
() async {
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
break;
case AuthenticatorType.supabase:
await useSupabaseCloud();
getIt.unregister<AuthService>();
getIt.registerFactory<AuthService>(
() => SupabaseMockAuthService(),
);
break;
case AuthenticatorType.appflowyCloud:
await useAppFlowyCloud();
getIt.unregister<AuthService>();
getIt.registerFactory<AuthService>(
() => AppFlowyCloudMockAuthService(email: userEmail),
);
break;
}
}
},
);
},
);
await waitUntilSignInPageShow();
return FlowyTestContext(
applicationDataDirectory: directory,
);
@@ -83,7 +131,7 @@ extension AppFlowyTestBase on WidgetTester {
}
Future<void> waitUntilSignInPageShow() async {
if (isCloudEnabled) {
if (isAuthEnabled) {
final finder = find.byType(SignInAnonymousButton);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
@@ -207,3 +255,16 @@ extension AppFlowyFinderTestBase on CommonFinders {
);
}
}
Future<void> useSupabaseCloud() async {
await setAuthenticatorType(AuthenticatorType.supabase);
await setSupbaseServer(
Some(TestEnv.supabaseUrl),
Some(TestEnv.supabaseAnonKey),
);
}
Future<void> useAppFlowyCloud() async {
await setAuthenticatorType(AuthenticatorType.appflowyCloud);
// await setAppFlowyCloudUrl(Some(TestEnv.afCloudUrl));
}
@@ -303,7 +303,7 @@ extension CommonOperations on WidgetTester {
KVKeys.showRenameDialogWhenCreatingNewFile,
(value) => bool.parse(value),
);
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
final showRenameDialog = settingsOrFailure.fold(() => false, (r) => r);
if (showRenameDialog) {
await tapOKButton();
}
@@ -21,7 +21,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
@@ -34,10 +33,10 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/or
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
@@ -52,6 +51,8 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting_action.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@@ -222,7 +223,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == content,
(widget) =>
widget is SelectOptionTag &&
(widget.name == content || widget.option?.name == content),
),
);
expect(finder, findsOneWidget);
@@ -239,7 +242,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == content,
(widget) =>
widget is SelectOptionTag &&
(widget.name == content || widget.option?.name == content),
),
);
expect(finder, findsOneWidget);
@@ -287,13 +292,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
skipOffstage: false,
);
final dateCellText = find.descendant(
of: findCell,
matching: find.byType(GridDateCellText),
);
final text = find.descendant(
of: dateCellText,
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is FlowyText) {
@@ -445,7 +445,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
}) async {
final findRow = find.byType(GridRow);
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == name,
(widget) =>
widget is SelectOptionTag &&
(widget.name == name || widget.option?.name == name),
);
final cell = find.descendant(
@@ -688,7 +690,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byType(DeleteFieldButton);
final deleteButton = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
);
await tapButton(deleteButton);
final confirmButton = find.descendant(
@@ -761,17 +766,36 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(field);
}
Future<void> tapInsertFieldButton({
required bool left,
required String name,
}) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
(left && widget.action == FieldAction.insertLeft ||
!left && widget.action == FieldAction.insertRight),
);
await tapButton(field);
await renameField(name);
}
/// Should call [tapGridFieldWithName] first.
Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.hide,
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
);
await tapButton(field);
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byType(FieldVisibilityToggleButton);
final button = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
);
await tapButton(button);
}
@@ -1131,7 +1155,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover);
final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
@@ -1148,7 +1172,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapDatabaseLayoutButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover);
final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
@@ -1164,7 +1188,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> tapCalendarLayoutSettingButton() async {
final findSettingItem = find.byType(DatabaseSettingListPopover);
final findSettingItem = find.byType(DatabaseSettingsList);
final findLayoutButton = find.byWidgetPredicate(
(widget) =>
widget is FlowyText &&
@@ -1242,12 +1266,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> hoverOnTodayCalendarCell() async {
Future<void> hoverOnTodayCalendarCell({
Future<void> Function()? onHover,
}) async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && widget.isToday,
);
await hoverOnWidget(todayCell);
await hoverOnWidget(todayCell, onHover: onHover);
}
Future<void> tapAddCalendarEventButton() async {
@@ -1355,7 +1381,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
Future<void> dragDropRescheduleCalendarEvent() async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
@@ -1381,13 +1407,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(unscheduledEvent);
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
Future<void> tapCreateLinkedDatabaseViewButton(
DatabaseLayoutPB layoutType,
) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);
final findCreateButton = find.byWidgetPredicate(
(widget) =>
widget is TarBarAddButtonActionCell && widget.action == action,
widget is TabBarAddButtonActionCell && widget.action == layoutType,
);
await tapButton(findCreateButton);
}
@@ -1554,7 +1582,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
final findText = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == layout.layoutName(),
(widget) => widget is FlowyText && widget.text == layout.layoutName,
);
final button = find.descendant(
@@ -1582,7 +1610,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
widget is DesktopDatabasePropertyCell &&
widget.fieldInfo.name == fieldName,
);
final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton));
@@ -12,9 +12,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@@ -1,4 +1,4 @@
import 'package:emoji_mart/emoji_mart.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
@@ -15,7 +15,7 @@ class MyMockClient extends Mock implements http.Client {
if (requestType == 'POST' &&
requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{
'content-type': 'text/event-stream'
'content-type': 'text/event-stream',
};
final responseBody = Stream.fromIterable([
utf8.encode(
+7 -1
View File
@@ -59,6 +59,8 @@ PODS:
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- keyboard_height_plugin (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -100,6 +102,7 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
@@ -145,6 +148,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
keyboard_height_plugin:
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -171,7 +176,7 @@ SPEC CHECKSUMS:
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
@@ -180,6 +185,7 @@ SPEC CHECKSUMS:
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
integration_test: 13825b8a9334a850581300559b8839134b124670
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
@@ -143,7 +143,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -515,7 +515,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = VHB67HRSZG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AppFlowy;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -1 +1,80 @@
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,13 +1,10 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:shared_preferences/shared_preferences.dart';
abstract class KeyValueStorage {
Future<void> set(String key, String value);
Future<Either<FlowyError, String>> get(String key);
Future<Either<FlowyError, T>> getWithFormat<T>(
Future<Option<String>> get(String key);
Future<Option<T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
);
@@ -20,25 +17,25 @@ class DartKeyValue implements KeyValueStorage {
SharedPreferences get sharedPreferences => _sharedPreferences!;
@override
Future<Either<FlowyError, String>> get(String key) async {
Future<Option<String>> get(String key) async {
await _initSharedPreferencesIfNeeded();
final value = sharedPreferences.getString(key);
if (value != null) {
return Right(value);
return Some(value);
}
return Left(FlowyError());
return none();
}
@override
Future<Either<FlowyError, T>> getWithFormat<T>(
Future<Option<T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
) async {
final value = await get(key);
return value.fold(
(l) => left(l),
(r) => right(formatter(r)),
() => none(),
(s) => Some(formatter(s)),
);
}
@@ -67,47 +64,3 @@ class DartKeyValue implements KeyValueStorage {
_sharedPreferences ??= await SharedPreferences.getInstance();
}
}
/// Key-value store
/// The data is stored in the local storage of the device.
class RustKeyValue implements KeyValueStorage {
@override
Future<void> set(String key, String value) async {
await ConfigEventSetKeyValue(
KeyValuePB.create()
..key = key
..value = value,
).send();
}
@override
Future<Either<FlowyError, String>> get(String key) async {
final payload = KeyPB.create()..key = key;
final response = await ConfigEventGetKeyValue(payload).send();
return response.swap().map((r) => r.value);
}
@override
Future<Either<FlowyError, T>> getWithFormat<T>(
String key,
T Function(String value) formatter,
) async {
final value = await get(key);
return value.fold(
(l) => left(l),
(r) => right(formatter(r)),
);
}
@override
Future<void> remove(String key) async {
await ConfigEventRemoveKeyValue(
KeyPB.create()..key = key,
).send();
}
@override
Future<void> clear() async {
// TODO(Lucas): implement clear
}
}
@@ -6,13 +6,6 @@ class KVKeys {
/// The key for the path location of the local data for the whole app.
static const String pathLocation = '$prefix.path_location';
/// The key for the last time login type.
///
/// The value is one of the following:
/// - local
/// - supabase
static const String loginType = '$prefix.login_type';
/// The key for saving the window size
///
/// The value is a json string with the following format:
@@ -49,4 +42,9 @@ class KVKeys {
/// The value is a boolean string.
static const String showRenameDialogWhenCreatingNewFile =
'showRenameDialogWhenCreatingNewFile';
static const String kCloudType = 'kCloudType';
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
static const String kSupabaseURL = 'kSupbaseURL';
static const String kSupabaseAnonKey = 'kSupabaseAnonKey';
}
+42 -7
View File
@@ -4,30 +4,40 @@ import 'package:json_annotation/json_annotation.dart';
part 'backend_env.g.dart';
@JsonSerializable()
class AppFlowyEnv {
class AppFlowyConfiguration {
final String root;
final String custom_app_path;
final String origin_app_path;
final String device_id;
final int authenticator_type;
final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config;
final Map<String, String> envs;
AppFlowyEnv({
AppFlowyConfiguration({
required this.root,
required this.custom_app_path,
required this.origin_app_path,
required this.device_id,
required this.authenticator_type,
required this.supabase_config,
required this.appflowy_cloud_config,
required this.envs,
});
factory AppFlowyEnv.fromJson(Map<String, dynamic> json) =>
_$AppFlowyEnvFromJson(json);
factory AppFlowyConfiguration.fromJson(Map<String, dynamic> json) =>
_$AppFlowyConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$AppFlowyEnvToJson(this);
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
}
@JsonSerializable()
class SupabaseConfiguration {
/// Indicates whether the sync feature is enabled.
final bool enable_sync;
final String url;
final String anon_key;
SupabaseConfiguration({
this.enable_sync = true,
required this.url,
required this.anon_key,
});
@@ -36,6 +46,17 @@ class SupabaseConfiguration {
_$SupabaseConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
static SupabaseConfiguration defaultConfig() {
return SupabaseConfiguration(
url: '',
anon_key: '',
);
}
bool get isValid {
return url.isNotEmpty && anon_key.isNotEmpty;
}
}
@JsonSerializable()
@@ -54,4 +75,18 @@ class AppFlowyCloudConfiguration {
_$AppFlowyCloudConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
static AppFlowyCloudConfiguration defaultConfig() {
return AppFlowyCloudConfiguration(
base_url: '',
ws_base_url: '',
gotrue_url: '',
);
}
bool get isValid {
return base_url.isNotEmpty &&
ws_base_url.isNotEmpty &&
gotrue_url.isNotEmpty;
}
}
+320
View File
@@ -0,0 +1,320 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
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.
///
/// This method updates the cloud type setting in the key-value storage
/// using the [KeyValueStorage] service. The cloud type is identified
/// by the [AuthenticatorType] enum.
///
/// [ty] - The type of cloud to be set. It must be one of the values from
/// [AuthenticatorType] enum. The corresponding integer value of the enum is stored:
/// - `CloudType.local` is stored as "0".
/// - `CloudType.supabase` is stored as "1".
/// - `CloudType.appflowyCloud` is stored as "2".
Future<void> setAuthenticatorType(AuthenticatorType ty) async {
switch (ty) {
case AuthenticatorType.local:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
break;
case AuthenticatorType.supabase:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
break;
case AuthenticatorType.appflowyCloud:
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
break;
}
}
/// Retrieves the currently set cloud type.
///
/// This method fetches the cloud type setting from the key-value storage
/// using the [KeyValueStorage] service and returns the corresponding
/// [AuthenticatorType] enum value.
///
/// Returns:
/// A Future that resolves to a [AuthenticatorType] enum value representing the
/// currently set cloud type. The default return value is `CloudType.local`
/// if no valid setting is found.
///
Future<AuthenticatorType> getAuthenticatorType() async {
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
return value.fold(() => AuthenticatorType.local, (s) {
switch (s) {
case "0":
return AuthenticatorType.local;
case "1":
return AuthenticatorType.supabase;
case "2":
return AuthenticatorType.appflowyCloud;
default:
return AuthenticatorType.local;
}
});
}
/// Determines whether authentication is enabled.
///
/// This getter evaluates if authentication should be enabled based on the
/// current integration mode and cloud type settings.
///
/// Returns:
/// A boolean value indicating whether authentication is enabled. It returns
/// `true` if the application is in release or develop mode, and the cloud type
/// is not set to `CloudType.local`. Additionally, it checks if either the
/// AppFlowy Cloud or Supabase configuration is valid.
/// Returns `false` otherwise.
bool get isAuthEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease ||
integrationMode().isDevelop ||
integrationMode().isIntegrationTest) {
final env = getIt<AppFlowyCloudSharedEnv>();
if (env.authenticatorType == AuthenticatorType.supabase) {
return env.supabaseConfig.isValid;
}
if (env.authenticatorType == AuthenticatorType.appflowyCloud) {
return env.appflowyCloudConfig.isValid;
}
return false;
} else {
return false;
}
}
/// Checks if Supabase is enabled.
///
/// This getter evaluates if Supabase should be enabled based on the
/// current integration mode and cloud type setting.
///
/// Returns:
/// A boolean value indicating whether Supabase is enabled. It returns `true`
/// if the application is in release or develop mode and the current cloud type
/// is `CloudType.supabase`. Otherwise, it returns `false`.
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease ||
integrationMode().isDevelop ||
integrationMode().isIntegrationTest) {
return currentCloudType() == AuthenticatorType.supabase;
} else {
return false;
}
}
/// Determines if AppFlowy Cloud is enabled.
///
/// This getter assesses if AppFlowy Cloud should be enabled based on the
/// current integration mode and cloud type setting.
///
/// Returns:
/// A boolean value indicating whether AppFlowy Cloud is enabled. It returns
/// `true` if the application is in release or develop mode and the current
/// cloud type is `CloudType.appflowyCloud`. Otherwise, it returns `false`.
bool get isAppFlowyCloudEnabled {
// Only enable appflowy cloud in release and develop mode.
if (integrationMode().isRelease ||
integrationMode().isDevelop ||
integrationMode().isIntegrationTest) {
return currentCloudType() == AuthenticatorType.appflowyCloud;
} else {
return false;
}
}
enum AuthenticatorType {
local,
supabase,
appflowyCloud;
bool get isEnabled => this != AuthenticatorType.local;
int get value {
switch (this) {
case AuthenticatorType.local:
return 0;
case AuthenticatorType.supabase:
return 1;
case AuthenticatorType.appflowyCloud:
return 2;
}
}
static fromValue(int value) {
switch (value) {
case 0:
return AuthenticatorType.local;
case 1:
return AuthenticatorType.supabase;
case 2:
return AuthenticatorType.appflowyCloud;
default:
return AuthenticatorType.local;
}
}
}
AuthenticatorType currentCloudType() {
return getIt<AppFlowyCloudSharedEnv>().authenticatorType;
}
Future<void> setAppFlowyCloudUrl(Option<String> url) async {
await url.fold(
() => getIt<KeyValueStorage>().remove(KVKeys.kAppflowyCloudBaseURL),
(s) => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, s),
);
}
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
class AppFlowyCloudSharedEnv {
final AuthenticatorType _authenticatorType;
final AppFlowyCloudConfiguration appflowyCloudConfig;
final SupabaseConfiguration supabaseConfig;
AppFlowyCloudSharedEnv({
required AuthenticatorType authenticatorType,
required this.appflowyCloudConfig,
required this.supabaseConfig,
}) : _authenticatorType = authenticatorType;
AuthenticatorType get authenticatorType => _authenticatorType;
static Future<AppFlowyCloudSharedEnv> fromEnv() async {
// If [Env.enableCustomCloud] is true, then use the custom cloud configuration.
if (Env.enableCustomCloud) {
// Use the custom cloud configuration.
final cloudType = await getAuthenticatorType();
final appflowyCloudConfig = await getAppFlowyCloudConfig();
final supabaseCloudConfig = await getSupabaseCloudConfig();
return AppFlowyCloudSharedEnv(
authenticatorType: cloudType,
appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: supabaseCloudConfig,
);
} else {
// Using the cloud settings from the .env file.
final appflowyCloudConfig = AppFlowyCloudConfiguration(
base_url: Env.afCloudUrl,
ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),
gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),
);
return AppFlowyCloudSharedEnv(
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
appflowyCloudConfig: appflowyCloudConfig,
supabaseConfig: SupabaseConfiguration.defaultConfig(),
);
}
}
}
Future<AppFlowyCloudConfiguration> configurationFromUri(
Uri baseUri,
String baseUrl,
) async {
// When the host is set to 'localhost', the application will utilize the local configuration. This setup assumes that 'localhost' does not employ a reverse proxy, therefore default port settings are used.
if (baseUri.host == "localhost") {
return AppFlowyCloudConfiguration(
base_url: "$baseUrl:8000",
ws_base_url: "ws://${baseUri.host}:8000/ws",
gotrue_url: "$baseUrl:9998",
);
} else {
return AppFlowyCloudConfiguration(
base_url: baseUrl,
ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),
gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),
);
}
}
Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig() async {
final baseURL = await getAppFlowyCloudUrl();
try {
final uri = Uri.parse(baseURL);
return await configurationFromUri(uri, baseURL);
} catch (e) {
Log.error("Failed to parse AppFlowy Cloud URL: $e");
return AppFlowyCloudConfiguration.defaultConfig();
}
}
Future<String> getAppFlowyCloudUrl() async {
final result =
await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);
return result.fold(
() => "",
(url) => url,
);
}
Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
try {
final uri = Uri.parse(baseURL);
// Construct the WebSocket URL directly from the parsed URI.
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws');
return wsUrl.toString();
} catch (e) {
Log.error("Failed to get WebSocket URL: $e");
return "";
}
}
Future<String> _getAppFlowyCloudGotrueUrl(String baseURL) async {
return "$baseURL/gotrue";
}
Future<void> setSupbaseServer(
Option<String> url,
Option<String> anonKey,
) async {
assert(
(url.isSome() && anonKey.isSome()) || (url.isNone() && anonKey.isNone()),
"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),
);
}
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
final url = await _getSupbaseUrl();
final anonKey = await _getSupabaseAnonKey();
return SupabaseConfiguration(
url: url,
anon_key: anonKey,
);
}
Future<String> _getSupbaseUrl() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
return result.fold(
() => "",
(url) => url,
);
}
Future<String> _getSupabaseAnonKey() async {
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
return result.fold(
() => "",
(url) => url,
);
}
+31
View File
@@ -0,0 +1,31 @@
// lib/env/env.dart
import 'package:envied/envied.dart';
part 'cloud_env_test.g.dart';
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
///
@Envied(path: '.env.cloud.test')
abstract class TestEnv {
/// AppFlowy Cloud Configuration
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_URL',
defaultValue: '',
)
static final String afCloudUrl = _TestEnv.afCloudUrl;
// Supabase Configuration:
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_URL',
defaultValue: '',
)
static final String supabaseUrl = _TestEnv.supabaseUrl;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_ANON_KEY',
defaultValue: '',
)
static final String supabaseAnonKey = _TestEnv.supabaseAnonKey;
}
+14 -114
View File
@@ -1,129 +1,29 @@
// lib/env/env.dart
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:envied/envied.dart';
part 'env.g.dart';
/// The environment variables are defined in `.env` file that is located in the
/// appflowy_flutter.
/// Run `dart run build_runner build --delete-conflicting-outputs`
/// to generate the keys from the env file.
///
/// If you want to regenerate the keys, you need to run `dart run
/// build_runner clean` before running `dart run build_runner build
/// --delete-conflicting-outputs`.
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
///
@Envied(path: '.env')
abstract class Env {
// This flag is used to decide if users can dynamically configure cloud settings. It turns true when a .env file exists containing the APPFLOWY_CLOUD_URL variable. By default, this is set to false.
static bool get enableCustomCloud {
return Env.authenticatorType == AuthenticatorType.appflowyCloud.value &&
_Env.afCloudUrl.isEmpty;
}
@EnviedField(
obfuscate: true,
varName: 'CLOUD_TYPE',
defaultValue: '0',
obfuscate: false,
varName: 'AUTHENTICATOR_TYPE',
defaultValue: 2,
)
static final int cloudType = _Env.cloudType;
static const int authenticatorType = _Env.authenticatorType;
/// AppFlowy Cloud Configuration
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_BASE_URL',
obfuscate: false,
varName: 'APPFLOWY_CLOUD_URL',
defaultValue: '',
)
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
defaultValue: '',
)
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
defaultValue: '',
)
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
// Supabase Configuration:
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_URL',
defaultValue: '',
)
static final String supabaseUrl = _Env.supabaseUrl;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_ANON_KEY',
defaultValue: '',
)
static final String supabaseAnonKey = _Env.supabaseAnonKey;
}
bool get isCloudEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType().isEnabled;
} else {
return false;
}
}
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.supabase;
} else {
return false;
}
}
bool get isAppFlowyCloudEnabled {
// Only enable appflowy cloud in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.appflowyCloud;
} else {
return false;
}
}
enum CloudType {
unknown,
supabase,
appflowyCloud;
bool get isEnabled => this != CloudType.unknown;
}
CloudType currentCloudType() {
final value = Env.cloudType;
if (value == 1) {
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
Log.error(
"Supabase is not configured correctly. The values are: "
"url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}",
);
return CloudType.unknown;
} else {
return CloudType.supabase;
}
}
if (value == 2) {
if (Env.afCloudBaseUrl.isEmpty ||
Env.afCloudWSBaseUrl.isEmpty ||
Env.afCloudGoTrueUrl.isEmpty) {
Log.error(
"AppFlowy cloud is not configured correctly. The values are: "
"baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}",
);
return CloudType.unknown;
} else {
return CloudType.appflowyCloud;
}
}
return CloudType.unknown;
static const String afCloudUrl = _Env.afCloudUrl;
}
@@ -1,29 +1,22 @@
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy/workspace/application/recent/recent_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileRouterRecord {
PropertyValueNotifier<String> lastPushedRouter =
PropertyValueNotifier<String>('');
}
extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName;
push(
Uri(
path: view.routeName,
queryParameters: view.queryParameters,
).toString(),
);
).then((value) {
RecentService().updateRecentViews([view.id], true);
});
}
}
@@ -1,270 +0,0 @@
// ThemeData in mobile
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:flowy_infra/colorscheme/colorscheme.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
const _primaryColor = Color(0xFF2DA2F6); //primary 100
const _onBackgroundColor = Color(0xff2F3030); // text/title color
const _onSurfaceColor = Color(0xff676666); // text/body color
const _onSecondaryColor = Color(0xFFC5C7CB); // text/body2 color
// TODO(yijing): improve theme color before release
ThemeData getMobileThemeData(
Brightness brightness,
FlowyColorScheme theme,
String fontFamily,
String monospaceFontFamily,
) {
final mobileColorTheme = (brightness == Brightness.light)
? ColorScheme(
brightness: brightness,
primary: _primaryColor,
onPrimary: Colors.white,
// TODO(yijing): add color later
secondary: Colors.white,
onSecondary: _onSecondaryColor,
error: const Color(0xffFB006D),
onError: const Color(0xffFB006D),
background: Colors.white,
onBackground: _onBackgroundColor,
outline: const Color(0xffBDC0C5), //caption
outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24),
//Snack bar
surface: Colors.white,
onSurface: _onSurfaceColor, // text/body color
surfaceVariant: const Color.fromARGB(255, 216, 216, 216),
)
: ColorScheme(
brightness: brightness,
primary: _primaryColor,
onPrimary: Colors.white,
// TODO(yijing): add color later
secondary: Colors.black,
onSecondary: Colors.white,
error: const Color(0xffFB006D),
onError: const Color(0xffFB006D),
background: const Color(0xff1C1C1E), // BG/Secondary color
onBackground: Colors.white,
outline: const Color(0xff96989C), //caption
outlineVariant: Colors.black,
//Snack bar
surface: const Color(0xff2F3030),
onSurface: const Color(0xffC5C6C7), // text/body color
);
return ThemeData(
// color
primaryColor: mobileColorTheme.primary, //primary 100
primaryColorLight: const Color(0xFF57B5F8), //primary 80
dividerColor: mobileColorTheme.outline, //caption
hintColor: mobileColorTheme.outline,
disabledColor: mobileColorTheme.outline,
scaffoldBackgroundColor: mobileColorTheme.background,
appBarTheme: AppBarTheme(
foregroundColor: mobileColorTheme.onBackground,
backgroundColor: mobileColorTheme.background,
centerTitle: false,
titleTextStyle: TextStyle(
color: mobileColorTheme.onBackground,
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
),
shadowColor: mobileColorTheme.outlineVariant,
),
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
return mobileColorTheme.primary;
}
return mobileColorTheme.outline;
}),
),
// button
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
fixedSize: MaterialStateProperty.all(const Size.fromHeight(48)),
elevation: MaterialStateProperty.all(0),
textStyle: MaterialStateProperty.all(
const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
shadowColor: MaterialStateProperty.all(null),
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return const Color(0xFF57B5F8);
}
return mobileColorTheme.primary;
},
),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: ButtonStyle(
textStyle: MaterialStateProperty.all(
const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
foregroundColor: MaterialStateProperty.all(
mobileColorTheme.onBackground,
),
backgroundColor: MaterialStateProperty.all(mobileColorTheme.background),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
side: MaterialStateProperty.all(
BorderSide(
color: mobileColorTheme.outline,
width: 0.5,
),
),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
),
// splash color
overlayColor: MaterialStateProperty.all(
Colors.grey[100],
),
),
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
textStyle: MaterialStateProperty.all(
const TextStyle(
fontFamily: 'Poppins',
),
),
),
),
// text
fontFamily: 'Poppins',
textTheme: TextTheme(
displayLarge: const TextStyle(
color: Color(0xFF57B5F8),
fontSize: 32,
fontWeight: FontWeight.w700,
height: 1.20,
letterSpacing: 0.16,
),
displayMedium: TextStyle(
color: mobileColorTheme.onBackground,
fontSize: 32,
fontWeight: FontWeight.w600,
height: 1.20,
letterSpacing: 0.16,
),
// H1 Semi 26
displaySmall: TextStyle(
color: mobileColorTheme.onBackground,
fontWeight: FontWeight.w600,
height: 1.10,
letterSpacing: 0.13,
),
// body2 14 Regular
bodyMedium: TextStyle(
color: mobileColorTheme.onBackground,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.20,
letterSpacing: 0.07,
),
// Trash empty title
labelLarge: TextStyle(
color: mobileColorTheme.onBackground,
fontSize: 22,
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
),
// setting item title
labelMedium: TextStyle(
color: mobileColorTheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w500,
),
// setting group title
labelSmall: TextStyle(
color: mobileColorTheme.onBackground,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
),
),
inputDecorationTheme: InputDecorationTheme(
contentPadding: const EdgeInsets.all(8),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
width: 2,
color: _primaryColor,
),
borderRadius: BorderRadius.all(Radius.circular(6)),
),
focusedErrorBorder: OutlineInputBorder(
borderSide: BorderSide(color: mobileColorTheme.error),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: mobileColorTheme.error),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: mobileColorTheme.outline,
),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
),
colorScheme: mobileColorTheme,
indicatorColor: Colors.blue,
extensions: [
AFThemeExtension(
warning: theme.yellow,
success: theme.green,
tint1: theme.tint1,
tint2: theme.tint2,
tint3: theme.tint3,
tint4: theme.tint4,
tint5: theme.tint5,
tint6: theme.tint6,
tint7: theme.tint7,
tint8: theme.tint8,
tint9: theme.tint9,
textColor: theme.text,
greyHover: theme.hoverBG1,
greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3,
toggleOffFill: theme.shader5,
progressBarBGColor: theme.progressBarBGColor,
toggleButtonBGColor: theme.toggleButtonBGColor,
calendarWeekendBGColor: theme.calendarWeekendBGColor,
gridRowCountColor: theme.gridRowCountColor,
code: getFontStyle(
fontFamily: monospaceFontFamily,
fontColor: theme.shader3,
),
callout: getFontStyle(
fontFamily: fontFamily,
fontSize: FontSizes.s11,
fontColor: theme.shader3,
),
calloutBGColor: theme.hoverBG3,
tableCellBGColor: theme.surface,
caption: getFontStyle(
fontFamily: fontFamily,
fontSize: FontSizes.s11,
fontWeight: FontWeight.w400,
fontColor: theme.hint,
),
),
],
);
}
@@ -1,7 +1,54 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AppBarBackButton extends StatelessWidget {
const AppBarBackButton({
super.key,
this.onTap,
});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap ?? () => context.pop(),
child: const Icon(Icons.arrow_back_ios_new),
);
}
}
class AppBarCloseButton extends StatelessWidget {
const AppBarCloseButton({
super.key,
this.onTap,
this.margin = const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
});
final VoidCallback? onTap;
final EdgeInsets margin;
@override
Widget build(BuildContext context) {
return FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.close,
),
margin: margin,
onTap: onTap ?? () => context.pop(),
);
}
}
class AppBarCancelButton extends StatelessWidget {
const AppBarCancelButton({
super.key,
required this.onTap,
});
@@ -12,7 +59,9 @@ class AppBarBackButton extends StatelessWidget {
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap,
child: const Icon(Icons.arrow_back_ios_new),
child: FlowyText(
LocaleKeys.button_cancel.tr(),
),
);
}
}
@@ -56,6 +105,9 @@ class AppBarButton extends StatelessWidget {
return InkWell(
enableFeedback: true,
borderRadius: BorderRadius.circular(28),
splashColor: Colors.transparent,
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(extent),
@@ -2,6 +2,7 @@ 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/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/document_page.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -92,20 +93,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
child: Builder(
builder: (context) {
final view = context.watch<ViewBloc>().state.view;
return _buildApp(
view,
actions,
body,
);
return _buildApp(view, actions, body);
},
),
);
} else {
return _buildApp(
null,
[],
body,
);
return _buildApp(null, [], body);
}
},
);
@@ -120,8 +113,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
FlowyText(
'$icon ',
EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
Expanded(
@@ -133,9 +126,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
),
],
),
leading: AppBarBackButton(
onTap: () => context.pop(),
),
leading: const AppBarBackButton(),
actions: actions,
),
body: SafeArea(
@@ -148,7 +139,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
return AppBarMoreButton(
onTap: (context) {
showMobileBottomSheet(
context: context,
context,
builder: (_) => _buildViewPageBottomSheet(context),
);
},
@@ -0,0 +1,56 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart';
class OptionColorList extends StatelessWidget {
const OptionColorList({
super.key,
this.selectedColor,
required this.onSelectedColor,
});
final SelectOptionColorPB? selectedColor;
final void Function(SelectOptionColorPB color) onSelectedColor;
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 6,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: SelectOptionColorPB.values.map(
(colorPB) {
final color = colorPB.toColor(context);
final isSelected = selectedColor?.value == colorPB.value;
return GestureDetector(
onTap: () => onSelectedColor(colorPB),
child: Container(
margin: const EdgeInsets.all(
8.0,
),
decoration: BoxDecoration(
color: color,
borderRadius: Corners.s12Border,
border: Border.all(
color: isSelected
? const Color(0xff00C6F1)
: Theme.of(context).dividerColor,
),
),
alignment: Alignment.center,
child: isSelected
? const FlowySvg(
FlowySvgs.blue_check_s,
size: Size.square(28.0),
blendMode: null,
)
: null,
),
);
},
).toList(),
);
}
}
@@ -2,6 +2,7 @@ export 'bottom_sheet_action_widget.dart';
export 'bottom_sheet_add_new_page.dart';
export 'bottom_sheet_drag_handler.dart';
export 'bottom_sheet_rename_widget.dart';
export 'bottom_sheet_view_item.dart';
export 'bottom_sheet_view_item_body.dart';
export 'bottom_sheet_view_item_header.dart';
export 'bottom_sheet_view_page.dart';
@@ -1,16 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class BottomSheetActionWidget extends StatelessWidget {
const BottomSheetActionWidget({
super.key,
required this.svg,
this.svg,
required this.text,
required this.onTap,
this.iconColor,
});
final FlowySvgData svg;
final FlowySvgData? svg;
final String text;
final VoidCallback onTap;
final Color? iconColor;
@@ -20,13 +21,30 @@ class BottomSheetActionWidget extends StatelessWidget {
final iconColor =
this.iconColor ?? Theme.of(context).colorScheme.onBackground;
if (svg == null) {
return OutlinedButton(
style: Theme.of(context)
.outlinedButtonTheme
.style
?.copyWith(alignment: Alignment.center),
onPressed: onTap,
child: FlowyText(
text,
textAlign: TextAlign.center,
),
);
}
return OutlinedButton.icon(
icon: FlowySvg(
svg,
svg!,
size: const Size.square(22.0),
color: iconColor,
),
label: Text(text),
label: FlowyText(
text,
overflow: TextOverflow.ellipsis,
),
style: Theme.of(context)
.outlinedButtonTheme
.style
@@ -25,7 +25,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.documents_s,
svg: FlowySvgs.document_s,
text: LocaleKeys.document_menuName.tr(),
onTap: () => onAction(ViewLayoutPB.Document),
),
@@ -0,0 +1,121 @@
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_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:go_router/go_router.dart';
import 'package:string_validator/string_validator.dart';
class MobileBottomSheetEditLinkWidget extends StatefulWidget {
const MobileBottomSheetEditLinkWidget({
super.key,
required this.text,
required this.href,
required this.onEdit,
});
final String text;
final String? href;
final void Function(String text, String href) onEdit;
@override
State<MobileBottomSheetEditLinkWidget> createState() =>
_MobileBottomSheetEditLinkWidgetState();
}
class _MobileBottomSheetEditLinkWidgetState
extends State<MobileBottomSheetEditLinkWidget> {
late final TextEditingController textController;
late final TextEditingController hrefController;
@override
void initState() {
super.initState();
textController = TextEditingController(
text: widget.text,
);
hrefController = TextEditingController(
text: widget.href,
);
}
@override
void dispose() {
textController.dispose();
hrefController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTextField(textController, null),
const VSpace(12.0),
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
const VSpace(12.0),
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_cancel.tr(),
onTap: () => context.pop(),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_done.tr(),
onTap: () {
widget.onEdit(textController.text, hrefController.text);
},
),
),
if (widget.href != null && isURL(widget.href)) ...[
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.editor_openLink.tr(),
onTap: () {
safeLaunchUrl(widget.href!);
},
),
),
],
],
),
],
);
}
Widget _buildTextField(
TextEditingController controller,
String? hintText,
) {
return SizedBox(
height: 44.0,
child: FlowyTextField(
controller: controller,
hintText: hintText,
suffixIcon: Padding(
padding: const EdgeInsets.all(4.0),
child: FlowyButton(
text: const FlowySvg(
FlowySvgs.close_lg,
),
margin: EdgeInsets.zero,
useIntrinsicWidth: true,
onTap: () {
controller.clear();
},
),
),
),
);
}
}
@@ -8,10 +8,12 @@ class MobileBottomSheetRenameWidget extends StatefulWidget {
super.key,
required this.name,
required this.onRename,
this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
});
final String name;
final void Function(String name) onRename;
final EdgeInsets padding;
@override
State<MobileBottomSheetRenameWidget> createState() =>
@@ -39,36 +41,33 @@ class _MobileBottomSheetRenameWidgetState
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
vertical: 16.0,
),
padding: widget.padding,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(8.0),
Expanded(
child: SizedBox(
height: 44.0,
height: 42.0,
child: FlowyTextField(
controller: controller,
textInputAction: TextInputAction.done,
onSubmitted: (text) => widget.onRename(text),
),
),
),
const HSpace(12.0),
FlowyTextButton(
LocaleKeys.button_edit.tr(),
constraints: const BoxConstraints.tightFor(height: 42),
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0,
),
fontColor: Colors.white,
fillColor: Colors.lightBlue.shade300,
fillColor: Theme.of(context).primaryColor,
onPressed: () {
widget.onRename(controller.text);
},
),
const HSpace(8.0),
],
),
);
@@ -0,0 +1,118 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
enum MobileBottomSheetType {
view,
rename,
}
class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
@override
State<MobileViewItemBottomSheet> createState() =>
_MobileViewItemBottomSheetState();
}
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
@override
initState() {
super.initState();
type = widget.defaultType;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
);
}
Widget _buildHeader() {
switch (type) {
case MobileBottomSheetType.view:
case MobileBottomSheetType.rename:
// header
return MobileViewItemBottomSheetHeader(
showBackButton: type != MobileBottomSheetType.view,
view: widget.view,
onBack: () {
setState(() {
type = MobileBottomSheetType.view;
});
},
);
}
}
Widget _buildBody() {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
setState(() {
type = MobileBottomSheetType.rename;
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
}
},
);
case MobileBottomSheetType.rename:
return MobileBottomSheetRenameWidget(
name: widget.view.name,
onRename: (name) {
if (name != widget.view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
}
@@ -98,7 +98,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
),
)
),
],
);
}
@@ -1,4 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -16,8 +17,8 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// back button,
showBackButton
@@ -31,23 +32,27 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
),
),
)
: const SizedBox.shrink(),
: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.close,
),
margin: EdgeInsets.zero,
onTap: () {
context.pop();
},
),
// title
Expanded(
child: Text(
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6,
),
child: FlowyText.medium(
view.name,
style: theme.textTheme.labelSmall,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
)
const HSpace(24.0),
],
);
}
@@ -43,18 +43,15 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
);
}
@@ -120,33 +117,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// undo, redo
// Row(
// mainAxisSize: MainAxisSize.max,
// children: [
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_undo_m,
// text: LocaleKeys.toolbar_undo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.undo,
// ),
// ),
// ),
// const HSpace(8),
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_redo_m,
// text: LocaleKeys.toolbar_redo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.redo,
// ),
// ),
// ),
// ],
// ),
// const VSpace(8),
// rename, duplicate
Row(
mainAxisSize: MainAxisSize.max,
@@ -217,16 +187,16 @@ class MobileViewBottomSheetBody extends StatelessWidget {
: MobileViewBottomSheetBodyAction.addToFavorites,
),
),
const VSpace(8),
// help center
BottomSheetActionWidget(
svg: FlowySvgs.m_help_center_m,
text: LocaleKeys.button_helpCenter.tr(),
onTap: () => onAction(
MobileViewBottomSheetBodyAction.helpCenter,
),
),
// Help Center
// const VSpace(8),
// BottomSheetActionWidget(
// svg: FlowySvgs.m_help_center_m,
// text: LocaleKeys.button_helpCenter.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.helpCenter,
// ),
// ),
],
);
}
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -51,7 +51,7 @@ enum MobilePaneActionType {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
showMobileBottomSheet(
context: context,
context,
builder: (context) {
return MultiBlocProvider(
providers: [
@@ -1,134 +1,107 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
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';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({
required BuildContext context,
Future<T?> showMobileBottomSheet<T>(
BuildContext context, {
required WidgetBuilder builder,
ShapeBorder? shape,
bool isDragEnabled = true,
bool resizeToAvoidBottomInset = true,
EdgeInsets padding = const EdgeInsets.fromLTRB(16, 16, 16, 32),
bool showDragHandle = false,
bool showHeader = false,
bool showCloseButton = false,
String title = '', // only works if showHeader is true
Color? backgroundColor,
bool isScrollControlled = true,
BoxConstraints? constraints,
}) async {
showModalBottomSheet(
assert(() {
if (showCloseButton || title.isNotEmpty) assert(showHeader);
return true;
}());
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
enableDrag: true,
isScrollControlled: isScrollControlled,
enableDrag: isDragEnabled,
useSafeArea: true,
builder: builder,
clipBehavior: Clip.antiAlias,
backgroundColor: backgroundColor,
constraints: constraints,
shape: shape ??
const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Corners.s12Radius,
),
),
builder: (context) {
final List<Widget> children = [];
if (showDragHandle) {
children.addAll([
const VSpace(4),
const DragHandler(),
]);
}
if (showHeader) {
children.addAll([
VSpace(padding.top),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
showCloseButton
? Padding(
padding: EdgeInsets.only(left: padding.left),
child: const AppBarCloseButton(
margin: EdgeInsets.zero,
),
)
: const SizedBox.shrink(),
FlowyText(
title,
),
showCloseButton
? HSpace(padding.right + 24)
: const SizedBox.shrink(),
],
),
const VSpace(4),
const Divider(),
]);
}
final child = builder(context);
if (resizeToAvoidBottomInset) {
children.add(
AnimatedPadding(
padding: EdgeInsets.only(
top: showHeader ? 0 : padding.top,
left: padding.left,
right: padding.right,
bottom: padding.bottom + MediaQuery.of(context).viewInsets.bottom,
),
duration: Duration.zero,
child: child,
),
);
} else {
children.add(child);
}
if (children.length == 1) {
return children.first;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
);
}
enum MobileBottomSheetType {
view,
rename,
}
class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
@override
State<MobileViewItemBottomSheet> createState() =>
_MobileViewItemBottomSheetState();
}
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
@override
initState() {
super.initState();
type = widget.defaultType;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
),
);
}
Widget _buildHeader() {
switch (type) {
case MobileBottomSheetType.view:
case MobileBottomSheetType.rename:
// header
return MobileViewItemBottomSheetHeader(
showBackButton: type != MobileBottomSheetType.view,
view: widget.view,
onBack: () {
setState(() {
type = MobileBottomSheetType.view;
});
},
);
}
}
Widget _buildBody() {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
setState(() {
type = MobileBottomSheetType.rename;
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
}
},
);
case MobileBottomSheetType.rename:
return MobileBottomSheetRenameWidget(
name: widget.view.name,
onRename: (name) {
if (name != widget.view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
}
@@ -0,0 +1,4 @@
export 'mobile_board_screen.dart';
export 'mobile_board_content.dart';
export 'widgets/mobile_hidden_groups_column.dart';
export 'widgets/mobile_board_trailing.dart';
@@ -0,0 +1,222 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.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';
class MobileBoardContent extends StatefulWidget {
const MobileBoardContent({
super.key,
});
@override
State<MobileBoardContent> createState() => _MobileBoardContentState();
}
class _MobileBoardContentState extends State<MobileBoardContent> {
final renderHook = RowCardRenderHook<String>();
late final ScrollController scrollController;
late final AppFlowyBoardScrollController scrollManager;
@override
void initState() {
super.initState();
//mobile may not need this
//scroll to bottom when add a new card
scrollManager = AppFlowyBoardScrollController();
scrollController = ScrollController();
renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId.
final isInGroup =
options.where((element) => element.id == groupId).isNotEmpty;
if (isInGroup || options.isEmpty) {
return const SizedBox.shrink();
}
return null;
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final config = AppFlowyBoardConfig(
groupCornerRadius: 8,
groupBackgroundColor: Theme.of(context).colorScheme.secondary,
groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12),
groupHeaderPadding: const EdgeInsets.all(8),
groupBodyPadding: const EdgeInsets.all(4),
groupFooterPadding: const EdgeInsets.all(8),
cardMargin: const EdgeInsets.all(4),
);
return BlocListener<BoardBloc, BoardState>(
listenWhen: (previous, current) =>
previous.recentAddedRowMeta != current.recentAddedRowMeta,
listener: (context, state) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: state.recentAddedRowMeta!.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final showCreateGroupButton =
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: scrollController,
controller: context.read<BoardBloc>().boardController,
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
config: config,
leading: showHiddenGroups
? MobileHiddenGroupsColumn(padding: config.groupHeaderPadding)
: const HSpace(16),
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
afGroupData: column,
afGroupItem: columnItem,
cardMargin: config.cardMargin,
),
);
},
),
);
}
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
final style = Theme.of(context);
return SizedBox(
height: 42,
width: double.infinity,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
),
icon: FlowySvg(
FlowySvgs.add_m,
color: style.colorScheme.onSurface,
),
label: Text(
LocaleKeys.board_column_createNewCard.tr(),
style: style.textTheme.bodyMedium?.copyWith(
color: style.colorScheme.onSurface,
),
),
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.createBottomRow(columnData.id)),
),
);
}
Widget _buildCard({
required BuildContext context,
required AppFlowyGroupData afGroupData,
required AppFlowyGroupItem afGroupItem,
required EdgeInsets cardMargin,
}) {
final boardBloc = context.read<BoardBloc>();
final groupItem = afGroupItem as GroupItem;
final groupData = afGroupData.customData as GroupData;
final rowMeta = groupItem.row;
final rowCache = boardBloc.getRowCache();
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
final cellCache = rowCache.cellCache;
final viewId = boardBloc.viewId;
final cellBuilder = CardCellBuilder<String>(cellCache);
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
final groupItemId = groupItem.row.id + groupData.group.groupId;
return Container(
key: ValueKey(groupItemId),
margin: cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard<String>(
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: groupData.group.groupId,
groupingFieldId: groupItem.fieldInfo.id,
groupId: groupData.group.groupId,
isEditing: isEditing,
cellBuilder: cellBuilder,
renderHook: renderHook,
openCard: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () => boardBloc
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
onEndEditing: () =>
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
styleConfiguration: const RowCardStyleConfiguration(
showAccessory: false,
hoverStyle: null,
cardPadding: EdgeInsets.all(8),
cellPadding: EdgeInsets.zero,
),
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context) {
return BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
),
boxShadow: [
// card shadow
BoxShadow(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
);
}
}
@@ -0,0 +1,175 @@
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_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
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';
// similar to [BoardColumnHeader] in Desktop
class GroupCardHeader extends StatefulWidget {
const GroupCardHeader({
super.key,
required this.groupData,
});
final AppFlowyGroupData groupData;
@override
State<GroupCardHeader> createState() => _GroupCardHeaderState();
}
class _GroupCardHeaderState extends State<GroupCardHeader> {
late final TextEditingController _controller =
TextEditingController.fromValue(
TextEditingValue(
selection: TextSelection.collapsed(
offset: widget.groupData.headerData.groupName.length,
),
text: widget.groupData.headerData.groupName,
),
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final boardCustomData = widget.groupData.customData as GroupData;
final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
);
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
Widget title = Text(
widget.groupData.headerData.groupName,
style: titleTextStyle,
overflow: TextOverflow.ellipsis,
);
// header can be edited if it's not default group(no status) and the field type can be edited
if (!boardCustomData.group.isDefault &&
boardCustomData.fieldType.canEditHeader) {
title = GestureDetector(
onTap: () => context
.read<BoardBloc>()
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
child: Text(
widget.groupData.headerData.groupName,
style: titleTextStyle,
overflow: TextOverflow.ellipsis,
),
);
}
if (state.isEditingHeader &&
state.editingHeaderId == widget.groupData.id) {
title = TextField(
controller: _controller,
autofocus: true,
onEditingComplete: () => context.read<BoardBloc>().add(
BoardEvent.endEditingHeader(
widget.groupData.id,
_controller.text,
),
),
maxLines: 1,
style: titleTextStyle,
);
}
return Padding(
padding: const EdgeInsets.only(left: 16),
child: SizedBox(
height: 42,
child: Row(
children: [
_buildHeaderIcon(boardCustomData),
Expanded(child: title),
IconButton(
icon: Icon(
Icons.more_horiz_rounded,
color: Theme.of(context).colorScheme.onSurface,
),
splashRadius: 5,
onPressed: () => showFlowyMobileBottomSheet(
context,
title: LocaleKeys.board_column_groupActions.tr(),
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();
},
),
),
],
);
},
),
),
IconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onSurface,
),
splashRadius: 5,
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(widget.groupData.id),
),
),
],
),
),
);
},
);
}
Widget _buildHeaderIcon(GroupData customData) =>
switch (customData.fieldType) {
FieldType.Checkbox => FlowySvg(
customData.asCheckboxGroup()!.isCheck
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
),
_ => const SizedBox.shrink(),
};
}
@@ -0,0 +1,129 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Add new group
class MobileBoardTrailing extends StatefulWidget {
const MobileBoardTrailing({
super.key,
});
@override
State<MobileBoardTrailing> createState() => _MobileBoardTrailingState();
}
class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
final TextEditingController _textController = TextEditingController();
bool isEditing = false;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final style = Theme.of(context);
return Container(
margin: const EdgeInsets.all(8),
child: SizedBox(
width: screenSize.width * 0.7,
child: isEditing
? DecoratedBox(
decoration: BoxDecoration(
color: style.colorScheme.secondary,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _textController,
autofocus: true,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
Icons.close,
color: style.colorScheme.onBackground,
),
onPressed: () => _textController.clear(),
),
isDense: true,
),
onEditingComplete: () {
context.read<BoardBloc>().add(
BoardEvent.createGroup(
_textController.text,
),
);
_textController.clear();
setState(() {
isEditing = false;
});
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
child: Text(
LocaleKeys.button_cancel.tr(),
style: style.textTheme.titleSmall?.copyWith(
color: style.colorScheme.onBackground,
),
),
onPressed: () {
setState(() {
isEditing = false;
});
},
),
TextButton(
child: Text(
LocaleKeys.button_add.tr(),
style: style.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: style.colorScheme.onBackground,
),
),
onPressed: () {
context.read<BoardBloc>().add(
BoardEvent.createGroup(
_textController.text,
),
);
_textController.clear();
setState(() {
isEditing = false;
});
},
),
],
),
],
),
),
)
: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
foregroundColor: style.colorScheme.onBackground,
backgroundColor: style.colorScheme.secondary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.add),
label: Text(
LocaleKeys.board_column_newGroup.tr(),
),
onPressed: () => setState(
() => isEditing = true,
),
),
),
);
}
}
@@ -0,0 +1,316 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.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';
class MobileHiddenGroupsColumn extends StatelessWidget {
const MobileHiddenGroupsColumn({super.key, required this.padding});
final EdgeInsets padding;
@override
Widget build(BuildContext context) {
final databaseController = context.read<BoardBloc>().databaseController;
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
selector: (state) => state.layoutSettings,
builder: (context, layoutSettings) {
if (layoutSettings == null) {
return const SizedBox.shrink();
}
final isCollapsed = layoutSettings.collapseHiddenGroups;
return Container(
padding: padding,
child: AnimatedSize(
alignment: AlignmentDirectional.topStart,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 150),
child: isCollapsed
? SizedBox(
height: 50,
child: _collapseExpandIcon(context, isCollapsed),
)
: SizedBox(
width: 180,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Spacer(),
_collapseExpandIcon(context, isCollapsed),
],
),
Text(
LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(context).colorScheme.tertiary,
),
),
const VSpace(8),
Expanded(
child: MobileHiddenGroupList(
databaseController: databaseController,
),
),
],
),
),
),
);
},
);
}
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
return CircleAvatar(
radius: 20,
backgroundColor: Theme.of(context).colorScheme.secondary,
child: IconButton(
icon: FlowySvg(
isCollapsed
? FlowySvgs.hamburger_s_s
: FlowySvgs.pull_left_outlined_s,
size: isCollapsed ? const Size.square(12) : const Size.square(40),
),
onPressed: () => context
.read<BoardBloc>()
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
),
);
}
}
class MobileHiddenGroupList extends StatelessWidget {
const MobileHiddenGroupList({
super.key,
required this.databaseController,
});
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
final bloc = context.read<BoardBloc>();
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
itemCount: state.hiddenGroups.length,
itemBuilder: (_, index) => MobileHiddenGroup(
key: ValueKey(state.hiddenGroups[index].groupId),
group: state.hiddenGroups[index],
index: index,
bloc: bloc,
),
physics: const ClampingScrollPhysics(),
onReorder: (oldIndex, newIndex) {
if (oldIndex < newIndex) {
newIndex--;
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
}
}
class MobileHiddenGroup extends StatelessWidget {
const MobileHiddenGroup({
super.key,
required this.group,
required this.index,
required this.bloc,
});
final GroupPB group;
final BoardBloc bloc;
final int index;
@override
Widget build(BuildContext context) {
final databaseController = bloc.databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
return BlocProvider<BoardBloc>.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == this.group.groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Row(
children: [
Expanded(
child: Text(
group.groupName,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
child: const Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.hide_m,
size: Size.square(20),
),
),
onTap: () => showFlowyMobileConfirmDialog(
context,
title: LocaleKeys.board_mobile_unhideGroup.tr(),
content: LocaleKeys.board_mobile_unhideGroupContent.tr(),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.primary,
onActionButtonPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
),
],
),
children: [
MobileHiddenGroupItemList(
bloc: bloc,
viewId: databaseController.viewId,
groupId: group.groupId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
),
],
);
},
),
);
}
}
class MobileHiddenGroupItemList extends StatelessWidget {
const MobileHiddenGroupItemList({
required this.bloc,
required this.groupId,
required this.viewId,
required this.primaryField,
required this.rowCache,
super.key,
});
final BoardBloc bloc;
final String groupId;
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
...group.rows.map(
(item) {
final cellContext = rowCache.loadCells(item)[primaryField.id]!;
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Row(
children: [
if (!cellContext.rowMeta.isDocumentEmpty) ...[
const FlowySvg(FlowySvgs.notes_s),
const HSpace(4),
],
Expanded(
child: Text(
text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
);
},
);
});
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
child: CardCellBuilder<String>(rowCache.cellCache).buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
);
},
),
];
return ListView.builder(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
);
},
),
);
}
}
@@ -0,0 +1,3 @@
export 'group_card_header.dart';
export 'mobile_board_trailing.dart';
export 'mobile_hidden_groups_column.dart';
@@ -0,0 +1,2 @@
export 'card_detail/mobile_card_detail_screen.dart';
export 'card_content/mobile_card_content.dart';
@@ -0,0 +1,9 @@
export 'checkbox.dart';
export 'date.dart';
export 'style.dart';
export 'time_stamp.dart';
export 'number.dart';
export 'text.dart';
export 'select_option.dart';
export 'url.dart';
export 'checklist.dart';
@@ -0,0 +1,68 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileCheckboxCardCell extends CardCell {
const MobileCheckboxCardCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileCheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<MobileCheckboxCardCell> {
late final CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: IconButton(
padding: EdgeInsets.zero,
alignment: Alignment.centerLeft,
visualDensity: VisualDensity.compact,
icon: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
);
},
),
);
}
}
@@ -0,0 +1,137 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class MobileChecklistCardCell extends CardCell {
const MobileChecklistCardCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<MobileChecklistCardCell> {
late final ChecklistCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController)
..add(const ChecklistCellEvent.initial());
}
@override
void dispose() {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: cellStyle.padding,
child: MobileChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
);
},
),
);
}
}
class MobileChecklistProgressBar extends StatefulWidget {
const MobileChecklistProgressBar({
super.key,
required this.tasks,
required this.percent,
});
final List<ChecklistSelectOption> tasks;
final double percent;
final int segmentLimit = 5;
@override
State<MobileChecklistProgressBar> createState() =>
_MobileChecklistProgresssBarState();
}
class _MobileChecklistProgresssBarState
extends State<MobileChecklistProgressBar> {
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length;
final completedTaskColor = numFinishedTasks == widget.tasks.length
? AFThemeExtension.of(context).success
: Theme.of(context).colorScheme.primary;
return Row(
children: [
Expanded(
child: Row(
children: [
if (widget.tasks.isNotEmpty &&
widget.tasks.length <= widget.segmentLimit)
...List<Widget>.generate(
widget.tasks.length,
(index) => Flexible(
child: Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(2)),
color: index < numFinishedTasks
? completedTaskColor
: AFThemeExtension.of(context).progressBarBGColor,
),
margin: const EdgeInsets.symmetric(horizontal: 1),
height: 6.0,
),
),
)
else
Expanded(
child: LinearPercentIndicator(
lineHeight: 6.0,
percent: widget.percent,
padding: EdgeInsets.zero,
progressColor: completedTaskColor,
backgroundColor:
AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(2),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
"${(widget.percent * 100).round()}%",
style: cellStyle.secondaryTextStyle(),
),
),
],
);
}
}
@@ -0,0 +1,74 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileDateCardCell<CustomCardData> extends CardCell {
const MobileDateCardCell({
super.key,
required this.cellControllerBuilder,
this.renderHook,
});
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
@override
State<MobileDateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<MobileDateCardCell> {
late final DateCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox();
} else {
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.dateStr,
style: cellStyle.secondaryTextStyle(),
),
);
}
},
),
);
}
}
Loaded 100 of 900 files, more files were not shown because too many files have changed in this diff. Show more