From 7e101b8bf1cf3bf96be1b0379a18d01df7646482 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:25:47 +0800 Subject: [PATCH 01/17] fix: slash command panel bugs (#3722) --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 18 ++++---- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +++---- .../document/BlockSlash/BlockSlashMenu.tsx | 10 ++++- .../document/BlockSlash/index.hooks.ts | 8 +--- .../TextBlock/useTurnIntoBlockEvents.ts | 27 ++--------- .../_shared/InlineBlock/PageInline.tsx | 11 +++-- .../document/_shared/SubscribeNode.hooks.ts | 8 ++++ .../document/_shared/usePanelSearchText.ts | 18 ++++---- .../effects/workspace/page/page_controller.ts | 1 + .../document/async-actions/blocks/insert.ts | 35 ++++++++------- .../document/async-actions/blocks/update.ts | 45 +++++++++++++------ .../document/async-actions/mention.ts | 16 ++----- .../document/async-actions/turn_to.ts | 2 +- frontend/rust-lib/Cargo.lock | 18 ++++---- frontend/rust-lib/Cargo.toml | 16 +++---- 15 files changed, 130 insertions(+), 119 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 6ae60b055b8a..697f49addf53 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -872,7 +872,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -902,7 +902,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "proc-macro2", "quote", @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "collab", @@ -934,7 +934,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "bytes", @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "chrono", @@ -990,7 +990,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "async-trait", "bincode", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -1038,7 +1038,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index fc71cc0ddd92..28c33270543c 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "52b # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx index 60eef0a49006..234ff085f847 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx @@ -27,6 +27,7 @@ import { slashCommandActions } from '$app_reducers/document/slice'; import { Keyboard } from '$app/constants/document/keyboard'; import { selectOptionByUpDown } from '$app/utils/document/menu'; import { turnToBlockThunk } from '$app_reducers/document/async-actions'; +import {useTranslation} from "react-i18next"; function BlockSlashMenu({ id, @@ -42,6 +43,7 @@ function BlockSlashMenu({ container: HTMLDivElement; }) { const dispatch = useAppDispatch(); + const { t } = useTranslation() const ref = useRef(null); const { docId, controller } = useSubscribeDocument(); const handleInsert = useCallback( @@ -279,6 +281,12 @@ function BlockSlashMenu({ [dispatch, docId] ); + const renderEmptyContent = useCallback(() => { + return
+ {t('findAndReplace.noResult')} +
+ }, [t]); + return (
{ @@ -288,7 +296,7 @@ function BlockSlashMenu({ className={'flex h-[100%] max-h-[40vh] w-[324px] min-w-[180px] max-w-[calc(100vw-32px)] flex-col p-1'} >
- {Object.entries(optionsByGroup).map(([group, options]) => ( + {options.length === 0 ? renderEmptyContent(): Object.entries(optionsByGroup).map(([group, options]) => (
{group}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts index bc41b76ef02f..10fa7a0297f7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts @@ -33,12 +33,6 @@ export function useBlockSlash() { setAnchorPosition(undefined); }, [blockId, visible]); - useEffect(() => { - if (!slashText) { - dispatch(slashCommandActions.closeSlashCommand(docId)); - } - }, [dispatch, docId, slashText]); - const searchText = useMemo(() => { if (!slashText) return ''; if (slashText[0] !== '/') return slashText; @@ -66,7 +60,7 @@ export function useSubscribeSlash() { const slashCommandState = useSubscribeSlashState(); const visible = slashCommandState.isSlashCommand; const blockId = slashCommandState.blockId; - const { searchText } = useSubscribePanelSearchText({ blockId: '', open: visible }); + const { searchText } = useSubscribePanelSearchText({ blockId: blockId || '', open: visible }); return { visible, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts index 7b377205d61c..bb4859017e3c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts @@ -4,11 +4,9 @@ import { useAppDispatch } from '$app/stores/store'; import { turnToBlockThunk } from '$app_reducers/document/async-actions'; import { blockConfig } from '$app/constants/document/config'; -import Delta, { Op } from 'quill-delta'; +import Delta from 'quill-delta'; import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks'; -import { getBlock, getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks'; -import isHotkey from 'is-hotkey'; -import { slashCommandActions } from '$app_reducers/document/slice'; +import { getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks'; import { getDeltaText } from '$app/utils/document/delta'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { turnIntoConfig } from './shortchut'; @@ -212,26 +210,9 @@ export function useTurnIntoBlockEvents(id: string) { dispatch(turnToBlockThunk({ id, data, type: BlockType.EquationBlock, controller })); }, - }, - { - // Here custom slash key event for TextBlock - canHandle: (e: React.KeyboardEvent) => { - const flag = getFlag(); - - return isHotkey('/', e) && flag === ''; - }, - handler: (_: React.KeyboardEvent) => { - if (!controller) return; - dispatch( - slashCommandActions.openSlashCommand({ - blockId: id, - docId, - }) - ); - }, - }, + } ]; - }, [canHandle, controller, dispatch, docId, getAttrs, getDeltaContent, getFlag, id, spaceTriggerMap]); + }, [canHandle, controller, dispatch, getAttrs, getDeltaContent, id, spaceTriggerMap]); return turnIntoBlockEvents; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx index 1dcd4ac31e6a..a97726adedf4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx @@ -13,9 +13,10 @@ function PageInline({ pageId }: { pageId: string }) { const { t } = useTranslation(); const page = useAppSelector((state) => state.pages.pageMap[pageId]); const navigate = useNavigate(); - const [currentPage, setCurrentPage] = useState(null); + const [currentPage, setCurrentPage] = useState(page); const loadPage = useCallback(async (id: string) => { const controller = new PageController(id); + const page = await controller.getPage(); setCurrentPage(page); }, []); @@ -29,13 +30,15 @@ function PageInline({ pageId }: { pageId: string }) { ); useEffect(() => { - if (page) { + if (!page) { + loadPage(pageId); + } else { setCurrentPage(page); - return; } - void loadPage(pageId); + }, [page, loadPage, pageId]); + return currentPage ? ( ((state) => { const documentState = state[DOCUMENT_NAME][docId]; const node = documentState?.nodes[id]; + // if node is root, return page name + if (!node?.parent) { + const delta = state.pages?.pageMap[docId]?.name; + return { + node, + delta: delta ? JSON.stringify(new Delta().insert(delta)) : '', + }; + } const externalId = node?.externalId; return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts index 1777423ea8bc..245041f702b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts @@ -6,7 +6,7 @@ import { getDeltaText } from '$app/utils/document/delta'; export function useSubscribePanelSearchText({ blockId, open }: { blockId: string; open: boolean }) { const [searchText, setSearchText] = useState(''); const beforeOpenDeltaRef = useRef([]); - const { delta } = useSubscribeNode(blockId); + const { delta: deltaStr } = useSubscribeNode(blockId); const handleSearch = useCallback((newDelta: Delta) => { const diff = new Delta(beforeOpenDeltaRef.current).diff(newDelta); const text = getDeltaText(diff); @@ -15,20 +15,22 @@ export function useSubscribePanelSearchText({ blockId, open }: { blockId: string }, []); useEffect(() => { - if (!open || !delta) return; - handleSearch(new Delta(JSON.parse(delta))); - }, [handleSearch, delta, open]); + if (!open || !deltaStr) return; + + handleSearch(new Delta(JSON.parse(deltaStr))); + }, [handleSearch, deltaStr, open]); useEffect(() => { if (!open) { beforeOpenDeltaRef.current = []; return; } + if (beforeOpenDeltaRef.current.length > 0) return; - beforeOpenDeltaRef.current = new Delta(JSON.parse(delta)).ops; - handleSearch(new Delta(JSON.parse(delta))); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open]); + const delta = new Delta(JSON.parse(deltaStr || "{}")); + beforeOpenDeltaRef.current = delta.ops; + handleSearch(delta); + }, [deltaStr, handleSearch, open]); return { searchText, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts index de09500d0ccc..5bdea28e3915 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts @@ -56,6 +56,7 @@ export class PageController { getPage = async (id?: string): Promise => { const result = await this.backendService.getPage(id || this.id); + if (result.ok) { return parserViewPBToPage(result.val); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts index fd269e4e09b1..76cdca31ba20 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts @@ -34,26 +34,11 @@ export const insertAfterNodeThunk = createAsyncThunk( const actions = []; let newNodeId; const deltaOperator = new BlockDeltaOperator(documentState, controller); - - if (defaultDelta) { - newNodeId = generateId(); - actions.push( - ...deltaOperator.getNewTextLineActions({ - blockId: newNodeId, - parentId, - prevId: node.id, - delta: defaultDelta, - type, - }) - ); - } else { + if (type === BlockType.DividerBlock) { const newNode = newBlock(type, parentId, data); actions.push(controller.getInsertAction(newNode, node.id)); newNodeId = newNode.id; - } - - if (type === BlockType.DividerBlock) { const nodeId = generateId(); actions.push( @@ -66,6 +51,24 @@ export const insertAfterNodeThunk = createAsyncThunk( }) ); newNodeId = nodeId; + } else { + if (defaultDelta) { + newNodeId = generateId(); + actions.push( + ...deltaOperator.getNewTextLineActions({ + blockId: newNodeId, + parentId, + prevId: node.id, + delta: defaultDelta, + type, + }) + ); + } else { + const newNode = newBlock(type, parentId, data); + + actions.push(controller.getInsertAction(newNode, node.id)); + newNodeId = newNode.id; + } } await controller.applyActions(actions); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts index 90a6cc0de0fd..72f68e1eddad 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts @@ -8,6 +8,7 @@ import { updatePageName } from '$app_reducers/pages/async_actions'; import { getDeltaText } from '$app/utils/document/delta'; import { BlockDeltaOperator } from '$app/utils/document/block_delta'; import { openMention, closeMention } from '$app_reducers/document/async-actions/mention'; +import {slashCommandActions} from "$app_reducers/document/slice"; const updateNodeDeltaAfterThunk = createAsyncThunk( 'document/updateNodeDeltaAfter', @@ -16,7 +17,7 @@ const updateNodeDeltaAfterThunk = createAsyncThunk( thunkAPI ) => { const { dispatch } = thunkAPI; - const { docId, ops, oldDelta, newDelta } = payload; + const { docId, ops, oldDelta, newDelta, id } = payload; const insertOps = ops.filter((op) => op.insert !== undefined); const deleteOps = ops.filter((op) => op.delete !== undefined); @@ -24,13 +25,32 @@ const updateNodeDeltaAfterThunk = createAsyncThunk( const newText = getDeltaText(newDelta); const deleteText = oldText.slice(newText.length); - if (insertOps.length === 1 && insertOps[0].insert === '@') { - dispatch(openMention({ docId })); + if (insertOps.length === 1) { + const char = insertOps[0].insert; + if (char === '@' && (oldText.endsWith(' ') || oldText === '')) { + dispatch(openMention({ docId })); + } + if (char === '/') { + dispatch( + slashCommandActions.openSlashCommand({ + blockId: id, + docId, + }) + ); + } } - if (deleteOps.length === 1 && deleteText === '@') { - dispatch(closeMention({ docId })); + if (deleteOps.length === 1) { + if (deleteText === '@') { + dispatch(closeMention({ docId })); + } + if (deleteText === '/') { + dispatch( + slashCommandActions.closeSlashCommand(docId) + ); + } } + } ); @@ -44,13 +64,6 @@ export const updateNodeDeltaThunk = createAsyncThunk( const docState = state[DOCUMENT_NAME][docId]; const node = docState.nodes[id]; - const deltaOperator = new BlockDeltaOperator(docState, controller); - const oldDelta = deltaOperator.getDeltaWithBlockId(id); - - if (!oldDelta) return; - const diff = oldDelta?.diff(newDelta); - - if (ops.length === 0 || diff?.ops.length === 0) return; // If the node is the root node, update the page name if (!node.parent) { await dispatch( @@ -62,7 +75,13 @@ export const updateNodeDeltaThunk = createAsyncThunk( return; } - if (!node.externalId) return; + const deltaOperator = new BlockDeltaOperator(docState, controller); + const oldDelta = deltaOperator.getDeltaWithBlockId(id); + + if (!oldDelta) return; + const diff = oldDelta?.diff(newDelta); + + if (ops.length === 0 || diff?.ops.length === 0 || !node.externalId) return; await controller.applyTextDelta(node.externalId, JSON.stringify(ops)); await dispatch(updateNodeDeltaAfterThunk({ docId, id, ops, newDelta, oldDelta, controller })); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts index 9549da3d5cc6..4ec94ea731df 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts @@ -62,24 +62,16 @@ export const formatMention = createAsyncThunk( const caret = rangeState.caret; if (!caret) return; - const index = caret.index - searchTextLength; + const charLength = searchTextLength + 1; + const index = caret.index - charLength; - const deltaOperator = new BlockDeltaOperator(documentState); + const deltaOperator = new BlockDeltaOperator(documentState, controller); const nodeDelta = deltaOperator.getDeltaWithBlockId(blockId); if (!nodeDelta) return; - const diffDelta = new Delta() - .retain(index) - .delete(searchTextLength) - .insert(`@`, { - mention: { - type, - [type]: value, - }, - }); + const diffDelta = new Delta().retain(index).delete(charLength).insert('@',{ mention: { type, [type]: value } }); const applyTextDeltaAction = deltaOperator.getApplyDeltaAction(blockId, diffDelta); - if (!applyTextDeltaAction) return; await controller.applyActions([applyTextDeltaAction]); dispatch( diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts index d9eac7d3e038..bdfd1d4fde34 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts @@ -61,7 +61,7 @@ export const turnToBlockThunk = createAsyncThunk( parentId: parent.id, prevId: block.id || null, delta: delta ? delta : new Delta([{ insert: '' }]), - type, + type: BlockType.TextBlock, data, }); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 69280ae014f8..900fa89becb4 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -720,7 +720,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -739,7 +739,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "proc-macro2", "quote", @@ -781,7 +781,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "collab", @@ -801,7 +801,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "bytes", @@ -815,7 +815,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "chrono", @@ -857,7 +857,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "async-trait", "bincode", @@ -878,7 +878,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "async-trait", @@ -905,7 +905,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2f5734ae#2f5734ae9e58efd611383b78cd9068d514643e75" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8a127b7a3af5..491f3cd4fc7e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "52b # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2f5734ae" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } From 8e5b6b6e272063d95daac1795be24574e1e731c3 Mon Sep 17 00:00:00 2001 From: Shreesh Nautiyal <114166000+Shreesh09@users.noreply.github.com> Date: Mon, 23 Oct 2023 08:00:47 +0530 Subject: [PATCH 02/17] fix: the horizontal scrollbar disappearing in grid view (#3751) --- .../lib/plugins/database_view/grid/presentation/grid_page.dart | 1 + .../lib/style_widget/scrolling/styled_scrollview.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 09da116783d3..defba2c6bff4 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -389,6 +389,7 @@ class _WrapScrollView extends StatelessWidget { barSize: GridSize.scrollBarSize, autoHideScrollbar: false, child: StyledSingleChildScrollView( + autoHideScrollbar: false, controller: scrollController.horizontalController, axis: Axis.horizontal, child: SizedBox( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index 82f5c2edfd63..1b10d7335d3a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -11,6 +11,7 @@ class StyledSingleChildScrollView extends StatefulWidget { final ScrollController? controller; final EdgeInsets? scrollbarPadding; final double barSize; + final bool autoHideScrollbar; final Widget? child; @@ -24,6 +25,7 @@ class StyledSingleChildScrollView extends StatefulWidget { this.controller, this.scrollbarPadding, this.barSize = 8, + this.autoHideScrollbar = true, }) : super(key: key); @override @@ -58,6 +60,7 @@ class StyledSingleChildScrollViewState @override Widget build(BuildContext context) { return ScrollbarListStack( + autoHideScrollbar: widget.autoHideScrollbar, contentSize: widget.contentSize, axis: widget.axis, controller: scrollController, From 1e9137fcfe8acc9f0ecca217a751158f53979b4e Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:43:31 +0800 Subject: [PATCH 03/17] chore: update client api commit id (#3745) * chore: update client api commit id * chore: fix test * chore: remove check_visual_studio_installation task * chore: update client api --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 65 ++++++++++++++-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 2 +- frontend/rust-lib/Cargo.lock | 75 +++++++++++++++++-- frontend/rust-lib/Cargo.toml | 2 +- .../rust-lib/event-integration/src/lib.rs | 23 +++--- .../tests/database/supabase_test/helper.rs | 3 +- .../tests/user/supabase_test/auth_test.rs | 8 +- .../src/deps_resolve/document_deps.rs | 8 ++ .../flowy-core/src/integrate/trait_impls.rs | 32 ++++++-- frontend/rust-lib/flowy-core/src/lib.rs | 5 +- .../rust-lib/flowy-database-deps/src/cloud.rs | 2 + .../rust-lib/flowy-database2/src/manager.rs | 16 +++- .../rust-lib/flowy-document-deps/src/cloud.rs | 13 +++- .../rust-lib/flowy-document2/src/manager.rs | 16 +++- .../flowy-document2/tests/document/util.rs | 18 ++++- .../src/af_cloud/impls/database.rs | 11 ++- .../src/af_cloud/impls/document.rs | 17 ++++- .../src/af_cloud/impls/file_storage.rs | 8 +- .../flowy-server/src/af_cloud/impls/folder.rs | 4 +- .../flowy-server/src/af_cloud/impls/user.rs | 26 ++++--- .../src/local_server/impls/database.rs | 2 + .../src/local_server/impls/document.rs | 13 +++- .../flowy-server/src/supabase/api/database.rs | 2 + .../flowy-server/src/supabase/api/document.rs | 13 +++- .../tests/supabase_test/database_test.rs | 2 +- frontend/rust-lib/flowy-user/src/manager.rs | 14 +++- 26 files changed, 320 insertions(+), 80 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 697f49addf53..37005a67164d 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -762,7 +762,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "bytes", @@ -775,6 +775,7 @@ dependencies = [ "gotrue-entity", "lib0", "mime", + "mime_guess", "parking_lot", "realtime-entity", "reqwest", @@ -1437,15 +1438,17 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "chrono", "collab-entity", "serde", "serde_json", + "serde_repr", "sqlx", "thiserror", + "tracing", "uuid", "validator", ] @@ -2778,7 +2781,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "futures-util", @@ -2794,9 +2797,11 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", + "jsonwebtoken", + "lazy_static", "reqwest", "serde", "serde_json", @@ -3227,7 +3232,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "reqwest", @@ -3373,6 +3378,20 @@ dependencies = [ "treediff", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.2", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kuchiki" version = "0.8.1" @@ -3894,6 +3913,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -4190,6 +4220,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -4876,7 +4915,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "bytes", "collab", @@ -5598,7 +5637,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "database-entity", @@ -5653,6 +5692,18 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.10" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 28c33270543c..ea85a88118e0 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "52b82e68" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7a309c6f69d8b34709292052e9ef0561e16c82a1" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 900fa89becb4..c2d899112a97 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -660,7 +660,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "bytes", @@ -673,6 +673,7 @@ dependencies = [ "gotrue-entity", "lib0", "mime", + "mime_guess", "parking_lot", "realtime-entity", "reqwest", @@ -1264,15 +1265,17 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "chrono", "collab-entity", "serde", "serde_json", + "serde_repr", "sqlx", "thiserror", + "tracing", "uuid", "validator", ] @@ -2437,7 +2440,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "futures-util", @@ -2453,9 +2456,11 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", + "jsonwebtoken", + "lazy_static", "reqwest", "serde", "serde_json", @@ -2811,7 +2816,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "reqwest", @@ -2885,6 +2890,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.3", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -3309,6 +3328,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -3517,6 +3557,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -4202,7 +4251,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "bytes", "collab", @@ -4823,7 +4872,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=52b82e68#52b82e68f6e61e9e38606b9110d782870ee911e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7a309c6f69d8b34709292052e9ef0561e16c82a1#7a309c6f69d8b34709292052e9ef0561e16c82a1" dependencies = [ "anyhow", "database-entity", @@ -4872,6 +4921,18 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 491f3cd4fc7e..561fdfeb7ca9 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -82,7 +82,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "52b82e68" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7a309c6f69d8b34709292052e9ef0561e16c82a1" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index 373f65e00155..18203bd9de04 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -87,9 +87,10 @@ impl FlowyCoreTest { } pub async fn get_document_update(&self, document_id: &str) -> Vec { + let workspace_id = self.user_manager.workspace_id().unwrap(); let cloud_service = self.document_manager.get_cloud_service().clone(); let remote_updates = cloud_service - .get_document_updates(document_id) + .get_document_updates(document_id, &workspace_id) .await .unwrap(); @@ -212,7 +213,7 @@ impl FlowyCoreTest { let mut map = HashMap::new(); map.insert(USER_SIGN_IN_URL.to_string(), sign_in_url); - map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, auth_type: AuthTypePB::AFCloud, @@ -261,7 +262,7 @@ impl FlowyCoreTest { .event(FolderEvent::GetCurrentWorkspace) .async_send() .await - .parse::() + .parse::() } pub async fn get_all_workspace_views(&self) -> Vec { @@ -269,7 +270,7 @@ impl FlowyCoreTest { .event(FolderEvent::ReadWorkspaceViews) .async_send() .await - .parse::() + .parse::() .items } @@ -281,7 +282,7 @@ impl FlowyCoreTest { }) .async_send() .await - .parse::() + .parse::() } pub async fn delete_view(&self, view_id: &str) { @@ -333,7 +334,7 @@ impl FlowyCoreTest { .payload(payload) .async_send() .await - .parse::() + .parse::() } pub async fn create_document( @@ -391,7 +392,7 @@ impl FlowyCoreTest { .payload(payload) .async_send() .await - .parse::() + .parse::() } pub async fn open_database(&self, view_id: &str) { @@ -434,7 +435,7 @@ impl FlowyCoreTest { .payload(payload) .async_send() .await - .parse::() + .parse::() } pub async fn create_calendar( @@ -459,7 +460,7 @@ impl FlowyCoreTest { .payload(payload) .async_send() .await - .parse::() + .parse::() } pub async fn get_database(&self, view_id: &str) -> DatabasePB { @@ -470,7 +471,7 @@ impl FlowyCoreTest { }) .async_send() .await - .parse::() + .parse::() } pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB { @@ -836,7 +837,7 @@ impl FlowyCoreTest { }) .async_send() .await - .parse::() + .parse::() } } diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs index 250ef4546f3d..c146644da2c8 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs @@ -71,9 +71,10 @@ impl FlowySupabaseDatabaseTest { } pub async fn get_database_collab_update(&self, database_id: &str) -> Vec { + let workspace_id = self.user_manager.workspace_id().unwrap(); let cloud_service = self.database_manager.get_cloud_service().clone(); let remote_updates = cloud_service - .get_collab_update(database_id, CollabType::Database) + .get_collab_update(database_id, CollabType::Database, &workspace_id) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index c64ecf2b7788..2027a0919288 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -273,12 +273,13 @@ async fn migrate_anon_document_on_cloud_signup() { let _ = test.supabase_party_sign_up().await; + let workspace_id = test.user_manager.workspace_id().unwrap(); // After sign up, the documents should be migrated to the cloud // So, we can get the document data from the cloud let data: DocumentData = test .document_manager .get_cloud_service() - .get_document_data(&view.id) + .get_document_data(&view.id, &workspace_id) .await .unwrap() .unwrap(); @@ -372,6 +373,7 @@ async fn migrate_anon_data_on_cloud_signup() { let rows = editor.get_rows(&database_view.id).await.unwrap(); assert_eq!(rows.len(), 3); + let workspace_id = test.user_manager.workspace_id().unwrap(); if i == 0 { let first_row = rows.first().unwrap().as_ref(); let icon_url = first_row.meta.icon_url.clone().unwrap(); @@ -381,7 +383,7 @@ async fn migrate_anon_data_on_cloud_signup() { let document_data: DocumentData = test .document_manager .get_cloud_service() - .get_document_data(&document_id) + .get_document_data(&document_id, &workspace_id) .await .unwrap() .unwrap(); @@ -407,7 +409,7 @@ async fn migrate_anon_data_on_cloud_signup() { } assert!(cloud_service - .get_collab_update(&database_id, CollabType::Database) + .get_collab_update(&database_id, CollabType::Database, &workspace_id) .await .is_ok()); } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index 7673e011d37a..ce9b2ae9da33 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -38,6 +38,14 @@ impl DocumentUser for DocumentUserImpl { .user_id() } + fn workspace_id(&self) -> Result { + self + .0 + .upgrade() + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? + .workspace_id() + } + fn token(&self) -> Result, FlowyError> { self .0 diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 0b02424f43d2..bd4df22ff4b5 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -190,13 +190,15 @@ impl DatabaseCloudService for ServerProvider { &self, object_id: &str, collab_type: CollabType, + workspace_id: &str, ) -> FutureResult { + let workspace_id = workspace_id.to_string(); let server = self.get_server(&self.get_server_type()); let database_id = object_id.to_string(); FutureResult::new(async move { server? .database_service() - .get_collab_update(&database_id, collab_type) + .get_collab_update(&database_id, collab_type, &workspace_id) .await }) } @@ -205,12 +207,14 @@ impl DatabaseCloudService for ServerProvider { &self, object_ids: Vec, object_ty: CollabType, + workspace_id: &str, ) -> FutureResult { + let workspace_id = workspace_id.to_string(); let server = self.get_server(&self.get_server_type()); FutureResult::new(async move { server? .database_service() - .batch_get_collab_updates(object_ids, object_ty) + .batch_get_collab_updates(object_ids, object_ty, &workspace_id) .await }) } @@ -232,13 +236,18 @@ impl DatabaseCloudService for ServerProvider { } impl DocumentCloudService for ServerProvider { - fn get_document_updates(&self, document_id: &str) -> FutureResult>, Error> { - let server = self.get_server(&self.get_server_type()); + fn get_document_updates( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult>, Error> { + let workspace_id = workspace_id.to_string(); let document_id = document_id.to_string(); + let server = self.get_server(&self.get_server_type()); FutureResult::new(async move { server? .document_service() - .get_document_updates(&document_id) + .get_document_updates(&document_id, &workspace_id) .await }) } @@ -247,24 +256,31 @@ impl DocumentCloudService for ServerProvider { &self, document_id: &str, limit: usize, + workspace_id: &str, ) -> FutureResult, Error> { + let workspace_id = workspace_id.to_string(); let server = self.get_server(&self.get_server_type()); let document_id = document_id.to_string(); FutureResult::new(async move { server? .document_service() - .get_document_snapshots(&document_id, limit) + .get_document_snapshots(&document_id, limit, &workspace_id) .await }) } - fn get_document_data(&self, document_id: &str) -> FutureResult, Error> { + fn get_document_data( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult, Error> { + let workspace_id = workspace_id.to_string(); let server = self.get_server(&self.get_server_type()); let document_id = document_id.to_string(); FutureResult::new(async move { server? .document_service() - .get_document_data(&document_id) + .get_document_data(&document_id, &workspace_id) .await }) } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 1bb72f1c267d..de97c12a51b8 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -125,6 +125,7 @@ impl AppFlowyCore { server_provider.clone(), Arc::downgrade(&collab_builder), ); + collab_builder .set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&user_manager)))); @@ -179,8 +180,8 @@ impl AppFlowyCore { let cloned_user_session = Arc::downgrade(&user_manager); runtime.block_on(async move { - if let Some(user_session) = cloned_user_session.upgrade() { - if let Err(err) = user_session + if let Some(user_manager) = cloned_user_session.upgrade() { + if let Err(err) = user_manager .init(user_status_callback, collab_interact_impl) .await { diff --git a/frontend/rust-lib/flowy-database-deps/src/cloud.rs b/frontend/rust-lib/flowy-database-deps/src/cloud.rs index bae09c5b300e..0a679a4ba5ee 100644 --- a/frontend/rust-lib/flowy-database-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-deps/src/cloud.rs @@ -16,12 +16,14 @@ pub trait DatabaseCloudService: Send + Sync { &self, object_id: &str, collab_type: CollabType, + workspace_id: &str, ) -> FutureResult; fn batch_get_collab_updates( &self, object_ids: Vec, object_ty: CollabType, + workspace_id: &str, ) -> FutureResult; fn get_collab_snapshots( diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index d35c81048c7c..96dd331e88ea 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -76,11 +76,12 @@ impl DatabaseManager { pub async fn initialize( &self, uid: i64, - _workspace_id: String, + workspace_id: String, database_views_aggregate_id: String, ) -> FlowyResult<()> { let collab_db = self.user.collab_db(uid)?; let collab_builder = UserDatabaseCollabServiceImpl { + workspace_id: workspace_id.clone(), collab_builder: self.collab_builder.clone(), cloud_service: self.cloud_service.clone(), }; @@ -92,7 +93,11 @@ impl DatabaseManager { trace!("workspace database not exist, try to fetch from remote"); match self .cloud_service - .get_collab_update(&database_views_aggregate_id, CollabType::WorkspaceDatabase) + .get_collab_update( + &database_views_aggregate_id, + CollabType::WorkspaceDatabase, + &workspace_id, + ) .await { Ok(updates) => { @@ -370,6 +375,7 @@ fn subscribe_block_event(workspace_database: &WorkspaceDatabase) { } struct UserDatabaseCollabServiceImpl { + workspace_id: String, collab_builder: Arc, cloud_service: Arc, } @@ -380,6 +386,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { object_id: &str, object_ty: CollabType, ) -> CollabFuture> { + let workspace_id = self.workspace_id.clone(); let object_id = object_id.to_string(); let weak_cloud_service = Arc::downgrade(&self.cloud_service); Box::pin(async move { @@ -390,7 +397,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { }, Some(cloud_service) => { let updates = cloud_service - .get_collab_update(&object_id, object_ty) + .get_collab_update(&object_id, object_ty, &workspace_id) .await?; Ok(updates) }, @@ -403,6 +410,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { object_ids: Vec, object_ty: CollabType, ) -> CollabFuture> { + let workspace_id = self.workspace_id.clone(); let weak_cloud_service = Arc::downgrade(&self.cloud_service); Box::pin(async move { match weak_cloud_service.upgrade() { @@ -412,7 +420,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { }, Some(cloud_service) => { let updates = cloud_service - .batch_get_collab_updates(object_ids, object_ty) + .batch_get_collab_updates(object_ids, object_ty, &workspace_id) .await?; Ok(updates) }, diff --git a/frontend/rust-lib/flowy-document-deps/src/cloud.rs b/frontend/rust-lib/flowy-document-deps/src/cloud.rs index cd37ab706d67..0a17113b7a66 100644 --- a/frontend/rust-lib/flowy-document-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-deps/src/cloud.rs @@ -7,15 +7,24 @@ use lib_infra::future::FutureResult; /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. pub trait DocumentCloudService: Send + Sync + 'static { - fn get_document_updates(&self, document_id: &str) -> FutureResult>, Error>; + fn get_document_updates( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult>, Error>; fn get_document_snapshots( &self, document_id: &str, limit: usize, + workspace_id: &str, ) -> FutureResult, Error>; - fn get_document_data(&self, document_id: &str) -> FutureResult, Error>; + fn get_document_data( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult, Error>; } pub struct DocumentSnapshot { diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 0d001d45d6f8..2fe3dd829e79 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -22,6 +22,9 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUser: Send + Sync { fn user_id(&self) -> Result; + + fn workspace_id(&self) -> Result; + fn token(&self) -> Result, FlowyError>; // unused now. fn collab_db(&self, uid: i64) -> Result, FlowyError>; } @@ -101,7 +104,10 @@ impl DocumentManager { let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { // Try to get the document from the cloud service - updates = self.cloud_service.get_document_updates(doc_id).await?; + updates = self + .cloud_service + .get_document_updates(&self.user.workspace_id()?, doc_id) + .await?; } let uid = self.user.user_id()?; @@ -120,7 +126,10 @@ impl DocumentManager { pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { - updates = self.cloud_service.get_document_updates(doc_id).await?; + updates = self + .cloud_service + .get_document_updates(doc_id, &self.user.workspace_id()?) + .await?; } let uid = self.user.user_id()?; let collab = self.collab_for_document(uid, doc_id, updates).await?; @@ -152,9 +161,10 @@ impl DocumentManager { document_id: &str, limit: usize, ) -> FlowyResult> { + let workspace_id = self.user.workspace_id()?; let snapshots = self .cloud_service - .get_document_snapshots(document_id, limit) + .get_document_snapshots(document_id, limit, &workspace_id) .await? .into_iter() .map(|snapshot| DocumentSnapshotPB { diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index 70378775bca0..4ee4fcb109a3 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -9,6 +9,7 @@ use nanoid::nanoid; use parking_lot::Once; use tempfile::TempDir; use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; +use uuid::Uuid; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, DefaultCollabStorageProvider}; use collab_integrate::RocksCollabDB; @@ -61,6 +62,10 @@ impl DocumentUser for FakeUser { Ok(1) } + fn workspace_id(&self) -> Result { + Ok(Uuid::new_v4().to_string()) + } + fn token(&self) -> Result, FlowyError> { Ok(None) } @@ -120,7 +125,11 @@ pub fn gen_id() -> String { pub struct LocalTestDocumentCloudServiceImpl(); impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { - fn get_document_updates(&self, _document_id: &str) -> FutureResult>, Error> { + fn get_document_updates( + &self, + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult>, Error> { FutureResult::new(async move { Ok(vec![]) }) } @@ -128,11 +137,16 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { &self, _document_id: &str, _limit: usize, + _workspace_id: &str, ) -> FutureResult, Error> { FutureResult::new(async move { Ok(vec![]) }) } - fn get_document_data(&self, _document_id: &str) -> FutureResult, Error> { + fn get_document_data( + &self, + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult, Error> { FutureResult::new(async move { Ok(None) }) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 21d2b4af0255..e5ec9038b45e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,6 +1,6 @@ use anyhow::Error; use client_api::entity::QueryCollabResult::{Failed, Success}; -use client_api::entity::{BatchQueryCollabParams, QueryCollabParams}; +use client_api::entity::{BatchQueryCollab, BatchQueryCollabParams, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; use collab_entity::CollabType; use tracing::error; @@ -22,11 +22,14 @@ where &self, object_id: &str, collab_type: CollabType, + workspace_id: &str, ) -> FutureResult { + let workspace_id = workspace_id.to_string(); let object_id = object_id.to_string(); let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let params = QueryCollabParams { + workspace_id, object_id, collab_type, }; @@ -47,20 +50,22 @@ where &self, object_ids: Vec, object_ty: CollabType, + workspace_id: &str, ) -> FutureResult { + let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let client = try_get_client?; let params = BatchQueryCollabParams( object_ids .into_iter() - .map(|object_id| QueryCollabParams { + .map(|object_id| BatchQueryCollab { object_id, collab_type: object_ty.clone(), }) .collect(), ); - let results = client.batch_get_collab(params).await?; + let results = client.batch_get_collab(&workspace_id, params).await?; Ok( results .0 diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 91b2e193e8b7..2c46cebbaef2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -16,11 +16,17 @@ impl DocumentCloudService for AFCloudDocumentCloudServiceImpl where T: AFServer, { - fn get_document_updates(&self, document_id: &str) -> FutureResult>, Error> { + fn get_document_updates( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult>, Error> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); let document_id = document_id.to_string(); FutureResult::new(async move { let params = QueryCollabParams { + workspace_id, object_id: document_id.to_string(), collab_type: CollabType::Document, }; @@ -36,15 +42,22 @@ where &self, _document_id: &str, _limit: usize, + _workspace_id: &str, ) -> FutureResult, Error> { FutureResult::new(async move { Ok(vec![]) }) } - fn get_document_data(&self, document_id: &str) -> FutureResult, Error> { + fn get_document_data( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult, Error> { let try_get_client = self.0.try_get_client(); let document_id = document_id.to_string(); + let workspace_id = workspace_id.to_string(); FutureResult::new(async move { let params = QueryCollabParams { + workspace_id, object_id: document_id.clone(), collab_type: CollabType::Document, }; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs index 507a26eabbc9..569ad3877bdf 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs @@ -33,10 +33,10 @@ where .to_string(); let mut buffer = Vec::new(); file.read_to_end(&mut buffer).await?; - Ok(client.put_file(&object.workspace_id, buffer, mime).await?) + Ok(client.put_blob(&object.workspace_id, buffer, mime).await?) }, ObjectValue::Bytes { bytes, mime } => { - Ok(client.put_file(&object.workspace_id, bytes, mime).await?) + Ok(client.put_blob(&object.workspace_id, bytes, mime).await?) }, } }) @@ -46,7 +46,7 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let client = try_get_client?; - client.delete_file(&object_url).await?; + client.delete_blob(&object_url).await?; Ok(()) }) } @@ -55,7 +55,7 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let client = try_get_client?; - let bytes = client.get_file(&object_url).await?; + let bytes = client.get_blob(&object_url).await?; Ok(bytes) }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 210b40b6110d..0f4d6283a641 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -25,6 +25,7 @@ where FutureResult::new(async move { let params = QueryCollabParams { object_id: workspace_id.clone(), + workspace_id: workspace_id.clone(), collab_type: CollabType::Folder, }; let updates = vec![try_get_client? @@ -50,7 +51,8 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let params = QueryCollabParams { - object_id: workspace_id, + object_id: workspace_id.clone(), + workspace_id, collab_type: CollabType::Folder, }; let update = try_get_client? diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs index f31d269ba21a..bd8419d93fe7 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs @@ -2,9 +2,10 @@ use std::collections::HashMap; use std::sync::Arc; use anyhow::{anyhow, Error}; -use client_api::entity::dto::UserUpdateParams; +use client_api::entity::dto::auth_dto::UpdateUsernameParams; +use client_api::entity::dto::workspace_dto::CreateWorkspaceMember; use client_api::entity::{ - AFUserProfileView, AFWorkspace, AFWorkspaces, InsertCollabParams, OAuthProvider, + AFRole, AFUserProfileView, AFWorkspace, AFWorkspaces, InsertCollabParams, OAuthProvider, }; use collab_entity::CollabObject; @@ -91,10 +92,11 @@ where FutureResult::new(async move { let client = try_get_client?; client - .update(UserUpdateParams { + .update_user(UpdateUsernameParams { name: params.name, email: params.email, password: params.password, + metadata: None, }) .await?; Ok(()) @@ -108,7 +110,7 @@ where let try_get_client = self.server.try_get_client(); FutureResult::new(async move { let client = try_get_client?; - let profile = client.profile().await?; + let profile = client.get_profile().await?; let encryption_type = encryption_type_from_profile(&profile); Ok(Some(UserProfile { email: profile.email.unwrap_or("".to_string()), @@ -131,7 +133,7 @@ where fn get_user_workspaces(&self, _uid: i64) -> FutureResult, Error> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { - let workspaces = try_get_client?.workspaces().await?; + let workspaces = try_get_client?.get_workspaces().await?; Ok(to_user_workspaces(workspaces)?) }) } @@ -145,7 +147,7 @@ where // from cloud let client = try_get_client?; - let profile = client.profile().await?; + let profile = client.get_profile().await?; let client_token = client.access_token()?; // compare and check @@ -167,7 +169,13 @@ where let try_get_client = self.server.try_get_client(); FutureResult::new(async move { try_get_client? - .add_workspace_members(workspace_id.parse()?, vec![user_email]) + .add_workspace_members( + workspace_id, + vec![CreateWorkspaceMember { + email: user_email, + role: AFRole::Member, + }], + ) .await?; Ok(()) }) @@ -181,7 +189,7 @@ where let try_get_client = self.server.try_get_client(); FutureResult::new(async move { try_get_client? - .remove_workspace_members(workspace_id.parse()?, vec![user_email]) + .remove_workspace_members(workspace_id, vec![user_email]) .await?; Ok(()) }) @@ -230,7 +238,7 @@ pub async fn user_sign_in_with_url( params: AFCloudOAuthParams, ) -> Result { let is_new_user = client.sign_in_with_url(¶ms.sign_in_url).await?; - let (profile, af_workspaces) = tokio::try_join!(client.profile(), client.workspaces())?; + let (profile, af_workspaces) = tokio::try_join!(client.get_profile(), client.get_workspaces())?; let latest_workspace = to_user_workspace( af_workspaces diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index f004912384ce..cec38613bf4d 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -13,6 +13,7 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { &self, _object_id: &str, _collab_type: CollabType, + _workspace_id: &str, ) -> FutureResult { FutureResult::new(async move { Ok(vec![]) }) } @@ -21,6 +22,7 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { &self, _object_ids: Vec, _object_ty: CollabType, + _workspace_id: &str, ) -> FutureResult { FutureResult::new(async move { Ok(CollabObjectUpdateByOid::default()) }) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index 71e1481a2825..6797270a0056 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -6,7 +6,11 @@ use lib_infra::future::FutureResult; pub(crate) struct LocalServerDocumentCloudServiceImpl(); impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { - fn get_document_updates(&self, _document_id: &str) -> FutureResult>, Error> { + fn get_document_updates( + &self, + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult>, Error> { FutureResult::new(async move { Ok(vec![]) }) } @@ -14,11 +18,16 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { &self, _document_id: &str, _limit: usize, + _workspace_id: &str, ) -> FutureResult, Error> { FutureResult::new(async move { Ok(vec![]) }) } - fn get_document_data(&self, _document_id: &str) -> FutureResult, Error> { + fn get_document_data( + &self, + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult, Error> { FutureResult::new(async move { Ok(None) }) } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index cc2e6b664027..963978c2fca2 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -30,6 +30,7 @@ where &self, object_id: &str, collab_type: CollabType, + _workspace_id: &str, ) -> FutureResult { let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); @@ -53,6 +54,7 @@ where &self, object_ids: Vec, object_ty: CollabType, + _workspace_id: &str, ) -> FutureResult { let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index 48987fcead61..9968a1c44b2d 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -27,7 +27,11 @@ where T: SupabaseServerService, { #[tracing::instrument(level = "debug", skip(self))] - fn get_document_updates(&self, document_id: &str) -> FutureResult>, Error> { + fn get_document_updates( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult>, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); @@ -52,6 +56,7 @@ where &self, document_id: &str, limit: usize, + _workspace_id: &str, ) -> FutureResult, Error> { let try_get_postgrest = self.server.try_get_postgrest(); let document_id = document_id.to_string(); @@ -72,7 +77,11 @@ where } #[tracing::instrument(level = "debug", skip(self))] - fn get_document_data(&self, document_id: &str) -> FutureResult, Error> { + fn get_document_data( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs index 6521c870eaa5..9f638bb22646 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs @@ -45,7 +45,7 @@ async fn supabase_create_database_test() { } let updates_by_oid = database_service - .batch_get_collab_updates(row_ids, CollabType::DatabaseRow) + .batch_get_collab_updates(row_ids, CollabType::DatabaseRow, "fake_workspace_id") .await .unwrap(); diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index a89c77ececad..6610583e7a39 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -5,7 +5,7 @@ use collab_user::core::MutexUserAwareness; use serde_json::Value; use tokio::sync::{Mutex, RwLock}; use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument}; +use tracing::{debug, error, event, info, instrument}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::RocksCollabDB; @@ -347,16 +347,18 @@ impl UserManager { UserAwarenessDataSource::Remote }; - debug!("Sign up response: {:?}", response); + event!(tracing::Level::DEBUG, "Sign up response: {:?}", response); if response.is_new_user { if let Some(old_user) = migration_user { let new_user = MigrationUser { user_profile: user_profile.clone(), session: new_session.clone(), }; - info!( + event!( + tracing::Level::INFO, "Migrate old user data from {:?} to {:?}", - old_user.user_profile.uid, new_user.user_profile.uid + old_user.user_profile.uid, + new_user.user_profile.uid ); self .migrate_local_user_to_cloud(&old_user, &new_user) @@ -488,6 +490,10 @@ impl UserManager { Ok(self.get_session()?.user_id) } + pub fn workspace_id(&self) -> Result { + Ok(self.get_session()?.user_workspace.id) + } + pub fn token(&self) -> Result, FlowyError> { Ok(None) } From a57ca5c0cb76806a9864014a9b01d7b70c165eeb Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:44:04 +0800 Subject: [PATCH 04/17] fix: integration tests failing on Sundays (#3753) --- .../database_calendar_test.dart | 18 ++++++++---------- .../util/database_test_op.dart | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart index 59987a0984da..af17cf15c166 100644 --- a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart @@ -174,25 +174,23 @@ void main() { await tester.openCalendarEvent(index: 0, date: sameDayNextWeek); await tester.deleteEventFromEventEditor(); - // Create a new event in today's calendar cell - await tester.scrollToToday(); - await tester.doubleClickCalendarCell(today); + // Create another event on the 5th of this month + final fifthOfThisMonth = DateTime(today.year, today.month, 5); + await tester.doubleClickCalendarCell(fifthOfThisMonth); await tester.dismissEventEditor(); - // Make sure that the event is today - tester.assertNumberOfEventsOnSpecificDay(1, today); + // Make sure that the event is on the 4t + tester.assertNumberOfEventsOnSpecificDay(1, fifthOfThisMonth); // Click on the event - await tester.openCalendarEvent(index: 0); + await tester.openCalendarEvent(index: 0, date: fifthOfThisMonth); // Open the date editor of the event await tester.tapDateCellInRowDetailPage(); await tester.findDateEditor(findsOneWidget); - // Edit the event's date. To avoid selecting a day outside of the current month, the new date will be one day closer to the middle of the month. - final newDate = today.day < 15 - ? today.add(const Duration(days: 1)) - : today.subtract(const Duration(days: 1)); + // Edit the event's date + final newDate = fifthOfThisMonth.add(const Duration(days: 1)); await tester.selectDay(content: newDate.day); await tester.dismissCellEditor(); diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 4458b3adad4f..5b909f4723d8 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1302,7 +1302,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await doubleTapAt(location); } - Future openCalendarEvent({required index, DateTime? date}) async { + Future openCalendarEvent({required int index, DateTime? date}) async { final findDayCell = find.byWidgetPredicate( (widget) => widget is CalendarDayCard && From d51c7f382fe9cf46dec333c7241a92dcfbf5fae2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 23 Oct 2023 18:35:07 +0800 Subject: [PATCH 05/17] feat: implement personal / favorites folder on the mobile platform (#3723) --- frontend/Makefile.toml | 8 + .../ios/Runner.xcodeproj/project.pbxproj | 46 +- .../AppIcon.appiconset/100.png | Bin 0 -> 6487 bytes .../AppIcon.appiconset/1024.png | Bin 0 -> 37551 bytes .../AppIcon.appiconset/114.png | Bin 0 -> 7355 bytes .../AppIcon.appiconset/120.png | Bin 0 -> 7858 bytes .../AppIcon.appiconset/128.png | Bin 0 -> 4239 bytes .../AppIcon.appiconset/144.png | Bin 0 -> 9546 bytes .../AppIcon.appiconset/152.png | Bin 0 -> 10094 bytes .../Assets.xcassets/AppIcon.appiconset/16.png | Bin 0 -> 616 bytes .../AppIcon.appiconset/167.png | Bin 0 -> 11268 bytes .../AppIcon.appiconset/180.png | Bin 0 -> 12178 bytes .../Assets.xcassets/AppIcon.appiconset/20.png | Bin 0 -> 969 bytes .../AppIcon.appiconset/256.png | Bin 0 -> 8303 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 0 -> 1547 bytes .../Assets.xcassets/AppIcon.appiconset/32.png | Bin 0 -> 1065 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 0 -> 2296 bytes .../Assets.xcassets/AppIcon.appiconset/50.png | Bin 0 -> 2941 bytes .../AppIcon.appiconset/512.png | Bin 0 -> 16806 bytes .../Assets.xcassets/AppIcon.appiconset/57.png | Bin 0 -> 3406 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 0 -> 3489 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 0 -> 3549 bytes .../Assets.xcassets/AppIcon.appiconset/64.png | Bin 0 -> 2123 bytes .../Assets.xcassets/AppIcon.appiconset/72.png | Bin 0 -> 4508 bytes .../Assets.xcassets/AppIcon.appiconset/76.png | Bin 0 -> 4715 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 0 -> 4974 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 0 -> 5408 bytes .../AppIcon.appiconset/Contents.json | 214 ++++++--- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../appflowy_flutter/ios/Runner/Info.plist | 2 +- .../lib/mobile/application/mobile_router.dart | 64 +++ .../mobile/application/mobile_theme_data.dart | 6 +- .../presentation/base/mobile_view_page.dart | 83 ++++ .../default_mobile_action_pane.dart | 98 ++++ .../bottom_sheet/mobile_bottom_sheet.dart | 116 +++++ .../mobile_bottom_sheet_action_widget.dart | 53 +++ .../mobile_bottom_sheet_body.dart | 100 +++++ .../mobile_bottom_sheet_header.dart | 45 ++ .../mobile_bottom_sheet_rename_widget.dart | 76 ++++ .../database/mobile_board_screen.dart | 28 ++ .../database/mobile_calendar_screen.dart | 28 ++ .../database/mobile_grid_screen.dart | 28 ++ .../editor/mobile_editor_screen.dart | 28 ++ .../mobile/presentation/error/error_page.dart | 49 ++ .../favorite/mobile_favorite_folder.dart | 71 +++ .../favorite/mobile_favorite_page.dart | 100 +++++ .../mobile_home_favorite_folder.dart | 80 ++++ .../mobile_home_favorite_folder_header.dart | 60 +++ .../presentation/home/mobile_folders.dart | 74 ++++ .../presentation/home/mobile_home_page.dart | 109 +++-- .../home/mobile_home_page_header.dart | 50 +++ .../home/mobile_home_page_recent_files.dart | 59 +++ .../home/mobile_home_page_ui.dart | 1 + .../mobile_home_personal_folder.dart | 72 +++ .../mobile_home_personal_folder_header.dart | 82 ++++ .../page_item/mobile_slide_action_button.dart | 35 ++ .../page_item/mobile_view_item.dart | 417 ++++++++++++++++++ .../mobile_view_item_add_button.dart | 28 ++ .../lib/mobile/presentation/presentation.dart | 5 +- .../lib/plugins/document/document_page.dart | 4 +- .../document/presentation/editor_page.dart | 132 ++++-- .../document/presentation/editor_style.dart | 35 +- .../lib/startup/tasks/generate_router.dart | 109 +++-- .../workspace/application/menu/menu_bloc.dart | 15 +- .../settings/appearance/appearance_cubit.dart | 187 +++++++- .../home/menu/sidebar/sidebar.dart | 12 +- .../packages/flowy_infra/lib/language.dart | 1 - .../lib/style_widget/button.dart | 12 +- .../flowy_infra_ui/lib/style_widget/text.dart | 4 + frontend/appflowy_flutter/pubspec.lock | 8 + frontend/appflowy_flutter/pubspec.yaml | 1 + .../flowy_icons/16x/three-dots-vertical.svg | 3 + .../resources/flowy_icons/24x/m_delete.svg | 3 + .../resources/flowy_icons/24x/m_duplicate.svg | 3 + .../resources/flowy_icons/24x/m_rename.svg | 3 + .../resources/flowy_icons/24x/m_share.svg | 3 + .../flowy_icons/24x/m_unfavorite.svg | 3 + frontend/resources/translations/en.json | 11 +- frontend/scripts/makefile/mobile.toml | 9 +- 93 files changed, 2624 insertions(+), 249 deletions(-) create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png create mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_action_widget.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_body.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_header.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_rename_widget.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_ui.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart create mode 100644 frontend/resources/flowy_icons/16x/three-dots-vertical.svg create mode 100644 frontend/resources/flowy_icons/24x/m_delete.svg create mode 100644 frontend/resources/flowy_icons/24x/m_duplicate.svg create mode 100644 frontend/resources/flowy_icons/24x/m_rename.svg create mode 100644 frontend/resources/flowy_icons/24x/m_share.svg create mode 100644 frontend/resources/flowy_icons/24x/m_unfavorite.svg diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index bcd71b472d48..ebfa6f121cbb 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -186,6 +186,14 @@ RUST_COMPILE_TARGET = "aarch64-apple-ios" BUILD_ARCHS = "arm64" CRATE_TYPE = "staticlib" +[env.production-ios-arm64] +BUILD_FLAG = "release" +TARGET_OS = "ios" +FLUTTER_OUTPUT_DIR = "Release" +RUST_COMPILE_TARGET = "aarch64-apple-ios" +BUILD_ARCHS = "arm64" +CRATE_TYPE = "staticlib" + [env.development-android] BUILD_FLAG = "debug" TARGET_OS = "android" diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index b66e03648f89..a3d9b4d790e4 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -13,22 +13,9 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 9D1D47ADD7F5DE8237063BCA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 197F72694BED43249F1523E8 /* Pods_Runner.framework */; }; + FB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 197F72694BED43249F1523E8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; @@ -54,7 +41,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D1D47ADD7F5DE8237063BCA /* Pods_Runner.framework in Frameworks */, + FB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -121,7 +108,6 @@ 580A1ED8E012CA1552E5EFD3 /* Pods-Runner.release.xcconfig */, 4C2CB38DA64605A62D45B098 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -137,7 +123,6 @@ 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */, ); @@ -359,11 +344,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEAD_CODE_STRIPPING = NO; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -483,11 +475,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEAD_CODE_STRIPPING = NO; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -502,11 +501,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEAD_CODE_STRIPPING = NO; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000000000000000000000000000000000..7112d6e103aee7d948ea6bacb679fe789b8de98c GIT binary patch literal 6487 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..f34332a39582fb1c63e33d82701c3081a84471d5 GIT binary patch literal 37551 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

r>t6O>Vi9U*}Tq;fG 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 Q{Qv*}07*qoM6N<$f@8Qt3IG5A literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000000000000000000000000000000000000..9a624fed6df7d79283d748c6bfee3abd7c78b108 GIT binary patch literal 4239 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 YL@x)TME&xgfDT4MMpe2-68P)?05Bho7ytkO literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000000000000000000000000000000000000..6940391fec8e75efe6a01d16db9c705a738c1a8c GIT binary patch literal 616 zcmV-u0+;=XP)JF^g!%_mqFX!JOdT64I&~mOtVqDZ0tymB z{1W1akN_bdAOevfqafJ$Zh>z|3!%ETotQdO~cNH3vs)8K6JohO`L)3TeZxd{f@_0FidqR z;?kze1a>DaHvDdY|EZ4XKMTQ-a~|yCQa%X~$z$gYfa9%8R8T@0IkbN$;OnS@*WHSn zAO@#~yj^rQ*9@_f%w~ngf1Xilp?vS|k_DpJHm~hO`fLF0XUT~DCnNILw(pcGuYXS? zT8&?-|mpb#9D1bi-H(pHUyW))`8ZTOy;)rjSl64+G9wZ9)F!hVO#?{N1+qXQ4#O}0000x literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000000000000000000000000000000000..2b0198fb3484b96b4e3378989801b213c76c129b GIT binary patch literal 11268 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000000000000000000000000000000000000..faf3c774ff842794aad7fda2404ac4bc32f6f606 GIT binary patch literal 12178 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000000000000000000000000000000000..eb697e0b1eb79c80fdfcacb0807349bc6d6e572e GIT binary patch literal 969 zcmV;)12+7LP)Px&gGod|R5(v{m0M_BRTPH5z0Wx_>0DA`Lt_+C8QX$wr6p8KV-?#7VyHg(Qi4io zRiwll2tm+F#2XqPQZ)sA&?wmIljK1q6-SG$4~?`KD6JSv8ah!sCTZ)WGiUFm`<$6% z661nl59ji&+rQRPA|fm`0kPojC>{T-fUQ2K0Ik5COHC~WI*!QRhc zUj~N8fEGYta|#aRVB5;L6t$!xU=8-`H-;~|HHadS0iA&9N!Zm3=N^a7BaplnLJP(~ z12D1y+OrFhDr-et#D5nJL-z_-FZ%c?um-dTr6N3d7;d-&+K$3c3cMgLX=vb<6r9=! z4b>2psfcJFejF_G-HQR~)e1533zw##?NMRps|qD>Y#Q26ftQM-^c92^7+MEi%@8_O zSRb4;iCgf>lVd5`?K7*Wku8g7p^;;Ox)Rd8os{hJQyeoZ?4rm+p6m$g#0@*oH=1> z+ug{f4ln9jl)AT|vYH$#*7DW`#qg|RVSHh%|cqgC7%Qb_w)frPT`8rIz0J;0d@Ny@LeaX rUkJA?+~~L=i2he0k=JV1|Go4NBFC+qGeoIS00000NkvXXu0mjf+={no literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000000000000000000000000000000000000..4fd559430eaa9d5a129ec90b3ec184a2e3ee299b GIT binary patch literal 8303 zcmb_?hf~wf^Y=R;KuG9PLT{q<1Vw2=Xd+E|Q#uIJ1f&-sp`&zBLBSF%bV3nS zS`-kZ7b((HFcyGIHs_1xUPiv84 zzi8JGJ3v{G9B{>4k}-~ntz(I>X<1hUW5Of?RjjWo5z_eG$YTA@}6j`W9tmy1haBH^_1!0&$2AR>6p7*8t1Mnu2t0Z-MZ63Un6b#9DHZ-Ub?o zVy%K`@Wfcp8EY~+rQjS92sQ;g*!Gjnr2OA~l|EA9y{WR=t<9kDuR%Jx&W|8P^S)ev zH~xwba0iR9DDrM3t|6oW$GyU&1tv{Gj-zKG7eW$n%+Bn`B?@occ63M%c_6@RLu1-M zx=D#hIe>d)S=r!-EfdUcq5$&O{bGM1NVgG!c6-umXbKA2udIA@)TF^Wbg8bkpmm(~ z12-^A692{RE6Q?PN5coiy234UiG@i=!|s2qxrQJ^DmE7^+>d;da)X@%v;)Cc9j^=1 zqD9mKE<7#J52g?10GdvjHBm{$cUJrRC(6Ph;i%HW(dN-;mE2*cQQ5G)f}PEIh)ZFE z0T3$Y@qr!iw=JQMJN~r`TA#PTO>4SuUnpm|1)a8+Uu^u;R+c9y4!*@EHVwrp&PNGK z-U|nsgE;d9LxoH#&ORKz8~&GFGjcW@Tj;L#l?zQwgp=R%Y1ckyY;ztb0{2)GmSOB;t31JThs zI!c<24@Y@1&p7?jxl-j{4g@o6O}F;@V%A`)%*!9{pMxW=Udhg~gb-+GT&{JQG|tA5 zS}93F3Fy57=@eYEUC6n@RbD6*1kX#OQ^MavW#0t7p9GAC$m9UrM6`vA%l=XH?md^c z`Jby`XLV+(3Vj!IjG8!FF1dV-{(6c($wa7Xv+BzVKXx))IU_iDnv|vL*y4lg<@x)+*hoGSaA27kO1JR4A0@`2Yl z8tcOsN`Y($B}vvVd`rxFd^Y)pEUCWNuAJ^4#+@Vi>JxA)r>v4hn7rX#wdD~MbGN9r zy^_h7oNXrAEtg<43$CZ(JzB}RK%%^Ojk@Z(TJf7%fg&Bv$vi9F`lIA}Fb4ip+*zT> zn?`LijV+cS;OPr4>Y*BY@>9A_8rjaz@1>s3-hJqNSTfsj_r&Md6?>nh89Y8RQ5u}j zv$(6vJH~cwq>+z1mh=2P9spXH5901pX}H1JZ{lbDUL`vO`|3MZbSO3vf4zv9YsXEn zhqSVaR&W8l)Q`vWp9)f|Cit=onKpIlDywP6pVH-B1QMhXcO9`3d$oK*Lr z>^tepzQ!uo=H8eM=Oq|Wd=tf<_strj((W~Fnj#H`S~YI2qMh1Oi>0OO89hc4k~ACt zW{ABu6^LB_W>^Y~CvRTkr*H;eRERPt0M7S+(cC}rs~V$WmwH<@9ZiCv6k+>jbsveH zxw1VnTB35G&ssE|=V%q>$}|kqcVjyyyQH0}&>W^QSnTQ|ofyq-k3>4OrjY9eaM2Oq zZ&yhd%IEJfs5ZIX<}V-qL!+@CVLRbj|Jw`8a&z&WwOeohTR=cBi=L)|wEM!p^d=eM zC3{NykeRPy2_ndgR$Qz`*g-Vp-#DJ_I{8>|Fb)@*o6(SVK@+=to=@8s+~y3+Usjd? zsSxb1tCfNQG%8s)^>pL_M>tA8k4RnmS}nEtLuK4g=58B19XrMwZo^e6wIa-sTW)WrpXkcEdQn1%K(PHRbo=2WQzr_WKJp2lnl< zcE#?;qlk4jKxGD4CcYn|Fw|@AEtbk!Zw(+$uY#Q?obt*XSBMo#u=23`F1M9q;Ge~3 zIJ2P6BIkrIo~ny~kV}a+Y%m3im{9p5pCphw0eW=Y_1aU)ISshScD4Gnt+>mQ93Mcs zJk+brqUN*|T$C6B^avyxwo2+feQ$_IwQB2;naaCggHs**SIw2qcMf*#0rfV zn7%NZhBD%`ZN4kXOmXleKwupr2bqM*w3wrKXdctP1Cn!IGC{@L8>PU_;gNk`s63~G z&3mQ$Q~UK9jL6k-^Z0p+{6cQvam+Gp_pA5spnAPs;T}^5l;M%4I;|{;_AZt!z_5u{ z)|Z}6zeqOkCbUlkcyGC!cV+>}eSCoKrw<#|T_Z;~JK;+nXr_t*Jy3j~1pbNA%Op&N z|9He~_1r^+FFjwW$28x#wY|;!^3De1aKb_=C?2~FC)gx| zjFL!RoP==qZ+xZvU0~zH-#$NBuq+~Z!Q63HEgh|vWv|i~w-|RJE~j45nEqAVH}HOZ z4oiMi=^@qpJjiPf^{$fgg?d+6#5VLgwg4C`1>OPj95n8-*gY~I8>Y5O>}8Yuqg^Ao zwLd|dQJK&5I%j?O1MKMUnUq(-a6Gj`dFc&skH_Spp7I!?_x^(IyYzz`S_@DUjVz8~ zF}$APlZG2N4S5aIFQjf9^%nzU+27o{S&gaSX*Ijfxi}3Bwt*C41QjY(*K~nG;EmdO z*#TzxudeUUnmtCHpO(V3pYTD%Vl`bD5JatDQ%v$rh;c+RKaol*#5?Syh2bx!CUb!!5fbBsO#W;eLDy$olU{7 zIlU{=O}22x!}|2dRtg2$`qshhoDb9=?%7cFOHYwZLsDVNPwc`2XOt+XLy@xZ7{dcw zDd!Vikze1HKJ+Vr7Qc20^9))5BY!fh>@kdcODlCX!D&uq)=rH#a$&ZDv>SpT1W_NB zM7^%vi?igU@T?NO_Hkz)@2CAaU3HcU2xa{|Mk2cwy;W`g>mwJ?-?4LJZN>w!ndxHc zeoB*Z;6F2#(Y4bsK>FfVfgOIGw7im7A0@Cnula zS|&dWIaBnroqPH?A?|bOABHWBg6>C@h=S^;|Q}s~iYj>x|BJvnU5jPY!(nkz9SfzCt z@Zd%Bndg+wyW1Z(hq4c$7Ubu_Df*5mhD8<6VoDYq<22u@8tLL z+M9^KDqao|9$m|z9lX)1Fd5;a{x~Oqg`U9z;<;~xT+3H*3;d=v@0v1M$Nvtz{;?_& zZ(`{~h%1nt1OY1gqUZOqtfmX$Sa(qyqt?pyIaN7Bs-Dvh{xAB?j;+~CSP_&JWo)h{}q>^}f z@E?iHnJeWIpO107OK5omfIqYH_wOnpt0l^@+RpM{v7&WaqTf^KpNqkAZjA_W!2Uwr zOrW#m;iq_gQH?!+Es{yv1;C-G8*{jas}Jqy%rJ$hyqbN5&+4~(LhOLLp$viRm(dqT zHYrUV57>7vLoU;{;lQDk?|fX})5I#HzIhSOwSK93ad1KzcA>$CD>QIpi0-g-n~idf z8UrzBz>94j61k_z=SlMKN}KvDVR~7&rQUiq8aL06;lCAz_K5-!Jkp3PJ#cA&s6J^{vO;1yf_ zC-Py0nXwIY`6=o9NCO`XNEBAIce2#zMK{l1J7;EkbhDBczfONSIpE;W=chHAk*DiT zAD3HLJ}M<4b#cZLVD6+jCOt_;LxEXM?LF_H^;2T)Snqzc4sfISjwhuo2ho1zNKXf! zRR~WhocpmnOIysL3uzC>-r8r)^lh-48jCPNvjLiV@)dj#c8G|*4geAPlmbAtp0I&f z6F9`c!cfUeRvqtMDTUrZSNw8%+epOt&2=MhtJ}lsOYn!Z~5_d+7(_CG#;}wp0ZPl!w z@Adoe9!Y^;7e^&~_iAQ042ut_2Pd{V8jvFXuY+Uz@PVL|xTNTrZvnq7L?KNY+C()q zT46@(if7I)!3Kf`dI3QW zPIXG7EHoJzihm!$(mBB0j3w6}331U=f3-$sLfHTuBn=I1gMPAb(y^GN7xpx;PZGM9 zbNX=}!)hi1)<7Kllgs+j(0Xg8FLJn)93Yx3Pf?F>8FC+m5p1G>{Mz~RE2Sl|KaQMk z>Uz_B>Ft?s?DZ2uOR9&W|5FhDTLpqAca=Rq$Z_9Xe`&?`rSCC+LW1)(>bh#I)3)&s z(ic5+aK9~R(b;5a+kHb%_pKOHa%ZyqLJ{Zb*&QO(|j%rGe|Ff%HKy*(Z4! zd1w{)23YFdU*M1cOP~2K?~Y(N*`OB$ddYzs5(c1uCV$^76Eu&8j>TRV2NOdvG+I!? zBGPXHIg3n&YBX~gJ0(0eh2*Z+E)|YLyp#L?Y>{gxSsGeR{#9Q`v!L+Au*T>1Rqb_( zr7Bh5?17$Gy}b?;v9`d=7P z4iGfD##AaV1(rBw!3S^XT(SytmLf7Iz>2yXaHCaRD`gKYB2M~)1n)PtTK8Q0r8}+; zo7^B{tVNR^wr>3MNOE~wGO&t3C8Ek z|H1lQt(ov~P^{93_S>uGEVkIY?ds|C#6yL1>%JF@hYNQWA;rsS^LLu=uxU`_d}DD| zKd$P!-y@lw!!Y8GUE>?2hlkJW@uzjJn*JBA8Ufp|mpsbM|6)R#c=l}v$k}PC73t*h z+xNen|7en~J_|Yy5(SoAehM><4P5&BYPUR6qcS-`3E5@qgzLYlUDn-z{SH%u`wwSg zDwO7+1=&F$_9QbPHMZBwL8*_2l3K9Z(J8cCcf8xGSTmUNbR0;y>2+M2z0wlgNawXr zVZ^ZVvT2`(Zg4K!Xz|z~jjn6o*8vppd!lnQ)ZySa;Mv3%BA9?ATz_w9fza}nfbxs7 z{AQh|ggL61IudiDdKcAK_Vi1zgPx|8~^$e2NGaxw%`gqnX+vSxwptLMej`KW@_ zyYFJND}Z!N(ZJ4%-Q8Kctd^5xTACh=t34lKO-uOBox|C3ODta~<1wO3x6>mIjB-sX zz2e6z;dXKo)$Ll8$yR~4az8Ka`2G1Wch3bNgSt6SCCZxsp}uiD&vuc;(NX~`(RdTr1fE-Z-cBIag;u>XG9#VFG!RUcw7eV zQX3-w6b?6P{n%3c{GlK4)DPr2)i!tPx~Nr`QGq8c?TN6_Ma9y0u!6&BHFBO{)Ym)$ zzLNNuL%Kck#rnBPSLOIJqUl90@1bELKq`hd?0231Cp1n8BK)7u`z2>?-}~<$KAj3< zZ4DWB_%1!PVntowZR*xZ;{qf6PeZJ+k4;PvcjHF+tf$OJ&hs8}2wI#aYo$TJ^$AHp3Tzw~3eIC$vg)g9u8!@U0)!0K9Y!ozbb~O0p;Vd3zIM<- zy%+R!o(j^!ztS`gg&nqd!Ar4jxY!z* z=X9)G5bX!>{|+&A+*-is*}#pzHq4I!gQf8L8Xw44SvI*u+HTXCI5B0YxKVQ=nkf+D z)(S-Q>8(^CZSSzYIY3q|o@qUR&*$t@+1@RNV|cshl4A_`>SZcrr#yfAYKVIQrC3vM z5F_<1NzsIDq(mr*^6CTmBG@)1Z4)}x9O3y^{eCsE=Cl-hV8{wa{lR?~{X_gB2C3!% zzE2UwIb;oKE0t9nc9(Xm6*O;Z-t0l8G|KNA*7PkK;38Y}t_9m7LNrj7+D2wvuZY~RAf(;+9~h%oT{2#@ z2~>?>XnS+VX)4wHO5U{qcaHX$&yAYLVtCic5eW%@*#Ixgzu48X&W$1CSB1=$ZRD8S zW3gUD%6V2=dC5Y}wUPF&q=YN9dduwCz6)Ph=a6nE??)HqzKetN;RsbSSArD6q95JN z+d%vWLw6NWfZEN;GSQ(Csc}2l1zuPGf_Yw~>{nF$$Yj`%gGIWris+jk|-7 z(k}oH48HED;d%_j!TdS$R**{aTdNKyjk~ni4$6X;|M|tttdyHTdv4K6y-v(pYWC6u~rUxvtzpU^v6eOR}` zW~u4m;Uf2G8b9;0C2q+P{h2{WV;j6^B_D)>c4?3jDp^pn5}a|(-jMS!cKw@qFkcma zBekOaB2Ei*l#mCeZ92GaF9#lUiqtdT8!{8 zP7qz-RVu)QTk5n}!i|^qEY$C2vVyaJ8j6CQT{#xDX%5H0O^ggLxUaG|uQ-_L(Dnv2 zy{mfdZnrrl`*Nil*-_)5qTz;XY5F1mXs(AmloJBiklUWeWEm*FWGZldKlRpc-9?G= z@{yan$IkEg<^l-usP5N2oVkR|&`FDUU~}@CLYpI#wBz%@F0Uqf5gPo5I2-Ypme1n{jyD<$txL{)Jf! zW^jE=rC9P`Ys)MW^R)bbn&m61(ZO9T@gXF)4sQ7|r%!hrtVB4mDAvHzc9!Oya{Q$Q*%XcCIqH2}IXhLPC z!!7UTw~1Onbmf}EelSQ)-FueU-jipL_U^SHZ@ZUldlHk-!=4zJMnV1l#|!dBsldI7 z+s|L@7|?RXxuWLKSbcG5d$NGQJr(Psoy80i6GlV=NP|5RG0h}j7hsvgUiq_}-$Fh$ z3TnG>IC*FL=!;kqVc`}WB-|8C>m4iohhL;Q_(D!gn4mh6+3E+$$6O@FVx3OG|0g!L z_V&o3N4Kpj13CHw8&INCbZc(gHkzH?zT+c7zia3>-}B_joytJKkDfux}tMAtm7n%L% zW$iX7eqf6c#r|1~b^Br_LYlHlum)06u+H)k7k$rld_cmY;JCzJ#jyt$c4)CFP$*Y+ z@}+*0G(^|oe02@VS5;n9LWKU~G%ooP;wt8gR7LBQ04h*XMC5P2GY(zhP1QnVgP)a{ zgPCCuXXYgF4U%?M<0&v6nFV_t!c+d)iATza z1kSQcq;M~g4^4X@Yr@8t?wzeYg<;ZTD>eEl4Tz>hBHc)iiJb8ASj5wd&%vfo z*db3exM~qM*42)Y-F}C8ipPfYM=^6VVJ;%zdWfTd$d=k_hgd)j^NnN4=uX09!Zgoe wt@)w5AR79p3v)1<^xgH>|9dAL(L<$S%a3%^^us0p`yT-4-!#^#)pCyeKhD`)761SM literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000000000000000000000000000000000000..ada952ed8c38b5abe3ef803411bbbcf553e05d9a GIT binary patch literal 1547 zcmV+m2K4!fP)Px)#Ysd#R7gv`mw#wgWf;dl?|aTY+q~B~-Igu2EVFE=p~;;6E~E@9B8ZZNL`bL% zs;PstKTOjtGbkzwDzYE{gq8gu3bIK0<&P3Ivs_DaIW=8f@8;Ee?>Xm<=lt02U0Vx+ zUbwJ(&gJ>M-{<*0&-+dpV+{ZEQvU{Od_cCQKN zZD64AvJqMT1RoSCz!-MoAfyj~lY+YGeW;z?u<=Y<(6EZGn7G7;MZ;xam32H86e?9LWHcKmr^MH;jPg=fI2+gM&+<1_Gdlubwb$ zeMaHC;3Pp=;V1JO%(xq_eHny|L6q}B!7hn91oY23iz5~%Scu;H^69^0O_ zNRC#4V&ChlZQ&HuTnzIzLi1jDd>`0n1}yPekW$%zpM#cjp?U1z!M#uktS$KA-8>&G zv`N+K5LVPs+J;kqz~npO`nAxJh5F5qO@VR3ECt4k;I0Q>Pl3i%DL8~iKh9-6UYp~R z?JUD8gV?=6pvV-3EX;i$&X^hoU)=`FegV501QIV%?747h6?{=2?mcdT02-^@{mv;q zy4N9DZ3VRqmq7&);N)?`)hi7R_k>w6I#e*@8`v%C?C^WM1mgR!WePMDsSTiUdu&|P zP5V2;DX)T{zA8-l>23o~o-3DWE}3s*d=;22Zj0?{Xxt5|EPNJoZP{x9TF-+eb)nJZ znKHgHk|)XXOE(^;XHN}IQr>`DD3D2?q^`kZ&SOc&Of3&mUOX(~K&ubJvV-6_VQ?{1 zB7#Z_tr$BH)Q9J;`;GK|O(J2FJ(0($@@ZU{;>tx;*oiQuqP%#CDp4m2=C#2aM*$nO z4vl48_;d@@f2rdJ-J zTTwYVjmvt$S}QWfrDs%I=c9?Fp`MbTwQi$B0UFaq~LE#!`2 zU$z{3=0k%(i@|%2P-xnK`uKY+Tr@V_*rZ8QwO|e-Uz&$$pO5NV-*-?-l6wT`;QgKq zhKk8{;mlq)1hfbojEB$LC~WwS{I(AA=`6Y~MODkKI1S@NW& z(@+#WPD4Y0rr{$HgpZ5(r~^<2iiD5|f&~^@7(uArkq8qKOUuIARAOUjL1LvaF))yB zPzF9iDyRsIAV8s_A$(e=uFZLNoW{;`FcienBVE1c`~2QH@80W|7hnPZ8A{NAWm#!9 z9kc_qBu&NdLo3^3T2@gMEz~TM_gMc1J$^1lM$V>JCQL5^`3FF(2}rfi`E#FvTOXW$ zTY@25$#BIHgma8URw{txDw|jS8Tk=l4NhNpdvM44{kto{dr= z1zHP5h!zpwV6XpeK|FNYOD^y=4?N&la*<<*(iW9hYy<|m{K4nI%?baCc0F~u4}Jl= zsvquMHf#YwDCV8Ei_w5tJFe-B(il_prDiA{BtUgLpPrd@$psQAr#;)kZ8gY62!?JZ zwJ_`PZmJsgAnA67FBu$+A_$@o_naxKW}Y)DJ076PHaiA6U7a)U3frvyX0prg~ zTt}~vivAQPvG(vJP7TGdpH+))c{a%q6Wk6~)mDE3oZQ@af_r zvyKsDEo8^N{&b>Ts9^+bKOD!Cp>J5z+K5f<*%wkNwp3JGFw?I)>N+*c(aVndG zo~ufe)=h>2g4wCC#Fy1u7nu*GJeD7AaA);XQ9Dxj? zsP){K6NQq7dYGM90^@DX?!N>H81@kL9>GlCaag{hbSLS8o00000NkvXXu0mjfQ1Rgu literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000000000000000000000000000000000000..879ee8d379c4263227758c8dc775f365556057bd GIT binary patch literal 2296 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 literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000000000000000000000000000000000000..85818631e1e319225a5b371ba1b5bafec8599eb1 GIT binary patch literal 16806 zcmd6P`9G9j`1iSI!Pp~9vW#7pRQ9bIN%kfCPN|SANoAj*R8-cGR3-`$Q&NhUQ7LI5 zlCn-E2HCeUW_j+<_xle#zdWzkg`zt z1^@#76#@8o;Fr~eZ%p_F6Km^x27o7;|9wM8s3H3u(v%*pH-s2`6x6|E|N(FZ` zavbpzst+5j!LH51BjSvL8!Yry-6I9H+o8+M`yNLu{fq7gNEV;&cIkh>&ilFK)-4X9cGg$B=JLkgsa2EeR%^I}|37`P zH86Qk{OY}Oa3kAswIhqU`e{LbBkSsNcy+wU|2c3stKk0E!|Uyj3hp6l?yZH1yh z=f?mZG2SymEgvG6m)=$3u8PJ&Wwkm%y0^kiIWKOdoGQ2!VfiZ`kSZ?$oaSLbF-mii z2Whq4pYPK=-jr9{ie{&yz*)84$UKEYjhcnLGKf-AU`pH*j@TnXNi*Zze2d+q0?^xe zx4tqG9iw@Y0bRvTP=8kqpI!Q(E#N09(iRHB&*IcTUXF_5P5lpeYnqT45#MP^AScR~ zXfWKndGj)}6LV&@%(jmlLWt&7k^VE*trwP+D1g-S0g8&^>|Keoo&%HgT3;7)R zZsQTOhY8QP_Y;Nr{-y#>jr|Ql*(%LET`|z}wvOF4`BNFtnlXyox`Z%9b&VAypHHXx zmMyB$KqdCp-M3+P#V6lLU8IR%9YZG*`(F*pMZVBN73TwE`()N+;!p~s>fqJ}gpE(m zJc!k60@e}mAn1+roro4J_x%Lsgh=2sH!BGwbO6D`?NSD9HU zL4!PvvMK4P^wq;qSLtT@cTRf)bJqqEALh9tYcN1m&PdM(6Ds?=Y>+r)8t5&{ejF7W z(gfl}$;{GuULxmO=@fb;sKvY2NQ@|GaB8kUm3@N+kqF0Mn(U-*MJR+r$5iBlj&zBD zK5v5_XFo*SJwzTP&RQGclw{IGfZ+{QQ#_qWiu3^5R~AAb_%Ib9*}K3?9P!M2NzAzk*_L8Bl?NyaVA;#C=i}1z$R<1@uOi`x zi#O+#9Ykae_&*WkeSQd$C$@rIzn@qtnt=l^CZ`$2p88nG){=Fh^XE4}YdBp%^xumz zCNX$V1^}>MTDQplGFk;(z%r1;5?jPY6e$Kp0E>RV3V|4LzbFgB@?|X5JQZw^&qj?k}wxo|YTkQjlH#o&1zS;%vA{vaghOjtAz z=U8UBgcCyFM0!|Em8VkKLnQXiwn89#gPISTj%09yR^lANeI*RjW_EPWZ#qw!5YwhMnuqP&6t$qT2&!X>I-cXjtB?8dY(NF&Ux8B&_sl9FVy7TmaO)$ zh9@v%Dv0Azc?%_K(D|}sw94{rn_bGCwtP8W*6)Hv)EAp8brAK!=IuqtupxzN&<`C* z2@1!L0&ixrOOp_Q7ZHIjm?$?W;9`V{Xk_AP=*rwE<=HCic|qx0D9+6y3U67kl(e{i!&UKhh~($)5{o{r^~Bfe;0A|XS;uzo zxiys_ZAp=ELdlOV*hJE3A{BOo7$Jj{SFgnvbW#jKio_me1f-tHDlI z_MO}E`Mpr`g^%?#Y^0utUdwHldYdXU;r+I!n{n1QT8MKeTmRT8drc%S3$&GO>Ldh`tw&hFD(ks7F8*DcM)H-FVMGhMhh za6)tc$#gZyZeL!?NTtfQknrA}mH~1Bt0*FpAnSQ?AAuZiNORvBt>vN5jK3VCG|Ug) z4D(q|yzEdw++(2Bk>U`L!Eh3)G{X&s! zeso=-WbCIt=4R2|I;5$3Ed7rPLfGt-81eLykD@r>;f`3*7RDB@*dcOkR-i;vR#N;> zxp@2lC8)xU@{wuFQ?LUPgQgO2qCVeHAqS*cXAtQWLV2p@cp##cqO~v4Yq`23SaJ3%`LL(#o0r-c9#5*7{WrYD9&h%@8E?kiIcULk z4Y#JX9K=}i-r|!T?l3{*ES~cZxhDp^xSM_c8||A#N9X?ZWDD~|Cm-DB$<+H$Qp_>3 zyV;}}>scHnvPXk1s9OiD9e~=!0m5X#eoFDyk;}q7oI3HJ^TJ&gQ13m;5sZHUpSGTU zO%ybVjQe|Bg0u$%>bNQNlfm9>y#{$nanVsF2HEA{2fSpQ6F3mWZm!6po?J&;b3XU<-3caJO$zOZE)vH9iB zNLNj@Uff>y-NIR6{Q2-LBiM6ye4XZS+ph)@z$S_~>6Q8wMjS zZuyFYn{sln{-J8$uVve0yFm$VA%TrEgBZf|^}jY0j;~5^*|J||xN2lT4c8M7bSGk&e@bw1_0N?n@p>glU#!KW5ly6O?GO&o$z!M(1vIDaLQ1o4ip zwT;mmQ}*tE5-!{CA*eIsVoB`i{`!&@bbqm2qElHRXsVtA?DnRcJ-x|0Z@+5WYo2>m zJ;NFN{)LNa-_cGQT(~^JLi@)JM|<6Gb71$9bLY4BZW1L2!00Y2k>nYA`rqjwI>kOT z!=r6U$BVuHi7&br72+<>W=Ld2(_335?+=bLBP`l|c~K%9d`M5w5}u9${#>`!-xR6M zr{hNk7Q|;gi9!YS2gZU;u~EF?2BclSW^NBuL-M7bsQ*A=cXwRF@SIy(&0u{H7w|%2 z{$Xl`@Q7Z8)at>70lB!3t!b(FMY463_LuDK9zQSZK?fQD{E79k$g68K?!28EIpu*T z@YgAO-^`!Vc&NhaqRf!ShXBxua+_j-q%I8T=OUS9f4nCsbAC7Kst5U4>+g&@vp-D% zI#;CO^C{xI7#3qY5c>N?o;aAIFm@*|mG0QAtM)407#=`y+YEPkg)jD`2!m%@D01aFG)C}z^y6&q zv1&Ej!gK5KhNNG{zdk&zQe9MflfLV8ifNY< zJF1pL1%EE?^h11z#oAG+x8+fS2G9>hx}60BciL-rTT919&RWLeEAB0ydr`zEX6W|F zlbAfz{nyhM=ot>+_uY>-Ns)Gol(Zy^4qV8BkNK-zPYr<%Xe}tlznWJ6sYo^KXfF!9 zi{yKPpbeycN{kfbM5>TyNAu7YuYVSQ8UiX{J;I~6IfJnuvM_Pg6<5O0%bw~)h!GKH zTSK=~q6S-0T*S*`yD(i%LR4A2;r9>MUxET+y7A7!1CHPw>SG^`dpX!t%Rw)akApt? zls|7e8XU8#14ClcN*HP4Q@sd9H6RFfR|c&cbr8}{bg|N6wrzi}`Z0>=D2zUuoAi7& zSw@7n0JWOQkC*&?fsLv-K0wScQgh}&nFSq!hb=*st7oF&eg(~INB?mk+G zwa)iZ;5o*FB`keNmtnm$BWWE!?HY~&fBxyPdd6|OVZnC_JtvKdv$#j=jz3O$zF+W# zMr``LsE?EoN}lC!8Rjfp-H+fKx!MzOLm_qLWZMMgkfA_!)Uh&laH0^0xBF_bh(6~k`92Sz4ykhr=h9kc6i(1Zgyxf6 z_l2Hypxvfgt6^%29mN(2=;kxgqIr_|koHg}M}{?E`m(GK7yTn|;CiZ%@Tl%NZA=vK zE>$7L1x|!}#%fJfJ+`t3{FHANi7AJi_wO{I#Z$Qa{Q!t5o2US-Xk( zaZMmu^ne;az@2gltRcuoZC0-4`j4&=CtmwW5K+{qStl);#3Us1Sb$p5xZR^~!-Zaq zBmI1Cqbl{i7OWuNdphwhFYe6fQXBk&;8TKPmx!6#?DFdUFTQRJ_Tf?qP5YL&eYjqv z2{6PhdnKDMS3RoG+r)rCx0@^Z_KTx;Kd$BOVpqz^flqO~cs(so;EY2`pa#ppbU^iL{>}wNW z>?MehUW{zwU=!kp1YUre(YXCu^k(wx#2WMc4bGF>x#mjFwr%{>bbQdyjYGfuq2s4* z!An^Y+VN)j&1LfC7$OW;U^)8 z9Q_iG_eZ`91iHq;9APe80#6on?tDbm8{#)kI+R_bEqr7r$wo$(d1O&!R66y*ZIACy ze}60xwh$p263;hDxT+?8lz`Jq{YCW$1nX&q(H9F(e?Lij@n{d>Ne(uHdPo{1$8xaeo~$2H zrN+s}o??(C?w~cQpJZA4gHyb#ZV)fR4)h&ueAtNkx86ZnJHf_jYQ%ZqODy@g(oqc7x7b={tOW% zpnX+295Sv=99Do$th~A9KiUqY+<;yD<5~2Bn$eTl{KQHhcBMiKQ{m_2;0*3w2e$wGvm+BaFfr{_UC^U>cfrlciqDWX;HQDzOt4AR7+jM zdXD#Lx=0JDj(dHW<+8n;b5Zb;CULL^aZDXfA+@2e1C;$)5+~R_?F4NbxF=%@hl;{! z!-yTIRe3W+qa~WU_j4V0V4tnUcIzd9EwY2{-LzO@I^G$*8bpH^I^ykI{4(YAT2CpL z{%(?YYC_>(kh0>A1ZH%yN5xYkqL`nkKjI=YuW))2Ro99|sZ}2?a zp&j&#_#rUX&yOcoBcmm4@_;dEZKiw-H`L$9Ro=3`Wb>LUF~8LvG6XD*IqerO?EoXI zZ(@R4|Ms*j|K}=^p0!DX9eqv5k4p6F!DbpioYG5wtKfNWr>uXsD=Av#eC@auQ7fvWcq&|F{E49({;XCi^k@aTTJ4pKGDG&wi5umj_ z)Uu4iOklAiX^Xi>7jQ!_XY+S#+H765kR2NQ2}XoOFB1Pde_y1|miz7Ml>T=h=2G+L zE;7zKt|qBn`-%w>>zGE}6Eb8A;=a9NRR#66i#*(-;V&Ph%3u2T`ntSF6#`Vc zAwCHnLvPA*xnIXuN?ut9xA|^c**(hc)o=W?a2giX%cF5s*%s`p`pbPv9&3lgTrjsO z&PpD1*COhqTatwGgvE&041!seP28S!%F%urrRi4&>nb@~k+E@a){AOj3Br;0FQBlY zWXG}nY~0($;Cs{i2|}D;=9~Ic8?)|%_23JNFGF7FQ#7$79~`@~jJp+BB69!vFR>0B zIV?-}bg(C&8p|Do< z!ra3uptI3ufl*#$_FPAl<^J7Mi>L=>>^bYX2Mc#Mpxn1*Zc*Vu2jv(i^Js5ns_w_u z#v^52C3jUB!@aOAikf_z-`F?@@x``v8E4|N_nYJA&Dz_Ak#UyK_rGY7XO*ruhyC=T zzS<)w4Ij`vR;wSna@&8l1X*w5QUt1|AJuUZ3lS--r?V3d?=hU`X9xI6{IoXrCWz+% zA}`kg%3~g|?-8z;$?wLsD zJSk=pl9%5!hHfNTt`|q41EdljnSR95u<(2?zKMAj<59ndo55`wA*(Y#cSVS=SJo_d zi7PJ;tM7ND;KGLZr74up!82H_Cm?(J2CrQ|UnSUN%GN#n+n;72#ApdlEBpNSE)ZbgzFCVA_mD{wy6FiZR@sPe`mD$PR{Y(`A^b`}aV9FrB<>e829d4^%U7vdsQLU(>`jzGFX=$)|^|^yn|ndQj&7mPh)n z)g9~2r7h%!n)x*$jCb_0Z4al`=;sjmMZj%bZJ<@AslM zJM9cvKJ`ZZtXb*gnQzu@2N1ouu@3E9oayDy4{vQsHA9o?esjSOH`b+Ejb%CBl*Up^ z3Z4#Gx)YArcMbk_Q5i%%O@E{H5Aok!u)ed-9lwujOWE1@J)(vz!NY!Ccw6kn%7aTT z1%QOtIOiMx{3=9M9Rj6U>+?PFvNMsHmXz4hb4Ntq5H&=L?@FFM19~eGn|~d^wk8G6 zsY|bFU1IJ^58LSnF}wF*`HB2Q>(M44`SQj{cAEEA*2FY&w1R?cuRPe z@3(RY@yW7%m365?>!u&Zdv*&ruWD6&&9ACY_kdjCU9~u%e0n!SJ5FJms-JbsXqNR; z-h1;i{9O69Gd_);TF|QTcg8r@0u_i)S_e4nzB?ZsV_z@d{-USR`?@M;QXr9e??PJe zs9(L-llnhX|g2Ux#q7we{t zH31tAn!x_T*yD!= z!V>@d3#@y+Pc2xH3)~U2^`IU#q8Qt&F<$bY=e|fZKCp0Q(&9wj#og1>x;NFYo)LY~ zCc;i)iYpbZD7ov2c|y{B@n70J0P~$$g=*VBgst|s+2x08LQA^(-wS2&! z>k%{mk~l?Uc`^9r`1D6HAyglKY&YH1vK*!iDz|<*tPMloulB6wTnK-$bdqfl3ENAK z$|U@`f%k))cPKY2@GayB_j%zzUvw?kgc9qQ%bv_!7D}=wJ%@5A%1sE_*E0k-ipuG; zCkAbAI%H^OBAuW|>pA}YW3X^t+K>1N%lw#fsfQ=E@GB1Mym5=3j|p&7d2{&4Lzuenw2$i`|tGEtK-Bf61IGkyQXDX7;x8A{C)UVsw6gSkWWB z@yBP^RuHFDk$rD+8E#B8+_8?Bwl_kD7RdX`+i}i^%Y5rYjIw{4UY)P;%RtF zZZWf1xDafp#j*L^UG9ks@OD82?X{z#kAxAA{~1Q@QiL8K`^=N!MUh^+N0+QJND=vusb<9A?jM$UhWX4YmA`$seX%@9W!V|%BVvxDOxa=5#|Nd;KIjLy9m zPt1raSWg+oE^hTzjpJ~fWh{4pr{DY_wI@&EK!qJuZDl@bQAZJDi|N3@K~V9jkuN3B z#k<*jcjP7YPo}*(&#mv$KR%mIuO~Qis&kX-R83K~J4psQ8{GNk=i6A@%q4Seh7Ah16v)P|lnTMs5(t8DPx{VIw!jfnU)22He zIDelCzoxbH0fg`hG}Fwt{}l5_%oW-1?#Taeg3z6G_VC+%pDN0?PCcn6H1&`#Z~im- zo0fqK$@ppGWI&s8Jm4y(ZRO2=&$&xgg45pVZtp2j<#PG_UKe#4hQiOM>Fz9|Wc+Sg z=2<0PCxejXg2#f9q04`6aH9U$^fYeao;qhw6t)h2j49jCCI1_TxuT((W)!D=IwnS9 z65*G?49b`5Oq%_axsh3OOHmlkH_vWXs(-Pk|L74}wTTfQN$h^rCt1^fkAhfbFK89n zdw$rV13v0wFOBeE7m*jfo}`wQOPa*_O)R=tR|&ide2ncF-|;P6Z?SiNah`90%JuJw zF48T%OcN2DCt^}`aVm`)&bw4H2SEuZ11+5{%}faxWJh6z+Eu{J#L|w*VdS~Xi?`!{jUY6{n>Q_e~qvc2GfA@=E{w;1$V%hHf9T*RR$Az8vWbhC)%DYv%udQ#-)K2E2dRRIx;I*{vbS>d-Q z75Hrax_NB7$)Yy#%9_O;iw}%%9G7k;7%9Px<-(1r&~gb=0aZmHX$#M2(p((jJ-jbZ zJ#cAueLsTW@SiIK$aP4V@rOM%@vm;(aLbT~ohm>M8z?@7Rt8?)AXLQ zu{;00kvOBo5|PJKE>3)_rQXo>Ax!r{0be+EoJl+R^tD0*dD(Xw>3R!9<aboS)DY9n_M=$cD z8X`)}H#Eoyr`DL}k*5PQ?gLfMS5vmm`u2IJIzA?Q_MBf^(77I+E0*y!%1v{8S7T^0 zJG1}^bey~oo^Z}eam_*y#1_ps$+tL0@WH-!;lBPkir`%8{kx)9y!gRIEevx?t&&upx%KYz8C%3bMBb$eH6X(% z5yr&?1xrcg2?D5FnQpXIid5Rb>7*;d^Z84R=w5w%iE4Bk!>WgUdDth*UmrpNR%Hig z!vyMbdjq2+$K$P(Dc)G0^D=24pq}3SXx%I^_Q1rOWED|( zQED7Skm@*r!L2Vdd-PNg#5ekjl++ul7DbfZlxFO-U>)F_Lgj(` ziZIgtXkF?&*SngR?RG>g-MWxZu#gX|Z^|>olMYdPDA>FIt=d=&ae75}o`sCrVzV{i zQC<=*%?87~>8qJyu)F2P1sh(od=R@M&w*c>RV05@|NQuiPYP*4;Qm1*XXD%<7u$~D z>1edzHGZNNzYMGFbD1V}NGdJ&%|&8b=8NAkliVDj39dx+l+%KnyHkWtz}vOV03aDD zk{M;w=Q>Mwb%ek{jH?1enKAzOwRzpU@{*5g+k7Tw5%E<<3+#~Ky$C|39-NC=Bl7f0 zzO0=8eCxhUY?BhWMb~6FxzFG>A7d_wyBQ}PRs1hqQ?{50i@5D}UcK(KUOa7QT*Ua` z1!F;sZRXvP4eyOoRLIC&|JJEKE@jR1=<0P8R`sJ zX*Qd?hU^q#UEf_UwwjB=s;}L{c{K(0&uK~GY!lQe2 z2+zRTfmg+j8?(>%uU_KE!yC;$w>s8MBtiS3Hv*<>b5%fl#O-?PC5Z_crF80Pt z<`Zm-8J_Gn*>!wE)pu>nbfS7&MH;U8;041slfL4(S(&>NXX^eCywuM;H9e7|NNum* z?&JAqgc(3j?zuCWNSKj(`FLTr@$cIbbk_}C_04Hr2Qj+0VM;(j#|1`3V(}-vu)CRz zP~!{y#N#)(E0qgJlqX*rZe|Be2=W}W_GTw#mtR%ijKB{{v@}dS^L~D~BCeEEF|v&i z#7(p9^X*U{xM231+aM(7iJ_t3Fhj?U({pED@TO7Er~K?k8socW%Vou>GJ$K9mYzEu z`a%;j#6tY-je?ltEV^#hxyebCNd+S0@bovc4Ayjo)%ckzkrb=RYG;AGmUo_nyB)#f zU(ZUAQxgM!#$3b6OE!(n}>1?S=Fk40w(kB_qrE+O)F2d>R- zvYEG@-M>$^OsgD`SxkK~`z-BRwP;q{>ypumq3sgDTV+A->e3ew5|UWYe0G}*1rbOF zPXl%5>lc&naa(!q5R#Zs_HV7QtLq%!2vcjRL#70M|HQ!EXY}=W^4DsE^?n>H!if=N z`%vC6yL9+jjn1}r%ueqwVwboh`eKwI8N%H}u)6!_U`+A=Ieia?+0s2oqpxUEb~3rc ze$zWJcrK&c*DVzE+I>i#DR}q=4$nU!Ns-T8Rw4zNABs(alQtu!)j!-r_ZqdwJ$Hp- zi`&U3cb-jyr}M3akWbF%eik3Id3NnKu~UiggvSaIVt0MH@#*T^(NxxxyCQr1H=h8V zpMGoNtHb@unaK>lZ#7``I{CIvzc+fm&nvXC_r>Fd+;BAQTm+fvzqX2DHusZ1WY2S# zhRmmau3be@B@x*x1)9WYp>K(R2-m&n7jPtg$rQe;n~F{?06V z%Nh}atg|KS!|1>2V?2+^H+2*LTVfQ8KoxL<%WH)ozK(o^(1;e&CZ;{#P_1Jh%P#*B z>Md2YU5wa~2Y$G=yVFY6oCuTOsl;hgu~G8OFF zm7X3UyXr4o$7Y>%hi+DG?Bf#y*yjz?J-5ZA^UmjE})W+r(I%ZG`RIz==km|bG8Dn`sJI(JHn5gAz4)6X9ZUn_ryLJ*8k&T+e8#81x_ocgMW zp9HTS=pCK=F1_Nu(A9ZiTy)~#N7nTVX{Le=7gCdmBI!6;2RY7+CXyI|g&x=F+e=Wx z?(`WsV?KTO>u$u$*8dUt@KE#0-T{TX=Oh4ki~Hq&%(}6pp5i){Gxj0x;=8%Yp@%;N zBhwml6^{6wVn(rbW(V@w!|1~+! z`oWVebeexdzpWM)+0wc2d5J!n?MXP@8$9WSfh!* zDDjzjz@ziHAR>wUzwYj9U^w1Y6+yU#&CdQR7A4_y8`c6=67Ec56(T4)MuIm<5_uUa zjNm&*lY{f0*@BtB1WoWz33_}ThVbLh{Nt-9LaH6yq3!M*Ya_x|H6LgCkGD4RL7)ee zQXOhD`l32UO0l7VjbKF4!IchYu#lVYYq!_b@h(fpws#@Bbv?f zBGB=67r4`>GjP!-zjCWtF}xW3p>X8IdnKmA^~B@1^;Eg%-(ff}Jl)}EjAEezcH9#Z zy%f}Nt6HKrJ&3+inTjHe*k2&@V~7rVvnqQ$PopsSOW{Zcc~}n;YeM+-&ZW7VIT;#! zrYb}mByUwqeu4X$2_N`P(CgmMrQmv%NSakI?edAgl-llAk?27WPH;<0)fj>(r^w=W zzN!u@3`PN`-;x?6*#NJ^UVdA8@Axx2xWj`-x5j_#TPT&mXTR6bS{{|q)~*w1SP$-C zH1y}T%H^gG;hMgCa0ZKT7-HbWuN^ywRy5uaA^O^Zz6aBhaYx1nA3WA)a6{Ckwn%MN zb1)PzY=g)%5NGY+Ssgz)ZGpL2|7v$huGjs%v3(D?Zl1*Arj2p9f5y#+L!>ZxVQC&g zD>8=IqR&ZL`od$ZcO(fbj%XTyTkIoH-w?hu)!TtMn5>azPCH9BNVqO+DNNqsxnoeo zMdCK$Fw9!2)5_}de)UCvT^poPi{YmIY1mCv#(zyck6#;)Wr(PAk6#W1CNOo|?T<>E zU2`NJRLsHbH#?GMrjDYA&wt1DZS@FezLYHzHdkBpr)-(TJJ1}vkm*T7<2&rRK9__! z2C|$t-zle-W=oZyqnPAzLt3;$`v$0!=4U8XMeCLBeEBT=S8$xC@zj%OjIZHKYmm-fZO7%SRrV6ePcEc z^jiXH)t2Jm?;Iw5K|vzK_Yud_^FJ1KuNADodVs;(@%;IVr#W^$nyEthNY)Xm;S*fY z2+h>FzANXWjb@rBv|Cmx#Mh~gyIC%|zI*#6;6AZo0(9Wc4Pu8O@ucI6s~s^{q$X?C zpqrv0zCv|`z)h)yaU2Gpo#6oND~Eqb(jR*6OW27IEKT^3n@&An%CeE7v(2(3iItax zQ*)xy<@;z9`@YlDiz~lheUyMq`>5lOnS-=ok`=)Z7BLsQJ@hCNoqmpYLeTE$4}tE@ zsPqMwgF1jDE*F;N%|)B9R!fKhZSt`pAc$hBfBsSxEfD|x!4!&KGyM(*&f%%*t`*bZ zL9m}HrS*}95xT7}bX>`-Hsf7*8}FF3nhS(eTXo*>>g|<-jc%Nfz%O88qy4dMFn!F+ zovaku#U7`9>ot`CB6NJn!+;+aPlN0uMP1=OO$qb1*lsGweY@B}`3|2}v6D>C2~g zp_E;VHhvd~7}%hKmFuj}cz=NaZy6fwaSw^h7YhLDyd+-FLNU*ah>JH1xiu?ST?nrrBJ8Wym+grgHwKbwHR zjuJ$EFck4)rRN*LAtuX88nJrqIpU%hQL67Q()Fb`Bd%%6V{2^iEFtnKW z9C0E7ZR|1y`b3WR;L=@PPZp zm=_z>5yaPY?JV!RiRB;cMvd4aShdkz&rkh}3zsb6Je$C6tOsV~)c(%P%Rw~QLs9$J z%i^BA%2c5q9zyHbG2`kGdyvFKN#6+p%<2V-dC5HFs;CMwBat-{f6b0p+97t|%(X*| z+*rqkD6MFMI)eVK>k9W4##N^pr}AJhz|Hkds6wHT3n3kLX*dZyCOv@|P?2GBu1X!E zZ=HYt(ppo8h<}9JqZYYtZZAf|`ol<-CjNxv>-IRLMQ=OT?bSH9ZJVX{ZU6i5(XpbI*e$JtHODP=L6bm3bEBW#rOVy z15z~90w*VJ4<)ZiJp|kDuCtt`gD<36F!WXj1x^6%TK*ZU^jj$H(qlvws*viFr{dVX z{)waJILTQ_0h6$+7uVT;O8YbW9a$UqG;=U&(3KtpP0qf;Dr4S!-G7bdB1Yr!H-p3m z8&4uhH}x5jXAozjmWt3wwrvwk#yV~wh@ zoobfQ^by?E(~DuP!IayNoG%xHSV!N&{H30OLhwL2pah-x_ztLvEG!-dS_vz!E1$~S-S-%am)#DyeJSCh`=ROz#>2fB+vk`s{HLBK3geaKcy8;} z1Fd+zJWJ~1&SAFXm*``E%kO3zYraL1l;|}knBH^X477f{2o+$$De!rE&O@~363wLt z=lNycSzs>|B1{svZb!7AM1*606b|SPuYK_T_YBTKUf)MrmS8x=9&cgF99?BcyeoaP z*9)lgGJ8($3}N5I?one5R$V^EQTONLoP(8x@b3A$zt|Sh(iFX*+_?6apsv;JKSTIV zBRGd+7+;2>i9)#;L`4VpwS8&wAFIZMC1J~%+NIc*!UJvZ^DU|Ja6N=V(y#L|Do}PY zaXj9LC;@~15?DdIMQ*Ez-5m2|x`f1OpBfDHK1n_4DoG8hJJdYR2`s#jB3*&I-Ub5T+?9`T#hAY$X&fcNh|?`pY9 zbsSlzO;?V$wXH}lzgu(kLggCNVfHCfuO1O@Ks<=82)^*j4>O^zwJ@s7tF( z5I>W>Sm?G*7~OclA5rF{O7b+ot7Z|O^Jd@Xn@BvD@ys78`^w+-`StKl-?i2UN_YA8 z|96)>Anh3sAs9^$$os8^Nc>{0eY^;SYvBvQu|pecJRR8JM*Y#~@uHkLp3(WrHlgy<}-%1mSTly+q z)uX2=?KtPBKN^gCx6ysFW6oaLSp^Ijze{)!{!qn}C>t(16!-gM)Oi7w_`2I($bwpY zkpEO4NlNWy@0iceU$ap~^L#J3=mO&YX-&ef>!0^+wdL*^rCjR^EEnZX*x}FrU*OkO zGA{|M)BlR4-;d|Ck38aM=xdQW#*6IV)qagCC{w|NsF5PqsN`X0QxA7{Dku68gO1C; zJ>7v5|9wx%u&@R(@o4xU-oOjpbbmf>U)~M2o^eujWZjA*xSFdn6!JE*HkE6P*2IJ!mn#;^D^k>vndXUbNZrgcYO9FV~!a6=?MGTtT zit*|yH+SZZcX!+NVk>jJj+Y!Av3RGb#+0Oo3{$NY4amlodllii$&g2WS+c;7Urh#n-3)zK>gDI6U-3 zAJC7ZpV_yMW3rUZ!xa!$n#xVS!xtVyIPAS8vq}y#xb7zV;8(r|W2$u{xS4IC4Pjx` zO}E%Hgf!o(t((M)IMy+?#5Uav1Z5%4kW8;Yrnw!efJD|xvuCY;^X5=E22#SDrKCmj z;a}q5cmcR$do;J8T7Soj^%1Qq%~-d|>aqNaK7pI7!k0qA;c|@htA&$g<~j|%aqaRS z{dE`0E*G_@whR&m!Z=R_-gD#)I|u;?ON3~pgyfif^X7wPTwwZG>|G3)FQp)-e|EZn=oStumtfw$r4i|VU}mJ$7?KNINX zm!8cmynFVL{xWH1^8GGPzIX%5F~qA+rGp<2_l9MbTLdnV8Fcc8US#kibts3gYPB4$ zsKaYv1D;Js?lU^u5+v7ad7!)wDZ?-_-@JKhPdcS3q8?v?!rGxG$HaVZ=IoEQ6+EdI zV?(~Gb#U^te|ciP&W-zqkwIbJV)Qc}x^v_)4Xg8n1DEEv3`#Q6lxq4287NYg9YXDs zKywMoxej?t^i$f_*+RkF=h_dDEQ5R21{Ep4p1_>vClANP@m>Fw#l40sJ3YyQ@S)OB z=Itc9+#nX-8{9gp=)3ZRz2@z;g`b3Lu+njJd7vqz0JOagxQr;})iACX)(+nnB8E&2 zyjGrhLqx^TcT7p^Ge6<@rn7HbXzL@xI#yA;>;&>+WOYvoTl+GKBs2*55*83td zxY7Sp-MC~)|>UNd{24C_?C;+02n`f?9VoZiNR7k#JSRv z(AQ5o04*aG<^lPlVf-!K4$&<(D(5N*@baq%%I{d4jWtWbDvCT5z(?1Lp}Dmrol8LH ztAhE;NZ5XJc;JudZ8{?2tP&Vk_BB7-EewIjE|=No%EJ5}Yx}{NktzV+Jyd1Z>-bs@ zp|Q%RO-@!P<_G|+jzkudEn(>bBRJc8(Ekh+Hx=0By#{?*^hc9k2$~E=lSSAp#V)3iN1<`c6xnn*b+h07(xL4M@l!g z>Xo5b*;X{zZfjkkNXe#}o&d6gkNxrG*#MuQDJD1hCVl^o3IuJ3`!;DkU6)&7C4e}t z`8peC1e3QXZ*EH%F)xG3RT45aNrkDBjthq0zIt9L+yH+W{T>BIJofgSnJf*d9T!po zP*xqHCDXMIaU3G1c&hr%dHc+@!nBjC+79WW8l}Y-t~)^9o$SPR9aQR6KG=>A*wK3t z?~gY7oeN=wJYViuRo}I+bpb^p%E09F%Tm!jwtOKbG9z_j=&fB@UYnljCAPp-s#&l* zPH$K5MWQ|A8sM3g!SY|4y@6SG&j^9>{mM&kv55r}ByA5NQ10};$Agtr`>V&(e)E?y zgdhK{bLIF;1I6B{pqDQN?k%l=f3@$(b`OJsbq!NelxgBLO9auebA|Uh#`4emzn2Ie zxaKFndj3jsPk&UEJPs|p&n=#}+cUrV@~RV|;W@)NQI%8)*HhzpPZwR7v3?@z2>n0* eF9P_}Y(n!^={tID-_8FTv$J+S*l-{uPx?2uVaiRA@u(nhS7T)p^H%_p!TLy{wll8R7ur7-I))H;}k?Gf5k_AbTK97-|}0 z22TPEDQ%u4(?V%!>%xRKGcD<08Yn4{v`q*pX(kQW6cQ#ugkT3Jw($dFAtU^dYCWu7 zY4<)(=ey@zX)Uc*cV%U!li|#0v?JZU_x!*A_xzu0DWw#@=F55;z!*`A}oSf()Ef1ErVx#u#VA?yNzoU;><%gEwD-V>@B!D1=3*T>y*L zz~aj_z!+#`7X^18hKIW0XhC~`_-t(o&db8uCiw6I_`L;?H3qE2{7g(&4KmuSamn0C z*!>9X|1J!^0imaX%fID1`LOU(xZ=yudLeiLq#Sts7;NkUG81t~7F0@u#RmddoPiq` z!Y%KFrnEMWJiA&}s{%RBo`PKu!^`)>a4)3mz^O6dCnb`x0jOUH*M0{Y7U?*<>kw>x z6`V8_0}y~TfMw!Xt`V$({BI1-cj2$khwI;=ITNsQP12PZWEg<$2xE#JU$l7RN04p+ zHv$H zJy0_}$oMfS8$1s#NyGCipivBy0BA*!agrH6>G6#Z`xH-RH9&Enq{}erxUl&tXk4Vf z_*5UP?*jRIAOHnuf>9x5X`maL;rq)WN!G0lQouNhZ+|>T&r@||Tdb-Xl=9(?LfeI~ z>H9jaqr_{$+AetUBsjGYh|4AbDF%vxe&t>8$L%u%ofc#qhrf6-#}95v(b$$IsM19v zS8^fW4{LAJKXPAX>u%@yQ}Dr`fRi#H%QOt-944w@3hEr#w-U}xYx_+1_3|K18Sq8} zzP2Gpp{Fq_5mD`{W{Il?IsvgumjdvMb^@YM?{ zvso5o%;K(Zo@U2q(loYaX2qf~G;#vg-J^KVC#(o?+ya_jf!;BA_YYyj0m-sSNBFo* zW-Qph63)v^pKZ&6)DKcTa?Jn(FE^8}v#Mq>2DtRoKa#En>G__aZ!-1EUq%h?kboJeTD5>bUi%Cv$xx{v^5fl zi4uU6a^R1_=xBkJTLRYImBCFp6C+rejmQ0HcOF*00J#PomPtZ0>X8-9bK&qRXmv|w z(~@URVKR=#Cq8|GgOAtJ)ZCzZ$SG?Ac+^KaFg%c>rel=f{i`e&Us)R&W_-r1z)b9B zUpopPKMZai_*Eo1dHmiLuq<1i&8abI_Wsb-z4Yy@rKzr&K+IjjWbAd3#T}0^FURTA zr&-#WV#C%Jn$Jx~rDAdnR`S3Y=*GS9_#n6`9b-wB>}TH!xF``wgGDyDt@BO(_qC9w zYzKCj*5gz36A<7L*kc6N7$bwDWLw}vx3#eP2C3+}7D~~tX2rw}7Gc$HIO>DzR0bLu zTS0RQUS9?CBtw@8tTUN3S4i^N*4aznp(3fQZ9rNmLX;JN6Hv&91O-^$S;ysn-b(8Q z?)VH@F2N)L9FI`1oQ8G#;8dvR&!~|vv*PS}^#XWe$@H~y+U)x1b;o)A$75vbEy7TZ z%NLSZiaABc@-)Bw*(NT&s&3ZomD_lnZFlEj<3Tu>2V7lh#0W87$*eM_NmY7z1+0{w zdK%qmYT}8r>w`l)d0Rh?3ta*a{6ZKp)wVdrd#`WcvdxX8YbWYwISnTXdzF9=_;5!L zJk$rCw3MZFRvgNPe_aBb+bYkOWfPBB%gcxS=jZ$B?iwT0VAHpyeAv-g>zv|gGHe1X_Y^D-i-5PuFoe`?~u+;RPrcx z#8<>;W*dMB(8i+Igmf8_^A(QcP-C_e4N5bN&(nhx{|^+F1Va!K?mCJZ8pUpJ#9G=8 z=~zY+unEV91i;E&5~`3;DID9;g)Gbw4F3|Va16U)Dby}Exxai@Ha*CQDVy-|R|xNX z5)PcyBCBM9ceP<}{0*#Ie+TOv(}zh>OxF2T0X4ETSg!ug$S+a74?^E}vBr8#<#%D# zccASmbM4gHRUV{Cap6N& z9ytCDJzoiqNM27tu?WqV!h3&a*qrKBQ$djN@ zow)z>2Ut-~KU+r4nS(qt%tQCR(DP*|j6}AMSQP`rWj|^uYhle{NVgkrI}_+skkT}| zHsNQUApFPw2Di~@f@MrDNyoKN-Vos}>&a}}sGClAmN8n&-l-jM;Bzpv2Sjw1?%$TW zAz80Pg!YV9J(EP?$d6Wl}=ufzdrsXKMy66DH;CES}^m)PM!6U&U%%S_Q~Z5iI{( z)PY{G%^}20Inl9E!6?J;yO5R_KC2V$jPYqoz5wXC8{Yg2NCl9QxbqVXl_DZ3)bSMj z=3cCpB?&;v#!O}nI=7*o{<+Z$lMQ#a>oDjKWB=tfG~Km+2Ddc;y;JboCK!GiM6eO_ zBv#Bc$lypF)(3WD&tIGnWSn^J-cESOH^FU4Ji$iZV?!SS3x4{iq%U2hv+|T?!5GaS zh6C#$zY9_+&0Zo+z_^t>&{GumwBcU$B6eF+kZ}ol^#s8sTOl+@Of$J_TwNTS{$P}F z)nYn+_Icg*O?KNTi`sB#0}MW807{yyVu_2({CLWtuy-MuP0v>0uB9%!;Q`bS*Mj*9| zsjedgYwtB@YzegpDaV#YpJc^sijIAE;w+LQuvp?2pFRcqmViHKTES#7KhCoO$cRm0 z_c^G*CUf00RWexuG`RjT)YpGlxha_BWs*Klxi)!!gof)rNXyr+HxsTLQ@b$EqRFCF zcxiO00qN~)!9UT6b)B6Z$PzaFr}qV3RyipxU!6FU%m`&jq?WlYP z9C>+2`|s?Tj5lw9jz0Pl!HthnScVihJf0<}z4r3~;VMmNMGa&DhP4k=fHtnk<%^B9u6dW7z1P z0|-WJCW0aQsV9 z=$<&`k(t?p@H7O+U4ni&Yqzy~nggD3Ye?7RkK(S_LaOtA{pT{;Xmw1+caLPWiJmkp z_!PQcr|{e%io1>y_6$OFor5PsC&zP91TnnO3$mn$G0uBXJjL27n4u;oN{MXWDYiRZGDfUH_{CIY%3u ki`%>naxSd+|KT402P{=Nu(VucqW}N^07*qoM6N<$g32vsqW}N^ literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000000000000000000000000000000000000..b2534d06bfddc6d929fd0626a3d51072e2a704a9 GIT binary patch literal 3489 zcmV;S4PNqzP)Px?TS-JgRA@u(ntO0vb)CRJzw@{^$<4iaH*IMtbYZJ4EoiOU!Kt7;3R7`*7;r=i zxGsXb2(#-lu&6_ESC*B5!OpO_MMqRt7nyZXv`j04r9&+&nl4IPTH408lBP-1Bu#Ga z<2-i0=lo7`^SHU^rWyWNIsfEw&iVa*pWpZW{T=1|zR#b*rQQYoeMH5pd`jbag=Ae#i|#A_Z+rVbL0}>q2P@Y!u6m z<>B5pVQViO*PwL<%uB)rX}F{nE}sJnYC}8$7TB5c&Ir_V!7#%G9N7V{KM6-(hT&fD z-7!@au%^YZ=F4!-+7Q;4#VWY;75LSkfTWg_XoUPOKSfJi$e8fSX1Hw;e6VQ@cq(@Eo^Mi08s1A@4gK?9)iLkBpbs2dyylWu(iqr zuK-2@)^CB#0&qN7wjK7J0&!NJNKs3y^!30t<-Y7s4%&f7&w;DwRSz2rG=K!0qQ}-d zect#lliEfwED$xWbcv+1J#f(m_`t2<)9e2i{<9NI8yqo%#_30~jBtY(fv=wl_bd(_ zm=)7#D}!ov^<0mi-dg0qvk6jjR0tR|OBQ<`ISJ>lhfDq~1bVIquKbPOERm>yR#;q3UZ6+uJCI!Jk1K(Z&`vIk+s z^-w56!iFFG32t~TR9`o(y>1LG32av$SMd4@SX`qMjA@ou7Bq;H3I@B1d~a<5BbNfP zr5FpDYCdsT*KdJ_GsnV=OtJE1c>M&J8E{ka#}}_Fn|@g{YGO9bndI^{ql-BG{k37e3K6J?xaAQ5MsCFwgg{ zC{Ux)GZUofn262r`OIc$I!lel;V9dY#eS*-eta0rHgIFuX|xyeu5A9w47_q)s14;( z#VJ9f%;%T)_tUX4MSZJXy)-2uH(~g=&*k6rS@CHj$eP6Fqf1){k`69=5e5?Ar*&}< zQ?ddk>)n1Hd?a07s*^!=)ltm3JifM{;^7voVM$bAu#K~jX7r|(f6m}x95$^KF3Wn2~>1m!ol{( zPO|MjXlZSV6;guA!hpfP5z>qETy}qwb3R%-TD45KPmv0D=V19x=+hNT9M&MU_Mt5S z2hNAIIW_ELP+gh)_uqH3=P8SprWRb$ukrg6{56`$SRT22mZ9N6mR)VJc0&{OEp}i# zQI%6kAtDw2$uYQkA6T_I<*V>w1e&qnt@EMHjtLqi@8ADS7l&SQX-%I2-sIfO!^Ji7 zI4aBF$zf9MK9}9o%=@m9G9+{maUmm?NQH0S2j4ypb^?kO>@@;CBN>9$=$d!piYMEs zu2Q~x?J+ucj!;)$gYBhInD}^jIJl~SZx+aBixl%d%h#p3{Hx70F0jIOimo{;Npa%+ zsJ&9~;Nm~PD??!0)58YN$bl(BJu>r5R^5yAIVzEbg-tTcZ1N@Y)b`6+9YEkB?*&OKJ^#-$Mm$#_|Pi z`J}$2byB*p(wz>XdJsTkmiciBCp;IlJcAx@)Oj20t;&B&vdXeEl&?p1XF-}*{xiSF ze`Xiz*dR33qE;cL4&q?2;hG1hxk9(rUww1K%t+Xz`=W=`{}Uk6Yc;;blMEp(r^K+dTo5KwPe#LgK9POcXr~g zegG_~h^6y9K~$n0rc6@6$AL{ZVc)!Z7GO)H=;xvH0qFb&6tZEnC~!Iw8uZ3XM_d+{ zoCB-h0#kZ2qY@ci^kh)2g5GUUa5=V z&_npI?u!R1lba^KJ4ANvN*bQKwF(v1&P{^Esn_AnTVQA>)GOVc)|*@|xfjrak3ZM~ z7w$*ZO3Ocv>S{;iu6+!DO9z;Bl_eVaWXs@`a|&bQjikT0CI)P93>@m>+mFGKJHZ%+ zq#OY_@j8wITJ-U{=b|p&gGx2(1ETREro4^b`3&Ae&xXc5osE{Nrc!u@M{mNQt>Yf- zCGFEBpwY>(35RZho=y5>O)BRY=N!?zTB5~6XJM`Dr~)+dy3fCfd+Ebq)anzYc*bU! z27PW8_rj&LZ@)u>>Lya@5TiYt&%^P@A!&zFthV1Mnc09(e*b$&T(=`s-O>}WD215x z`AuL8V%#py();017pR0j+p7#K3RAF5jyr?2Y`l^BsZ=!b{#;9)~as5lE+G->!lSa1s zbO-LWkAhVpahR~tIiN2%Gbqsho4ZJS_^j{3HP|1NarEhL~{L1BpUdnFrV~Jb-p#7D* zNqlfoh|~KRbp9P!5=yF6TY*e~dniTWU>nNv!wsxyJPlA;luikq&OUMRq3T*S9_?z#UhCzjz*V0Io;v7rk9L`845>_%n4XUpo*Uh5pT1notGyLEwR#IQd`vt|uSdVDr z`Mr#MX9w=qz0lW>LJlhw1q>}CAD=vR7k_}(=e~fFx>7&{${U8RuR-6FP<(qdZxC(v zv*4aEal7mBvo@F>O5jT&qm7i$a6bAkSS!CUcBDRmqvBU7Wx0co^CGWA3c50Xf&B9a7en3 z93|CJQ%yFaZ2LDV*=9f)%vWOHWrmnN&>5`poqXk8(06jT--)^YY$W=By8=zE@NRP$ P00000NkvXXu0mjfR;azd literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000000000000000000000000000000000..039cbd2a87b14192b260341d69797d95d042caed GIT binary patch literal 3549 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 XD+Y?E>a4-a00000NkvXXu0mjfxkJxT literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000000000000000000000000000000000000..883cc226b22b3855151e680d6e44a94f16bb0fbc GIT binary patch literal 2123 zcmV-R2(~zX^fR^4He>(Ky4>skJ>=;6lB$hS7@`1+R{6neirCdh7`}RZp}ty?KCIJ$ay< zdaxZbI-?_XEa}wZSRJ;1Xl<*IKTsgaX8rxL8%Q?E?t2@kwB|GOW|OyX7kxR%WQRV?Ci44(rbz{a6p^MFaX%Vm?XJ_rz%Yc^x=rC!1psq!*l^ z%Na#DT0CpSzUhV`M1BK(mG`{~PMY6g4{`N*{^z3`PDdO&e8#qXI zQGlE(cGFe?B)meGYgmi!j?7L1ljn?UG5jP@=f^;sWnNk*D5NjtbX46vBa`@Dm5?F9nz$zqYNtLIK9NYgA za6*9$y`+RuYiCIX=tiEOhXCR8#;mWzAq-o>(aXj)k_2A(Hs@vywFZY5kRsa2I>rtP zTLae#UDi&o%>zLQz=D5&T26R|W5jP`#^+`-_uw{&{Z=_+*r~h17S}0F(4183Yf3)= z2Yx)4$SA-qxk~HaopU1%VN|l=t{dV?uy)ufpv~qUem=j1tLH?fN{nLx!xA#2AcaZ_ z>!J)2aehv*cDRxtI-A0Gr!{)mhjIvXu6<_V`xWy`8d6rks3iQ*eaB&nwZkhjILpPp zC5lt^D2IkXz5*DI>Z<`Q32Dwephy?99nxg&khXy9V~dDR%O&Aryw+1_uWSV)2hA^% zZjfbYR&Kns1^haeD%cv1x0bw%#s!WyQj*RdZimwH7;W#j`7lswMVDz#0SSbbE zxw`C10Xo@oZD0}I7j*2{FZLt*TaCKkjT=O0-Jl98PgnuV@pQ%oWYCbHNsMCu(HK4) z+k&0@<=iLZ3L0D?ssQ0=7QJVLWfscL3V5~?Sfn8S1XT?Qy!+XU*!PyZtii4@LMy+X zKjE@saYo7lGFMt3r7by1qiD5)IEnobG2EpwbJ+8?7oS`SxE4N3_fB#L7yAJ1bz%|z}L#ZuFiyuJ`4AKg-SGfYg(6C zFUGYFaZ4E417U^4MnYacIyzUh06uD}bHT%k;IzjnlXr>Qw%R^cuqh&? zfaib$1-Q0ZeTwXG%3OIU+u#ZA#eYVMHh$+rzyfU#)l>1Xqg(h zRp4f?p@3reXzTAt(G^jF3rIz;^w1?+;TgW4q!~z@7u+A0foFMU2>7A3wD8$+3sVq5 z$_3J+KM9Rvvq~TT3aOTD@U2&DV1aqodG;BeIv>^~;O|^0@e-PCHyIInt+)knz>A1& zy)s;grFy=A=3p_7@NC{X3H(Bqn^FO;v6{@r<*wgs)#i7}ojPF!=n}$b@zFC26fj-G zQT-~FfRD}?e<hl?wKEWi_d6>iu< zt}rL+3e)84ix=IWUZS#Ci*FSkE|ms7-LJz9Yy~SKuJC|TTuLl`l|oa?P628`4+-z>55f(sfHb*6jEbqE%$`KKWM+TP zRsm6Khx9tuJ3#fmvK_KylhPs;Q!AlA@^2u@@w&Of}{4AfMhdrIn3G%5bJ}9X`0yghO+7Qtw*s+GQOY~eC$#ojZdJ32u zrPUwleQC{F+0@f9KeYP|nb~*3*Ljj!-Z9}(n*H8lV?~b22I(>Z7w{4~LHh{F@V!-Y z_mQPx`R!KxbRCr$PoC$DU)p^H%cX{t=mn_?oF*XJpECROJ(v&@zmKX{cV+sjdX_-kv z5(pVw3?7E00}NY%NeN*}CMh(w3A80ZOeq)`2r*DF+ZbbG3mF^ByDeMqNl$ONOXs`i z9_d*;y-iX(o$;N~$a?SGbImZJDk41x0XhiK$Q0~B z2K|3#keFA`hg>sj0Kt!;evkDvW(*3P2M^@8VTQYs&4#;`&t#LsU9JyS5d>t6T;>>p zHR=HQAt`E&p4h%5VJV#W+ zY2#pCE1c5;4YnF0_l_o`Mg<_x1*14S(D@3)w?Ocnh?AwnOIRaHrD-~xek-(H zs6LbLg$1?&4{w4SKLldx-T*%lrwI5_aODG;C#!x>jloru;QP~HTFoF(#YD6eri}=o zfHk3xNo<36ABNAK1z}I57Oa@kGkTmXD9}3wWcwl82XpR(lfJ8%5vf+X1mgB?7{V zLQ$R;tJHqDWEmVj2OI}%8|Hri&u;}Q++@I>CdHrwOj<|*0n0MM5mqJPNDF@62Isdb z>C~356vhl4QxQN~B>MOHyt2q(!*Yn#E5>+E>B|tY%JxD_JACIk#UR^++t$N9pMe!s zVi)XC0LoFy`JN)fED=>dX>@x>W49Bgv> zA|hA(CroHlqu%U>Z@#6ND1K0>0O@B0N?PXagYPuMl3Af145upn2t}OBNOuf?vpLJZ zf7>IuEf$EBCnr|nB`gx1b@9K#5oar|P69$Q;DkC`)F_o^ncpN-) ztYT=n7OrG~v_9$Yad`5QG>MK{Vh!bqR9>+#A)SB)OW^RcAmc#Ph6gsn&7XiRVkQWQ z29A~okUTE{DFr{61dkpyXemoQ&yoRBrrhU0zL#Lr@;YiJm?L6N?gE9GSG)k@XMz{{ zMf|-UocEU|cy(cv#)&c9GL&&4 z{o|xz+;q6|zm(My0AylDAm_I^S9~cqvL=RPBibBA} zx?SE1Paau1i;4oI7yZKGeXM<=p1SdNS*CngSr6n!g&9}942_2!7boRUO`VO!fz!hCoA$tRW9GE*EUTRY=M6uwuAV8WuFW=eC$G@zlVZ!L>9*NsB z326SB$K}r&*j2_r2$Bns-oUzEIN?ojOz_54jL)Fn@!_L(Xs;;_((nL<4qB?$bvct00BVl(B$ z*nrh4wnxpP)p7XxN+o7qm_8|X@6hkXHi&Qtlh(Jx^eW}44;SVYRnq@_K^MJio2jj- z36L-C(g9Q$JUSkUcpq)^UFP0dOWlM>$>8Yc>0HtMop4Jh*v(<|UfO~V7$u5XBxJ=g za86@kYnB(FAdejQJ}!^W-9og#g_u=SME?we61Xao2eKUEJ#ozXe$HN8$2YE437ul8 z)_i-G*@a0Gj?H-=-pJ&nL&^k6TeGLz;Ii?B^-z9*LjR=e<21isu$B7Qc&hwH{IA&8 zL9b2!%frRBQl#Bp68n=JaaxoM?w&|%TQpF{LBgv-XHTq}-1}t`j{6XL#c1SYWJ|?K z1N8f&;bLK8n90m{e_nvJg8uxCetvuP4r=Nn)OeGyg7Z7@QVVo&NZ}a$cvgyJJWC{I zaOU;nIP-?4z!}U<%a*#4eB<a^5dNUhdOS1Hu zom4f7hh-6^7Bfie$;r96ICx}&--hksW_;3dpXsO7aPH5VncN-?j(DK-Q^qC=4b>q1 zeJ4D)9U@_3trY(;NYnZ-B?cRggDO+8a5z#I6tT_krFi_Z9Ykvkd`|@It4lB~`OYA< z-=n_O=8WqbIr-X#QDseD)`t1!A9unpwyRi8l&`Lp4F_hc)whgUhbj$+{Iu>RT*4#}DY~q1!~vi2tLX zUlp5h$O0J0BDppP|Wz&_6$t~|Z(+$7b z1#cxFE{Ua3E7aP`V%IvvzT;rglJI8^zlVw z8V1Hl3s99HlXK6|4R6oDh7|0{Lapg>Otr^hRq6)MH!*eHsmODS=7Tgy`4nYL|4 z#{yENPR@V4FK|&?xQFiU<@v%C_ur39S>g$R&ssR1*^Ltk;a%% z3$rF;%$htnW1uOE6tG4CjDl16E*>VnVJH)x+KLf>7qf4jx^2Q-sQ-EhX<@ImI6xtH z{f<3&H$IL3(pvDPqafsuQH62BWQ@N(1@qcdp;n6L-e4hWtii~gKXBCrk??@_z64u; z4c)ImCO()*lIr4#FGBOT1{(v0S!ZE@G&mo1;m&;+c6WmzMfVW(AV88uWmG3Y+f>YZ z7hukB&uIjzFs>*ISn)tj_PhX_?t^_R6)5s?&|MMwC*lY-odkcjDrC{{j4zNd z3tPK-`f-lG7rxvHks8S6NeYR;OB;rh3OsCnG8g-yOG5n=22f>fxQxNMk#FJy==>?{ zeo;*js0E3dh4P-uz#54&(rx+jb)N?G~f7$aigWP5NQ zxSE=U(%YgKIkGG4z>>{4Jz2|&x&;Ldv( zteWzZz@nBcNE#lIcW)(j!j#eqQNtk^DB&=2>V6H@{Sf*#h!?18_O^T<-drfGmoaGz z_PO}&&tObmFaicW{|WB=UsMK=tkpE}y%h0ThcWK`MVP`Mt&_^3k`^h6XEy#6w%n}> zh}AY2pVk*j>&Wvf07wTmJK{Jq?to-U zY5vME!m|x_IcX-mdJ|QDF*D3dl%S6^ID4LhFRlSYnjghjjG%KO=&S-~y^nFyi&$;v z6-+@C_Ja%q_v8oh-`fCYb@_TIJU7!c==L*YrnPX$>ctqf(%q>$I){-pEmnJ;f-f!~ zv{3m>cRAuJ&&?QQK8|9Y^(@xRN&(V-(A_KWZhIQ+`mmF!oYknYfnKyNb~|wze{e2M zk6v1sMH-yMN?3muSem*e*PVu6q_{`>qNG2rCUW62C1M2<=EE~ci`k}boTKl6j5rZ8 zd-(wAJ9b+JSvSq3H*Y3-#*AS=3YuTQ*$?ZEgKU>Fha+WN3iQEW|3%|9?K-|rl^`Vjg}7=VFX>`LteDd?D_IK*!h^!Iiib3 z;}R+-4bp3-;3fKqUi8}$Fh~{@7NYzOyKraUrBWzE!d1Drg0q*%yrm-qD2GMf!sqZFS`KzY#Wi2q<{rbq zFP+n?GH=N!Feq0Sz0rX?=Rr(6rox6YnJ;=MA-X8&SN5SQ!XfK!$C@f#QQ;dg{cB*I zh*z1D4;!MWR~X#QHDo$lf&ym{$)-mWAbEIb_EY!2hH=X>>_|h9iY*7E0X-xVWt+H8 zg2qMjXuR(t)u$^Z()i2Jac-F48ev0{U@R4kJrOc%CWVZY{M1+g6oMqV>Eyy^u^w7Z z)Na5Fm{i(n2Vs!tAu;5pRAe3ON#mH*aXUtxgyAM6R=|e2Dgig}qF9NRAa_^@j^AUG zS>1~7+F*vPA2Wc0vVn(hne^Va9OLfisi7L*vhYi6`4WQf*7-fl#B-BOeC`ISFPg3J z$aX3nRH?ZXkYt@v@VcX9KW)L!TFTkf=Qw5n=@Sb5qu$5=o9qowQQMcqwrlZ3aR+ao z4m1s}VSgg_s@cr1YBY^xp6^_8V)cA&O;1k@sVy*GX_8@i~*B0>y9@;|zo=lg-sbcY}X07Wc3w~y*M?M!_2`tXI*usX5t zN!WO`0x77o>F7UKRODl6^YHQ0Htx21oUR7QSPC31ZeKYm$>i-Odf9T!sb^G>?kGnm z>Qbg*;3c#4{caWgzj+(~oiDIRV_{)p<6)69#Yr@z-W`adPZp0P6Mpr5YQKBxV6I4q z8@=)#!p+dPYVg&;0WneX>h*!U+YA`vCnEtchM}4)@?Mg*$@XH+IFIQ3r&S-H&Q#{P zp?Kl2RIr9rRMYQorvJrHNx!~cuD-M5rE+(qIkVg2$Zmg5h~(>TF`efn`dI3cpZkk>^59&J>2)`0Tj?# zF+;uPJcz8fKZ(0#Ki;kcUNVDG9l<)ZiAYri6{Q0@YCJeR8&f;P1={68g1RSiYP{qF!Y uN{ju!nC2ir2N^Un-W_DnK?aSCLH`5bBo?9Te-@+w0000Px{B}qgj2ihei~L(GfHti!mbOmcXc}QD=sT&Zww~j+1!8 znT%On63qyj=tPrQ)Z}C|xQtuW5XX$71{c)esGtHO0?NKL(DYVaT~%+H^S%2nlx=m_ ztEHVY=kTx(Sasih_x|ty+rQU_@B2OnuE+=&2LOwVRWR@XV1?mz0I&k_Isn)KzzRj6 z!14dDbXc}IHwxTnxrTo>fcX$OFM7waTLX*;yOvvAKNybHZ@hdi-^<6Q%vUZ{2AOB* z0F%vl>dA(cdjq4~<*_JrWXoibM!5JgK4&}4lz?B8CY|%=0!+Q_G%7YO4G2r?&aLfBpQ3Xz@uL58zL?+d}0lG)$_2 zxntmxF^XjZTDiouWWcgnwfYHI`Y*8OLx@#FtZKi=eJWx#z%&)Jgnh@&g)iTzKywWR z*!j!h@y%ey6kq|HVnAcauq?AFzzHbwk(UN~;n-@pg_oFA*NC}_sH^fZPedmT7c7L0Xcx@Lj_(7|GBA5MJUX^;(TvP6*$}4B!*e>=zPOR9CZlAXrS~Jc zOO*boX>i`7`<+;A(f+#)&RVL(GEn|a5?B_;vcNA4L3{W}xS*vl&>{h*MRD0<-8?tf zreRnWZixy$`|!o{@7)bEegac2Q^rF+%l4h7zX31q1j`1uHoB$EMZzEo^Px2k?@v@= zR#ef<3z+t$`nnzNIlT+7tvL|IVsuDO2@>7qKxG45{G5s}v%@6KvgKWH>=LyxuM+$s z^_X6RX4&^f!);@tvn(%Qnq9Bn-NBN7s-Usl?21964W`fY`QqOh=;vjFt3eAK*|lIx zsMJuPw(fUY0i%4C^;k0r4iL^I~d+@NJ;FHRE}KT?I}ham*UKyRnR&%I#~Th^T{1BYZ=(X!c0byfCc$7 z2hM7Nr^n^DmIDLk_<<5$e0w`D|8tTN!wxCRQ4Mss4awdleNH!Dx-!P}@7H6;tVm** z3kNNlUw;pNvj^R{P~{Y906mA=*~k9~6|??08MwQ+Q0hH0hg84ixw zOGkSbjfeT1HNTm$Uy8l@s&dIk3_X zjp04BwzKK2K3ZxH!SdocRamxF@o;fS;@U})eZ8cT4pY8X#~C-YP~BoFeI=_b(a>tE z`C2=iv{J0C5~o5{tQN^>4e^>^u5ae(a}}#{Ii>l}bl?$MH0N!A8#Y6%5;9?FFY3Hn z>0dSy?m4UkrLQ0OtFyMTV^tbk(!!pBMH~|=1j`M;5mt%YY+45C9v|N|n0#I>e{oF{ zb*+}ll|?MvM%}1BFkzK!`m!~!U>C%~baqsxX@>p&C^)C71f{Rtiu*6z!TKdhDr%Ku z>4#9MjfrP&1VA|{pH#QUA(JYY@q;GDPFEF>a)=0jf)P{24qezC z=nynm6KY}E(JI%UF9^?TC7->io!9Q}q`JlYgq1W*BPcXzpc+TpOuM|Esh8GcR~SRa zs$84b>3-2Yw{L^%HbGj7$zc{k0L^BcZaa_+t^mDc0!*vT&#pmrnD+8FFH7<3v$s)M zZw8+DzM5j6kCXOk8f7zic5MJu?Qm7@j^ec!B6?BA?qiw(Z}-5po8kGr;hm;RegmrfVfCZSF0orcfh6+WM@ROZz64Xk`YY}UH+0v3o^ zR^LPRo<8BT1d2km?_gd`nGlIMjaK@qa4V~oAC=10-iKOk^bAZSz=A0=9)2tYW5qU% z^}8VL!pM4zaU-BrQlQ#7D??2_A}INXc}Qoyr&;8?7(MS{_N)Y_3u=#qrWuhziw2l0 z=ETH*Wj)^Y3-Mq7IBZ~$ld+(w4&$W5F~9NW80UoXMR-sC4Gq^-s_seOH4g<^~5$Gi%Xm z+?l^1sD8+UBR57Feo-C7kUUm%iier1+e}GNJxMg0+p+sC|HxF%HhLiz?2vAisit7i5qO2`2YD9 z?nS>WEa@b`$i0~koQtMY`M|{(fmg_P^cX4R&j zD^^JfUD|a_d6usjruA0;M~muBn2_?IegaHgk#}Wz^I`5Wx8X1U2+WGYGj!rSS_XYi z8vEt%QgQO(>NHFC)v_#LaxJXB3fi6r>5Yk75R=xQeu=GI=?h`IW5Qk!YA3?9cZ&o} z>m1+59XB6;?KY+Gi%Myl7=DVbLtCkT=US}ViqiC1wiVlYFRZ;9ybh?f!Itb~h)mS{ zj)-K3hi^^6IC0tF*~5GZ#K7oH{3-s54aG4mJTKF>=ykeC&6!TieHRulnpyaROMoN2dEEV&}Y)*C^#x`rftw zg?H;hu<8m{`q_Ayra?Cj-OaTeyz+X?;bABhrL9?mwQ~V%yj+Q%6gW%m5e+B+A+|97LP33-zQT&?yz+T%V;Us8%^jlQTo-(*-YOs1< zhczefSLpk44SDxB_yAE=X-L0Yi(hp@iGazYLchto;SYH8o&>u@v6S}>OoMhZ#9A8} zvFeAIjq*xIt|%Z(Ji7^4Jqc1DgB2?G5^>shyO4EPoYaa2jDsdq@i))qoq6zBQU}l5 zJ8{O{1ifkS!`ChHPMv#z2-0rb!g0E&zw&e%e>poh%QU;Teh;?&3}WK7m$B_pmghs} zgBG0aHs-+-%K%v5S13m_Q2sw%81@+z-!mi5cDn&dDWdopTHm;a__6(40K?Q|dK0{V zB>3`5hc0y#c~bpv6L#&fNv%F4c-IAIskv00Jt0dpLpH6R481GEH>%2H6=iDiNH$fGTHlHhGlKL_`52~m8j5CS z&Vv{aya=&+ki=V&*KQyaq=J%EO!+ff-~6tcNpe9!ANodke=PWtnA9&u7WpuJTL_ll zX^~zr5?`AB#ShF31z=%8&r7CBop3v`_cnw~D;zZ0zLX4mg=ChA>-N(6%y+1qd6atV zz?M$oSK)(`!p@a4uuH%aec*^2F#NAgJ*wa_4?l$q_dtQb$UoH<|8Z5DjD~I8<*J9W~--BZFkO7vZ z&ob+FlQ{F=sb0GUJ64A$phXPn1h5`a{NW4jVpxpI7zcg$BdpQ#nuokyyG~iiQoC5g zI*s_cqU4?3wHUDwtUv_}HDFnwIXk=QzVJ81|M(77MlHT+;ztP3KLMD`XG`Bk55pE* zLG>k5Rmxb3ENhN|#Csvj3S;rU!;696Ym-^sg4Cz&j937kT+%hzsc>aIC8jA11NlWrIbw*LsW-5mZ72aU4s&#F%a;xGsLi!cR$UQAAdnVMLS#$q4m4 z@h#)En8U|M-fj69mIJt;>!*;i+CXhW0rH`hETAPx|C`m*?RCr$PoC%a%RhfXlw^Vg?S1;8`cRC3nk_1db0vMJELBNAbFhC$+#6e{B zfXGM=j*6_JEIK#{9uP2*fgoWZdRPpaA%LMe8rci-!-G}T>e z=P+mb<@D)v)qD5d``v%L|NY<7T-S9OJn|->4Fb%Yop<3Nz{2E?Q0Z``3RVDIh=5<`cB%B2?*5o-Da5jhTpcSB+6s#>#7?9TiO)bNx*{Y z5S&{JSAPah9HKx13j#u1dxS*2SGq3 zpsNu+dp68?2uzV2`5Z7c_|Y2per zV1&U6QXfKJfXJk!Nr%W}2~bgAw*$;1%oz!{kB4$owIgj8dK3iEQ#D_TR{s&+xD&+Q zMPy(iZK<{@i=GO^wtAR37sj6p2@Ap|+_MR8SOsPPP=JMefLY{{3|*-&Z7?|uk4}QA z)y09BH^9J7IgZQUe&X`}?{rGa!6*U8%7Rv!Q^fuy+F;a)Fnb}`4#?o&YJgMTR*uJW z2=XIpUYTC{`ntPmfR%tq7t9+E7avkM#Jm6ol9x=_EV|ZW zB`-tiP}N?-h4HV$)^=rQ#TgL>s&WG;V8!-%iqQtY9t+nURv5(G0s1!2#o@WDJK40T zjPhX`p3yGG;d}9m{sPrwAZbI$fE(AsFV^^KEC5Ykh4Z+l7aNLNcfcd#;ObHNL(CbV z^0qFEZ*FDPyeJiQM)BZ@1d7^*;o|>>%0qh8cvA-)`MQ!uFM<&Y)d$9mxv&fZY&#Aw zPl7KG=?R;1YA2VBbg)`j_H-Ld=7gxKEy2#y-lglLcuuiVRiofrOO!nodnYOd6LV))E-3F7|DS^TdoeB~B%-9}s7A zqioI*`_|b66KDCMj30Q5;}EzT+Tp0zRjll-uO&<>%^#rjNkSPb2^ZGE(}(9Sg}DLL zUHsG@9>%k0xB@&Z|BkA2|526Ks zJ`!fca)X!^z+fP2UT)*LOI>2s71%|FCn1d01+mY-Hy8V>5kJP1U%`5|Aqf*-hlaRv zB#s|*7BZ0%aEp1G5QXJODXC0J8uiMA`|#6x1dj(zae z2V5r2)(()q;5NbP;kVo1JMa5aSmDQndD_9CZ7;k@!lR#sD{8a%${7K6OW!MPJbS4{ zbu3nN=@WQeO)&aYhYOxmbKh4A%u7*eaPr?_X+0PfV2dp*5TIwy9VLY;Qlv1GjAR7p zIS(D4x}u&POR9-hg^FtXq-{wim{Es|7C~&ZlJ33$iYklBt!RPc-^6odPPHG_WEyRT z@4dCN1usp4Gpe$)h5Z5y2GX!K!MrmYh`PfI?Bao!O9H}8LNaM_-V-q96w|XseI-_V ztMYB@;f@c$tO3gpc?#*E*gjF?ONPVaL&~eAn2tO9T;5->yCyJ$xUX6)w7bo z1y}51+ma};s>%ZGpNpnxUTD(ZU{f}_lNtA$9Cl(PYj^>lAgqxL^;A*iYH-W~*W!NT zVue>74U@+?i-+wKHlc?o3Cug-CIK>R8~;3z%U?bIkdL4GX7kL>35V5`izi3vVRUI@-(q<#_}vlgG!yrD9t% zBwqUj*d4yS)PbmQb!;dlb__SIRY*+}_v{JT*w{t~3A>$^rVhqTE8(2GYpI(MR&k^R ztOaI;gJs^9b~t7kbVxqZk6rU&7y$gev4fIWd*bvt9IJZA!?thc6{h?1W1Y zdTJ>%ZXd#^W7QsGe=%M@1MVH#T(uVF?S)V{cZebklBAG5la0r~uuuVBI8b3Ff%}LK z(XoBwqI>YX+xh?0x=#1D#WcE_93oW)U$~}q}R^3pw&R`t%l^h8LSu6edB-sX4`Hu20dzVxGN zYL8F}wtx#XTok46<-PwIR;pXko>91}CM&Mvl z&kMRJ$=tif!c8NylfHf>FjaCEUQ^HVKekahLY7*tCs6_{(J6pn)RYpYURKG`=anm{ zIn=}yQ;mbR0(BN(t2*G;?eIc_U%4RGNAIb%@w)2Aj<<}4J4R&>FDrmz>KeBtdFt|d z_N+;GfW)iHUdUmek8sp^o|L7vw=}=K;QajEh67y?09M@Yz%KG-xb_8k5pt?&+LpH@> zcT!2=p<$P?5mkF63ZbVnSpf_ny5gcjx|*ML^{}%^EhR=&qK&Ch8@QfL@$1_@bs$|_*T&V% z?$Aa{>_cx~4T;@g#-RE%(1YAqW=YHrpjS?z(h}~%WjOb~jQjphNXo{9`2R54xS?p% zC!$?;656qc_JEYyj8DUny6AS|ux}CUUI0yRLVSNuuvqmun6enc!~9epx`cM^(KYAK7da!xBOJ#&PIBI|prCjgm%j)tM)AU@)gvdpe!4^D)@| zAhdquPrhG5>&0cVEtr5Y--ZeEvdVO12IyD#INyH`=e`#p6jSCypr#tZpq@Gwy{@RE6r9)<#|#R1%ji+&Nz*bpHit2NPndx;8I`4L8F6*Oc4!zy;;8 zX@ou~jXmLQsG62h3i}7>r(^9??#F$5HRxqZC9^rCBZh`+HDh1#6-pkx2u_Y4gCI=B{SQpuG5=o_F9&t1P4 zd*YpWC)u=+6dyHV-FN|Icb}~!*c<@Ik}7rk{|sxdhgK7>sE4(w90}o^95MTb<~O<`6l1QcuZMHjyYKr81QLYvf;sWpq82POt0KD&SINx6Y zX1S7{d=AOhV9{1vNp$7Sm^D$=N}+LLs)ucS4L-UCTGv3Ct}0kfi2(c#XR_PA523?^ zW(#-n0<=RfDgwZJ|BCa2rwRioCdV*H*sXNTJdvRy`q&GL0PyjD;9NehV1S;$nL7KdR!V<+B^6gq%O7H3*xT2@ z`m3Ss4TuJ1`FUzdAc098XNQA3@edf|zotqRBsHJ26A40?w>Dv)@=MUd1#h~_o^Y4$ z&=4^hvHWIC3AFtcPS%zofLH$vY`F^b7G>-5t!(ctftH57-i2eH#W-@-fi-sNqMkl0 zRD%pcQzzEgpF(4+GSmfCk&7YMskb{YPo2!L*RJbvDA|NI0lZxg!nW_JLUXS^q`=MK z0F3k@u8~-2U`$(xF{u!M(x{(MvuDo5eeqr8iSu5pSfJih=5l&1Yq#(nyKu>a+e{6m?P zZb~05O=GX_K*VIYcxB1t^ry-Jyta2??KIGXbX2ZhA~RTfTP2CL2H|t(`ZlotvWfmz zBA&Wx(2>BJa3}282U@77Tr_ik(w-p@_ZvE$c018CkE3Ssclwb+iSy(q4j)Z{uC;z0 zNRiYwReif4v3ex-{!YR(ivci23Z4IW73YT%&Qum$fFf$#1~eNwuH8=U!tYW#drEqU zf!*7ECv3k}ISl!%*oV{^eq2Ig^C6JfPiV%Y7!xiiP=%$F-s}VI39KXUDvA_(Y7Fd^ z{Gyx0JhXxgWM(KcLcQZz&_WWf+7s z+|CBVMFp4w(VqS=?viC-lv7Btpr~=Pcx0&;)NHh9gyCy%#TZ_xB;H%*`OmAb`vSDS zqcS--_+;(|N$_+5vRx}H z6x(X0^7^xh{c4u){rv|GJoOz5mc*f|oTR9S3&~YASWV?UbFm!$F>LLq2h3Tsa)ESK!CbP^Lw2MfAM~d2>7VizD&&b;Kjb2~R$Zy5%=0Z!4SJ z@eQzgLa$eKdIiJd(%}^^IuNK3Bjnkp=uRcWYf!F>i2H^ZnID1z))|MDfmx&hYXJ^p^*hqNB zT#Sjci?9;OX0n5~Lr6gX-ZonAT8#7jN+QH5A%ba`{)=*gb*-u>Yg7w>?oB$a#Kev_ zQGL%hl_T+z8J?R4y9qX)3(aq;&$@TY(v+Y|vO#Q{qv14^Vr{F!ZY)JJB*ic1r^}uB z#6^2L_EU1%`FxcmbA%9x5e#@)%-w&mw%%VQC%_>K-K|KZfST(Ed-bJ_frB*ODbz2&W~C z(^`Tnsw$p5uytvr?vsPlM;?!P+`mFds%%s4*$0Q>0}O8D)su-TrR)L6Ev(J^ur}?- z-r0=UO)YF-jHtkzG?LKxI(2oQ&sX&-*T{8p2jKjH1K6$F^419d6-SvWwSvq~#_djV z0)ZDz^R!MOt35BVn|+bxzynO3^#G77p7}s=5RYB(KJfrZSf(ZqF2GE^`9JvmAix4A sZ4lrfzyb*rc=@0N4gxHYK!KP4FRe=?m{iQtPx}+DSw~RCr$PoC%m*Rh7qoZ>g&8uHL)T*;oQ0EG8iYF)Wcq7KK3u!4YwQA%NR3 zh{~44u!F$BpyQ07Fk%pa0cFt)hzbmZ2pMGykdT-S>Fk}Px2j&M>MeffzW2H-TlcH3 zSIzi+efh%oB~^9bd%t(?|DJR1xmR;t*JY%Ygn%{zwrmXJURyZ)QDTyfCt0laiw+#wCL5&|5k>*0m#g6n|rP!BCnFD?Jyb?>vm znm{!|d{6obUVuK#poayn02P3mF&HwRwe5o+1m30$EZh$7cEaW?WE`k8U`zrINWzpV zI4B7fdDj)t(yf3lbt@@c1y!unx6qz8*z^W$d=pw%LHBmBvk224B!l1)Bj z0g(qmEM9_j8@PTY{P+WC&+G<~TvvW)z&_EB>SEsahXZm+;HqqefbpSj zd;^v}4u4q)sg2;+pc~#Ffgs;^EKf43#=v10!Hmni-^*tK;6MtlTM4%=1reXBjLeN z^HBa|B#<=El6%`=;;Hc2dm&!wf6a$zxp3gi(A?!&X5WM+eVIVD_wPaL11A=r1-c>b z!nsW_cM=?2>3!32qnC>4;L4=rTWfkVymlA7{}ebmNK|{^9AA3})H)3e6VhAYfYad2 z2m19_`bb+aX(4Rx@hv;(OyJf42_d!nE6DE+9W1ftE=byN<=!y&Kq!-ccfpKMiz9(6 z!#R+E{sya-lwfNpvgr$&HSt6VQc zQxlq4z1&v{rkBC}Q{af&XaN@uHvns8U0%8l-hD_TmISlR)6pR}vGg2NG{QeU5AiCn z958h_=WTeR8H~6GL&24aN-Vd4rsg~YtX?R~z`X~;MSDjFxG1;**zPuq1z&Snzc8j? zC|@>$6ufA6wiC{N1ok}+au&o)n7#spZiQ3sglQLmZFzdUH46v444pYo%hijN z$*gwhS5{Umx|@c1d%>+f;9@j*NN@vJE0?u-{Jd^j7FSZ30%z$$WGB}R-`fwqy-#rxF(kO2pX&1KZ+G(NM=Pmn zGO#1G*no>zt^^)`cMlwLmN%)nx(D~2PB?s#H(~ZU2co&6y+ufpvS1CYEIdCMPOkH$ zCv2#l|4s{r8w3Ea|GbkIuZmMMs!R>nk~H7%J@;2Ida9@8;*|ssd)8a}AS-Q~Y8+RldG z*HKYxsG(Yl7U=DaZ@_W$`u*JCT7mWMpuIAtL;AX5?pdTjM&a^kHT`T2_c>jl<(_Xlv*yI{t^63tCU zBMovb1vm$f9SmP=D9n2Z1=q1%bVEa|_Q?yg)YsOR%xcwym6g!h<-F&hqTUPk`x5JU zbVXVv{FOhScn#iZ1)~h?0wcA73`C-88MV`@;LR!CH^Z)Ogn}E)(vQsA!p0YBN!BDv zqqQ>ZbaB62&V10}fKMBHwA!7ucX1oceAWBCQv+_1E?GWsnLGvXOOxS*0LUA?eb+%3#$rFFhHCz@Tu0m4Nx@ZlvY4Dyk~0a8#Zz z8n|MaP8PbmGJN8j7N443f$M1A>O%pS2qw2zTH*LVfGLqxBANt}(pf*Scw;JjJQ>a{ z8xU>)Hr--#&!^WD?;cIuD2pcJCHiVWS9=b#p_`L#GMW0hp@9udEs%$+m&3oU1+&RZ zQ%2E(pyI)U;KIgm(l8+0z_(rVN)Hd6zlny*@fc2`r~pVAq-39*EbZ-`OgPcu^xJD_ z*w>pR!)WZDU0*@g5=o9KgMK0$evf3b?-BE`FesXXb5XbmKS-s~k6! z;`Ad3Mz@QDZT65#r*Mp3j{9ac$6sEDhE_D#oxO*5I$*||U{`vsTCovv0C)C2a7W05 zdML|%?#5RBa9krc>FW<_NCsQPA<0S2|=oq_LLjK(2h=r2H zLV)&ynI)6<0@kB0s^O$->WGz#wR*ux)Ga7;5+I*>2Y#RSqqic=w@-5qgbPPSAPa$y z`-^{WVd;V{%4#(Fd*#DD~ zy4p2Y5H;Yyq1l>&8B3s9*572kY>0pZ=Vj@ka7cN$rT9>?@YFZA^7a#HDr$9QZStRh z_27Dc7r;T&bsbI9=&mn^m( zSp%<4_VhbU0w)yQ=WlCe;eU3Jtd|tyo?pEHRf~#U4ze0N>4b7lo>RxTsgjH>y|wP{ zWd#23vMxAt1+2+{X?lKYSR;;Ukjud{li~E*a6Yc`jR)Gz6Szhq36Fohg^GH8x69?? zT8Y!2>v75UxQv|~=eWyinEE-doJI9BOx8LO;4N9WWDPvq>eV^OdYMZ6>|uzA6bRUC z7A_v=g}#L>`Gg80x?632c+v*4U9MWr4pt)~E-hZ}>T(3*A(O}%vIQz{kS27T^k6Hr4 zJaqoh?sZ^s3SLda%3es@P;0>SB%DzP6C^+3cUq{^7z*ydH|#3^rq5@U^8UIzOWpYZH9CP>XY!X(P&5Qhc?MC4Oik@ zg2LBGiF;7d{Ne=ZJN+KCy$tEMA=d`6(J=l(s6N`a=x~6;!wtaZ9GvU_2j?Cs6Yt#V zF*Qh3pdC9A{cFdf{lj$cb4g1{A=dx5mcR08r(pBru<1eQSnMSu@)IOd4td0jP=!lQw!pwuv?>GznlM_7)RxxCu(?SW} zh=S@n-U4;~{jmOrkp7D|>XpwGShC!=30WDPGvJ8->#y=2tWr!p7ScQ=2g4o`A>PzmG9sxX|Uj+ zTVcYy2;lk?k^9V2>@)rYjEbSJnZyj-TnE`Rj-q10SJ7ldh-H^jg7d9)>r=4uN>5iM zVW27qCF$X7tQfDTLXZQI`x53-8Y@#s0QZ2oF!8ns;0BT~``0*kEdaycm{5Sx=6_X- z8CbavtQ*fEdBdswn;eGN-=2S#=U(Nu!K$la{e%9-KNEDR|MzQo1?oVKj6jwxR%zdI zCw>PG_-+Jneap4anUDL+*T75`w_c)Fd^Ni4EU`t`P&RYF=-ssZtwr0Ph2@ud)>>wG z8_;5s)XiJA52J7bSm35D-2JXaJLr}O;fj~=>>FUnf_;%osk)BqWayYUhWaIQya_HU z7cH1%oL*Qp7n=VaB<)ddf@$rT`YL3!$o=)2&}QN6eIxpm8zO{z&QEcFxwv@C^&dz# z4N_Jo*2O2&@W7WMkcYfUtMe`R^H-qjEvS+jf93y$&y4R5x{?MHcdLan_EwC;rC?ZI zR$f$Bu(uFs?iEkteE(T6D~pqb{E67q*irKl{)kMECGrftxdM*4sE^@4!52c7$+$_9vWK_eTm>*EQO(=xMBC+MrOUrYY=Hr9+Z?zrD(YC5|Up%X4l{%2yNG# z25rCdsyd=}xpuNzdu@{Jx>0B`4Yzwcv6=HCwp`^k`1)%**hk!n``(AqRTlejb7HM- z4s%RBqgPyuUgecRd8-$`9Aul}{R6?3W0~@b42oB%9ea@X)^3npHWt^Gt?~wL_qGzj zRod%q-*#Vb#yTM@m9L;m8lLs=;VjttGp|%JYPbSXVv+UbWLGtTp6A{T8(g(6CN8i` z>a6=oJo6{wu^OBqTChI~)ug5#N2JHe5TCX`qu;#RC+=Hr^H-qdC(*+Vz-~#9T|U~^ z`Hupwn&7+(J2{8W^ByGj#H*AW)!rVpA-HJL+t&Oe!8A`EM!zwK_{aD5HUvwF<(503 zd2V#F5b%@3h&GjzTQSNTKiW<~$*@^&pmY8LrfkLUxaw)#yMIF!adb0|qfSN(-*$m( zN-c1Ty6=9Gs_%RnH^4FnKR6Al^@(+6V~&>pGn>J9PUdiD95IZGUdZ6Dcebx zWpPz1>1B}jt+dNhywgqDrw(P*^Z%09w;xROyaTJxfQ+0VBpR7tEu)6ij+=+Y>iykC zPHQ<oPR0FSx4ep4w{6!>U007)7%DFcLlWn!h18p z5_#~3RB4spUk04kIM$|0oDOfhmL~3!ohHSd(Z=S^&4K-9>eJWM6Pu@fM zQ(moX*Xz*!By{}|to7jZC`)&9GdP=h+!;s2VnjrDkpyi!zyH%Q0d_YHyRv9Q%G5V z=(=9Nh;9g_^`Qsm&S!*&_~G!u&A%BH;kkX@!w%R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8CFBundleInfoDictionaryVersion 6.0 CFBundleName - appflowy_flutter + AppFlowy CFBundlePackageType APPL CFBundleShortVersionString diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart new file mode 100644 index 000000000000..c32463c7d8b6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -0,0 +1,64 @@ +import 'package:appflowy/mobile/presentation/database/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_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +extension MobileRouter on BuildContext { + Future pushView(ViewPB view) async { + push( + Uri( + path: view.routeName, + queryParameters: view.queryParameters, + ).toString(), + ); + } +} + +extension on ViewPB { + String get routeName { + switch (layout) { + case ViewLayoutPB.Document: + return MobileEditorScreen.routeName; + case ViewLayoutPB.Grid: + return MobileGridScreen.routeName; + case ViewLayoutPB.Calendar: + return MobileCalendarScreen.routeName; + case ViewLayoutPB.Board: + return MobileBoardScreen.routeName; + default: + throw UnimplementedError('routeName for $this is not implemented'); + } + } + + Map get queryParameters { + switch (layout) { + case ViewLayoutPB.Document: + return { + MobileEditorScreen.viewId: id, + MobileEditorScreen.viewTitle: name, + }; + case ViewLayoutPB.Grid: + return { + MobileGridScreen.viewId: id, + MobileGridScreen.viewTitle: name, + }; + case ViewLayoutPB.Calendar: + return { + MobileCalendarScreen.viewId: id, + MobileCalendarScreen.viewTitle: name, + }; + case ViewLayoutPB.Board: + return { + MobileBoardScreen.viewId: id, + MobileBoardScreen.viewTitle: name, + }; + default: + throw UnimplementedError( + 'queryParameters for $this is not implemented', + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart index 8117a144ff15..5f14f8605525 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart @@ -1,7 +1,9 @@ // ThemeData in mobile import 'package:flutter/material.dart'; -ThemeData getMobileThemeData() { +ThemeData getMobileThemeData( + Theme theme, +) { const mobileColorTheme = ColorScheme( brightness: Brightness.light, primary: Color(0xFF2DA2F6), //primary 100 @@ -106,7 +108,7 @@ ThemeData getMobileThemeData() { ), // body2 14 Regular bodyMedium: TextStyle( - color: Color(0xFFC5C7CB), + color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1.20, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart new file mode 100644 index 000000000000..04f0953a6dfa --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -0,0 +1,83 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/error/error_page.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:dartz/dartz.dart' hide State; +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 MobileViewPage extends StatefulWidget { + const MobileViewPage({ + super.key, + required this.id, + this.title, + required this.viewLayout, + }); + + /// view id + final String id; + final String? title; + final ViewLayoutPB viewLayout; + + @override + State createState() => _MobileViewPageState(); +} + +class _MobileViewPageState extends State { + late final Future> future; + + @override + void initState() { + super.initState(); + + future = ViewBackendService.getView(widget.id); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: future, + builder: (context, state) { + Widget body; + String? title; + if (state.connectionState != ConnectionState.done) { + body = const Center( + child: CircularProgressIndicator(), + ); + } else if (!state.hasData) { + body = MobileErrorPage( + message: LocaleKeys.error_loadingViewError.tr(), + ); + } else { + body = state.data!.fold((view) { + title = view.name; + return view.plugin().widgetBuilder.buildWidget(shrinkWrap: false); + }, (error) { + return MobileErrorPage( + message: error.toString(), + ); + }); + } + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: FlowyText( + title ?? widget.title ?? '', + fontSize: 14.0, + ), + leading: BackButton( + onPressed: () => context.pop(), + ), + ), + body: SafeArea( + child: body, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart new file mode 100644 index 000000000000..7a402eabe240 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/mobile_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'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +enum MobilePaneActionType { + delete, + addToFavorites, + removeFromFavorites, + more; + + MobileSlideActionButton actionButton( + BuildContext context, + ) { + switch (this) { + case MobilePaneActionType.delete: + return MobileSlideActionButton( + backgroundColor: Colors.red, + svg: FlowySvgs.delete_s, + size: 30.0, + onPressed: (context) => + context.read().add(const ViewEvent.delete()), + ); + case MobilePaneActionType.removeFromFavorites: + return MobileSlideActionButton( + backgroundColor: Colors.red, + svg: FlowySvgs.favorite_s, + onPressed: (context) => context + .read() + .add(FavoriteEvent.toggle(context.read().view)), + ); + case MobilePaneActionType.addToFavorites: + return MobileSlideActionButton( + backgroundColor: Colors.orange, + svg: FlowySvgs.m_favorite_unselected_lg, + size: 34.0, + onPressed: (context) => context + .read() + .add(FavoriteEvent.toggle(context.read().view)), + ); + case MobilePaneActionType.more: + return MobileSlideActionButton( + backgroundColor: Colors.grey, + svg: FlowySvgs.three_dots_vertical_s, + size: 28.0, + onPressed: (context) { + final viewBloc = context.read(); + final favoriteBloc = context.read(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + builder: (context) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: viewBloc), + BlocProvider.value(value: favoriteBloc), + ], + child: BlocBuilder( + builder: (context, state) { + return MobileViewItemBottomSheet( + view: viewBloc.view, + ); + }, + ), + ); + }, + ); + }, + ); + } + } +} + +ActionPane buildEndActionPane( + BuildContext context, + List actions, +) { + return ActionPane( + motion: const ScrollMotion(), + extentRatio: actions.length / 5, + children: actions + .map( + (action) => action.actionButton(context), + ) + .toList(), + ); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet.dart new file mode 100644 index 000000000000..77b3865c079e --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet.dart @@ -0,0 +1,116 @@ +import 'package:appflowy/mobile/presentation/bottom_sheet/mobile_bottom_sheet_body.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/mobile_bottom_sheet_header.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/mobile_bottom_sheet_rename_widget.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'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +enum MobileBottomSheetType { + view, + rename, +} + +class MobileViewItemBottomSheet extends StatefulWidget { + const MobileViewItemBottomSheet({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State createState() => + _MobileViewItemBottomSheetState(); +} + +class _MobileViewItemBottomSheetState extends State { + MobileBottomSheetType type = MobileBottomSheetType.view; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // drag handler + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 12.0), + child: Container( + width: 64, + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + color: Colors.grey, + ), + ), + ), + + // header + MobileBottomSheetHeader( + showBackButton: type != MobileBottomSheetType.view, + view: widget.view, + onBack: () { + setState(() { + type = MobileBottomSheetType.view; + }); + }, + ), + const VSpace(8.0), + const Divider(), + + // body + _buildBody(), + const VSpace(12.0), + ], + ); + } + + 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.read().add(const ViewEvent.duplicate()); + Navigator.pop(context); + break; + case MobileViewItemBottomSheetBodyAction.share: + // unimplemented + Navigator.pop(context); + break; + case MobileViewItemBottomSheetBodyAction.delete: + context.read().add(const ViewEvent.delete()); + Navigator.pop(context); + break; + case MobileViewItemBottomSheetBodyAction.addToFavorites: + case MobileViewItemBottomSheetBodyAction.removeFromFavorites: + context + .read() + .add(FavoriteEvent.toggle(widget.view)); + Navigator.pop(context); + break; + } + }, + ); + case MobileBottomSheetType.rename: + return MobileBottomSheetRenameWidget( + name: widget.view.name, + onRename: (name) { + if (name != widget.view.name) { + context.read().add(ViewEvent.rename(name)); + } + Navigator.pop(context); + }, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_action_widget.dart new file mode 100644 index 000000000000..4dd4aecbe5a6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_action_widget.dart @@ -0,0 +1,53 @@ +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, + required this.text, + required this.onTap, + }); + + final FlowySvgData svg; + final String text; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey, + ), + borderRadius: BorderRadius.circular(8.0), + ), + child: InkWell( + onTap: onTap, + enableFeedback: true, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowySvg( + svg, + size: const Size.square(22.0), + blendMode: BlendMode.dst, + ), + const HSpace(6.0), + FlowyText(text), + const Spacer(), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_body.dart new file mode 100644 index 000000000000..efba4ea031ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_body.dart @@ -0,0 +1,100 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/mobile_bottom_sheet_action_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +enum MobileViewItemBottomSheetBodyAction { + rename, + duplicate, + share, + delete, + addToFavorites, + removeFromFavorites, +} + +class MobileViewItemBottomSheetBody extends StatelessWidget { + const MobileViewItemBottomSheetBody({ + super.key, + this.isFavorite = false, + required this.onAction, + }); + + final bool isFavorite; + final void Function(MobileViewItemBottomSheetBodyAction action) onAction; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // rename, duplicate + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_rename_m, + text: LocaleKeys.button_rename.tr(), + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.rename, + ), + ), + ), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_duplicate_m, + text: LocaleKeys.button_duplicate.tr(), + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.duplicate, + ), + ), + ), + ], + ), + + // share, delete + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_share_m, + text: LocaleKeys.button_share.tr(), + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.share, + ), + ), + ), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_delete_m, + text: LocaleKeys.button_delete.tr(), + onTap: () => onAction( + MobileViewItemBottomSheetBodyAction.delete, + ), + ), + ), + ], + ), + + // remove from favorites + + BottomSheetActionWidget( + svg: isFavorite + ? FlowySvgs.m_favorite_selected_lg + : FlowySvgs.m_favorite_unselected_lg, + text: isFavorite + ? LocaleKeys.button_removeFromFavorites.tr() + : LocaleKeys.button_addToFavorites.tr(), + onTap: () => onAction( + isFavorite + ? MobileViewItemBottomSheetBodyAction.removeFromFavorites + : MobileViewItemBottomSheetBodyAction.addToFavorites, + ), + ) + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_header.dart new file mode 100644 index 000000000000..94cefdca9b33 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_header.dart @@ -0,0 +1,45 @@ +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class MobileBottomSheetHeader extends StatelessWidget { + const MobileBottomSheetHeader({ + super.key, + required this.view, + required this.showBackButton, + required this.onBack, + }); + + final ViewPB view; + final bool showBackButton; + final VoidCallback onBack; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // back button, + showBackButton + ? InkWell( + onTap: onBack, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 24.0, + ), + ), + ) + : const HSpace(40.0), + // title + FlowyText.regular( + view.name, + fontSize: 16.0, + ), + // placeholder, ensure the title is centered + const HSpace(40.0), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_rename_widget.dart new file mode 100644 index 000000000000..e4be17616e5e --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/mobile_bottom_sheet_rename_widget.dart @@ -0,0 +1,76 @@ +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'; + +class MobileBottomSheetRenameWidget extends StatefulWidget { + const MobileBottomSheetRenameWidget({ + super.key, + required this.name, + required this.onRename, + }); + + final String name; + final void Function(String name) onRename; + + @override + State createState() => + _MobileBottomSheetRenameWidgetState(); +} + +class _MobileBottomSheetRenameWidgetState + extends State { + late final TextEditingController controller; + + @override + void initState() { + super.initState(); + + controller = TextEditingController(text: widget.name); + } + + @override + void dispose() { + controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 16.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(8.0), + Expanded( + child: SizedBox( + height: 44.0, + child: FlowyTextField( + controller: controller, + ), + ), + ), + const HSpace(12.0), + FlowyTextButton( + LocaleKeys.button_edit.tr(), + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0, + ), + fontColor: Colors.white, + fillColor: Colors.lightBlue.shade300, + onPressed: () { + widget.onRename(controller.text); + }, + ), + const HSpace(8.0), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart new file mode 100644 index 000000000000..1537024e9c05 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_board_screen.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class MobileBoardScreen extends StatelessWidget { + static const routeName = '/board'; + static const viewId = 'id'; + static const viewTitle = 'title'; + + const MobileBoardScreen({ + super.key, + required this.id, + this.title, + }); + + /// view id + final String id; + final String? title; + + @override + Widget build(BuildContext context) { + return MobileViewPage( + id: id, + title: title, + viewLayout: ViewLayoutPB.Document, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart new file mode 100644 index 000000000000..4d72b4c8001f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class MobileCalendarScreen extends StatelessWidget { + static const routeName = '/calendar'; + static const viewId = 'id'; + static const viewTitle = 'title'; + + const MobileCalendarScreen({ + super.key, + required this.id, + this.title, + }); + + /// view id + final String id; + final String? title; + + @override + Widget build(BuildContext context) { + return MobileViewPage( + id: id, + title: title, + viewLayout: ViewLayoutPB.Document, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart new file mode 100644 index 000000000000..5045167c83d4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class MobileGridScreen extends StatelessWidget { + static const routeName = '/grid'; + static const viewId = 'id'; + static const viewTitle = 'title'; + + const MobileGridScreen({ + super.key, + required this.id, + this.title, + }); + + /// view id + final String id; + final String? title; + + @override + Widget build(BuildContext context) { + return MobileViewPage( + id: id, + title: title, + viewLayout: ViewLayoutPB.Document, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart new file mode 100644 index 000000000000..31bc880ae735 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class MobileEditorScreen extends StatelessWidget { + static const routeName = '/docs'; + static const viewId = 'id'; + static const viewTitle = 'title'; + + const MobileEditorScreen({ + super.key, + required this.id, + this.title, + }); + + /// view id + final String id; + final String? title; + + @override + Widget build(BuildContext context) { + return MobileViewPage( + id: id, + title: title, + viewLayout: ViewLayoutPB.Document, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart new file mode 100644 index 000000000000..09ef2cf62709 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart @@ -0,0 +1,49 @@ +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'; + +class MobileErrorPage extends StatelessWidget { + const MobileErrorPage({ + super.key, + this.header, + this.title, + required this.message, + }); + + final Widget? header; + final String? title; + final String message; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + header != null + ? header! + : const FlowyText.semibold( + '😔', + fontSize: 50, + ), + const VSpace(14.0), + FlowyText.semibold( + title ?? LocaleKeys.error_weAreSorry.tr(), + fontSize: 32, + textAlign: TextAlign.center, + ), + const VSpace(4.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: FlowyText.regular( + message, + fontSize: 16, + maxLines: 100, + color: Colors.grey, // FIXME: use theme color + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart new file mode 100644 index 000000000000..2b194b5cf6cc --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -0,0 +1,71 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class MobileFavoritePageFolder extends StatelessWidget { + const MobileFavoritePageFolder({ + super.key, + required this.userProfile, + required this.workspaceSetting, + }); + + final UserProfilePB userProfile; + final WorkspaceSettingPB workspaceSetting; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => MenuBloc( + user: userProfile, + workspace: workspaceSetting.workspace, + )..add(const MenuEvent.initial()), + ), + BlocProvider( + create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), + ) + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedView?.id != c.lastCreatedView?.id, + listener: (context, state) => + context.pushView(state.lastCreatedView!), + ), + ], + child: Builder( + builder: (context) { + final favoriteState = context.watch().state; + if (favoriteState.views.isEmpty) { + return const Center( + // todo: i18n + child: FlowyText('No favorite pages'), + ); + } + return SlidableAutoCloseBehavior( + child: Column( + children: [ + MobileFavoriteFolder( + showHeader: false, + forceExpanded: true, + views: favoriteState.views, + ), + const VSpace(100.0), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart new file mode 100644 index 000000000000..ad38dd87fc81 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -0,0 +1,100 @@ +import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dart'; +import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter/material.dart'; + +class MobileFavoriteScreen extends StatelessWidget { + const MobileFavoriteScreen({ + super.key, + }); + + static const routeName = '/favorite'; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Future.wait([ + FolderEventGetCurrentWorkspace().send(), + getIt().getUser(), + ]), + builder: (context, snapshots) { + if (!snapshots.hasData) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) { + return workspaceSettingPB as WorkspaceSettingPB?; + }, + (error) => null, + ); + final userProfile = + snapshots.data?[1].fold((error) => null, (userProfilePB) { + return userProfilePB as UserProfilePB?; + }); + + // In the unlikely case either of the above is null, eg. + // when a workspace is already open this can happen. + if (workspaceSetting == null || userProfile == null) { + return const WorkspaceFailedScreen(); + } + + return Scaffold( + body: SafeArea( + child: MobileFavoritePage( + userProfile: userProfile, + workspaceSetting: workspaceSetting, + ), + ), + ); + }, + ); + } +} + +class MobileFavoritePage extends StatelessWidget { + const MobileFavoritePage({ + super.key, + required this.userProfile, + required this.workspaceSetting, + }); + + final UserProfilePB userProfile; + final WorkspaceSettingPB workspaceSetting; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: MobileHomePageHeader( + userProfile: userProfile, + ), + ), + const Divider(), + + // Folder + Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: MobileFavoritePageFolder( + userProfile: userProfile, + workspaceSetting: workspaceSetting, + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart new file mode 100644 index 000000000000..b6765ad8ecd3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; +import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileFavoriteFolder extends StatelessWidget { + const MobileFavoriteFolder({ + super.key, + required this.views, + this.showHeader = true, + this.forceExpanded = false, + }); + + final bool showHeader; + final bool forceExpanded; + final List views; + + @override + Widget build(BuildContext context) { + if (views.isEmpty) { + return const SizedBox.shrink(); + } + + return BlocProvider( + create: (context) => FolderBloc(type: FolderCategoryType.favorite) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + if (showHeader) ...[ + MobileFavoriteFolderHeader( + isExpanded: context.read().state.isExpanded, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () => context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ), + ), + const VSpace(8.0), + const Divider( + height: 1, + ), + ], + if (forceExpanded || state.isExpanded) + ...views.map( + (view) => MobileViewItem( + key: ValueKey( + '${FolderCategoryType.favorite.name} ${view.id}', + ), + categoryType: FolderCategoryType.favorite, + isDraggable: false, + isFirstChild: view.id == views.first.id, + isFeedback: false, + view: view, + level: 0, + onSelected: (view) async { + await context.pushView(view); + }, + endActionPane: (context) => buildEndActionPane(context, [ + MobilePaneActionType.removeFromFavorites, + MobilePaneActionType.more, + ]), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart new file mode 100644 index 000000000000..997f6043ac63 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart @@ -0,0 +1,60 @@ +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'; + +class MobileFavoriteFolderHeader extends StatefulWidget { + const MobileFavoriteFolderHeader({ + super.key, + required this.onPressed, + required this.onAdded, + required this.isExpanded, + }); + + final VoidCallback onPressed; + final VoidCallback onAdded; + final bool isExpanded; + + @override + State createState() => + _MobileFavoriteFolderHeaderState(); +} + +class _MobileFavoriteFolderHeaderState + extends State { + double _turns = 0; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: FlowyButton( + text: FlowyText.semibold( + LocaleKeys.sideBar_favorites.tr(), + fontSize: 20.0, + ), + margin: const EdgeInsets.symmetric(vertical: 8), + expandText: false, + mainAxisAlignment: MainAxisAlignment.start, + rightIcon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _turns, + child: const Icon( + Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + ), + ), + onTap: () { + setState(() { + _turns = widget.isExpanded ? -0.25 : 0; + }); + widget.onPressed(); + }, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart new file mode 100644 index 000000000000..606206568278 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -0,0 +1,74 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; +import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class MobileFolders extends StatelessWidget { + const MobileFolders({ + super.key, + required this.user, + required this.workspaceSetting, + required this.showFavorite, + }); + + final UserProfilePB user; + final WorkspaceSettingPB workspaceSetting; + final bool showFavorite; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => MenuBloc( + user: user, + workspace: workspaceSetting.workspace, + )..add(const MenuEvent.initial()), + ), + BlocProvider( + create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), + ) + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedView?.id != c.lastCreatedView?.id, + listener: (context, state) => + context.pushView(state.lastCreatedView!), + ), + ], + child: Builder( + builder: (context) { + final menuState = context.watch().state; + final favoriteState = context.watch().state; + return SlidableAutoCloseBehavior( + child: Column( + children: [ + // TODO: Uncomment this when we have favorite folder in home page + if (showFavorite && favoriteState.views.isNotEmpty) ...[ + MobileFavoriteFolder( + views: favoriteState.views, + ), + const VSpace(18.0), + ], + MobilePersonalFolder( + views: menuState.views, + ), + const VSpace(8.0), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index e54c0dfe1f12..8c244a8ecdc9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -1,12 +1,14 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; +import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; +import 'package:appflowy/mobile/presentation/home/mobile_home_page_recent_files.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; -// TODO(yijing): This is just a placeholder for now. class MobileHomeScreen extends StatelessWidget { const MobileHomeScreen({super.key}); @@ -34,47 +36,80 @@ class MobileHomeScreen extends StatelessWidget { snapshots.data?[1].fold((error) => null, (userProfilePB) { return userProfilePB as UserProfilePB?; }); - // TODO(yijing): implement home page later + + // In the unlikely case either of the above is null, eg. + // when a workspace is already open this can happen. + if (workspaceSetting == null || userProfile == null) { + return const WorkspaceFailedScreen(); + } + return Scaffold( - key: ValueKey(userProfile?.id), - // TODO(yijing):Need to change to workspace when it is ready - appBar: AppBar( - title: Text( - userProfile?.email.toString() ?? 'No email found', + body: SafeArea( + child: MobileHomePage( + userProfile: userProfile, + workspaceSetting: workspaceSetting, ), - actions: [ - IconButton( - onPressed: () { - // TODO(yijing): Navigate to setting page - }, - icon: const FlowySvg( - FlowySvgs.m_setting_m, - ), - ) - ], ), - body: Center( - child: Column( - children: [ - const Text( - 'User', - ), - Text( - userProfile.toString(), + ); + }, + ); + } +} + +class MobileHomePage extends StatelessWidget { + const MobileHomePage({ + super.key, + required this.userProfile, + required this.workspaceSetting, + }); + + final UserProfilePB userProfile; + final WorkspaceSettingPB workspaceSetting; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // TODO: header + option icon button + // Header + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: MobileHomePageHeader( + userProfile: userProfile, + ), + ), + const Divider(), + + // Folder + Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + // Recent files + const MobileHomePageRecentFilesWidget(), + const Divider(), + + // Folders + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: MobileFolders( + showFavorite: false, + user: userProfile, + workspaceSetting: workspaceSetting, + ), + ), + ], ), - Text('Workspace name: ${workspaceSetting?.workspace.name}'), - ElevatedButton( - onPressed: () async { - await getIt().signOut(); - runAppFlowy(); - }, - child: const Text('Logout'), - ) - ], + ), ), ), - ); - }, + ), + + // TODO: Trash + ], ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart new file mode 100644 index 000000000000..dadd6ae2b5a9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -0,0 +1,50 @@ +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class MobileHomePageHeader extends StatelessWidget { + const MobileHomePageHeader({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + // TODO: implement the details later. + return SizedBox( + height: 80, + child: Row( + children: [ + const FlowyText( + '🐻', + fontSize: 26, + ), + const HSpace(14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowyText.medium( + 'AppFlowy', + fontSize: 18, + ), + const VSpace(4.0), + FlowyText.regular( + userProfile.email, + fontSize: 12, + color: Colors.grey, + ) + ], + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.more_horiz_outlined), + onPressed: () {}, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart new file mode 100644 index 000000000000..427b5a98e4bd --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart @@ -0,0 +1,59 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +const rainbowColors = [ + Color(0xFFFF0064), + Color(0xFFFF7600), + Color(0xFFFFD500), + Color(0xFF8CFE00), + Color(0xFF00E86C), + Color(0xFF00F4F2), + Color(0xFF00CCFF), + Color(0xFF70A2FF), + Color(0xFFA96CFF), +]; + +class MobileHomePageRecentFilesWidget extends StatelessWidget { + const MobileHomePageRecentFilesWidget({super.key}); + + @override + Widget build(BuildContext context) { + // TODO: implement the details later. + return SizedBox( + height: 160, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: FlowyText.semibold( + 'Recent', + fontSize: 20.0, + ), + ), + Expanded( + child: ListView.separated( + separatorBuilder: (context, index) => const HSpace(20), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + scrollDirection: Axis.horizontal, + itemCount: rainbowColors.length, + itemBuilder: (context, index) { + return Container( + alignment: Alignment.center, + width: 144, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + color: rainbowColors[index], + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_ui.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_ui.dart new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_ui.dart @@ -0,0 +1 @@ + diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart new file mode 100644 index 000000000000..a43559f4924a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart @@ -0,0 +1,72 @@ +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; +import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobilePersonalFolder extends StatelessWidget { + const MobilePersonalFolder({ + super.key, + required this.views, + }); + + final List views; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FolderBloc(type: FolderCategoryType.personal) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + MobilePersonalFolderHeader( + isExpanded: context.read().state.isExpanded, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () => context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ), + ), + const VSpace(8.0), + const Divider( + height: 1, + ), + if (state.isExpanded) + ...views.map( + (view) => MobileViewItem( + key: ValueKey( + '${FolderCategoryType.personal.name} ${view.id}', + ), + isDraggable: false, + categoryType: FolderCategoryType.personal, + isFirstChild: view.id == views.first.id, + view: view, + level: 0, + leftPadding: 16, + isFeedback: false, + onSelected: (view) async { + await context.pushView(view); + }, + endActionPane: (context) => buildEndActionPane(context, [ + MobilePaneActionType.delete, + MobilePaneActionType.addToFavorites, + MobilePaneActionType.more, + ]), + ), + ) + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart new file mode 100644 index 000000000000..c8d4448fd3b1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.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'; + +class MobilePersonalFolderHeader extends StatefulWidget { + const MobilePersonalFolderHeader({ + super.key, + required this.onPressed, + required this.onAdded, + required this.isExpanded, + }); + + final VoidCallback onPressed; + final VoidCallback onAdded; + final bool isExpanded; + + @override + State createState() => + _MobilePersonalFolderHeaderState(); +} + +class _MobilePersonalFolderHeaderState + extends State { + double _turns = 0; + + @override + Widget build(BuildContext context) { + const iconSize = 32.0; + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: FlowyButton( + text: FlowyText.semibold( + LocaleKeys.sideBar_personal.tr(), + fontSize: 20.0, + ), + margin: const EdgeInsets.symmetric(vertical: 8), + expandText: false, + mainAxisAlignment: MainAxisAlignment.start, + rightIcon: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: _turns, + child: const Icon( + Icons.keyboard_arrow_down_rounded, + color: Colors.grey, + ), + ), + onTap: () { + setState(() { + _turns = widget.isExpanded ? -0.25 : 0; + }); + widget.onPressed(); + }, + ), + ), + FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + iconPadding: const EdgeInsets.all(2), + height: iconSize, + width: iconSize, + icon: const FlowySvg( + FlowySvgs.add_s, + size: Size.square(iconSize), + ), + onPressed: () { + context.read().add( + MenuEvent.createApp( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + index: 0, + ), + ); + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart new file mode 100644 index 000000000000..5aebb53100cb --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart @@ -0,0 +1,35 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class MobileSlideActionButton extends StatelessWidget { + const MobileSlideActionButton({ + super.key, + required this.svg, + this.size = 32.0, + this.backgroundColor = Colors.transparent, + required this.onPressed, + }); + + final FlowySvgData svg; + final double size; + final Color backgroundColor; + final SlidableActionCallback onPressed; + + @override + Widget build(BuildContext context) { + return CustomSlidableAction( + backgroundColor: backgroundColor, + onPressed: (context) { + HapticFeedback.mediumImpact(); + onPressed(context); + }, + child: FlowySvg( + svg, + size: Size.square(size), + color: Colors.white, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart new file mode 100644 index 000000000000..6ca7fbf68d1f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -0,0 +1,417 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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:flutter_slidable/flutter_slidable.dart'; + +typedef ViewItemOnSelected = void Function(ViewPB); +typedef ActionPaneBuilder = ActionPane Function(BuildContext context); + +const _itemHeight = 48.0; + +class MobileViewItem extends StatelessWidget { + const MobileViewItem({ + super.key, + required this.view, + this.parentView, + required this.categoryType, + required this.level, + this.leftPadding = 10, + required this.onSelected, + this.isFirstChild = false, + this.isDraggable = true, + required this.isFeedback, + this.startActionPane, + this.endActionPane, + }); + + final ViewPB view; + final ViewPB? parentView; + + final FolderCategoryType categoryType; + + // indicate the level of the view item + // used to calculate the left padding + final int level; + + // the left padding of the view item for each level + // the left padding of the each level = level * leftPadding + final double leftPadding; + + // Selected by normal conventions + final ViewItemOnSelected onSelected; + + // used for indicating the first child of the parent view, so that we can + // add top border to the first child + final bool isFirstChild; + + // it should be false when it's rendered as feedback widget inside DraggableItem + final bool isDraggable; + + // identify if the view item is rendered as feedback widget inside DraggableItem + final bool isFeedback; + + // the actions of the view item, such as favorite, rename, delete, etc. + final ActionPaneBuilder? startActionPane; + final ActionPaneBuilder? endActionPane; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), + child: BlocConsumer( + listenWhen: (p, c) => + c.lastCreatedView != null && + p.lastCreatedView?.id != c.lastCreatedView!.id, + listener: (context, state) => context.pushView(state.lastCreatedView!), + builder: (context, state) { + // don't remove this code. it's related to the backend service. + view.childViews + ..clear() + ..addAll(state.childViews); + return InnerMobileViewItem( + view: state.view, + parentView: parentView, + childViews: state.childViews, + categoryType: categoryType, + level: level, + leftPadding: leftPadding, + showActions: true, + isExpanded: state.isExpanded, + onSelected: onSelected, + isFirstChild: isFirstChild, + isDraggable: isDraggable, + isFeedback: isFeedback, + startActionPane: startActionPane, + endActionPane: endActionPane, + ); + }, + ), + ); + } +} + +class InnerMobileViewItem extends StatelessWidget { + const InnerMobileViewItem({ + super.key, + required this.view, + required this.parentView, + required this.childViews, + required this.categoryType, + this.isDraggable = true, + this.isExpanded = true, + required this.level, + required this.leftPadding, + required this.showActions, + required this.onSelected, + this.isFirstChild = false, + required this.isFeedback, + this.startActionPane, + this.endActionPane, + }); + + final ViewPB view; + final ViewPB? parentView; + final List childViews; + final FolderCategoryType categoryType; + + final bool isDraggable; + final bool isExpanded; + final bool isFirstChild; + // identify if the view item is rendered as feedback widget inside DraggableItem + final bool isFeedback; + + final int level; + final double leftPadding; + + final bool showActions; + final ViewItemOnSelected onSelected; + + final ActionPaneBuilder? startActionPane; + final ActionPaneBuilder? endActionPane; + + @override + Widget build(BuildContext context) { + Widget child = SingleMobileInnerViewItem( + key: ValueKey('${categoryType.name} ${view.id} $isExpanded'), + view: view, + parentView: parentView, + level: level, + showActions: showActions, + categoryType: categoryType, + onSelected: onSelected, + isExpanded: isExpanded, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + startActionPane: startActionPane, + endActionPane: endActionPane, + ); + + // if the view is expanded and has child views, render its child views + if (isExpanded) { + if (childViews.isNotEmpty) { + final children = childViews.map((childView) { + return MobileViewItem( + key: ValueKey('${categoryType.name} ${childView.id}'), + parentView: view, + categoryType: categoryType, + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + startActionPane: startActionPane, + endActionPane: endActionPane, + ); + }).toList(); + + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + const Divider( + height: 1, + ), + ...children, + ], + ); + } else { + child = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const Divider( + height: 1, + ), + Container( + height: _itemHeight, + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(left: (level + 2) * leftPadding), + child: FlowyText.medium( + LocaleKeys.noPagesInside.tr(), + color: Colors.grey, + ), + ), + ), + const Divider( + height: 1, + ), + ], + ); + } + } else { + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + const Divider( + height: 1, + ), + ], + ); + } + + // wrap the child with DraggableItem if isDraggable is true + if (isDraggable && !isReferencedDatabaseView(view, parentView)) { + child = DraggableViewItem( + isFirstChild: isFirstChild, + view: view, + child: child, + feedback: (context) { + return MobileViewItem( + view: view, + parentView: parentView, + categoryType: categoryType, + level: level, + onSelected: onSelected, + isDraggable: false, + leftPadding: leftPadding, + isFeedback: true, + startActionPane: startActionPane, + endActionPane: endActionPane, + ); + }, + ); + } + + return child; + } +} + +class SingleMobileInnerViewItem extends StatefulWidget { + const SingleMobileInnerViewItem({ + super.key, + required this.view, + required this.parentView, + required this.isExpanded, + required this.level, + required this.leftPadding, + this.isDraggable = true, + required this.categoryType, + required this.showActions, + required this.onSelected, + required this.isFeedback, + this.startActionPane, + this.endActionPane, + }); + + final ViewPB view; + final ViewPB? parentView; + final bool isExpanded; + // identify if the view item is rendered as feedback widget inside DraggableItem + final bool isFeedback; + + final int level; + final double leftPadding; + + final bool isDraggable; + final bool showActions; + final ViewItemOnSelected onSelected; + final FolderCategoryType categoryType; + final ActionPaneBuilder? startActionPane; + final ActionPaneBuilder? endActionPane; + + @override + State createState() => + _SingleMobileInnerViewItemState(); +} + +class _SingleMobileInnerViewItemState extends State { + @override + Widget build(BuildContext context) { + final children = [ + // expand icon + _buildLeftIcon(), + const HSpace(4), + // icon + SizedBox.square( + dimension: 22, + child: widget.view.defaultIcon(), + ), + const HSpace(12), + // title + Expanded( + child: FlowyText.regular( + widget.view.name, + fontSize: 18.0, + overflow: TextOverflow.ellipsis, + ), + ) + ]; + + // hover action + + // ··· more action button + // children.add(_buildViewMoreActionButton(context)); + // only support add button for document layout + if (widget.view.layout == ViewLayoutPB.Document) { + // + button + children.add(_buildViewAddButton(context)); + } + + Widget child = GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => widget.onSelected(widget.view), + child: SizedBox( + height: _itemHeight, + child: Padding( + padding: EdgeInsets.only(left: widget.level * widget.leftPadding), + child: Row( + children: children, + ), + ), + ), + ); + + if (widget.startActionPane != null || widget.endActionPane != null) { + child = Slidable( + // Specify a key if the Slidable is dismissible. + key: ValueKey(widget.view.id), + startActionPane: widget.startActionPane?.call(context), + endActionPane: widget.endActionPane?.call(context), + child: child, + ); + } + + return child; + } + + // > button or · button + // show > if the view is expandable. + // show · if the view can't contain child views. + Widget _buildLeftIcon() { + if (isReferencedDatabaseView(widget.view, widget.parentView)) { + return const _DotIconWidget(); + } + + return GestureDetector( + child: AnimatedRotation( + duration: const Duration(milliseconds: 200), + turns: widget.isExpanded ? 0 : -0.25, + child: const Icon( + Icons.keyboard_arrow_down_rounded, + size: 28, + ), + ), + onTap: () { + context + .read() + .add(ViewEvent.setIsExpanded(!widget.isExpanded)); + }, + ); + } + + // + button + Widget _buildViewAddButton(BuildContext context) { + return MobileViewAddButton( + onPressed: () { + context.read().add( + ViewEvent.createView( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ViewLayoutPB.Document, + ), + ); + }, + ); + } +} + +class _DotIconWidget extends StatelessWidget { + const _DotIconWidget(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(6.0), + child: Container( + width: 4, + height: 4, + decoration: BoxDecoration( + color: Theme.of(context).iconTheme.color, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + } +} + +// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field. +bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { + if (parentView == null) { + return false; + } + return view.layout.isDatabaseView && parentView.layout.isDatabaseView; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart new file mode 100644 index 000000000000..eb2f8ea9f887 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +const _iconSize = 32.0; + +class MobileViewAddButton extends StatelessWidget { + const MobileViewAddButton({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + iconPadding: const EdgeInsets.all(2), + width: _iconSize, + height: _iconSize, + icon: const FlowySvg( + FlowySvgs.add_s, + size: Size.square(_iconSize), + ), + onPressed: onPressed, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart b/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart index fedc53004c18..72514a3a4c18 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart @@ -1,4 +1,5 @@ -export 'home/home.dart'; -export 'root_placeholder_page.dart'; export 'details_placeholder_page.dart'; +export 'editor/mobile_editor_screen.dart'; +export 'home/home.dart'; export 'mobile_bottom_navigation_bar.dart'; +export 'root_placeholder_page.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index e4b03f946915..3c4299b6dff7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -103,7 +103,9 @@ class _DocumentPageState extends State { styleCustomizer: EditorStyleCustomizer( context: context, // the 44 is the width of the left action list - padding: const EdgeInsets.only(left: 40, right: 40 + 44), + padding: PlatformExtension.isMobile + ? const EdgeInsets.only(left: 20, right: 20) + : const EdgeInsets.only(left: 40, right: 40 + 44), ), header: _buildCoverAndIcon(context), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index f4f82132b714..f5ac817bb591 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -196,38 +196,80 @@ class _AppFlowyEditorPageState extends State { scrollController: effectiveScrollController, ); - final editor = AppFlowyEditor( - editorState: widget.editorState, - editable: true, - editorScrollController: editorScrollController, - // setup the auto focus parameters - autoFocus: widget.autoFocus ?? autoFocus, - focusedSelection: selection, - // setup the theme - editorStyle: styleCustomizer.style(), - // customize the block builders - blockComponentBuilders: blockComponentBuilders, - // customize the shortcuts - characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcutEvents, - // customize the context menu items - contextMenuItems: customContextMenuItems, - // customize the header and footer. - header: widget.header, - footer: const VSpace(200), + final editor = Directionality( + textDirection: textDirection, + child: AppFlowyEditor( + editorState: widget.editorState, + editable: true, + editorScrollController: editorScrollController, + // setup the auto focus parameters + autoFocus: widget.autoFocus ?? autoFocus, + focusedSelection: selection, + // setup the theme + editorStyle: styleCustomizer.style(), + // customize the block builders + blockComponentBuilders: blockComponentBuilders, + // customize the shortcuts + characterShortcutEvents: characterShortcutEvents, + commandShortcutEvents: commandShortcutEvents, + // customize the context menu items + contextMenuItems: customContextMenuItems, + // customize the header and footer. + header: widget.header, + footer: const VSpace(200), + ), ); + final editorState = widget.editorState; + + if (PlatformExtension.isMobile) { + return Column( + children: [ + Expanded( + child: MobileFloatingToolbar( + editorState: editorState, + editorScrollController: editorScrollController, + toolbarBuilder: (context, anchor) { + return AdaptiveTextSelectionToolbar.editable( + clipboardStatus: ClipboardStatus.pasteable, + onCopy: () => copyCommand.execute(editorState), + onCut: () => cutCommand.execute(editorState), + onPaste: () => pasteCommand.execute(editorState), + onSelectAll: () => selectAllCommand.execute(editorState), + anchors: TextSelectionToolbarAnchors( + primaryAnchor: anchor, + ), + ); + }, + child: editor, + ), + ), + MobileToolbar( + editorState: editorState, + toolbarItems: [ + textDecorationMobileToolbarItem, + buildTextAndBackgroundColorMobileToolbarItem(), + headingMobileToolbarItem, + todoListMobileToolbarItem, + listMobileToolbarItem, + linkMobileToolbarItem, + quoteMobileToolbarItem, + dividerMobileToolbarItem, + codeMobileToolbarItem, + ], + ), + ], + ); + } + return Center( child: FloatingToolbar( style: styleCustomizer.floatingToolbarStyleBuilder(), items: toolbarItems, - editorState: widget.editorState, + editorState: editorState, editorScrollController: editorScrollController, textDirection: textDirection, - child: Directionality( - textDirection: textDirection, - child: editor, - ), + child: editor, ), ); } @@ -241,7 +283,8 @@ class _AppFlowyEditorPageState extends State { // OptionAction.moveDown, ]; - final calloutBGColor = AFThemeExtension.of(context).calloutBGColor; + const calloutBGColor = Colors.black; + // AFThemeExtension.of(context).calloutBGColor; final configuration = BlockComponentConfiguration( padding: (_) => const EdgeInsets.symmetric(vertical: 5.0), @@ -417,24 +460,27 @@ class _AppFlowyEditorPageState extends State { if (supportAlignBuilderType.contains(entry.key)) ...alignAction, ]; - builder.showActions = - (node) => node.parent?.type != TableCellBlockKeys.type; - builder.actionBuilder = (context, state) { - final top = builder.configuration.padding(context.node).top; - final padding = context.node.type == HeadingBlockKeys.type - ? EdgeInsets.only(top: top + 8.0) - : EdgeInsets.only(top: top + 2.0); - return Padding( - padding: padding, - child: BlockActionList( - blockComponentContext: context, - blockComponentState: state, - editorState: widget.editorState, - actions: actions, - showSlashMenu: () => showSlashMenu(widget.editorState), - ), - ); - }; + // only show the ... and + button on the desktop platform. + if (PlatformExtension.isDesktop) { + builder.showActions = + (node) => node.parent?.type != TableCellBlockKeys.type; + builder.actionBuilder = (context, state) { + final top = builder.configuration.padding(context.node).top; + final padding = context.node.type == HeadingBlockKeys.type + ? EdgeInsets.only(top: top + 8.0) + : EdgeInsets.only(top: top + 2.0); + return Padding( + padding: padding, + child: BlockActionList( + blockComponentContext: context, + blockComponentState: state, + editorState: widget.editorState, + actions: actions, + showSlashMenu: () => showSlashMenu(widget.editorState), + ), + ); + }; + } } return builders; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 8e4eb9d1f124..305865a82307 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -75,40 +75,9 @@ class EditorStyleCustomizer { } EditorStyle mobile() { - final theme = Theme.of(context); - final fontSize = context.read().state.fontSize; - final fontFamily = context.read().state.fontFamily; - - return EditorStyle.desktop( + return EditorStyle.mobile( padding: padding, - cursorColor: theme.colorScheme.primary, - textStyleConfiguration: TextStyleConfiguration( - text: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize, - color: theme.colorScheme.onBackground, - height: 1.5, - ), - bold: baseTextStyle(fontFamily).copyWith( - fontWeight: FontWeight.w600, - ), - italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic), - underline: baseTextStyle(fontFamily) - .copyWith(decoration: TextDecoration.underline), - strikethrough: baseTextStyle(fontFamily) - .copyWith(decoration: TextDecoration.lineThrough), - href: baseTextStyle(fontFamily).copyWith( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - ), - code: GoogleFonts.arefRuqaaInk( - textStyle: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize, - fontWeight: FontWeight.normal, - color: Colors.red, - backgroundColor: theme.colorScheme.inverseSurface, - ), - ), - ), + textSpanDecorator: customizeAttributeDecorator, ); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 99d1a830b3ac..38ab60eedacc 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -1,3 +1,7 @@ +import 'package:appflowy/mobile/presentation/database/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/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; @@ -25,10 +29,14 @@ GoRouter generateRouter(Widget child) { // Desktop only if (!PlatformExtension.isMobile) _desktopHomeScreenRoute(), // Mobile only - if (PlatformExtension.isMobile) _mobileHomeScreenWithNavigationBarRoute(), + if (PlatformExtension.isMobile) ...[ + _mobileEditorScreenRoute(), + _mobileGridScreenRoute(), + _mobileBoardScreenRoute(), + _mobileCalendarScreenRoute(), + _mobileHomeScreenWithNavigationBarRoute(), + ], - // Unused routes for now, it may need to be used in the future. - // TODO(yijing): extract route method like other routes when it comes to be used. // Desktop and Mobile GoRoute( path: WorkspaceStartScreen.routeName, @@ -77,8 +85,6 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() { StatefulShellBranch( routes: [ GoRoute( - // The screen to display as the root in the first tab of the - // bottom navigation bar. path: MobileHomeScreen.routeName, builder: (BuildContext context, GoRouterState state) { return const MobileHomeScreen(); @@ -86,33 +92,14 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() { ), ], ), - // TODO(yijing): implement other tabs later - // The following code comes from the example of StatefulShellRoute.indexedStack. I left there just for placeholder purpose. They will be updated in the future. - // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( - // It's not necessary to provide a navigatorKey if it isn't also - // needed elsewhere. If not provided, a default key will be used. routes: [ GoRoute( - // The screen to display as the root in the second tab of the - // bottom navigation bar. - path: '/b', - builder: (BuildContext context, GoRouterState state) => - const RootPlaceholderScreen( - label: 'Favorite', - detailsPath: '/b/details/1', - secondDetailsPath: '/b/details/2', - ), - routes: [ - GoRoute( - path: 'details/:param', - builder: (BuildContext context, GoRouterState state) => - DetailsPlaceholderScreen( - label: 'Favorite details', - param: state.pathParameters['param'], - ), - ), - ], + path: MobileFavoriteScreen.routeName, + builder: (BuildContext context, GoRouterState state) { + return const MobileFavoriteScreen(); + }, ), ], ), @@ -261,6 +248,70 @@ GoRoute _signInScreenRoute() { ); } +GoRoute _mobileEditorScreenRoute() { + return GoRoute( + path: MobileEditorScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileEditorScreen.viewId]!; + final title = state.uri.queryParameters[MobileEditorScreen.viewTitle]; + return MaterialPage( + child: MobileEditorScreen( + id: id, + title: title, + ), + ); + }, + ); +} + +GoRoute _mobileGridScreenRoute() { + return GoRoute( + path: MobileGridScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileGridScreen.viewId]!; + final title = state.uri.queryParameters[MobileGridScreen.viewTitle]; + return MaterialPage( + child: MobileGridScreen( + id: id, + title: title, + ), + ); + }, + ); +} + +GoRoute _mobileBoardScreenRoute() { + return GoRoute( + path: MobileBoardScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileBoardScreen.viewId]!; + final title = state.uri.queryParameters[MobileBoardScreen.viewTitle]; + return MaterialPage( + child: MobileBoardScreen( + id: id, + title: title, + ), + ); + }, + ); +} + +GoRoute _mobileCalendarScreenRoute() { + return GoRoute( + path: MobileCalendarScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileCalendarScreen.viewId]!; + final title = state.uri.queryParameters[MobileCalendarScreen.viewTitle]!; + return MaterialPage( + child: MobileCalendarScreen( + id: id, + title: title, + ), + ); + }, + ); +} + GoRoute _rootRoute(Widget child) { return GoRoute( path: '/', diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart index 3ad37b75007a..3ffb9a218b43 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart @@ -1,16 +1,15 @@ import 'dart:async'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; + import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; part 'menu_bloc.freezed.dart'; @@ -42,7 +41,7 @@ class MenuBloc extends Bloc { index: event.index, ); result.fold( - (app) => emit(state.copyWith(plugin: app.plugin())), + (app) => emit(state.copyWith(lastCreatedView: app)), (error) { Log.error(error); emit(state.copyWith(successOrFailure: right(error))); @@ -120,12 +119,12 @@ class MenuState with _$MenuState { const factory MenuState({ required List views, required Either successOrFailure, - required Plugin plugin, + ViewPB? lastCreatedView, }) = _MenuState; factory MenuState.initial(WorkspacePB workspace) => MenuState( views: workspace.views, successOrFailure: left(unit), - plugin: makePlugin(pluginType: PluginType.blank), + lastCreatedView: null, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index b97d9e062209..dd7a397f2099 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; -import 'package:appflowy/mobile/application/mobile_theme_data.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -405,8 +404,190 @@ class AppearanceSettingsState with _$AppearanceSettingsState { if (PlatformExtension.isMobile) { // Mobile version has only one theme(light mode) for now. // The desktop theme and the mobile theme are independent. - final mobileThemeData = getMobileThemeData(); - return mobileThemeData; + const mobileColorTheme = ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF2DA2F6), //primary 100 + onPrimary: Colors.white, + // TODO(yijing): add color later + secondary: Colors.white, + onSecondary: Colors.white, + error: Color(0xffFB006D), + onError: Color(0xffFB006D), + background: Colors.white, + onBackground: Color(0xff2F3030), // title text + outline: Color(0xffBDC0C5), //caption + //Snack bar + surface: Colors.white, + onSurface: Color(0xff2F3030), // title text + ); + return ThemeData( + // color + primaryColor: mobileColorTheme.primary, //primary 100 + primaryColorLight: const Color(0xFF57B5F8), //primary 80 + dividerColor: mobileColorTheme.outline, //caption + scaffoldBackgroundColor: mobileColorTheme.background, + appBarTheme: AppBarTheme( + foregroundColor: mobileColorTheme.onBackground, + backgroundColor: mobileColorTheme.background, + elevation: 0, + centerTitle: false, + titleTextStyle: TextStyle( + fontFamily: 'Poppins', + color: mobileColorTheme.onBackground, + fontSize: 18, + fontWeight: FontWeight.w600, + letterSpacing: 0.05, + ), + ), + // button + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + elevation: MaterialStateProperty.all(0), + shadowColor: MaterialStateProperty.all(null), + backgroundColor: MaterialStateProperty.resolveWith( + (Set states) { + if (states.contains(MaterialState.disabled)) { + return const Color(0xFF57B5F8); + } + return mobileColorTheme.primary; + }, + ), + foregroundColor: MaterialStateProperty.all(Colors.white), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + mobileColorTheme.onBackground, + ), + backgroundColor: MaterialStateProperty.all(Colors.white), + 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: 16), + ), + // splash color + overlayColor: MaterialStateProperty.all( + Colors.grey[100], + ), + ), + ), + // text + fontFamily: 'Poppins', + textTheme: const TextTheme( + displayLarge: TextStyle( + color: Color(0xFF57B5F8), + fontSize: 32, + fontWeight: FontWeight.w700, + height: 1.20, + letterSpacing: 0.16, + ), + displayMedium: TextStyle( + color: Color(0xff2F3030), + fontSize: 32, + fontWeight: FontWeight.w600, + height: 1.20, + letterSpacing: 0.16, + ), + // H1 Semi 26 + displaySmall: TextStyle( + color: Color(0xFF2F3030), + fontSize: 26, + fontWeight: FontWeight.w600, + height: 1.10, + letterSpacing: 0.13, + ), + // body2 14 Regular + bodyMedium: TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.20, + letterSpacing: 0.07, + ), + // blue text button + labelMedium: TextStyle( + color: Color(0xFF2DA2F6), + fontSize: 14, + fontWeight: FontWeight.w500, + height: 1.20, + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + width: 2, + color: Color(0xFF2DA2F6), //primary 100 + ), + 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: const OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xffBDC0C5), //caption + ), + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + ), + colorScheme: mobileColorTheme, + 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, + ), + ), + ], + ); } // Due to Desktop version has multiple themes, it relies on the current theme to build the ThemeData diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 1e96a5685ebc..0bde0c4518d8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,9 +1,10 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; @@ -56,10 +57,11 @@ class HomeSideBar extends StatelessWidget { child: MultiBlocListener( listeners: [ BlocListener( - listenWhen: (p, c) => p.plugin.id != c.plugin.id, - listener: (context, state) => context - .read() - .add(TabsEvent.openPlugin(plugin: state.plugin)), + listenWhen: (p, c) => + p.lastCreatedView?.id != c.lastCreatedView?.id, + listener: (context, state) => context.read().add( + TabsEvent.openPlugin(plugin: state.lastCreatedView!.plugin()), + ), ), BlocListener( listener: (context, state) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 97153d1d748c..ee5496d76113 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -64,7 +64,6 @@ String languageFromLocale(Locale locale) { case "hin": return "हिन्दी"; - // If not found then the language code will be displayed default: return locale.languageCode; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 136ff4a7435a..906058e6d1d9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -22,6 +22,8 @@ class FlowyButton extends StatelessWidget { final bool disable; final double disableOpacity; final Size? leftIconSize; + final bool expandText; + final MainAxisAlignment mainAxisAlignment; const FlowyButton({ Key? key, @@ -40,6 +42,8 @@ class FlowyButton extends StatelessWidget { this.disable = false, this.disableOpacity = 0.5, this.leftIconSize = const Size.square(16), + this.expandText = true, + this.mainAxisAlignment = MainAxisAlignment.center, }) : super(key: key); @override @@ -79,7 +83,11 @@ class FlowyButton extends StatelessWidget { children.add(const HSpace(6)); } - children.add(Expanded(child: text)); + if (expandText) { + children.add(Expanded(child: text)); + } else { + children.add(text); + } if (rightIcon != null) { children.add(const HSpace(6)); @@ -88,7 +96,7 @@ class FlowyButton extends StatelessWidget { } Widget child = Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.center, children: children, ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index e90b45fdf8d8..849538a36bdd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -89,6 +89,10 @@ class FlowyText extends StatelessWidget { maxLines: maxLines, textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: fontSize, fontWeight: fontWeight, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 819b14debb58..6e1359128b11 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -525,6 +525,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_svg: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 392fd275373b..7b371bf66ded 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -116,6 +116,7 @@ dependencies: # to gather notification handling for all platforms local_notifier: ^0.1.5 app_links: ^3.4.1 + flutter_slidable: ^3.0.0 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/resources/flowy_icons/16x/three-dots-vertical.svg b/frontend/resources/flowy_icons/16x/three-dots-vertical.svg new file mode 100644 index 000000000000..91b93eda223b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/three-dots-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_delete.svg b/frontend/resources/flowy_icons/24x/m_delete.svg new file mode 100644 index 000000000000..9fa5510e01bc --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_duplicate.svg b/frontend/resources/flowy_icons/24x/m_duplicate.svg new file mode 100644 index 000000000000..dc843ae000d0 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_duplicate.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_rename.svg b/frontend/resources/flowy_icons/24x/m_rename.svg new file mode 100644 index 000000000000..de114a1142ba --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_rename.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_share.svg b/frontend/resources/flowy_icons/24x/m_share.svg new file mode 100644 index 000000000000..fbbb4d089dff --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_share.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_unfavorite.svg b/frontend/resources/flowy_icons/24x/m_unfavorite.svg new file mode 100644 index 000000000000..e495c3b202ae --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_unfavorite.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 78057d86377e..a9dd5584a619 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -142,6 +142,7 @@ "defaultNewPageName": "Untitled", "renameDialog": "Rename" }, + "noPagesInside": "No pages inside", "toolbar": { "undo": "Undo", "redo": "Redo", @@ -212,7 +213,11 @@ "delete": "Delete", "duplicate": "Duplicate", "done": "Done", - "putback": "Put Back" + "putback": "Put Back", + "share": "Share", + "removeFromFavorites": "Remove from favorites", + "addToFavorites": "Add to favorites", + "rename": "Rename" }, "label": { "welcome": "Welcome!", @@ -860,5 +865,9 @@ "replaceAll": "Replace all", "noResult": "No results", "caseSensitive": "Case sensitive" + }, + "error": { + "weAreSorry": "We're sorry", + "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." } } diff --git a/frontend/scripts/makefile/mobile.toml b/frontend/scripts/makefile/mobile.toml index 746a951543f0..7291d159e86b 100644 --- a/frontend/scripts/makefile/mobile.toml +++ b/frontend/scripts/makefile/mobile.toml @@ -26,8 +26,13 @@ script = [ """ cd rust-lib/ rustup show - echo cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" - cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" + if [ "${BUILD_FLAG}" == "debug" ]; then + echo "🚀 🚀 🚀 Building for debug" + cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" + else + echo "🚀 🚀 🚀 Building for release" + cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" --release + fi cd ../ """, ] From b16a102f85b3e7f3a82dc7fa1551a6282b12a46d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:23:27 +0800 Subject: [PATCH 06/17] chore: improve setting tab bar dropdown (#3756) * chore: improve setting tab bar dropdown * test: fix integration tests --- .../util/database_test_op.dart | 11 +- .../application/setting/property_bloc.dart | 32 ++-- .../toolbar/calendar_layout_setting.dart | 2 + .../widgets/setting/database_setting.dart | 75 --------- .../widgets/setting/setting_button.dart | 142 +++++++++++------- .../setting_property_list.dart} | 88 ++++++++--- 6 files changed, 178 insertions(+), 172 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart rename frontend/appflowy_flutter/lib/plugins/database_view/widgets/{field/grid_property.dart => setting/setting_property_list.dart} (67%) diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 5b909f4723d8..ce07d0d64b44 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -38,7 +38,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar 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/widgets/database_layout_ext.dart'; -import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart'; @@ -52,7 +52,6 @@ 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.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -1132,7 +1131,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapDatabaseSettingButton] first. Future tapViewPropertiesButton() async { - final findSettingItem = find.byType(DatabaseSettingItem); + final findSettingItem = find.byType(DatabaseSettingListPopover); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1149,7 +1148,7 @@ extension AppFlowyDatabaseTest on WidgetTester { /// Should call [tapDatabaseSettingButton] first. Future tapDatabaseLayoutButton() async { - final findSettingItem = find.byType(DatabaseSettingItem); + final findSettingItem = find.byType(DatabaseSettingListPopover); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1165,7 +1164,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future tapCalendarLayoutSettingButton() async { - final findSettingItem = find.byType(DatabaseSettingItem); + final findSettingItem = find.byType(DatabaseSettingListPopover); final findLayoutButton = find.byWidgetPredicate( (widget) => widget is FlowyText && @@ -1505,7 +1504,7 @@ extension AppFlowyDatabaseTest on WidgetTester { ) async { final field = find.byWidgetPredicate( (widget) => - widget is GridPropertyCell && widget.fieldInfo.name == fieldName, + widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName, ); final toggleVisibilityButton = find.descendant(of: field, matching: find.byType(FlowyIconButton)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart index bbef6db8943a..658c43358374 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/property_bloc.dart @@ -25,34 +25,44 @@ class DatabasePropertyBloc ) { on( (event, emit) async { - await event.map( - initial: (_Initial value) { + await event.when( + initial: () { _startListening(); }, - setFieldVisibility: (_SetFieldVisibility value) async { + setFieldVisibility: (fieldId, visibility) async { final fieldSettingsSvc = FieldSettingsBackendService( viewId: viewId, ); final result = await fieldSettingsSvc.updateFieldSettings( - fieldId: value.fieldId, - fieldVisibility: value.visibility, + fieldId: fieldId, + fieldVisibility: visibility, ); result.fold((l) => null, (err) => Log.error(err)); }, - didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { - emit(state.copyWith(fieldContexts: value.fields)); + didReceiveFieldUpdate: (fields) { + emit(state.copyWith(fieldContexts: fields)); }, - moveField: (_MoveField value) async { + moveField: (fieldId, fromIndex, toIndex) async { + if (fromIndex < toIndex) { + toIndex--; + } + final fieldContexts = List.from(state.fieldContexts); + fieldContexts.insert( + toIndex, + fieldContexts.removeAt(fromIndex), + ); + emit(state.copyWith(fieldContexts: fieldContexts)); + final fieldBackendService = FieldBackendService( viewId: viewId, - fieldId: value.fieldId, + fieldId: fieldId, ); final result = await fieldBackendService.moveField( - value.fromIndex, - value.toIndex, + fromIndex, + toIndex, ); result.fold((l) => null, (r) => Log.error(r)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart index 47e828072e72..97dc3cbef797 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -228,6 +228,7 @@ class LayoutDateField extends StatelessWidget { Widget build(BuildContext context) { return AppFlowyPopover( direction: PopoverDirection.leftWithTopAligned, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, constraints: BoxConstraints.loose(const Size(300, 400)), mutex: popoverMutex, offset: const Offset(-16, 0), @@ -346,6 +347,7 @@ class FirstDayOfWeek extends StatelessWidget { return AppFlowyPopover( direction: PopoverDirection.leftWithTopAligned, constraints: BoxConstraints.loose(const Size(300, 400)), + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, mutex: popoverMutex, offset: const Offset(-16, 0), popupBuilder: (context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart deleted file mode 100644 index 8c77cbf0a678..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; - -import '../../grid/presentation/layout/sizes.dart'; -import 'setting_button.dart'; - -class DatabaseSettingList extends StatelessWidget { - final DatabaseController databaseContoller; - final Function(DatabaseSettingAction, DatabaseController) onAction; - const DatabaseSettingList({ - required this.databaseContoller, - required this.onAction, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout) - .map((action) { - return DatabaseSettingItem( - action: action, - onAction: (action) => onAction(action, databaseContoller), - ); - }).toList(); - - return ListView.separated( - shrinkWrap: true, - controller: ScrollController(), - itemCount: cells.length, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return cells[index]; - }, - ); - } -} - -class DatabaseSettingItem extends StatelessWidget { - final DatabaseSettingAction action; - final Function(DatabaseSettingAction) onAction; - - const DatabaseSettingItem({ - required this.action, - required this.onAction, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText.medium( - action.title(), - color: AFThemeExtension.of(context).textColor, - ), - onTap: () => onAction(action), - leftIcon: FlowySvg( - action.iconData(), - color: Theme.of(context).iconTheme.color, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart index ccea4aa933b5..653aaf96f60b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart @@ -11,12 +11,10 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:styled_widget/styled_widget.dart'; import '../../grid/presentation/layout/sizes.dart'; import '../../grid/presentation/widgets/toolbar/grid_layout.dart'; -import '../field/grid_property.dart'; -import 'database_setting.dart'; +import 'setting_property_list.dart'; class SettingButton extends StatefulWidget { final DatabaseController databaseController; @@ -39,7 +37,6 @@ class _SettingButtonState extends State { constraints: BoxConstraints.loose(const Size(200, 400)), direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 8), - margin: EdgeInsets.zero, triggerActions: PopoverTriggerFlags.none, child: FlowyTextButton( LocaleKeys.settings_title.tr(), @@ -53,7 +50,7 @@ class _SettingButtonState extends State { onPressed: () => _popoverController.show(), ), popupBuilder: (BuildContext context) { - return _DatabaseSettingListPopover( + return DatabaseSettingListPopover( databaseController: widget.databaseController, ); }, @@ -61,10 +58,10 @@ class _SettingButtonState extends State { } } -class _DatabaseSettingListPopover extends StatefulWidget { +class DatabaseSettingListPopover extends StatefulWidget { final DatabaseController databaseController; - const _DatabaseSettingListPopover({ + const DatabaseSettingListPopover({ required this.databaseController, Key? key, }) : super(key: key); @@ -74,59 +71,40 @@ class _DatabaseSettingListPopover extends StatefulWidget { } class _DatabaseSettingListPopoverState - extends State<_DatabaseSettingListPopover> { - DatabaseSettingAction? _action; + extends State { + late final PopoverMutex popoverMutex; + + @override + void initState() { + super.initState(); + popoverMutex = PopoverMutex(); + } @override Widget build(BuildContext context) { - if (_action == null) { - return DatabaseSettingList( - databaseContoller: widget.databaseController, - onAction: (action, settingContext) { - setState(() { - _action = action; - }); - }, - ).padding(all: 6.0); - } else { - switch (_action!) { - case DatabaseSettingAction.showLayout: - return DatabaseLayoutList( - viewId: widget.databaseController.viewId, - currentLayout: widget.databaseController.databaseLayout, - ); - case DatabaseSettingAction.showGroup: - return DatabaseGroupList( - viewId: widget.databaseController.viewId, - fieldController: widget.databaseController.fieldController, - onDismissed: () { - // widget.popoverController.close(); - }, - ); - case DatabaseSettingAction.showProperties: - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - DatabasePropertyList( - viewId: widget.databaseController.viewId, - fieldController: widget.databaseController.fieldController, + final cells = + actionsForDatabaseLayout(widget.databaseController.databaseLayout) + .map( + (action) => action.build( + context, + widget.databaseController, + popoverMutex, ), - FlowyText.regular( - LocaleKeys.grid_settings_reorderPropertiesTooltip.tr(), - ), - const VSpace(8), - ], - ); - case DatabaseSettingAction.showCalendarLayout: - return CalendarLayoutSetting( - viewId: widget.databaseController.viewId, - fieldController: widget.databaseController.fieldController, - calendarSettingController: ICalendarSettingImpl( - widget.databaseController, - ), - ); - } - } + ) + .toList(); + + return ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ); } } @@ -179,6 +157,58 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { return LocaleKeys.calendar_settings_name.tr(); } } + + Widget build( + BuildContext context, + DatabaseController databaseController, + PopoverMutex popoverMutex, + ) { + final popover = switch (this) { + DatabaseSettingAction.showLayout => DatabaseLayoutList( + viewId: databaseController.viewId, + currentLayout: databaseController.databaseLayout, + ), + DatabaseSettingAction.showGroup => DatabaseGroupList( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + onDismissed: () {}, + ), + DatabaseSettingAction.showProperties => DatabasePropertyList( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + ), + DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + calendarSettingController: ICalendarSettingImpl( + databaseController, + ), + ), + }; + + return AppFlowyPopover( + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + direction: PopoverDirection.leftWithTopAligned, + mutex: popoverMutex, + margin: EdgeInsets.zero, + offset: const Offset(-16, 0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + title(), + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: FlowySvg( + iconData(), + color: Theme.of(context).iconTheme.color, + ), + ), + ), + popupBuilder: (context) => popover, + ); + } } /// Returns the list of actions that should be shown for the given database layout. diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart similarity index 67% rename from frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart index c06b9eaf1354..53e43ea4dc80 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/field/grid_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; @@ -7,12 +9,12 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:reorderables/reorderables.dart'; import 'package:styled_widget/styled_widget.dart'; import '../../grid/presentation/layout/sizes.dart'; @@ -44,29 +46,43 @@ class _DatabasePropertyListState extends State { )..add(const DatabasePropertyEvent.initial()), child: BlocBuilder( builder: (context, state) { - final cells = state.fieldContexts.map((field) { - return GridPropertyCell( + final cells = state.fieldContexts.mapIndexed((index, field) { + return DatabasePropertyCell( key: ValueKey(field.id), viewId: widget.viewId, fieldInfo: field, popoverMutex: _popoverMutex, + index: index, ); }).toList(); - return ReorderableColumn( - needsLongPressDraggable: false, - buildDraggableFeedback: (context, constraints, child) => - ConstrainedBox( - constraints: constraints, - child: Material(color: Colors.transparent, child: child), - ), - onReorder: (from, to) => context.read().add( - DatabasePropertyEvent.moveField( - fieldId: cells[from].fieldInfo.id, - fromIndex: from, - toIndex: to, + return ReorderableListView( + proxyDecorator: (child, index, _) => Material( + color: Colors.transparent, + child: Stack( + children: [ + child, + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), ), - ), + ], + ), + ), + buildDefaultDragHandles: false, + shrinkWrap: true, + onReorder: (from, to) { + context.read().add( + DatabasePropertyEvent.moveField( + fieldId: cells[from].fieldInfo.id, + fromIndex: from, + toIndex: to, + ), + ); + }, + onReorderStart: (_) => _popoverMutex.close(), padding: const EdgeInsets.symmetric(vertical: 6.0), children: cells, ); @@ -77,23 +93,25 @@ class _DatabasePropertyListState extends State { } @visibleForTesting -class GridPropertyCell extends StatefulWidget { +class DatabasePropertyCell extends StatefulWidget { final FieldInfo fieldInfo; final String viewId; final PopoverMutex popoverMutex; + final int index; - const GridPropertyCell({ + const DatabasePropertyCell({ super.key, required this.fieldInfo, required this.viewId, required this.popoverMutex, + required this.index, }); @override - State createState() => _GridPropertyCellState(); + State createState() => _DatabasePropertyCellState(); } -class _GridPropertyCellState extends State { +class _DatabasePropertyCellState extends State { final PopoverController _popoverController = PopoverController(); @override @@ -109,7 +127,7 @@ class _GridPropertyCellState extends State { return AppFlowyPopover( mutex: widget.popoverMutex, controller: _popoverController, - offset: const Offset(8, 0), + offset: const Offset(-8, 0), direction: PopoverDirection.leftWithTopAligned, constraints: BoxConstraints.loose(const Size(240, 400)), triggerActions: PopoverTriggerFlags.none, @@ -122,9 +140,31 @@ class _GridPropertyCellState extends State { widget.fieldInfo.name, color: AFThemeExtension.of(context).textColor, ), - leftIcon: FlowySvg( - widget.fieldInfo.fieldType.icon(), - color: Theme.of(context).iconTheme.color, + leftIconSize: const Size(36, 18), + leftIcon: Row( + children: [ + ReorderableDragStartListener( + index: widget.index, + child: MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, + child: SizedBox( + width: 14, + height: 14, + child: FlowySvg( + FlowySvgs.drag_element_s, + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ), + const HSpace(6.0), + FlowySvg( + widget.fieldInfo.fieldType.icon(), + color: Theme.of(context).iconTheme.color, + ), + ], ), rightIcon: FlowyIconButton( hoverColor: Colors.transparent, From cd6e133c867249f95540f82d0d20b31b5ad8d0f5 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:51:59 +0200 Subject: [PATCH 07/17] fix: notification settings initial state (#3763) --- .../notifications/notification_settings_cubit.dart | 8 +++++++- frontend/rust-lib/flowy-user/src/entities/user_setting.rs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart index 75874cf432b5..b495845909b0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart @@ -18,6 +18,11 @@ class NotificationSettingsCubit extends Cubit { .getNotificationSettings() .then((notificationSettings) { _notificationSettings = notificationSettings; + emit( + state.copyWith( + isNotificationsEnabled: _notificationSettings.notificationsEnabled, + ), + ); _initCompleter.complete(); }); } @@ -26,13 +31,14 @@ class NotificationSettingsCubit extends Cubit { await _initCompleter.future; _notificationSettings.notificationsEnabled = !state.isNotificationsEnabled; - _saveNotificationSettings(); emit( state.copyWith( isNotificationsEnabled: _notificationSettings.notificationsEnabled, ), ); + + _saveNotificationSettings(); } Future _saveNotificationSettings() async { diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index 6dab6b4583e1..a4e91f4a5c34 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -241,6 +241,7 @@ impl std::default::Default for DateTimeSettingsPB { #[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] pub struct NotificationSettingsPB { #[pb(index = 1)] + #[serde(default)] pub notifications_enabled: bool, } From 25a98cda81c13eeb64b75bd2c4b21a39f826d7f5 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Mon, 23 Oct 2023 17:47:11 +0330 Subject: [PATCH 08/17] ci: reduce build time (#3519) --- .github/workflows/flutter_ci.yaml | 295 +++++++++++++++++- .../appflowy_flutter/cargokit_options.yaml | 1 + .../packages/appflowy_backend/lib/ffi.dart | 3 +- frontend/appflowy_flutter/pubspec.lock | 16 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/rust-lib/Cargo.lock | 4 +- frontend/rust-lib/flowy-server/Cargo.toml | 2 +- frontend/rust-lib/flowy-sqlite/Cargo.toml | 1 - .../code_generation/env/generate_env.cmd | 17 - .../code_generation/env/generate_env.sh | 19 -- .../flowy_icons/generate_flowy_icons.cmd | 2 +- .../flowy_icons/generate_flowy_icons.sh | 12 +- .../freezed/generate_freezed.cmd | 5 +- .../freezed/generate_freezed.sh | 23 +- frontend/scripts/code_generation/generate.cmd | 9 - frontend/scripts/code_generation/generate.sh | 9 - .../generate_language_files.cmd | 2 +- .../language_files/generate_language_files.sh | 12 +- frontend/scripts/docker-buildfiles/Dockerfile | 8 +- frontend/scripts/makefile/flutter.toml | 12 +- frontend/scripts/makefile/tests.toml | 72 ++++- 21 files changed, 426 insertions(+), 100 deletions(-) create mode 100644 frontend/appflowy_flutter/cargokit_options.yaml delete mode 100644 frontend/scripts/code_generation/env/generate_env.cmd delete mode 100755 frontend/scripts/code_generation/env/generate_env.sh diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 72e169ca708a..4d931a1fbec9 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -22,15 +22,17 @@ on: - "!frontend/appflowy_tauri/**" env: + CARGO_TERM_COLOR: always FLUTTER_VERSION: "3.10.1" RUST_TOOLCHAIN: "1.70" + CARGO_MAKE_VERSION: "0.36.6" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - build: + prepare: if: github.event.pull_request.draft != true strategy: fail-fast: false @@ -83,21 +85,20 @@ jobs: prefix-key: ${{ matrix.os }} workspaces: | frontend/rust-lib + cache-all-crates: true - - uses: davidB/rust-cargo-make@v1 + - uses: taiki-e/install-action@v2 with: - version: '0.36.6' + tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}, duckscript_cli - name: Install prerequisites working-directory: frontend run: | - cargo install --force duckscript_cli if [ "$RUNNER_OS" == "Linux" ]; then sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev - sudo apt-get install keybinder-3.0 libnotify-dev + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev elif [ "$RUNNER_OS" == "Windows" ]; then vcpkg integrate install elif [ "$RUNNER_OS" == "macOS" ]; then @@ -106,6 +107,171 @@ jobs: cargo make appflowy-flutter-deps-tools shell: bash + - name: Build AppFlowy + working-directory: frontend + run: | + cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev + + - name: Run code generation + working-directory: frontend + run: | + cargo make code_generation + + - name: Flutter Analyzer + working-directory: frontend/appflowy_flutter + run: | + flutter analyze . + + - name: Compress appflowy_flutter + run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter + + - uses: actions/upload-artifact@v3 + with: + name: ${{ github.run_id }}-${{ matrix.os }} + path: appflowy_flutter.tar.gz + + unit_test: + needs: [prepare] + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + flutter_profile: development-linux-x86_64 + target: x86_64-unknown-linux-gnu + - os: macos-latest + flutter_profile: development-mac-x86_64 + target: x86_64-apple-darwin + - os: windows-latest + flutter_profile: development-windows-x86 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: ${{ matrix.target }} + override: true + profile: minimal + + - name: Install flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.os }} + workspaces: | + frontend/rust-lib + cache-all-crates: true + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}, duckscript_cli + + - name: Install prerequisites + working-directory: frontend + run: | + if [ "$RUNNER_OS" == "macOS" ]; then + cargo make appflowy-flutter-deps-tools + 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 unit tests + working-directory: frontend + run: | + if [ "$RUNNER_OS" == "macOS" ]; then + cargo make dart_unit_test + else + 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 + + integration_test: + needs: [prepare] + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest,windows-latest] + include: + - os: ubuntu-latest + flutter_profile: development-linux-x86_64 + target: x86_64-unknown-linux-gnu + - os: windows-latest + flutter_profile: development-windows-x86 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: ${{ matrix.target }} + override: true + profile: minimal + + - name: Install flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-make@${{ env.CARGO_MAKE_VERSION }} + + - name: Install prerequisites + working-directory: frontend + run: | + 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 @@ -118,20 +284,33 @@ jobs: fi shell: bash - - name: Build AppFlowy + - 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 code generation + if: matrix.os == 'windows-latest' working-directory: frontend run: | - cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev + cargo make code_generation - - name: Flutter Analyzer + - name: Run Flutter integration tests working-directory: frontend/appflowy_flutter run: | - flutter analyze . - - - name: Run Flutter unit tests - working-directory: frontend - run: | - cargo make dart_unit_test + if [ "$RUNNER_OS" == "Linux" ]; then + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + sudo apt-get install network-manager + flutter test integration_test/runner.dart -d Linux --coverage + elif [ "$RUNNER_OS" == "macOS" ]; then + flutter test integration_test/runner.dart -d macOS --coverage + elif [ "$RUNNER_OS" == "Windows" ]; then + flutter test integration_test/runner.dart -d Windows --coverage + fi + shell: bash - name: Upload coverage to Codecov uses: Wandalen/wretry.action@v1.0.36 @@ -139,7 +318,7 @@ jobs: action: codecov/codecov-action@v3 with: | name: appflowy - flags: appflowy_flutter_unit_test + flags: appflowy_flutter_integrateion_test fail_ci_if_error: true verbose: true os: ${{ matrix.os }} @@ -147,3 +326,87 @@ jobs: attempt_limit: 20 attempt_delay: 10000 + build: + needs: [prepare] + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + flutter_profile: development-linux-x86_64 + target: x86_64-unknown-linux-gnu + - os: macos-latest + flutter_profile: development-mac-x86_64 + target: x86_64-apple-darwin + - os: windows-latest + flutter_profile: development-windows-x86 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + target: ${{ matrix.target }} + override: true + profile: minimal + + - name: Install flutter + id: flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-make@${{ env.CARGO_MAKE_VERSION }} + + - name: Install prerequisites + working-directory: frontend + run: | + 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 code generation + if: matrix.os == 'windows-latest' + working-directory: frontend + run: | + cargo make code_generation + + - name: Build flutter product + working-directory: frontend + run: | + cargo make --profile ${{ matrix.flutter_profile }} appflowy-make-product-dev diff --git a/frontend/appflowy_flutter/cargokit_options.yaml b/frontend/appflowy_flutter/cargokit_options.yaml new file mode 100644 index 000000000000..70d10df337dd --- /dev/null +++ b/frontend/appflowy_flutter/cargokit_options.yaml @@ -0,0 +1 @@ +use_precompiled_binaries: true diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart index 9d4e17cca156..fef155d2cef5 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart @@ -20,7 +20,8 @@ DynamicLibrary _open() { return DynamicLibrary.open('${prefix}/libdart_ffi.so'); if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib'); - if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a'); + if (Platform.isIOS) + return DynamicLibrary.open('${prefix}/libdart_ffi.a'); if (Platform.isWindows) return DynamicLibrary.open('${prefix}/dart_ffi.dll'); } else { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 6e1359128b11..a951b447788f 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -749,18 +749,18 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "6431b11844472574a90803c02f1e55221e6a390a872786735f6757a67dacd678" + sha256: "559d4f156ba4d9a33c48185a1d7d5e887083bb601b0b130180d0aae3c1449338" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.0-dev.2" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: "4114739083d1c63e6a1a8b93f09dd69b3cf9a9d6ee215ae7f23079307197ebba" + sha256: "500daa1fbe679f7d28a5258df3ff47dab6de352e680dc93c1ca9eae1555d8db5" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" isolates: dependency: transitive description: @@ -1507,18 +1507,18 @@ packages: dependency: "direct main" description: name: super_clipboard - sha256: ba484eb42ce621b69241d18d2a8494109589e8796eb6a3399e6c8f9cdaab8303 + sha256: "80f7f42e9778101d77e930dcd9206cf72a1d8b696b3cbc1e3f9cbeb71ac4cde7" url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.0-dev.5" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: dfe0a1c74430db946be973878da3f8611b8f124137f1dcbdf883e4605db40bd8 + sha256: e3903ee807e82a930288811cf1b31a0dee30378bb544f30e3ea58d0ed2f884f2 url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.0-dev.5" sync_http: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 7b371bf66ded..dcb81df99f15 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -106,7 +106,7 @@ dependencies: url_protocol: hive: ^2.2.3 hive_flutter: ^1.1.0 - super_clipboard: ^0.6.3 + super_clipboard: ^0.7.0-dev.4 go_router: ^10.1.2 string_validator: ^1.0.0 unsplash_client: ^2.1.1 diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index c2d899112a97..1f7b9ad4368d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -3434,9 +3434,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.3+3.1.2" +version = "300.1.5+3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" +checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" dependencies = [ "cc", ] diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 82a79bd89eb8..45b2b13d49ef 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" tracing = { version = "0.1" } futures = "0.3.26" futures-util = "0.3.26" -reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart","blocking"] } +reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } hyper = "0.14" config = { version = "0.10.1", default-features = false, features = ["yaml"] } serde = { version = "1.0", features = ["derive"] } diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 81727f8e6e91..810be1006553 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -23,7 +23,6 @@ error-chain = "=0.12.0" openssl = { version = "0.10.45", optional = true, features = ["vendored"] } openssl-sys = { version = "0.9.80", optional = true, features = ["vendored"] } - [dev-dependencies] tempfile = "3.5.0" diff --git a/frontend/scripts/code_generation/env/generate_env.cmd b/frontend/scripts/code_generation/env/generate_env.cmd deleted file mode 100644 index d15d078dddfc..000000000000 --- a/frontend/scripts/code_generation/env/generate_env.cmd +++ /dev/null @@ -1,17 +0,0 @@ -@echo off - -REM Store the current working directory -set "original_dir=%CD%" - -REM Change the current working directory to the script's location -cd /d "%~dp0" - -REM Navigate to the project root -cd ..\..\..\appflowy_flutter - -REM Navigate to the appflowy_flutter directory and generate files -echo Generating env files -call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build --delete-conflicting-outputs -echo Done generating env files - -cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/env/generate_env.sh b/frontend/scripts/code_generation/env/generate_env.sh deleted file mode 100755 index fcadc3b252f2..000000000000 --- a/frontend/scripts/code_generation/env/generate_env.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Store the current working directory -original_dir=$(pwd) - -cd "$(dirname "$0")" - -# Navigate to the project root -cd ../../../appflowy_flutter - -# Navigate to the appflowy_flutter directory and generate files -echo "Generating env files" -# flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && -flutter packages pub get >/dev/null 2>&1 -dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs -echo "Done generating env files" - -# Return to the original directory -cd "$original_dir" diff --git a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.cmd b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.cmd index d7ada3b641d7..72cdd54cae2f 100644 --- a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.cmd +++ b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.cmd @@ -15,7 +15,7 @@ REM the appflowy_flutter/assets/translation directory echo Copying resources/flowy_icons to appflowy_flutter/assets/flowy_icons xcopy /E /Y /I ..\resources\flowy_icons assets\flowy_icons -call flutter packages pub get +REM call flutter packages pub get echo Generating FlowySvg class call dart run flowy_svg diff --git a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh index fb6eee67c65a..f969e49f4437 100755 --- a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh +++ b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh @@ -1,5 +1,13 @@ #!/bin/bash +no_pub_get=false + +while getopts 's' flag; do + case "${flag}" in + s) no_pub_get=true ;; + esac +done + echo "Generating flowy icon files" # Store the current working directory @@ -14,7 +22,9 @@ rm -rf assets/flowy_icons/ mkdir -p assets/flowy_icons/ rsync -r ../resources/flowy_icons/ assets/flowy_icons/ -flutter packages pub get +if [ "$no_pub_get" = false ]; then + flutter packages pub get +fi echo "Generating FlowySvg classes" dart run flowy_svg diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.cmd b/frontend/scripts/code_generation/freezed/generate_freezed.cmd index 4b607948fad4..64d7e8754e64 100644 --- a/frontend/scripts/code_generation/freezed/generate_freezed.cmd +++ b/frontend/scripts/code_generation/freezed/generate_freezed.cmd @@ -11,7 +11,8 @@ cd ..\..\..\appflowy_flutter REM Navigate to the appflowy_flutter directory and generate files echo Generating files for appflowy_flutter -call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d +REM call flutter packages pub get +call dart run build_runner clean && call dart run build_runner build -d echo Done generating files for appflowy_flutter echo Generating files for packages @@ -24,7 +25,7 @@ for /D %%d in (*) do ( if exist "pubspec.yaml" ( echo Generating freezed files in %%d... echo Please wait while we clean the project and fetch the dependencies. - call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d + call dart run build_runner clean && call dart run build_runner build -d echo Done running build command in %%d ) else ( echo No pubspec.yaml found in %%d, it can't be a Dart project. Skipping. diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 01f578d3dc16..24e4e55fa3bc 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -1,5 +1,13 @@ #!/bin/bash +no_pub_get=false + +while getopts 's' flag; do + case "${flag}" in + s) no_pub_get=true ;; + esac +done + # Store the current working directory original_dir=$(pwd) @@ -10,9 +18,12 @@ cd ../../../appflowy_flutter # Navigate to the appflowy_flutter directory and generate files echo "Generating files for appflowy_flutter" -# flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && -flutter packages pub get >/dev/null 2>&1 -dart run build_runner build -d + +if [ "$no_pub_get" = false ]; then + flutter packages pub get >/dev/null 2>&1 +fi + +dart run build_runner clean && dart run build_runner build -d echo "Done generating files for appflowy_flutter" echo "Generating files for packages" @@ -25,8 +36,10 @@ for d in */; do if [ -f "pubspec.yaml" ]; then echo "Generating freezed files in $d..." echo "Please wait while we clean the project and fetch the dependencies." - flutter packages pub get >/dev/null 2>&1 - dart run build_runner build -d + if [ "$no_pub_get" = false ]; then + flutter packages pub get >/dev/null 2>&1 + fi + dart run build_runner clean && dart run build_runner build -d echo "Done running build command in $d" else echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping." diff --git a/frontend/scripts/code_generation/generate.cmd b/frontend/scripts/code_generation/generate.cmd index 1dc853d2a221..855c288ab3d0 100644 --- a/frontend/scripts/code_generation/generate.cmd +++ b/frontend/scripts/code_generation/generate.cmd @@ -26,15 +26,6 @@ call generate_freezed.cmd %* REM Return to the main script directory cd .. -echo Generating env files using build_runner -cd env -REM Allow execution permissions on CI -chmod +x generate_env.cmd -call generate_env.cmd %* - -REM Return to the main script directory -cd .. - echo Generating svg files using flowy_svg cd flowy_icons REM Allow execution permissions on CI diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index 567feddc7dcc..f71ceba2df28 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -26,15 +26,6 @@ chmod +x ./generate_freezed.sh # Return to the main script directory cd .. -echo "Generating env files using build_runner" -cd env -# Allow execution permissions on CI -chmod +x ./generate_env.sh -./generate_env.sh "$@" - -# Return to the main script directory -cd .. - echo "Generating svg files using flowy_svg" cd flowy_icons # Allow execution permissions on CI diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.cmd b/frontend/scripts/code_generation/language_files/generate_language_files.cmd index 470f1654e5c8..7b14503810f0 100644 --- a/frontend/scripts/code_generation/language_files/generate_language_files.cmd +++ b/frontend/scripts/code_generation/language_files/generate_language_files.cmd @@ -15,7 +15,7 @@ REM the appflowy_flutter/assets/translation directory echo Copying resources/translations to appflowy_flutter/assets/translations xcopy /E /Y /I ..\resources\translations assets\translations -call flutter packages pub get +REM call flutter packages pub get echo Specifying source directory for AppFlowy Localizations. call dart run easy_localization:generate -S assets/translations/ diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.sh b/frontend/scripts/code_generation/language_files/generate_language_files.sh index d93c88d1a304..03bf7a906ad6 100755 --- a/frontend/scripts/code_generation/language_files/generate_language_files.sh +++ b/frontend/scripts/code_generation/language_files/generate_language_files.sh @@ -1,5 +1,13 @@ #!/bin/bash +no_pub_get=false + +while getopts 's' flag; do + case "${flag}" in + s) no_pub_get=true ;; + esac +done + echo "Generating language files" # Store the current working directory @@ -16,7 +24,9 @@ rm -rf assets/translations/ mkdir -p assets/translations/ cp -f ../resources/translations/*.json assets/translations/ -flutter packages pub get +if [ "$no_pub_get" = false ]; then + flutter packages pub get +fi echo "Specifying source directory for AppFlowy Localizations." dart run easy_localization:generate -S assets/translations/ diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index d874ec7805aa..eecf5fd48596 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -47,8 +47,10 @@ RUN flutter doctor RUN dart pub global activate protoc_plugin 20.0.1 # Install build dependencies for AppFlowy -RUN sudo pacman -S --noconfirm git libkeybinder3 sqlite clang rsync libnotify -RUN source ~/.cargo/env && cargo install --force cargo-make duckscript_cli +RUN yay -S --noconfirm jemalloc4 cargo-make cargo-binstall +RUN sudo pacman -S --noconfirm git libkeybinder3 sqlite clang rsync libnotify rocksdb zstd +RUN sudo ln -s /usr/bin/sha1sum /usr/bin/shasum +RUN source ~/.cargo/env && cargo binstall duckscript_cli -y # Build AppFlowy COPY . /appflowy @@ -58,7 +60,7 @@ RUN cd frontend && \ source ~/.cargo/env && \ cargo make appflowy-flutter-deps-tools && \ cargo make flutter_clean && \ - cargo make -p production-linux-x86_64 appflowy-linux + OPENSSL_STATIC=1 ZSTD_SYS_USE_PKG_CONFIG=1 ROCKSDB_LIB_DIR="/usr/lib/" cargo make -p production-linux-x86_64 appflowy-linux #================ diff --git a/frontend/scripts/makefile/flutter.toml b/frontend/scripts/makefile/flutter.toml index 8d0bd8cd6c78..9f6b2a5369b1 100644 --- a/frontend/scripts/makefile/flutter.toml +++ b/frontend/scripts/makefile/flutter.toml @@ -94,6 +94,14 @@ run_task = { name = [ ] } script_runner = "@shell" +[tasks.appflowy-make-product-dev] +run_task = { name = [ + "set-app-version", + "flutter-build", + "copy-to-product", +] } + + [tasks.copy-to-product] mac_alias = "copy-to-product-macos" windows_alias = "copy-to-product-windows" @@ -227,8 +235,9 @@ script = [ cd appflowy_flutter flutter clean flutter pub get + flutter packages pub get cd ../ - sh scripts/code_generation/generate.sh + sh scripts/code_generation/generate.sh -s """ ] @@ -239,6 +248,7 @@ script = [ cd ./appflowy_flutter/ exec cmd.exe /c flutter clean exec cmd.exe /c flutter pub get + exec cmd.exe /c flutter packages pub get cd ../ exec scripts/code_generation/generate.cmd """, diff --git a/frontend/scripts/makefile/tests.toml b/frontend/scripts/makefile/tests.toml index 0ce5b6f49c06..ef06f98a9090 100644 --- a/frontend/scripts/makefile/tests.toml +++ b/frontend/scripts/makefile/tests.toml @@ -36,6 +36,31 @@ cd appflowy_flutter flutter test --dart-define=RUST_LOG=${RUST_LOG} -j, --concurrency=1 --coverage ''' +[tasks.dart_unit_test_no_build] +script = ''' +cargo make --profile test-macos-$(uname -m) run_dart_unit_test_no_build +''' + +[tasks.dart_unit_test_no_build.windows] +script = ''' +cargo make --profile test-windows run_dart_unit_test_no_build +''' + +[tasks.dart_unit_test_no_build.linux] +script = ''' +cargo make --profile test-linux run_dart_unit_test_no_build +''' + +[tasks.run_dart_unit_test_no_build] +env = { RUST_LOG = "info" } +dependencies = ["copy-from-build-to-sandbox-folder"] +description = "Run flutter unit tests" +script = ''' +cd appflowy_flutter +flutter test --dart-define=RUST_LOG=${RUST_LOG} -j, --concurrency=1 --coverage +''' +script_runner = "@shell" + [tasks.rust_unit_test] run_task = { name = ["rust_lib_unit_test", "shared_lib_unit_test"] } @@ -323,4 +348,49 @@ script = [ ${dest} """, ] -script_runner = "@duckscript" \ No newline at end of file +script_runner = "@duckscript" + +[tasks.copy-from-build-to-sandbox-folder] +mac_alias = "copy-from-build-to-sandbox-folder-macos" +windows_alias = "copy-from-build-to-sandbox-folder-windows" +linux_alias = "copy-from-build-to-sandbox-folder-linux" + +[tasks.copy-from-build-to-sandbox-folder-windows] +private = true +script = [ + """ + # Copy the appflowy_backend lib to system temp directory for flutter unit test. + lib = set ${LIB_NAME}.${TEST_LIB_EXT} + dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/.sandbox/${lib} + dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/windows/flutter/dart_ffi + rm ${dest} + + cp ${dart_ffi_dir}/${lib} ${dest} + """, +] +script_runner = "@duckscript" + +[tasks.copy-from-build-to-sandbox-folder-macos] +private = true +script = [ + """ + echo "This is not possible for mac because the tests wont work with .a lib" + exit 127 + """, +] +script_runner = "@duckscript" + +[tasks.copy-from-build-to-sandbox-folder-linux] +private = true +script = [ + """ + # Copy the appflowy_backend lib to system temp directory for flutter unit test. + lib = set lib${LIB_NAME}.${TEST_LIB_EXT} + dest = set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/.sandbox/${lib} + dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/linux/flutter/dart_ffi + rm ${dest} + + cp ${dart_ffi_dir}/${lib} ${dest} + """, +] +script_runner = "@duckscript" From 6c3d7d2079cdc3596551dbd756bfa78118436fc4 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:15:28 +0800 Subject: [PATCH 09/17] feat: show checklist items inline in row page (#3737) * feat: show checklist items inline in row page * fix: tauri build --- .../util/database_test_op.dart | 2 +- .../cell/checklist_cell_service.dart | 10 - .../presentation/calendar_event_editor.dart | 5 +- .../card/cells/checklist_card_cell.dart | 3 +- .../widgets/row/accessory/cell_accessory.dart | 10 +- .../cells/checklist_cell/checklist_cell.dart | 203 ++++++++++++++---- .../checklist_cell/checklist_cell_bloc.dart | 116 +++++++--- .../checklist_cell/checklist_cell_editor.dart | 177 ++++++++------- .../checklist_cell_editor_bloc.dart | 176 --------------- .../widgets/row/row_property.dart | 8 +- .../components/database/database_bd_svc.ts | 13 -- frontend/resources/translations/en.json | 6 +- .../rust-lib/event-integration/src/lib.rs | 12 +- .../checklist_entities.rs | 20 -- .../flowy-database2/src/event_handler.rs | 16 +- .../rust-lib/flowy-database2/src/event_map.rs | 6 +- .../src/services/database/database_editor.rs | 11 +- .../tests/database/database_editor.rs | 11 +- .../filter_test/checklist_filter_test.rs | 27 ++- .../tests/database/filter_test/script.rs | 6 +- 20 files changed, 402 insertions(+), 436 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index ce07d0d64b44..c1c9734c708c 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -529,7 +529,7 @@ extension AppFlowyDatabaseTest on WidgetTester { final widget = this.widget(task); assert( - widget.option.data.name == name && widget.option.isSelected == isChecked, + widget.task.data.name == name && widget.task.isSelected == isChecked, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart index dfb9670669b8..3e43d67f3168 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart @@ -1,5 +1,4 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -69,13 +68,4 @@ class ChecklistCellBackendService { return DatabaseEventUpdateChecklistCell(payload).send(); } - - Future> getCellData() { - final payload = CellIdPB.create() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - - return DatabaseEventGetChecklistCellData(payload).send(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart index 1c10a7c00d8e..8ccef33fdf48 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart @@ -187,7 +187,10 @@ class _PropertyCellState extends State { final gesture = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => cell.requestFocus.notify(), - child: AccessoryHover(child: cell), + child: AccessoryHover( + fieldType: widget.cellContext.fieldType, + child: cell, + ), ); return Container( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart index 33d607a24dd4..2d1deb00555a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart @@ -33,10 +33,9 @@ class _ChecklistCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - if (state.allOptions.isEmpty) { + if (state.tasks.isEmpty) { return const SizedBox.shrink(); } - return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: ChecklistProgressBar(percent: state.percent), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart index ef46713b1281..ddf2494df140 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -92,7 +93,12 @@ class _PrimaryCellAccessoryState extends State class AccessoryHover extends StatefulWidget { final CellAccessory child; - const AccessoryHover({required this.child, super.key}); + final FieldType fieldType; + const AccessoryHover({ + super.key, + required this.child, + required this.fieldType, + }); @override State createState() => _AccessoryHoverState(); @@ -106,7 +112,7 @@ class _AccessoryHoverState extends State { final List children = [ DecoratedBox( decoration: BoxDecoration( - color: _isHover + color: _isHover && widget.fieldType != FieldType.Checklist ? AFThemeExtension.of(context).lightGreyHover : Colors.transparent, borderRadius: Corners.s6Border, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart index 8ede7c50f95f..dce1ec7382a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart @@ -1,7 +1,12 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,24 +16,30 @@ import 'checklist_cell_editor.dart'; import 'checklist_progress_bar.dart'; class ChecklistCellStyle extends GridCellStyle { - String placeholder; - EdgeInsets? cellPadding; + final String placeholder; + final EdgeInsets? cellPadding; + final bool showTasksInline; - ChecklistCellStyle({ - required this.placeholder, + const ChecklistCellStyle({ + this.placeholder = "", this.cellPadding, + this.showTasksInline = false, }); } class GridChecklistCell extends GridCellWidget { final CellControllerBuilder cellControllerBuilder; - late final ChecklistCellStyle? cellStyle; + late final ChecklistCellStyle cellStyle; GridChecklistCell({ required this.cellControllerBuilder, GridCellStyle? style, super.key, }) { - cellStyle = style as ChecklistCellStyle?; + if (style != null) { + cellStyle = (style as ChecklistCellStyle); + } else { + cellStyle = const ChecklistCellStyle(); + } } @override @@ -38,14 +49,15 @@ class GridChecklistCell extends GridCellWidget { class GridChecklistCellState extends GridCellState { late ChecklistCellBloc _cellBloc; late final PopoverController _popover; + bool showIncompleteOnly = false; @override void initState() { _popover = PopoverController(); final cellController = widget.cellControllerBuilder.build() as ChecklistCellController; - _cellBloc = ChecklistCellBloc(cellController: cellController); - _cellBloc.add(const ChecklistCellEvent.initial()); + _cellBloc = ChecklistCellBloc(cellController: cellController) + ..add(const ChecklistCellEvent.initial()); super.initState(); } @@ -53,44 +65,153 @@ class GridChecklistCellState extends GridCellState { Widget build(BuildContext context) { return BlocProvider.value( value: _cellBloc, - child: AppFlowyPopover( - margin: EdgeInsets.zero, - controller: _popover, - constraints: BoxConstraints.loose(const Size(360, 400)), - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onCellFocus.value = true; - }); - return GridChecklistCellEditor( - cellController: - widget.cellControllerBuilder.build() as ChecklistCellController, + child: BlocBuilder( + builder: (context, state) { + if (widget.cellStyle.showTasksInline) { + final tasks = List.from(state.tasks); + if (showIncompleteOnly) { + tasks.removeWhere((task) => task.isSelected); + } + final children = tasks + .mapIndexed( + (index, task) => ChecklistItem( + task: task, + autofocus: state.newTask && index == tasks.length - 1, + ), + ) + .toList(); + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: + widget.cellStyle.cellPadding ?? GridSize.cellContentInsets, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ChecklistProgressBar(percent: state.percent), + ), + const HSpace(6.0), + FlowyIconButton( + tooltipText: showIncompleteOnly + ? LocaleKeys.grid_checklist_showComplete.tr() + : LocaleKeys.grid_checklist_hideComplete.tr(), + width: 32, + iconColorOnHover: + Theme.of(context).colorScheme.onSecondary, + icon: FlowySvg( + showIncompleteOnly + ? FlowySvgs.show_m + : FlowySvgs.hide_m, + size: const Size.square(16), + ), + onPressed: () { + setState( + () => showIncompleteOnly = !showIncompleteOnly, + ); + }, + ), + ], + ), + ), + const VSpace(4), + ...children, + const ChecklistItemControl(), + ], + ), + ), + ); + } + + return AppFlowyPopover( + margin: EdgeInsets.zero, + controller: _popover, + constraints: BoxConstraints.loose(const Size(360, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCellFocus.value = true; + }); + return GridChecklistCellEditor( + cellController: widget.cellControllerBuilder.build() + as ChecklistCellController, + ); + }, + onClose: () => widget.onCellFocus.value = false, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: + widget.cellStyle.cellPadding ?? GridSize.cellContentInsets, + child: state.tasks.isEmpty + ? FlowyText.medium( + widget.cellStyle.placeholder, + color: Theme.of(context).hintColor, + ) + : ChecklistProgressBar(percent: state.percent), + ), + ), ); }, - onClose: () => widget.onCellFocus.value = false, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: - widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, - child: BlocBuilder( - builder: (context, state) { - if (state.allOptions.isEmpty) { - return FlowyText.medium( - widget.cellStyle?.placeholder ?? "", - color: Theme.of(context).hintColor, - ); - } - return ChecklistProgressBar(percent: state.percent); - }, - ), - ), - ), ), ); } @override - void requestBeginFocus() => _popover.show(); + void requestBeginFocus() { + if (!widget.cellStyle.showTasksInline) { + _popover.show(); + } + } +} + +class ChecklistItemControl extends StatelessWidget { + const ChecklistItemControl({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), + child: SizedBox( + height: 12, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: Row( + children: [ + const Flexible(child: Center(child: Divider())), + const HSpace(12.0), + FlowyTooltip( + message: LocaleKeys.grid_checklist_addNew.tr(), + child: FilledButton( + style: FilledButton.styleFrom( + minimumSize: const Size.square(12), + maximumSize: const Size.square(12), + padding: EdgeInsets.zero, + ), + onPressed: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: FlowySvg( + FlowySvgs.add_s, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + const HSpace(12.0), + const Flexible(child: Center(child: Divider())), + ], + ), + ), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart index 705557f52c56..b998b50a130c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart @@ -8,13 +8,20 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; part 'checklist_cell_bloc.freezed.dart'; +class ChecklistSelectOption { + final bool isSelected; + final SelectOptionPB data; + + ChecklistSelectOption(this.isSelected, this.data); +} + class ChecklistCellBloc extends Bloc { final ChecklistCellController cellController; - final ChecklistCellBackendService _checklistCellSvc; + final ChecklistCellBackendService _checklistCellService; void Function()? _onCellChangedFn; ChecklistCellBloc({ required this.cellController, - }) : _checklistCellSvc = ChecklistCellBackendService( + }) : _checklistCellService = ChecklistCellBackendService( viewId: cellController.viewId, fieldId: cellController.fieldId, rowId: cellController.rowId, @@ -23,28 +30,43 @@ class ChecklistCellBloc extends Bloc { on( (event, emit) async { await event.when( - initial: () async { + initial: () { _startListening(); - _loadOptions(); }, didReceiveOptions: (data) { if (data == null) { emit( const ChecklistCellState( - allOptions: [], - selectedOptions: [], + tasks: [], percent: 0, + newTask: false, ), ); - } else { - emit( - state.copyWith( - allOptions: data.options, - selectedOptions: data.selectedOptions, - percent: data.percentage, - ), - ); + return; } + + emit( + state.copyWith( + tasks: _makeChecklistSelectOptions(data), + percent: data.percentage, + ), + ); + }, + updateTaskName: (option, name) { + _updateOption(option, name); + }, + selectTask: (option) async { + await _checklistCellService.select(optionId: option.id); + }, + createNewTask: (name) async { + final result = await _checklistCellService.create(name: name); + result.fold( + (l) => emit(state.copyWith(newTask: true)), + (err) => Log.error(err), + ); + }, + deleteTask: (option) async { + await _deleteOption([option]); }, ); }, @@ -63,9 +85,6 @@ class ChecklistCellBloc extends Bloc { void _startListening() { _onCellChangedFn = cellController.startListening( - onCellFieldChanged: () { - _loadOptions(); - }, onCellChanged: (data) { if (!isClosed) { add(ChecklistCellEvent.didReceiveOptions(data)); @@ -74,15 +93,18 @@ class ChecklistCellBloc extends Bloc { ); } - void _loadOptions() { - _checklistCellSvc.getCellData().then((result) { - if (isClosed) return; + void _updateOption(SelectOptionPB option, String name) async { + final result = + await _checklistCellService.updateName(option: option, name: name); - return result.fold( - (data) => add(ChecklistCellEvent.didReceiveOptions(data)), - (err) => Log.error(err), - ); - }); + result.fold((l) => null, (err) => Log.error(err)); + } + + Future _deleteOption(List options) async { + final result = await _checklistCellService.delete( + optionIds: options.map((e) => e.id).toList(), + ); + result.fold((l) => null, (err) => Log.error(err)); } } @@ -92,21 +114,53 @@ class ChecklistCellEvent with _$ChecklistCellEvent { const factory ChecklistCellEvent.didReceiveOptions( ChecklistCellDataPB? data, ) = _DidReceiveCellUpdate; + const factory ChecklistCellEvent.updateTaskName( + SelectOptionPB option, + String name, + ) = _UpdateTaskName; + const factory ChecklistCellEvent.selectTask(SelectOptionPB task) = + _SelectTask; + const factory ChecklistCellEvent.createNewTask(String description) = + _CreateNewTask; + const factory ChecklistCellEvent.deleteTask(SelectOptionPB option) = + _DeleteTask; } @freezed class ChecklistCellState with _$ChecklistCellState { const factory ChecklistCellState({ - required List allOptions, - required List selectedOptions, + required List tasks, required double percent, + required bool newTask, }) = _ChecklistCellState; factory ChecklistCellState.initial(ChecklistCellController cellController) { - return const ChecklistCellState( - allOptions: [], - selectedOptions: [], - percent: 0, + final cellData = cellController.getCellData(loadIfNotExist: true); + + return ChecklistCellState( + tasks: _makeChecklistSelectOptions(cellData), + percent: cellData?.percentage ?? 0, + newTask: false, + ); + } +} + +List _makeChecklistSelectOptions( + ChecklistCellDataPB? data, +) { + if (data == null) { + return []; + } + + final List options = []; + final List allOptions = List.from(data.options); + final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); + + for (final option in allOptions) { + options.add( + ChecklistSelectOption(selectedOptionIds.contains(option.id), option), ); } + + return options; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart index f62f129d7295..fd388d93bf3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'checklist_cell_editor_bloc.dart'; +import 'checklist_cell_bloc.dart'; import 'checklist_progress_bar.dart'; class GridChecklistCellEditor extends StatefulWidget { @@ -22,12 +22,11 @@ class GridChecklistCellEditor extends StatefulWidget { const GridChecklistCellEditor({required this.cellController, super.key}); @override - State createState() => - _GridChecklistCellEditorState(); + State createState() => _GridChecklistCellState(); } -class _GridChecklistCellEditorState extends State { - late ChecklistCellEditorBloc _bloc; +class _GridChecklistCellState extends State { + late ChecklistCellBloc _bloc; /// Focus node for the new task text field late final FocusNode newTaskFocusNode; @@ -45,17 +44,17 @@ class _GridChecklistCellEditorState extends State { return KeyEventResult.ignored; }, ); - _bloc = ChecklistCellEditorBloc(cellController: widget.cellController) - ..add(const ChecklistCellEditorEvent.initial()); + _bloc = ChecklistCellBloc(cellController: widget.cellController) + ..add(const ChecklistCellEvent.initial()); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _bloc, - child: BlocConsumer( + child: BlocConsumer( listener: (context, state) { - if (state.allOptions.isEmpty) { + if (state.tasks.isEmpty) { newTaskFocusNode.requestFocus(); } }, @@ -65,7 +64,7 @@ class _GridChecklistCellEditorState extends State { children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: state.allOptions.isEmpty + child: state.tasks.isEmpty ? const SizedBox.shrink() : Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), @@ -75,10 +74,10 @@ class _GridChecklistCellEditorState extends State { ), ), ChecklistItemList( - options: state.allOptions, + options: state.tasks, onUpdateTask: () => newTaskFocusNode.requestFocus(), ), - if (state.allOptions.isNotEmpty) + if (state.tasks.isNotEmpty) const TypeOptionSeparator(spacing: 0.0), Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -123,11 +122,15 @@ class _ChecklistItemListState extends State { final itemList = widget.options .mapIndexed( - (index, option) => ChecklistItem( - option: option, - onSubmitted: - index == widget.options.length - 1 ? widget.onUpdateTask : null, - key: ValueKey(option.data.id), + (index, option) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ChecklistItem( + task: option, + onSubmitted: index == widget.options.length - 1 + ? widget.onUpdateTask + : null, + key: ValueKey(option.data.id), + ), ), ) .toList(); @@ -147,12 +150,14 @@ class _ChecklistItemListState extends State { /// Represents an existing task @visibleForTesting class ChecklistItem extends StatefulWidget { - final ChecklistSelectOption option; + final ChecklistSelectOption task; final VoidCallback? onSubmitted; + final bool autofocus; const ChecklistItem({ - required this.option, + required this.task, Key? key, this.onSubmitted, + this.autofocus = false, }) : super(key: key); @override @@ -168,7 +173,7 @@ class _ChecklistItemState extends State { @override void initState() { super.initState(); - _textController = TextEditingController(text: widget.option.data.name); + _textController = TextEditingController(text: widget.task.data.name); _focusNode = FocusNode( onKey: (node, event) { if (event is RawKeyDownEvent && @@ -179,72 +184,83 @@ class _ChecklistItemState extends State { return KeyEventResult.ignored; }, ); + if (widget.autofocus) { + _focusNode.requestFocus(); + } + } + + @override + void didUpdateWidget(ChecklistItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.task.data.name != oldWidget.task.data.name && + !_focusNode.hasFocus) { + _textController.text = widget.task.data.name; + } } @override Widget build(BuildContext context) { final icon = FlowySvg( - widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, + widget.task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, blendMode: BlendMode.dst, ); return MouseRegion( onEnter: (event) => setState(() => _isHovered = true), onExit: (event) => setState(() => _isHovered = false), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), - child: DecoratedBox( - decoration: BoxDecoration( - color: _isHovered - ? AFThemeExtension.of(context).lightGreyHover - : Colors.transparent, - borderRadius: Corners.s6Border, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + constraints: BoxConstraints(maxHeight: GridSize.popoverItemHeight), + decoration: BoxDecoration( + color: _isHovered + ? AFThemeExtension.of(context).lightGreyHover + : Colors.transparent, + borderRadius: Corners.s6Border, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowyIconButton( + width: 32, + icon: icon, + hoverColor: Colors.transparent, + onPressed: () => context.read().add( + ChecklistCellEvent.selectTask(widget.task.data), + ), + ), + Expanded( + child: TextField( + controller: _textController, + focusNode: _focusNode, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 1, + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.only( + top: 6.0, + bottom: 6.0, + left: 2.0, + right: _isHovered ? 2.0 : 8.0, + ), + hintText: LocaleKeys.grid_checklist_taskHint.tr(), + ), + onChanged: _debounceOnChangedText, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); + widget.onSubmitted?.call(); + }, + ), + ), + if (_isHovered) FlowyIconButton( width: 32, - icon: icon, + icon: const FlowySvg(FlowySvgs.delete_s), hoverColor: Colors.transparent, - onPressed: () => context.read().add( - ChecklistCellEditorEvent.selectTask(widget.option.data), + iconColorOnHover: Theme.of(context).colorScheme.error, + onPressed: () => context.read().add( + ChecklistCellEvent.deleteTask(widget.task.data), ), ), - Expanded( - child: TextField( - controller: _textController, - focusNode: _focusNode, - style: Theme.of(context).textTheme.bodyMedium, - maxLines: 1, - decoration: InputDecoration( - border: InputBorder.none, - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 2.0, - ), - hintText: LocaleKeys.grid_checklist_taskHint.tr(), - ), - onChanged: _debounceOnChangedText, - onSubmitted: (description) { - _submitUpdateTaskDescription(description); - widget.onSubmitted?.call(); - }, - ), - ), - if (_isHovered) - FlowyIconButton( - width: 32, - icon: const FlowySvg(FlowySvgs.delete_s), - hoverColor: Colors.transparent, - iconColorOnHover: Theme.of(context).colorScheme.error, - onPressed: () => context.read().add( - ChecklistCellEditorEvent.deleteTask(widget.option.data), - ), - ), - ], - ), + ], ), ), ); @@ -258,10 +274,10 @@ class _ChecklistItemState extends State { } void _submitUpdateTaskDescription(String description) { - context.read().add( - ChecklistCellEditorEvent.updateTaskName( - widget.option.data, - description, + context.read().add( + ChecklistCellEvent.updateTaskName( + widget.task.data, + description.trim(), ), ); } @@ -316,8 +332,8 @@ class _NewTaskItemState extends State { ), onSubmitted: (taskDescription) { if (taskDescription.trim().isNotEmpty) { - context.read().add( - ChecklistCellEditorEvent.newTask( + context.read().add( + ChecklistCellEvent.createNewTask( taskDescription.trim(), ), ); @@ -340,11 +356,10 @@ class _NewTaskItemState extends State { fontColor: Theme.of(context).colorScheme.onPrimary, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), onPressed: () { - if (_textEditingController.text.trim().isNotEmpty) { - context.read().add( - ChecklistCellEditorEvent.newTask( - _textEditingController.text..trim(), - ), + final text = _textEditingController.text.trim(); + if (text.isNotEmpty) { + context.read().add( + ChecklistCellEvent.createNewTask(text), ); } widget.focusNode.requestFocus(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart deleted file mode 100644 index 55185a521ceb..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'checklist_cell_editor_bloc.freezed.dart'; - -class ChecklistSelectOption { - final bool isSelected; - final SelectOptionPB data; - - ChecklistSelectOption(this.isSelected, this.data); -} - -class ChecklistCellEditorBloc - extends Bloc { - final ChecklistCellBackendService _checklistCellService; - final ChecklistCellController cellController; - - ChecklistCellEditorBloc({ - required this.cellController, - }) : _checklistCellService = ChecklistCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - super(ChecklistCellEditorState.initial(cellController)) { - on( - (event, emit) async { - await event.when( - initial: () async { - _startListening(); - _loadOptions(); - }, - didReceiveTasks: (data) { - emit( - state.copyWith( - allOptions: _makeChecklistSelectOptions(data), - percent: data?.percentage ?? 0, - ), - ); - }, - newTask: (optionName) async { - await _createOption(optionName); - emit( - state.copyWith( - createOption: Some(optionName), - ), - ); - }, - deleteTask: (option) async { - await _deleteOption([option]); - }, - updateTaskName: (option, name) { - _updateOption(option, name); - }, - selectTask: (option) async { - await _checklistCellService.select(optionId: option.id); - }, - ); - }, - ); - } - - @override - Future close() async { - await cellController.dispose(); - return super.close(); - } - - Future _createOption(String name) async { - final result = await _checklistCellService.create(name: name); - result.fold((l) => {}, (err) => Log.error(err)); - } - - Future _deleteOption(List options) async { - final result = await _checklistCellService.delete( - optionIds: options.map((e) => e.id).toList(), - ); - result.fold((l) => null, (err) => Log.error(err)); - } - - void _updateOption(SelectOptionPB option, String name) async { - final result = - await _checklistCellService.updateName(option: option, name: name); - - result.fold((l) => null, (err) => Log.error(err)); - } - - void _loadOptions() { - _checklistCellService.getCellData().then((result) { - if (isClosed) return; - - return result.fold( - (data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)), - (err) => Log.error(err), - ); - }); - } - - void _startListening() { - cellController.startListening( - onCellChanged: ((data) { - if (!isClosed) { - add(ChecklistCellEditorEvent.didReceiveTasks(data)); - } - }), - onCellFieldChanged: () { - _loadOptions(); - }, - ); - } -} - -@freezed -class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent { - const factory ChecklistCellEditorEvent.initial() = _Initial; - const factory ChecklistCellEditorEvent.didReceiveTasks( - ChecklistCellDataPB? data, - ) = _DidReceiveTasks; - const factory ChecklistCellEditorEvent.newTask(String taskName) = _NewOption; - const factory ChecklistCellEditorEvent.selectTask( - SelectOptionPB option, - ) = _SelectTask; - const factory ChecklistCellEditorEvent.updateTaskName( - SelectOptionPB option, - String name, - ) = _UpdateTaskName; - const factory ChecklistCellEditorEvent.deleteTask(SelectOptionPB option) = - _DeleteTask; -} - -@freezed -class ChecklistCellEditorState with _$ChecklistCellEditorState { - const factory ChecklistCellEditorState({ - required List allOptions, - required Option createOption, - required double percent, - }) = _ChecklistCellEditorState; - - factory ChecklistCellEditorState.initial(ChecklistCellController context) { - final data = context.getCellData(loadIfNotExist: true); - - return ChecklistCellEditorState( - allOptions: _makeChecklistSelectOptions(data), - createOption: none(), - percent: data?.percentage ?? 0, - ); - } -} - -List _makeChecklistSelectOptions( - ChecklistCellDataPB? data, -) { - if (data == null) { - return []; - } - - final List options = []; - final List allOptions = List.from(data.options); - final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); - - for (final option in allOptions) { - options.add( - ChecklistSelectOption(selectedOptionIds.contains(option.id), option), - ); - } - - return options; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index 20a274328a0b..90099a9e71bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -167,7 +167,10 @@ class _PropertyCellState extends State<_PropertyCell> { final gesture = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => cell.requestFocus.notify(), - child: AccessoryHover(child: cell), + child: AccessoryHover( + fieldType: widget.cellContext.fieldType, + child: cell, + ), ); return Container( @@ -271,7 +274,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { case FieldType.Checklist: return ChecklistCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + cellPadding: const EdgeInsets.symmetric(vertical: 6), + showTasksInline: true, ); case FieldType.Number: return GridNumberCellStyle( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts index 48fed99cbefd..518a4f493dfc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts @@ -86,7 +86,6 @@ import { DatabaseEventUpdateCell, DatabaseEventGetSelectOptionCellData, DatabaseEventUpdateSelectOptionCell, - DatabaseEventGetChecklistCellData, DatabaseEventUpdateChecklistCell, DatabaseEventUpdateDateCell, DatabaseEventExportCSV, @@ -623,18 +622,6 @@ export async function updateSelectOptionCell( return result.unwrap(); } -export async function getChecklistCell(viewId: string, rowId: string, fieldId: string): Promise { - const payload = CellIdPB.fromObject({ - view_id: viewId, - row_id: rowId, - field_id: fieldId, - }); - - const result = await DatabaseEventGetChecklistCellData(payload); - - return result.unwrap(); -} - export async function updateChecklistCell( viewId: string, rowId: string, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a9dd5584a619..4c55473840ed 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -533,7 +533,9 @@ "checklist": { "taskHint": "Task description", "addNew": "Add a new task", - "submitNewTask": "Create" + "submitNewTask": "Create", + "hideComplete": "Hide completed tasks", + "showComplete": "Show all tasks" }, "menuName": "Grid", "referencedGridPrefix": "View of" @@ -870,4 +872,4 @@ "weAreSorry": "We're sorry", "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index 18203bd9de04..ea2b28c33dee 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -696,16 +696,8 @@ impl FlowyCoreTest { field_id: &str, row_id: &str, ) -> ChecklistCellDataPB { - EventBuilder::new(self.clone()) - .event(DatabaseEvent::GetChecklistCellData) - .payload(CellIdPB { - view_id: view_id.to_string(), - row_id: row_id.to_string(), - field_id: field_id.to_string(), - }) - .async_send() - .await - .parse::() + let cell = self.get_cell(view_id, row_id, field_id).await; + ChecklistCellDataPB::try_from(Bytes::from(cell.data)).unwrap() } pub async fn update_checklist_cell( diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs index 7f616c935a1a..ac27d959abd8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs @@ -5,7 +5,6 @@ use flowy_error::{ErrorCode, FlowyError}; use crate::entities::parser::NotEmptyStr; use crate::entities::SelectOptionPB; -use crate::services::field::checklist_type_option::ChecklistCellData; use crate::services::field::SelectOption; #[derive(Debug, Clone, Default, ProtoBuf)] @@ -20,25 +19,6 @@ pub struct ChecklistCellDataPB { pub percentage: f64, } -impl From for ChecklistCellDataPB { - fn from(cell_data: ChecklistCellData) -> Self { - let selected_options = cell_data.selected_options(); - let percentage = cell_data.percentage_complete(); - Self { - options: cell_data - .options - .into_iter() - .map(|option| option.into()) - .collect(), - selected_options: selected_options - .into_iter() - .map(|option| option.into()) - .collect(), - percentage, - } - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct ChecklistCellDataChangesetPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 7129193c7c39..02efd04ac3bb 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -596,20 +596,6 @@ pub(crate) async fn update_select_option_cell_handler( Ok(()) } -#[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn get_checklist_cell_data_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - let data = database_editor - .get_checklist_option(params.row_id, ¶ms.field_id) - .await; - data_result_ok(data) -} - #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_checklist_cell_handler( data: AFPluginData, @@ -625,7 +611,7 @@ pub(crate) async fn update_checklist_cell_handler( update_options: params.update_options, }; database_editor - .set_checklist_options(¶ms.view_id, params.row_id, ¶ms.field_id, changeset) + .update_cell_with_changeset(¶ms.view_id, params.row_id, ¶ms.field_id, changeset) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 026d16a8db7b..0d7d8e5d7802 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -50,7 +50,6 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler) .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) // Checklist - .event(DatabaseEvent::GetChecklistCellData, get_checklist_cell_data_handler) .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) // Date .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) @@ -256,11 +255,8 @@ pub enum DatabaseEvent { #[event(input = "SelectOptionCellChangesetPB")] UpdateSelectOptionCell = 72, - #[event(input = "CellIdPB", output = "ChecklistCellDataPB")] - GetChecklistCellData = 73, - #[event(input = "ChecklistCellDataChangesetPB")] - UpdateChecklistCell = 74, + UpdateChecklistCell = 73, /// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB] /// contains the date and the time string. It can be cast to [CellChangesetPB] that diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 88ce9c9f12a9..ed539a137854 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -21,7 +21,7 @@ use crate::services::cell::{ use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database::UpdatedRow; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; -use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; +use crate::services::field::checklist_type_option::ChecklistCellChangeset; use crate::services::field::{ default_type_option_data_from_type, select_type_option_from_field, transform_type_option, type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset, @@ -858,15 +858,6 @@ impl DatabaseEditor { } } - pub async fn get_checklist_option(&self, row_id: RowId, field_id: &str) -> ChecklistCellDataPB { - let row_cell = self.database.lock().get_cell(field_id, &row_id); - let cell_data = match row_cell.cell { - None => ChecklistCellData::default(), - Some(cell) => ChecklistCellData::from(&cell), - }; - ChecklistCellDataPB::from(cell_data) - } - pub async fn set_checklist_options( &self, view_id: &str, diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 125b770cdb48..4b102fe0e96d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -8,7 +8,7 @@ use strum::EnumCount; use event_integration::folder_event::ViewTest; use event_integration::FlowyCoreTest; -use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB, SelectOptionPB}; +use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB}; use flowy_database2::services::cell::{CellBuilder, ToCellChangeset}; use flowy_database2::services::database::DatabaseEditor; use flowy_database2::services::field::checklist_type_option::{ @@ -221,7 +221,7 @@ impl DatabaseEditorTest { pub(crate) async fn set_checklist_cell( &mut self, row_id: RowId, - f: Box) -> Vec>, + selected_options: Vec, ) -> FlowyResult<()> { let field = self .editor @@ -233,13 +233,8 @@ impl DatabaseEditorTest { }) .unwrap() .clone(); - let options = self - .editor - .get_checklist_option(row_id.clone(), &field.id) - .await - .options; let cell_changeset = ChecklistCellChangeset { - selected_option_ids: f(options), + selected_option_ids: selected_options, ..Default::default() }; self diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index 115ecd919ef7..b8abeeca9cb8 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::ChecklistFilterConditionPB; +use flowy_database2::entities::{ChecklistFilterConditionPB, FieldType}; +use flowy_database2::services::field::checklist_type_option::ChecklistCellData; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -8,10 +9,12 @@ async fn grid_filter_checklist_is_incomplete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 6; let row_count = test.row_details.len(); + let option_ids = get_checklist_cell_options(&test).await; + let scripts = vec![ UpdateChecklistCell { row_id: test.row_details[0].row.id.clone(), - f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), + selected_option_ids: option_ids, }, CreateChecklistFilter { condition: ChecklistFilterConditionPB::IsIncomplete, @@ -30,10 +33,11 @@ async fn grid_filter_checklist_is_complete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 1; let row_count = test.row_details.len(); + let option_ids = get_checklist_cell_options(&test).await; let scripts = vec![ UpdateChecklistCell { row_id: test.row_details[0].row.id.clone(), - f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), + selected_option_ids: option_ids, }, CreateChecklistFilter { condition: ChecklistFilterConditionPB::IsComplete, @@ -46,3 +50,20 @@ async fn grid_filter_checklist_is_complete_test() { ]; test.run_scripts(scripts).await; } + +async fn get_checklist_cell_options(test: &DatabaseFilterTest) -> Vec { + let field = test.get_first_field(FieldType::Checklist); + let row_cell = test + .editor + .get_cell(&field.id, &test.row_details[0].row.id) + .await; + row_cell + .map_or_else( + || ChecklistCellData::default(), + |cell| ChecklistCellData::from(&cell), + ) + .options + .into_iter() + .map(|option| option.id) + .collect() +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index 80ca12f72ea8..de07820b6c43 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -30,7 +30,7 @@ pub enum FilterScript { }, UpdateChecklistCell{ row_id: RowId, - f: Box) -> Vec> , + selected_option_ids: Vec, }, UpdateSingleSelectCell { row_id: RowId, @@ -138,8 +138,8 @@ impl DatabaseFilterTest { self.assert_future_changed(changed).await; self.update_text_cell(row_id, &text).await.unwrap(); } - FilterScript::UpdateChecklistCell { row_id, f } => { - self.set_checklist_cell( row_id, f).await.unwrap(); + FilterScript::UpdateChecklistCell { row_id, selected_option_ids } => { + self.set_checklist_cell( row_id, selected_option_ids).await.unwrap(); } FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => { self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); From 56c5d69b0f7f85946a08e560ab6f222a586c6806 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 24 Oct 2023 13:39:05 +0800 Subject: [PATCH 10/17] feat: redefine i18n for editor (#3768) --- .../document/presentation/editor_page.dart | 8 +- .../editor_plugins/actions/option_action.dart | 2 +- .../editor_plugins/header/cover_editor.dart | 2 +- .../editor_plugins/i18n/editor_i18n.dart | 673 ++++++++++++++++++ .../image/image_selection_menu.dart | 2 +- .../table/table_option_action.dart | 2 +- .../lib/startup/tasks/app_widget.dart | 31 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/resources/translations/en.json | 113 +++ 10 files changed, 815 insertions(+), 24 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index f5ac817bb591..252ed4fdbd52 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; @@ -145,6 +146,7 @@ class _AppFlowyEditorPageState extends State { void initState() { super.initState(); + _initEditorL10n(); _initializeShortcuts(); indentableBlockTypes.add(ToggleListBlockKeys.type); convertibleBlockTypes.add(ToggleListBlockKeys.type); @@ -489,7 +491,7 @@ class _AppFlowyEditorPageState extends State { List _customSlashMenuItems() { final items = [...standardSelectionMenuItems]; final imageItem = items.firstWhereOrNull( - (element) => element.name == AppFlowyEditorLocalizations.current.image, + (element) => element.name == AppFlowyEditorL10n.current.image, ); if (imageItem != null) { final imageItemIndex = items.indexOf(imageItem); @@ -620,4 +622,8 @@ class _AppFlowyEditorPageState extends State { return null; }; } + + void _initEditorL10n() { + AppFlowyEditorL10n.current = EditorI18n(); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index 215904790026..75fff9846c65 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -259,7 +259,7 @@ class ColorOptionAction extends PopoverActionCell { ...FlowyTint.values.map( (e) => FlowyColorOption( color: e.color(context), - i18n: e.tintName(AppFlowyEditorLocalizations.current), + i18n: e.tintName(AppFlowyEditorL10n.current), id: e.id, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index bb7c916b90d2..db316a6d30f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -284,7 +284,7 @@ class _ChangeCoverPopoverState extends State { .map( (t) => ColorOption( colorHex: t.color(context).toHex(), - name: t.tintName(AppFlowyEditorLocalizations.current), + name: t.tintName(AppFlowyEditorL10n.current), ), ) .toList(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart new file mode 100644 index 000000000000..8029e2a8705b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart @@ -0,0 +1,673 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class EditorI18n extends AppFlowyEditorL10n { + // static AppFlowyEditorLocalizations current = EditorI18n(); + EditorI18n(); + + @override + String get bold { + return LocaleKeys.editor_bold.tr(); + } + + /// `Bulleted List` + @override + String get bulletedList { + return LocaleKeys.editor_bulletedList.tr(); + } + + /// `Checkbox` + @override + String get checkbox { + return LocaleKeys.editor_checkbox.tr(); + } + + /// `Embed Code` + @override + String get embedCode { + return LocaleKeys.editor_embedCode.tr(); + } + + /// `H1` + @override + String get heading1 { + return LocaleKeys.editor_heading1.tr(); + } + + /// `H2` + @override + String get heading2 { + return LocaleKeys.editor_heading2.tr(); + } + + /// `H3` + @override + String get heading3 { + return LocaleKeys.editor_heading3.tr(); + } + + /// `Highlight` + @override + String get highlight { + return LocaleKeys.editor_highlight.tr(); + } + + /// `Color` + @override + String get color { + return LocaleKeys.editor_color.tr(); + } + + /// `Image` + @override + String get image { + return LocaleKeys.editor_image.tr(); + } + + /// `Italic` + @override + String get italic { + return LocaleKeys.editor_italic.tr(); + } + + /// `Link` + @override + String get link { + return LocaleKeys.editor_link.tr(); + } + + /// `Numbered List` + @override + String get numberedList { + return LocaleKeys.editor_numberedList.tr(); + } + + /// `Quote` + @override + String get quote { + return LocaleKeys.editor_quote.tr(); + } + + /// `Strikethrough` + @override + String get strikethrough { + return LocaleKeys.editor_strikethrough.tr(); + } + + /// `Text` + @override + String get text { + return LocaleKeys.editor_text.tr(); + } + + /// `Underline` + @override + String get underline { + return LocaleKeys.editor_underline.tr(); + } + + /// `Default` + @override + String get fontColorDefault { + return LocaleKeys.editor_fontColorDefault.tr(); + } + + /// `Gray` + @override + String get fontColorGray { + return LocaleKeys.editor_fontColorGray.tr(); + } + + /// `Brown` + @override + String get fontColorBrown { + return LocaleKeys.editor_fontColorBrown.tr(); + } + + /// `Orange` + @override + String get fontColorOrange { + return LocaleKeys.editor_fontColorOrange.tr(); + } + + /// `Yellow` + @override + String get fontColorYellow { + return LocaleKeys.editor_fontColorYellow.tr(); + } + + /// `Green` + @override + String get fontColorGreen { + return LocaleKeys.editor_fontColorGreen.tr(); + } + + /// `Blue` + @override + String get fontColorBlue { + return LocaleKeys.editor_fontColorBlue.tr(); + } + + /// `Purple` + @override + String get fontColorPurple { + return LocaleKeys.editor_fontColorPurple.tr(); + } + + /// `Pink` + @override + String get fontColorPink { + return LocaleKeys.editor_fontColorPink.tr(); + } + + /// `Red` + @override + String get fontColorRed { + return LocaleKeys.editor_fontColorRed.tr(); + } + + /// `Default background` + @override + String get backgroundColorDefault { + return LocaleKeys.editor_backgroundColorDefault.tr(); + } + + /// `Gray background` + @override + String get backgroundColorGray { + return LocaleKeys.editor_backgroundColorGray.tr(); + } + + /// `Brown background` + @override + String get backgroundColorBrown { + return LocaleKeys.editor_backgroundColorBrown.tr(); + } + + /// `Orange background` + @override + String get backgroundColorOrange { + return LocaleKeys.editor_backgroundColorOrange.tr(); + } + + /// `Yellow background` + @override + String get backgroundColorYellow { + return LocaleKeys.editor_backgroundColorYellow.tr(); + } + + /// `Green background` + @override + String get backgroundColorGreen { + return LocaleKeys.editor_backgroundColorGreen.tr(); + } + + /// `Blue background` + @override + String get backgroundColorBlue { + return LocaleKeys.editor_backgroundColorBlue.tr(); + } + + /// `Purple background` + @override + String get backgroundColorPurple { + return LocaleKeys.editor_backgroundColorPurple.tr(); + } + + /// `Pink background` + @override + String get backgroundColorPink { + return LocaleKeys.editor_backgroundColorPink.tr(); + } + + /// `Red background` + @override + String get backgroundColorRed { + return LocaleKeys.editor_backgroundColorRed.tr(); + } + + /// `Done` + @override + String get done { + return LocaleKeys.editor_done.tr(); + } + + /// `Cancel` + @override + String get cancel { + return LocaleKeys.editor_cancel.tr(); + } + + /// `Tint 1` + @override + String get tint1 { + return LocaleKeys.editor_tint1.tr(); + } + + /// `Tint 2` + @override + String get tint2 { + return LocaleKeys.editor_tint2.tr(); + } + + /// `Tint 3` + @override + String get tint3 { + return LocaleKeys.editor_tint3.tr(); + } + + /// `Tint 4` + @override + String get tint4 { + return LocaleKeys.editor_tint4.tr(); + } + + /// `Tint 5` + @override + String get tint5 { + return LocaleKeys.editor_tint5.tr(); + } + + /// `Tint 6` + @override + String get tint6 { + return LocaleKeys.editor_tint6.tr(); + } + + /// `Tint 7` + @override + String get tint7 { + return LocaleKeys.editor_tint7.tr(); + } + + /// `Tint 8` + @override + String get tint8 { + return LocaleKeys.editor_tint8.tr(); + } + + /// `Tint 9` + @override + String get tint9 { + return LocaleKeys.editor_tint9.tr(); + } + + /// `Purple` + @override + String get lightLightTint1 { + return LocaleKeys.editor_lightLightTint1.tr(); + } + + /// `Pink` + @override + String get lightLightTint2 { + return LocaleKeys.editor_lightLightTint2.tr(); + } + + /// `Light Pink` + @override + String get lightLightTint3 { + return LocaleKeys.editor_lightLightTint3.tr(); + } + + /// `Orange` + @override + String get lightLightTint4 { + return LocaleKeys.editor_lightLightTint4.tr(); + } + + /// `Yellow` + @override + String get lightLightTint5 { + return LocaleKeys.editor_lightLightTint5.tr(); + } + + /// `Lime` + @override + String get lightLightTint6 { + return LocaleKeys.editor_lightLightTint6.tr(); + } + + /// `Green` + @override + String get lightLightTint7 { + return LocaleKeys.editor_lightLightTint7.tr(); + } + + /// `Aqua` + @override + String get lightLightTint8 { + return LocaleKeys.editor_lightLightTint8.tr(); + } + + /// `Blue` + @override + String get lightLightTint9 { + return LocaleKeys.editor_lightLightTint9.tr(); + } + + /// `URL` + @override + String get urlHint { + return LocaleKeys.editor_urlHint.tr(); + } + + /// `Heading 1` + @override + String get mobileHeading1 { + return LocaleKeys.editor_mobileHeading1.tr(); + } + + /// `Heading 2` + @override + String get mobileHeading2 { + return LocaleKeys.editor_mobileHeading2.tr(); + } + + /// `Heading 3` + @override + String get mobileHeading3 { + return LocaleKeys.editor_mobileHeading3.tr(); + } + + /// `Text Color` + @override + String get textColor { + return LocaleKeys.editor_textColor.tr(); + } + + /// `Background Color` + @override + String get backgroundColor { + return LocaleKeys.editor_backgroundColor.tr(); + } + + /// `Add your link` + @override + String get addYourLink { + return LocaleKeys.editor_addYourLink.tr(); + } + + /// `Open link` + @override + String get openLink { + return LocaleKeys.editor_openLink.tr(); + } + + /// `Copy link` + @override + String get copyLink { + return LocaleKeys.editor_copyLink.tr(); + } + + /// `Remove link` + @override + String get removeLink { + return LocaleKeys.editor_removeLink.tr(); + } + + /// `Edit link` + @override + String get editLink { + return LocaleKeys.editor_editLink.tr(); + } + + /// `Text` + @override + String get linkText { + return LocaleKeys.editor_linkText.tr(); + } + + /// `Please enter text` + @override + String get linkTextHint { + return LocaleKeys.editor_linkTextHint.tr(); + } + + /// `Please enter URL` + @override + String get linkAddressHint { + return LocaleKeys.editor_linkAddressHint.tr(); + } + + /// `Highlight color` + @override + String get highlightColor { + return LocaleKeys.editor_highlightColor.tr(); + } + + /// `Clear highlight color` + @override + String get clearHighlightColor { + return LocaleKeys.editor_clearHighlightColor.tr(); + } + + /// `Custom color` + @override + String get customColor { + return LocaleKeys.editor_customColor.tr(); + } + + /// `Hex value` + @override + String get hexValue { + return LocaleKeys.editor_hexValue.tr(); + } + + /// `Opacity` + @override + String get opacity { + return LocaleKeys.editor_opacity.tr(); + } + + /// `Reset to default color` + @override + String get resetToDefaultColor { + return LocaleKeys.editor_resetToDefaultColor.tr(); + } + + /// `LTR` + @override + String get ltr { + return LocaleKeys.editor_ltr.tr(); + } + + /// `RTL` + @override + String get rtl { + return LocaleKeys.editor_rtl.tr(); + } + + /// `Auto` + @override + String get auto { + return LocaleKeys.editor_auto.tr(); + } + + /// `Cut` + @override + String get cut { + return LocaleKeys.editor_cut.tr(); + } + + /// `Copy` + @override + String get copy { + return LocaleKeys.editor_copy.tr(); + } + + /// `Paste` + @override + String get paste { + return LocaleKeys.editor_paste.tr(); + } + + /// `Find` + @override + String get find { + return LocaleKeys.editor_find.tr(); + } + + /// `Previous match` + @override + String get previousMatch { + return LocaleKeys.editor_previousMatch.tr(); + } + + /// `Next match` + @override + String get nextMatch { + return LocaleKeys.editor_nextMatch.tr(); + } + + /// `Close` + @override + String get closeFind { + return LocaleKeys.editor_closeFind.tr(); + } + + /// `Replace` + @override + String get replace { + return LocaleKeys.editor_replace.tr(); + } + + /// `Replace all` + @override + String get replaceAll { + return LocaleKeys.editor_replaceAll.tr(); + } + + /// `Regex` + @override + String get regex { + return LocaleKeys.editor_regex.tr(); + } + + /// `Case sensitive` + @override + String get caseSensitive { + return LocaleKeys.editor_caseSensitive.tr(); + } + + /// `Upload Image` + @override + String get uploadImage { + return LocaleKeys.editor_uploadImage.tr(); + } + + /// `URL Image` + @override + String get urlImage { + return LocaleKeys.editor_urlImage.tr(); + } + + /// `Incorrect Link` + @override + String get incorrectLink { + return LocaleKeys.editor_incorrectLink.tr(); + } + + /// `Upload` + @override + String get upload { + return LocaleKeys.editor_upload.tr(); + } + + /// `Choose an image` + @override + String get chooseImage { + return LocaleKeys.editor_chooseImage.tr(); + } + + /// `Loading` + @override + String get loading { + return LocaleKeys.editor_loading.tr(); + } + + /// `Could not load the image` + @override + String get imageLoadFailed { + return LocaleKeys.editor_imageLoadFailed.tr(); + } + + /// `Divider` + @override + String get divider { + return LocaleKeys.editor_divider.tr(); + } + + /// `Table` + @override + String get table { + return LocaleKeys.editor_table.tr(); + } + + /// `Add before` + @override + String get colAddBefore { + return LocaleKeys.editor_colAddBefore.tr(); + } + + /// `Add before` + @override + String get rowAddBefore { + return LocaleKeys.editor_rowAddBefore.tr(); + } + + /// `Add after` + @override + String get colAddAfter { + return LocaleKeys.editor_colAddAfter.tr(); + } + + /// `Add after` + @override + String get rowAddAfter { + return LocaleKeys.editor_rowAddAfter.tr(); + } + + /// `Remove` + @override + String get colRemove { + return LocaleKeys.editor_colRemove.tr(); + } + + /// `Remove` + @override + String get rowRemove { + return LocaleKeys.editor_rowRemove.tr(); + } + + /// `Duplicate` + @override + String get colDuplicate { + return LocaleKeys.editor_colDuplicate.tr(); + } + + /// `Duplicate` + @override + String get rowDuplicate { + return LocaleKeys.editor_rowDuplicate.tr(); + } + + /// `Clear Content` + @override + String get colClear { + return LocaleKeys.editor_colClear.tr(); + } + + /// `Clear Content` + @override + String get rowClear { + return LocaleKeys.editor_rowClear.tr(); + } + + /// `Enter a / to insert a block, or start typing` + @override + String get slashPlaceHolder { + return LocaleKeys.editor_slashPlaceHolder.tr(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart index ccaa14694ead..9a1d98a11eae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart @@ -4,7 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:flutter/material.dart'; final customImageMenuItem = SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.image, + name: AppFlowyEditorL10n.current.image, icon: (editorState, isSelected, style) => SelectionMenuIconWidget( name: 'image', isSelected: isSelected, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart index bc3529508571..c3d7cbf35be8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart @@ -127,7 +127,7 @@ class TableColorOptionAction extends PopoverActionCell { ...FlowyTint.values.map( (e) => FlowyColorOption( color: e.color(context), - i18n: e.tintName(AppFlowyEditorLocalizations.current), + i18n: e.tintName(AppFlowyEditorL10n.current), id: e.id, ), ), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 9a0d8ef03918..baa24c0fe559 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -1,23 +1,20 @@ +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/user_settings_service.dart'; +import 'package:appflowy/workspace/application/notifications/notification_service.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; - -import 'prelude.dart'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:go_router/go_router.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; -import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; -import 'package:appflowy/workspace/application/notifications/notification_service.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/startup/startup.dart'; +import 'prelude.dart'; class InitAppWidgetTask extends LaunchTask { const InitAppWidgetTask(); @@ -144,8 +141,10 @@ class _ApplicationWidgetState extends State { theme: state.lightTheme, darkTheme: state.darkTheme, themeMode: state.themeMode, - localizationsDelegates: context.localizationDelegates + - [AppFlowyEditorLocalizations.delegate], + localizationsDelegates: [ + ...context.localizationDelegates, + AppFlowyEditorLocalizations.delegate + ], supportedLocales: context.supportedLocales, locale: state.locale, routerConfig: routerConfig, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index a951b447788f..758831789269 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,8 +54,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9ae85ea" - resolved-ref: "9ae85ea162606b79483c49550266c154c0cb500c" + ref: "6d163b8" + resolved-ref: "6d163b88976f6481c4eea5e91c0ed4d68378e56f" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "1.4.4" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index dcb81df99f15..3260ab2032e3 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "9ae85ea" + ref: "6d163b8" appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4c55473840ed..05929b0fa3d8 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -871,5 +871,118 @@ "error": { "weAreSorry": "We're sorry", "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." + }, + "editor": { + "bold": "Bold", + "bulletedList": "Bulleted List", + "checkbox": "Checkbox", + "embedCode": "Embed Code", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "Highlight", + "color": "Color", + "image": "Image", + "italic": "Italic", + "link": "Link", + "numberedList": "Numbered List", + "quote": "Quote", + "strikethrough": "Strikethrough", + "text": "Text", + "underline": "Underline", + "fontColorDefault": "Default", + "fontColorGray": "Gray", + "fontColorBrown": "Brown", + "fontColorOrange": "Orange", + "fontColorYellow": "Yellow", + "fontColorGreen": "Green", + "fontColorBlue": "Blue", + "fontColorPurple": "Purple", + "fontColorPink": "Pink", + "fontColorRed": "Red", + "backgroundColorDefault": "Default background", + "backgroundColorGray": "Gray background", + "backgroundColorBrown": "Brown background", + "backgroundColorOrange": "Orange background", + "backgroundColorYellow": "Yellow background", + "backgroundColorGreen": "Green background", + "backgroundColorBlue": "Blue background", + "backgroundColorPurple": "Purple background", + "backgroundColorPink": "Pink background", + "backgroundColorRed": "Red background", + "done": "Done", + "cancel": "Cancel", + "tint1": "Tint 1", + "tint2": "Tint 2", + "tint3": "Tint 3", + "tint4": "Tint 4", + "tint5": "Tint 5", + "tint6": "Tint 6", + "tint7": "Tint 7", + "tint8": "Tint 8", + "tint9": "Tint 9", + "lightLightTint1": "Purple", + "lightLightTint2": "Pink", + "lightLightTint3": "Light Pink", + "lightLightTint4": "Orange", + "lightLightTint5": "Yellow", + "lightLightTint6": "Lime", + "lightLightTint7": "Green", + "lightLightTint8": "Aqua", + "lightLightTint9": "Blue", + "urlHint": "URL", + "mobileHeading1": "Heading 1", + "mobileHeading2": "Heading 2", + "mobileHeading3": "Heading 3", + "textColor": "Text Color", + "backgroundColor": "Background Color", + "addYourLink": "Add your link", + "openLink": "Open link", + "copyLink": "Copy link", + "removeLink": "Remove link", + "editLink": "Edit link", + "linkText": "Text", + "linkTextHint": "Please enter text", + "linkAddressHint": "Please enter URL", + "highlightColor": "Highlight color", + "clearHighlightColor": "Clear highlight color", + "customColor": "Custom color", + "hexValue": "Hex value", + "opacity": "Opacity", + "resetToDefaultColor": "Reset to default color", + "ltr": "LTR", + "rtl": "RTL", + "auto": "Auto", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "find": "Find", + "previousMatch": "Previous match", + "nextMatch": "Next match", + "closeFind": "Close", + "replace": "Replace", + "replaceAll": "Replace all", + "regex": "Regex", + "caseSensitive": "Case sensitive", + "uploadImage": "Upload Image", + "urlImage": "URL Image", + "incorrectLink": "Incorrect Link", + "upload": "Upload", + "chooseImage": "Choose an image", + "loading": "Loading", + "imageLoadFailed": "Could not load the image", + "divider": "Divider", + "table": "Table", + "colAddBefore": "Add before", + "rowAddBefore": "Add before", + "colAddAfter": "Add after", + "rowAddAfter": "Add after", + "colRemove": "Remove", + "rowRemove": "Remove", + "colDuplicate": "Duplicate", + "rowDuplicate": "Duplicate", + "colClear": "Clear Content", + "rowClear": "Clear Content", + "slashPlaceHolder": "Enter a / to insert a block, or start typing" } } \ No newline at end of file From a84c47dc33081c8feae37073b4c4f3a8fb763ea3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 24 Oct 2023 13:52:55 +0800 Subject: [PATCH 11/17] feat: android deeplink and update icons (#3769) * feat: android deeplink and update icons * chore: remove integration test --- .github/workflows/integration_test.yml | 150 ------------------ frontend/.vscode/launch.json | 4 +- .../android/app/src/main/AndroidManifest.xml | 12 +- .../android/app/src/main/CMakeLists.txt | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 5494 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 3569 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 7716 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 11992 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 16583 bytes 9 files changed, 14 insertions(+), 154 deletions(-) delete mode 100644 .github/workflows/integration_test.yml diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml deleted file mode 100644 index 76460117222b..000000000000 --- a/.github/workflows/integration_test.yml +++ /dev/null @@ -1,150 +0,0 @@ -name: integration test - -on: - push: - branches: - - "main" - - "release/*" - paths: - - ".github/workflows/integration_test.yml" - - "frontend/**" - - "!frontend/appflowy_tauri/**" - - pull_request: - branches: - - "main" - - "release/*" - paths: - - ".github/workflows/integration_test.yml" - - "frontend/**" - - "!frontend/appflowy_tauri/**" - -env: - CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.10.1" - RUST_TOOLCHAIN: "1.70" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.event.pull_request.draft != true - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] - include: - - os: ubuntu-latest - flutter_profile: development-linux-x86_64 - target: x86_64-unknown-linux-gnu - - os: windows-latest - flutter_profile: development-windows-x86 - target: x86_64-pc-windows-msvc - runs-on: ${{ matrix.os }} - - steps: - # the following step is required to avoid running out of space - - name: Maximize build space - if: matrix.os == 'ubuntu-latest' - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - df -h - - - name: Checkout source code - uses: actions/checkout@v2 - - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - target: ${{ matrix.target }} - override: true - profile: minimal - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.os }} - workspaces: | - frontend/rust-lib - cache-all-crates: true - - - uses: davidB/rust-cargo-make@v1 - with: - version: '0.36.6' - - - name: Install prerequisites - working-directory: frontend - run: | - cargo install --force duckscript_cli - if [ "$RUNNER_OS" == "Linux" ]; then - sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub - sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list - sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev - sudo apt-get install keybinder-3.0 libnotify-dev - elif [ "$RUNNER_OS" == "Windows" ]; then - vcpkg integrate install - elif [ "$RUNNER_OS" == "macOS" ]; then - echo 'do nothing' - fi - cargo make appflowy-flutter-deps-tools - shell: bash - - - name: 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 - - - name: Build AppFlowy - working-directory: frontend - run: | - cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev - - - name: Run Flutter integration tests - working-directory: frontend/appflowy_flutter - run: | - if [ "$RUNNER_OS" == "Linux" ]; then - export DISPLAY=:99 - sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & - sudo apt-get install network-manager - flutter test integration_test/runner.dart -d Linux --coverage - elif [ "$RUNNER_OS" == "macOS" ]; then - flutter test integration_test/runner.dart -d macOS --coverage - elif [ "$RUNNER_OS" == "Windows" ]; then - flutter test integration_test/runner.dart -d Windows --coverage - 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_integrateion_test - fail_ci_if_error: true - verbose: true - os: ${{ matrix.os }} - token: ${{ secrets.CODECOV_TOKEN }} - attempt_limit: 20 - attempt_delay: 10000 \ No newline at end of file diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 9d7a00c4497f..34866a9537ce 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -67,11 +67,11 @@ "cwd": "${workspaceRoot}/appflowy_flutter" }, { - "name": "AF-Android-Simlator: Clean + Rebuild All", + "name": "AF-Android: Clean + Rebuild All", "request": "launch", "program": "./lib/main.dart", "type": "dart", - "preLaunchTask": "AF: Clean + Rebuild All (Android Simulator)", + "preLaunchTask": "AF: Clean + Rebuild All (Android)", "env": { "RUST_LOG": "trace" }, diff --git a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index 03eb9bc4dacf..43f4005b72ec 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + + + + + + + + + +