From e6a52e27e4e19afd4086d8dab488c0b0632a49e6 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 18 Sep 2024 16:19:21 +0500 Subject: [PATCH] mobile: improve attachments manager --- .../common/filesystem/download-attachment.js | 160 +++--- .../app/components/attachments/actions.tsx | 12 +- .../attachments/attachment-item.tsx | 41 +- .../attachments/download-attachments.tsx | 297 ---------- .../app/components/attachments/index.tsx | 527 ++++++++++++------ apps/mobile/app/components/header/index.tsx | 2 - .../app/components/header/right-menus.tsx | 3 +- .../app/screens/settings/components.tsx | 24 +- apps/mobile/app/screens/settings/group.tsx | 18 +- .../settings/offline-mode-progress.tsx | 124 +++++ .../app/screens/settings/settings-data.tsx | 10 +- apps/mobile/app/screens/settings/types.ts | 1 + apps/mobile/app/services/event-manager.ts | 1 + .../mobile/app/stores/use-attachment-store.ts | 39 +- 14 files changed, 654 insertions(+), 605 deletions(-) delete mode 100644 apps/mobile/app/components/attachments/download-attachments.tsx create mode 100644 apps/mobile/app/screens/settings/offline-mode-progress.tsx diff --git a/apps/mobile/app/common/filesystem/download-attachment.js b/apps/mobile/app/common/filesystem/download-attachment.js index e5b82cdcb..444445523 100644 --- a/apps/mobile/app/common/filesystem/download-attachment.js +++ b/apps/mobile/app/common/filesystem/download-attachment.js @@ -54,15 +54,10 @@ export async function downloadAllAttachments() { * @param onProgress * @returns */ -export async function downloadAttachments( - attachments, - onProgress, - canceled, - groupId -) { +export async function downloadAttachments(attachments) { await createCacheDir(); if (!attachments || !attachments.length) return; - const result = new Map(); + const groupId = `download-all-${Date.now()}`; let outputFolder; if (Platform.OS === "android") { @@ -86,92 +81,117 @@ export async function downloadAttachments( await RNFetchBlob.fs.mkdir(zipSourceFolder); + const isCancelled = () => { + if (useAttachmentStore.getState().downloading[groupId]?.canceled) { + RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); + useAttachmentStore.getState().setDownloading({ + groupId, + current: 0, + total: 0, + success: false, + message: "Download cancelled", + canceled: true + }); + return true; + } + }; + for (let i = 0; i < attachments.length; i++) { + if (isCancelled()) return; let attachment = await db.attachments.attachment(attachments[i]); const hash = attachment.hash; try { - if (canceled.current) { - RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); - return; - } - onProgress?.( - i + 1 / attachments.length, - `Downloading attachments (${i + 1}/${ - attachments.length - })... Please wait` - ); + useAttachmentStore.getState().setDownloading({ + groupId: groupId, + current: i + 1, + total: attachments.length, + filename: attachment.hash + }); // Download to cache let uri = await downloadAttachment(hash, false, { silent: true, cache: true, groupId: groupId }); - if (canceled.current) { - RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); - return; - } + + if (isCancelled()) return; + if (!uri) throw new Error("Failed to download file"); // Move file to the source folder we will zip eventually and rename the file to it's actual name. const filePath = `${zipSourceFolder}/${attachment.filename}`; await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath); - result.set(hash, { - filename: attachment.filename, - status: FileDownloadStatus.Success, - attachment: attachment - }); } catch (e) { - result.set(hash, { - filename: attachment.filename, - status: FileDownloadStatus.Fail, - reason: e - }); - ToastManager.error(e, "Error downloading attachment"); + DatabaseLogger.error(e); } } - if (canceled.current) { - RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); - return; - } - if (result?.size) { - let sub; - try { - onProgress?.(0, `Zipping... Please wait`); - // If all goes well, zip the notesnook-attachments folder in cache. - sub = subscribe(({ progress }) => { - onProgress( - progress, - `Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait` - ); + useAttachmentStore.getState().setDownloading({ + groupId: groupId, + current: 0, + total: 0, + success: true + }); + + if (isCancelled()) return; + + let sub; + try { + useAttachmentStore.getState().setDownloading({ + current: 0, + total: 1, + message: "Saving zip file... Please wait", + groupId + }); + // If all goes well, zip the notesnook-attachments folder in cache. + + sub = subscribe(({ progress }) => { + useAttachmentStore.getState().setDownloading({ + groupId, + current: progress, + total: 1, + message: `Saving zip file (${(progress * 100).toFixed( + 1 + )}%)... Please wait` }); - await zip(zipSourceFolder, zipOutputFile); - sub?.remove(); - onProgress(1, `Saving zip file... Please wait`); - if (Platform.OS === "android") { - // Move the zip to user selected directory. - const file = await ScopedStorage.createFile( - outputFolder, - `notesnook-attachments-${Date.now()}.zip`, - "application/zip" - ); - await copyFileAsync(`file://${zipOutputFile}`, file.uri); - } + }); + await zip(zipSourceFolder, zipOutputFile); + sub?.remove(); - onProgress?.(1, `Done`); - releasePermissions(outputFolder); - } catch (e) { - releasePermissions(outputFolder); - sub?.remove(); - ToastManager.error(e, "Error zipping attachments"); - } - // Remove source & zip file from cache. - RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); if (Platform.OS === "android") { - RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log); + // Move the zip to user selected directory. + const file = await ScopedStorage.createFile( + outputFolder, + `notesnook-attachments-${Date.now()}.zip`, + "application/zip" + ); + await copyFileAsync(`file://${zipOutputFile}`, file.uri); } - } - return result; + useAttachmentStore.getState().setDownloading({ + current: 0, + total: 0, + message: undefined, + success: true, + groupId + }); + releasePermissions(outputFolder); + } catch (e) { + useAttachmentStore.getState().setDownloading({ + current: 0, + total: 0, + message: undefined, + success: true, + groupId + }); + releasePermissions(outputFolder); + sub?.remove(); + ToastManager.error(e, "Error zipping attachments"); + } + // Remove source & zip file from cache. + RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log); + if (Platform.OS === "android") { + RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log); + } } export default async function downloadAttachment( diff --git a/apps/mobile/app/components/attachments/actions.tsx b/apps/mobile/app/components/attachments/actions.tsx index 2511af137..22b903795 100644 --- a/apps/mobile/app/components/attachments/actions.tsx +++ b/apps/mobile/app/components/attachments/actions.tsx @@ -30,6 +30,8 @@ import filesystem from "../../common/filesystem"; import downloadAttachment from "../../common/filesystem/download-attachment"; import { useAttachmentProgress } from "../../hooks/use-attachment-progress"; import picker from "../../screens/editor/tiptap/picker"; +import { useTabStore } from "../../screens/editor/tiptap/use-tab-store"; +import { editorController } from "../../screens/editor/tiptap/utils"; import { ToastManager, eSendEvent, @@ -49,14 +51,12 @@ import { Dialog } from "../dialog"; import { presentDialog } from "../dialog/functions"; import { openNote } from "../list-items/note/wrapper"; import { DateMeta } from "../properties/date-meta"; +import SheetProvider from "../sheet-provider"; import { Button } from "../ui/button"; import { Notice } from "../ui/notice"; import { Pressable } from "../ui/pressable"; import Heading from "../ui/typography/heading"; import Paragraph from "../ui/typography/paragraph"; -import { useTabStore } from "../../screens/editor/tiptap/use-tab-store"; -import { editorController } from "../../screens/editor/tiptap/utils"; -import SheetProvider from "../sheet-provider"; const Actions = ({ attachment, @@ -88,7 +88,7 @@ const Actions = ({ useAttachmentStore.getState().remove(attachment.hash); } downloadAttachment(attachment.hash, false); - eSendEvent(eCloseSheet, contextId); + fwdRef.current?.hide(); }, icon: "download" }, @@ -149,9 +149,8 @@ const Actions = ({ }, { name: "Rename", - onPress: () => { + onPress: async () => { presentDialog({ - context: contextId as any, input: true, title: "Rename file", paragraph: "Enter a new name for the file", @@ -219,6 +218,7 @@ const Actions = ({ style={{ maxHeight: "100%" }} + keyboardShouldPersistTaps="never" > diff --git a/apps/mobile/app/components/attachments/attachment-item.tsx b/apps/mobile/app/components/attachments/attachment-item.tsx index 357e846ce..44edb2d1f 100644 --- a/apps/mobile/app/components/attachments/attachment-item.tsx +++ b/apps/mobile/app/components/attachments/attachment-item.tsx @@ -22,7 +22,6 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core"; import { useThemeColors } from "@notesnook/theme"; import React from "react"; import { TouchableOpacity, View } from "react-native"; -import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import { db } from "../../common/database"; import { useAttachmentProgress } from "../../hooks/use-attachment-progress"; import { useDBItem } from "../../hooks/use-db-item"; @@ -44,7 +43,8 @@ export const AttachmentItem = ({ setAttachments, pressable = true, hideWhenNotDownloading, - context + context, + errorOnly }: { id: string | number; attachments?: VirtualizedGrouping; @@ -53,9 +53,9 @@ export const AttachmentItem = ({ pressable?: boolean; hideWhenNotDownloading?: boolean; context?: string; + errorOnly?: boolean; }) => { const [attachment] = useDBItem(id, "attachment", attachments); - const { colors } = useThemeColors(); const [currentProgress, setCurrentProgress] = useAttachmentProgress( attachment, @@ -67,8 +67,7 @@ export const AttachmentItem = ({ Actions.present(attachment, setAttachments, context); }; - return hideWhenNotDownloading && - (!currentProgress || !currentProgress.value) ? null : ( + return errorOnly && attachment && !attachment?.failed ? null : ( @@ -96,20 +93,26 @@ export const AttachmentItem = ({ style={{ justifyContent: "center", alignItems: "center", - marginLeft: -5 + marginLeft: -5, + borderWidth: 1, + borderColor: colors.secondary.border, + paddingHorizontal: 2, + minWidth: 20, + height: 30, + borderRadius: 5 }} > - - - {getFileExtension(attachment.filename).toUpperCase()} + {getFileExtension(attachment.filename).toUpperCase() || + attachment.mimeType.split("/")?.[1]?.toUpperCase()} @@ -120,10 +123,9 @@ export const AttachmentItem = ({ }} > - {formatBytes(attachment.size)}{" "} - {currentProgress?.type - ? "(" + currentProgress.type + "ing - tap to cancel)" - : ""} + File size: {formatBytes(attachment.size)} ) : null} diff --git a/apps/mobile/app/components/attachments/download-attachments.tsx b/apps/mobile/app/components/attachments/download-attachments.tsx deleted file mode 100644 index b66995efd..000000000 --- a/apps/mobile/app/components/attachments/download-attachments.tsx +++ /dev/null @@ -1,297 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -import React, { useRef, useState } from "react"; -import { Platform, View } from "react-native"; -import { db } from "../../common/database"; -import { downloadAttachments } from "../../common/filesystem/download-attachment"; -import { - PresentSheetOptions, - presentSheet -} from "../../services/event-manager"; -import { Button } from "../ui/button"; -import Heading from "../ui/typography/heading"; -import Paragraph from "../ui/typography/paragraph"; -import { ProgressBarComponent } from "../ui/svg/lazy"; -import { useThemeColors } from "@notesnook/theme"; -import { FlatList } from "react-native-actions-sheet"; -import { AttachmentItem } from "./attachment-item"; -import { Attachment, VirtualizedGrouping } from "@notesnook/core"; - -const DownloadAttachments = ({ - close, - attachments, - isNote, - update -}: { - attachments: VirtualizedGrouping; - close?: ((ctx?: string | undefined) => void) | undefined; - isNote?: boolean; - update?: (props: PresentSheetOptions) => void; -}) => { - const { colors } = useThemeColors(); - const [downloading, setDownloading] = useState(false); - const [progress, setProgress] = useState({ - value: 0, - statusText: "Download started... Please wait" - }); - const [result, setResult] = useState(new Map()); - const canceled = useRef(false); - const groupId = useRef(); - - const onDownload = async () => { - update?.({ - disableClosing: true - } as PresentSheetOptions); - setDownloading(true); - canceled.current = false; - groupId.current = Date.now().toString(); - const result = await downloadAttachments( - await attachments.ids(), - (progress: number, statusText: string) => - setProgress({ value: progress, statusText }), - canceled, - groupId.current - ); - if (canceled.current) return; - setResult(result || new Map()); - setDownloading(false); - update?.({ - disableClosing: false - } as PresentSheetOptions); - }; - - const cancel = async () => { - update?.({ - disableClosing: false - } as PresentSheetOptions); - canceled.current = true; - if (!groupId.current) return; - console.log(groupId.current, "canceling groupId downloads"); - await db.fs().cancel(groupId.current); - setDownloading(false); - setResult(new Map()); - groupId.current = undefined; - }; - - const failedResults = () => { - const results = []; - for (const value of result.values()) { - if (value.status === 0) results.push(value.attachment); - } - return results; - }; - - function getResultText() { - const downloadedAttachmentsCount = - attachments?.placeholders?.length - failedResults().length; - if (downloadedAttachmentsCount === 0) - return "Failed to download all attachments"; - return `Successfully downloaded ${downloadedAttachmentsCount}/${ - attachments?.placeholders.length - } attachments as a zip file at ${ - Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads" - }`; - } - - return ( - - - {downloading - ? "Downloading attachments" - : result?.size - ? "Downloaded attachments" - : "Download attachments"} - - - {downloading ? ( - - {progress.statusText} - - ) : result?.size ? ( - - {getResultText()} - - ) : ( - - Are you sure you want to download all attachments - {isNote ? " of this note?" : "?"} - - )} - - {downloading ? ( - - - - ) : null} - - - - No downloads in progress. - - - } - keyExtractor={(index) => "attachment_download" + index} - renderItem={({ index }) => { - return ( - {}} - pressable={false} - hideWhenNotDownloading={true} - attachments={attachments} - /> - ); - }} - /> - - {result?.size ? ( -