diff --git a/apps/web/src/common/multi-select.ts b/apps/web/src/common/multi-select.ts
index 98e3cce99..3e4a5cc38 100644
--- a/apps/web/src/common/multi-select.ts
+++ b/apps/web/src/common/multi-select.ts
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { showMultiDeleteConfirmation } from "./dialog-controller";
+import { confirm, showMultiDeleteConfirmation } from "./dialog-controller";
import { store as noteStore } from "../stores/note-store";
import { store as notebookStore } from "../stores/notebook-store";
import { store as attachmentStore } from "../stores/attachment-store";
@@ -27,12 +27,7 @@ import { showToast } from "../utils/toast";
import Vault from "./vault";
import { TaskManager } from "./task-manager";
import { pluralize } from "@notesnook/common";
-
-type Item = {
- id: string;
- locked?: boolean;
- metadata?: Record;
-};
+import { Reminder } from "@notesnook/core";
async function moveNotesToTrash(ids: string[], confirm = true) {
if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return;
@@ -86,27 +81,13 @@ async function moveNotebooksToTrash(ids: string[]) {
showToast("success", `${pluralize(ids.length, "notebook")} moved to trash`);
}
-async function deleteTopics(notebookId: string, topics: Item[]) {
- await TaskManager.startTask({
- type: "status",
- id: "deleteTopics",
- action: async (report) => {
- report({
- text: `Deleting ${pluralize(topics.length, "topic")}...`
- });
- await db.notebooks.topics(notebookId).delete(...topics.map((t) => t.id));
- notebookStore.setSelectedNotebook(notebookId);
- noteStore.refresh();
- }
- });
- showToast("success", `${pluralize(topics.length, "topic")} deleted`);
-}
-
-async function deleteAttachments(attachments: Item[]) {
+async function deleteAttachments(ids: string[]) {
if (
- !window.confirm(
- "Are you sure you want to permanently delete these attachments? This action is IRREVERSIBLE."
- )
+ !(await confirm({
+ title: "Are you sure?",
+ message:
+ "Are you sure you want to permanently delete these attachments? This action is IRREVERSIBLE."
+ }))
)
return;
@@ -114,24 +95,24 @@ async function deleteAttachments(attachments: Item[]) {
type: "status",
id: "deleteAttachments",
action: async (report) => {
- for (let i = 0; i < attachments.length; ++i) {
- const attachment = attachments[i];
+ for (let i = 0; i < ids.length; ++i) {
+ const id = ids[i];
+ const attachment = await attachmentStore.get().attachments?.item(id);
+ if (!attachment) continue;
+
report({
- text: `Deleting ${pluralize(attachments.length, "attachment")}...`,
+ text: `Deleting ${pluralize(ids.length, "attachment")}...`,
current: i,
- total: attachments.length
+ total: ids.length
});
- await attachmentStore.permanentDelete(attachment.metadata?.hash);
+ await attachmentStore.permanentDelete(attachment);
}
}
});
- showToast(
- "success",
- `${pluralize(attachments.length, "attachment")} deleted`
- );
+ showToast("success", `${pluralize(ids.length, "attachment")} deleted`);
}
-async function moveRemindersToTrash(reminders: Item[]) {
+async function moveRemindersToTrash(reminders: Reminder[]) {
const isMultiselect = reminders.length > 1;
if (isMultiselect) {
if (!(await showMultiDeleteConfirmation(reminders.length))) return;
@@ -155,6 +136,5 @@ export const Multiselect = {
moveRemindersToTrash,
moveNotebooksToTrash,
moveNotesToTrash,
- deleteTopics,
deleteAttachments
};
diff --git a/apps/web/src/components/attachment/index.tsx b/apps/web/src/components/attachment/index.tsx
index 33db9374b..0e35acf19 100644
--- a/apps/web/src/components/attachment/index.tsx
+++ b/apps/web/src/components/attachment/index.tsx
@@ -59,6 +59,7 @@ import { useEffect, useState } from "react";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { getFormattedDate } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
+import { Attachment } from "@notesnook/core";
const FILE_ICONS: Record = {
"image/": FileImage,
@@ -85,7 +86,7 @@ type AttachmentProgressStatus = {
};
type AttachmentProps = {
- attachment: any;
+ attachment: Attachment;
isSelected?: boolean;
onSelected?: () => void;
compact?: boolean;
diff --git a/apps/web/src/dialogs/attachments-dialog.tsx b/apps/web/src/dialogs/attachments-dialog.tsx
index 666ad18ae..81a7e319b 100644
--- a/apps/web/src/dialogs/attachments-dialog.tsx
+++ b/apps/web/src/dialogs/attachments-dialog.tsx
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { useEffect, useState, memo, useMemo, useRef } from "react";
+import { useEffect, useState, memo, useRef } from "react";
import {
Box,
Button,
@@ -28,11 +28,10 @@ import {
Label,
Text
} from "@theme-ui/components";
-import { getTotalSize } from "../common/attachments";
-import { useStore, store } from "../stores/attachment-store";
+import { store, useStore } from "../stores/attachment-store";
import { formatBytes } from "@notesnook/common";
import Dialog from "../components/dialog";
-import { TableVirtuoso } from "react-virtuoso";
+import { ItemProps, TableVirtuoso } from "react-virtuoso";
import {
ChevronDown,
ChevronUp,
@@ -52,21 +51,20 @@ import NavigationItem from "../components/navigation-menu/navigation-item";
import { pluralize } from "@notesnook/common";
import { db } from "../common/db";
import { Perform } from "../common/dialog-controller";
-import { Multiselect } from "../common/multi-select";
import { CustomScrollbarsVirtualList } from "../components/list-container";
import { Attachment } from "../components/attachment";
-import {
- isDocument,
- isImage,
- isVideo
-} from "@notesnook/core/dist/utils/filename";
-import { alpha } from "@theme-ui/color";
import { ScopedThemeProvider } from "../components/theme-provider";
+import {
+ Attachment as AttachmentType,
+ VirtualizedGrouping
+} from "@notesnook/core";
+import usePromise from "../hooks/use-promise";
+import { Multiselect } from "../common/multi-select";
type ToolbarAction = {
title: string;
icon: Icon;
- onClick: ({ selected }: { selected: any[] }) => void;
+ onClick: ({ selected }: { selected: string[] }) => void;
};
const TOOLBAR_ACTIONS: ToolbarAction[] = [
@@ -80,9 +78,7 @@ const TOOLBAR_ACTIONS: ToolbarAction[] = [
{
title: "Recheck",
icon: DoubleCheckmark,
- onClick: async ({ selected }) => {
- await store.recheck(selected.map((a) => a.metadata.hash));
- }
+ onClick: async ({ selected }) => await store.recheck(selected)
},
{
title: "Delete",
@@ -91,53 +87,65 @@ const TOOLBAR_ACTIONS: ToolbarAction[] = [
}
];
+const COLUMNS = [
+ { id: "filename" as const, title: "Name", width: "65%" },
+ { id: "status" as const, width: "24px" },
+ { id: "size" as const, title: "Size", width: "15%" },
+ { id: "dateUploaded" as const, title: "Date uploaded", width: "20%" }
+];
+
+type SortOptions = {
+ id: "filename" | "size" | "dateUploaded";
+ direction: "asc" | "desc";
+};
type AttachmentsDialogProps = { onClose: Perform };
function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
const allAttachments = useStore((store) => store.attachments);
- const [attachments, setAttachments] = useState(allAttachments);
+ const [attachments, setAttachments] =
+ useState>();
+ const [counts, setCounts] = useState>({
+ all: 0,
+ documents: 0,
+ images: 0,
+ orphaned: 0,
+ uploads: 0,
+ videos: 0
+ });
const [selected, setSelected] = useState([]);
- const [sortBy, setSortBy] = useState({ id: "name", direction: "asc" });
+ const [sortBy, setSortBy] = useState({
+ id: "filename",
+ direction: "asc"
+ });
const currentRoute = useRef("all");
const refresh = useStore((store) => store.refresh);
+ const download = useStore((store) => store.download);
useEffect(() => {
refresh();
}, [refresh]);
useEffect(() => {
- setAttachments(filterAttachments(currentRoute.current, allAttachments));
- }, [allAttachments]);
+ (async function () {
+ setAttachments(
+ await filterAttachments(currentRoute.current).sorted({
+ sortBy: sortBy.id,
+ sortDirection: sortBy.direction
+ })
+ );
+ })();
+ }, [sortBy, allAttachments]);
useEffect(() => {
- setAttachments((a) => {
- const attachments = a.slice();
- if (sortBy.id === "name") {
- attachments.sort(
- sortBy.direction === "asc"
- ? (a, b) => a.metadata.filename.localeCompare(b.metadata.filename)
- : (a, b) => b.metadata.filename.localeCompare(a.metadata.filename)
- );
- } else if (sortBy.id === "size") {
- attachments.sort(
- sortBy.direction === "asc"
- ? (a, b) => a.length - b.length
- : (a, b) => b.length - a.length
- );
- } else if (sortBy.id === "dateUploaded") {
- attachments.sort(
- sortBy.direction === "asc"
- ? (a, b) => a.dateUploaded - b.dateUploaded
- : (a, b) => b.dateUploaded - a.dateUploaded
- );
- }
- return attachments;
- });
- }, [sortBy]);
+ (async function () {
+ setCounts(await getCounts());
+ })();
+ }, [allAttachments]);
- const totalSize = useMemo(
- () => getTotalSize(allAttachments),
- [allAttachments]
- );
+ const totalSize = 0;
+ // useMemo(
+ // () => getTotalSize(allAttachments),
+ // [allAttachments]
+ // );
return (
@@ -379,6 +384,7 @@ const routes: { id: Route; icon: Icon; title: string }[] = [
];
type SidebarProps = {
+ onDownloadAll: () => void;
onRouteChange: (route: Route) => void;
filter: (query: string) => void;
counts: Record;
@@ -386,11 +392,10 @@ type SidebarProps = {
};
const Sidebar = memo(
function Sidebar(props: SidebarProps) {
- const { onRouteChange, filter, counts, totalSize } = props;
+ const { onRouteChange, filter, counts, totalSize, onDownloadAll } = props;
const [route, setRoute] = useState("all");
const downloadStatus = useStore((store) => store.status);
const cancelDownload = useStore((store) => store.cancel);
- const download = useStore((store) => store.download);
return (
@@ -446,7 +451,7 @@ const Sidebar = memo(
if (downloadStatus) {
await cancelDownload();
} else {
- await download(db.attachments.all);
+ onDownloadAll();
}
}}
>
@@ -482,38 +487,57 @@ const Sidebar = memo(
prev.counts.orphaned === next.counts.orphaned
);
-function getCounts(attachments: any[]): Record {
- const counts: Record = {
- all: 0,
- documents: 0,
- images: 0,
- videos: 0,
- uploads: 0,
- orphaned: 0
- };
- for (const attachment of attachments) {
- counts.all++;
-
- if (isDocument(attachment.metadata.type)) counts.documents++;
- else if (isImage(attachment.metadata.type)) counts.images++;
- else if (isVideo(attachment.metadata.type)) counts.videos++;
-
- if (!attachment.dateUploaded) counts.uploads++;
- if (!attachment.noteIds.length) counts.orphaned++;
+function TableRow(
+ props: ItemProps & {
+ context?: {
+ isSelected: (id: string) => boolean;
+ select: (id: string) => void;
+ attachments: VirtualizedGrouping;
+ };
}
- return counts;
+) {
+ const { context, item, ...restProps } = props;
+ const result = usePromise(
+ () => context?.attachments.item(item),
+ [item, context]
+ );
+
+ if (result.status !== "fulfilled" || !result.value)
+ return ;
+ return (
+ {
+ context?.select(item);
+ }}
+ />
+ );
}
-function filterAttachments(route: Route, attachments: any[]): any[] {
- return route === "all"
- ? attachments
- : route === "images"
- ? attachments.filter((a) => a.metadata.type.startsWith("image/"))
- : route === "videos"
- ? attachments.filter((a) => a.metadata.type.startsWith("video/"))
- : route === "documents"
- ? attachments.filter((a) => isDocument(a.metadata.type))
- : route === "orphaned"
- ? attachments.filter((a) => !a.noteIds.length)
- : attachments.filter((a) => !a.dateUploaded);
+async function getCounts(): Promise> {
+ return {
+ all: await db.attachments.all.count(),
+ documents: await db.attachments.documents.count(),
+ images: await db.attachments.images.count(),
+ videos: await db.attachments.videos.count(),
+ uploads: await db.attachments.pending.count(),
+ orphaned: await db.attachments.orphaned.count()
+ };
+}
+
+function filterAttachments(route: Route) {
+ return route === "all"
+ ? db.attachments.all
+ : route === "images"
+ ? db.attachments.images
+ : route === "videos"
+ ? db.attachments.videos
+ : route === "documents"
+ ? db.attachments.documents
+ : route === "orphaned"
+ ? db.attachments.orphaned
+ : db.attachments.pending;
}
diff --git a/apps/web/src/stores/attachment-store.js b/apps/web/src/stores/attachment-store.js
deleted file mode 100644
index 4a66e464a..000000000
--- a/apps/web/src/stores/attachment-store.js
+++ /dev/null
@@ -1,152 +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 createStore from "../common/store";
-import { db } from "../common/db";
-import BaseStore from "./index";
-import { store as editorStore } from "./editor-store";
-import { checkAttachment } from "../common/attachments";
-import { showToast } from "../utils/toast";
-import { AttachmentStream } from "../utils/streams/attachment-stream";
-import { ZipStream } from "../utils/streams/zip-stream";
-import { createWriteStream } from "../utils/stream-saver";
-
-let abortController = undefined;
-/**
- * @extends {BaseStore}
- */
-class AttachmentStore extends BaseStore {
- attachments = [];
- /**
- * @type {{current: number, total: number}}
- */
- status = undefined;
-
- refresh = () => {
- this.set((state) => (state.attachments = db.attachments.all));
- };
-
- init = () => {
- this.refresh();
- };
-
- download = async (attachments) => {
- if (this.get().status)
- throw new Error(
- "Please wait for the previous download to finish or cancel it."
- );
-
- this.set(
- (state) => (state.status = { current: 0, total: attachments.length })
- );
-
- abortController = new AbortController();
- const attachmentStream = new AttachmentStream(
- attachments,
- abortController.signal,
- (current) => {
- this.set(
- (state) => (state.status = { current, total: attachments.length })
- );
- }
- );
- await attachmentStream
- .pipeThrough(new ZipStream())
- .pipeTo(
- await createWriteStream("attachments.zip", {
- signal: abortController.signal
- })
- );
-
- this.set((state) => (state.status = undefined));
- };
-
- cancel = async () => {
- if (abortController) {
- await abortController.abort();
- abortController = undefined;
- this.set((state) => (state.status = undefined));
- }
- };
-
- recheck = async (hashes) => {
- const attachments = this.get().attachments;
- for (let hash of hashes) {
- const index = attachments.findIndex((a) => a.metadata.hash === hash);
- try {
- this._changeWorkingStatus(index, "recheck", undefined);
-
- const { failed, success } = await checkAttachment(hash);
- this._changeWorkingStatus(index, false, success ? null : failed);
- } catch (e) {
- console.error(e);
- this._changeWorkingStatus(index, false, false);
- showToast("error", `Rechecking failed: ${e.message}`);
- }
- }
- };
-
- rename = async (hash, newName) => {
- await db.attachments.add({
- hash,
- filename: newName
- });
- this.get().refresh();
- };
-
- permanentDelete = async (hash) => {
- const index = this.get().attachments.findIndex(
- (a) => a.metadata.hash === hash
- );
- if (index <= -1) return;
- const noteIds = this.get().attachments[index].noteIds.slice();
-
- try {
- this._changeWorkingStatus(index, "delete", undefined);
- if (await db.attachments.remove(hash, false)) {
- this.get().refresh();
-
- if (noteIds.includes(editorStore.get().session.id)) {
- await editorStore.clearSession();
- }
- }
- } catch (e) {
- console.error(e);
- this._changeWorkingStatus(index, false, false);
- showToast("error", `Failed to delete: ${e.message}`);
- throw e;
- }
- };
-
- /**
- *
- * @param {*} index
- * @param {"delete"|"recheck"} workType
- * @param {*} failed
- */
- _changeWorkingStatus = (index, workType, failed) => {
- this.set((state) => {
- state.attachments[index].failed = failed;
- state.attachments[index].working = workType;
- });
- };
-}
-
-const [useStore, store] = createStore(AttachmentStore);
-export { useStore, store };
diff --git a/apps/web/src/stores/attachment-store.ts b/apps/web/src/stores/attachment-store.ts
new file mode 100644
index 000000000..758d60c34
--- /dev/null
+++ b/apps/web/src/stores/attachment-store.ts
@@ -0,0 +1,154 @@
+/*
+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 createStore from "../common/store";
+import { db } from "../common/db";
+import BaseStore from "./index";
+import { store as editorStore } from "./editor-store";
+import { checkAttachment } from "../common/attachments";
+import { showToast } from "../utils/toast";
+import { AttachmentStream } from "../utils/streams/attachment-stream";
+import { ZipStream } from "../utils/streams/zip-stream";
+import { createWriteStream } from "../utils/stream-saver";
+import { Attachment, VirtualizedGrouping } from "@notesnook/core";
+
+let abortController: AbortController | undefined = undefined;
+class AttachmentStore extends BaseStore {
+ attachments?: VirtualizedGrouping;
+ status?: { current: number; total: number };
+ processing: Record<
+ string,
+ { failed?: string; working?: "delete" | "recheck" }
+ > = {};
+
+ refresh = async () => {
+ this.set({
+ attachments: await db.attachments.all.sorted({
+ sortBy: "dateCreated",
+ sortDirection: "desc"
+ })
+ });
+ };
+
+ init = () => {
+ this.refresh();
+ };
+
+ download = async (ids: string[]) => {
+ if (this.get().status)
+ throw new Error(
+ "Please wait for the previous download to finish or cancel it."
+ );
+
+ this.set({ status: { current: 0, total: ids.length } });
+
+ abortController = new AbortController();
+ const attachmentStream = new AttachmentStream(
+ ids,
+ (id) => this.attachments?.item(id),
+ abortController.signal,
+ (current) => {
+ this.set({ status: { current, total: ids.length } });
+ }
+ );
+ await attachmentStream.pipeThrough(new ZipStream()).pipeTo(
+ await createWriteStream("attachments.zip", {
+ signal: abortController.signal
+ })
+ );
+
+ this.set((state) => (state.status = undefined));
+ };
+
+ cancel = async () => {
+ if (abortController) {
+ abortController.abort();
+ abortController = undefined;
+ this.set((state) => (state.status = undefined));
+ }
+ };
+
+ recheck = async (ids: string[]) => {
+ for (const id of ids) {
+ const attachment = await this.attachments?.item(id);
+ if (!attachment) continue;
+ try {
+ this._changeWorkingStatus(attachment.hash, "recheck");
+
+ const { failed, success } = await checkAttachment(attachment.hash);
+ this._changeWorkingStatus(
+ attachment.hash,
+ undefined,
+ success ? undefined : failed
+ );
+ } catch (e) {
+ console.error(e);
+ this._changeWorkingStatus(attachment.hash);
+ if (e instanceof Error)
+ showToast("error", `Rechecking failed: ${e.message}`);
+ }
+ }
+ };
+
+ rename = async (hash: string, newName: string) => {
+ await db.attachments.add({
+ hash,
+ filename: newName
+ });
+ await this.get().refresh();
+ };
+
+ permanentDelete = async (attachment: Attachment) => {
+ try {
+ this._changeWorkingStatus(attachment.hash, "delete");
+ if (await db.attachments.remove(attachment.hash, false)) {
+ await this.get().refresh();
+
+ const sessionId = editorStore.get().session.id;
+ if (
+ sessionId &&
+ (await db.relations
+ .to({ id: attachment.id, type: "attachment" }, "note")
+ .has(sessionId))
+ ) {
+ await editorStore.clearSession();
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ this._changeWorkingStatus(attachment.hash);
+ if (e instanceof Error)
+ showToast("error", `Failed to delete: ${e.message}`);
+ throw e;
+ }
+ };
+
+ private _changeWorkingStatus = (
+ hash: string,
+ working?: "delete" | "recheck",
+ failed?: string
+ ) => {
+ this.set((state) => {
+ state.processing[hash] = { failed, working };
+ });
+ };
+}
+
+const [useStore, store] = createStore(AttachmentStore);
+export { useStore, store };
diff --git a/apps/web/src/utils/streams/attachment-stream.ts b/apps/web/src/utils/streams/attachment-stream.ts
index df32cf176..11c596477 100644
--- a/apps/web/src/utils/streams/attachment-stream.ts
+++ b/apps/web/src/utils/streams/attachment-stream.ts
@@ -20,12 +20,14 @@ along with this program. If not, see .
import { db } from "../../common/db";
import { lazify } from "../lazify";
import { ZipFile } from "./zip-stream";
+import { Attachment } from "@notesnook/core";
export const METADATA_FILENAME = "metadata.json";
const GROUP_ID = "all-attachments";
export class AttachmentStream extends ReadableStream {
constructor(
- attachments: Array,
+ ids: string[],
+ resolve: (id: string) => Promise | undefined,
signal?: AbortSignal,
onProgress?: (current: number) => void
) {
@@ -45,16 +47,12 @@ export class AttachmentStream extends ReadableStream {
}
onProgress && onProgress(index);
- const attachment = attachments[index++];
+ const attachment = await resolve(ids[index++]);
+ if (!attachment) return;
await db
.fs()
- .downloadFile(
- GROUP_ID,
- attachment.metadata.hash,
- attachment.chunkSize,
- attachment.metadata
- );
+ .downloadFile(GROUP_ID, attachment.hash, attachment.chunkSize);
const key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
@@ -65,14 +63,14 @@ export class AttachmentStream extends ReadableStream {
decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
- name: attachment.metadata.filename,
- type: attachment.metadata.type,
+ name: attachment.filename,
+ type: attachment.mimeType,
isUploaded: !!attachment.dateUploaded
})
);
if (file) {
- const filePath: string = attachment.metadata.filename;
+ const filePath: string = attachment.filename;
controller.enqueue({
path: makeUniqueFilename(filePath, counters),
data: new Uint8Array(await file.arrayBuffer())
@@ -81,7 +79,7 @@ export class AttachmentStream extends ReadableStream {
controller.error(new Error("Failed to decrypt file."));
}
- if (index === attachments.length) {
+ if (index === ids.length) {
controller.close();
}
}
diff --git a/packages/core/src/collections/attachments.ts b/packages/core/src/collections/attachments.ts
index 0ae995289..50beb15b9 100644
--- a/packages/core/src/collections/attachments.ts
+++ b/packages/core/src/collections/attachments.ts
@@ -24,6 +24,7 @@ import { EV, EVENTS } from "../common";
import dataurl from "../utils/dataurl";
import dayjs from "dayjs";
import {
+ DocumentMimeTypes,
getFileNameWithExtension,
isImage,
isWebClip
@@ -390,14 +391,47 @@ export class Attachments implements ICollection {
);
}
- // get images() {
- // return this.all.filter((attachment) => isImage(attachment.metadata.type));
- // }
+ get images() {
+ return this.collection.createFilter(
+ (qb) => qb.where("mimeType", "like", `image/%`),
+ this.db.options?.batchSize
+ );
+ }
- // get webclips() {
- // return this.all.filter((attachment) => isWebClip(attachment.metadata.type));
- // }
+ get videos() {
+ return this.collection.createFilter(
+ (qb) => qb.where("mimeType", "like", `video/%`),
+ this.db.options?.batchSize
+ );
+ }
+ get documents() {
+ return this.collection.createFilter(
+ (qb) => qb.where("mimeType", "in", DocumentMimeTypes),
+ this.db.options?.batchSize
+ );
+ }
+
+ get webclips() {
+ return this.collection.createFilter(
+ (qb) => qb.where("mimeType", "==", `application/vnd.notesnook.web-clip`),
+ this.db.options?.batchSize
+ );
+ }
+
+ get orphaned() {
+ return this.collection.createFilter(
+ (qb) =>
+ qb.where("id", "not in", (eb) =>
+ eb
+ .selectFrom("relations")
+ .where("toType", "==", "attachment")
+ .select("toId as id")
+ .$narrowType<{ id: string }>()
+ ),
+ this.db.options?.batchSize
+ );
+ }
// get media() {
// return this.all.filter(
// (attachment) =>
diff --git a/packages/core/src/database/sql-collection.ts b/packages/core/src/database/sql-collection.ts
index 68484feda..1f7a4ebf4 100644
--- a/packages/core/src/database/sql-collection.ts
+++ b/packages/core/src/database/sql-collection.ts
@@ -18,7 +18,13 @@ along with this program. If not, see .
*/
import { EVENTS } from "../common";
-import { GroupOptions, Item, MaybeDeletedItem, isDeleted } from "../types";
+import {
+ GroupOptions,
+ Item,
+ MaybeDeletedItem,
+ SortOptions,
+ isDeleted
+} from "../types";
import EventManager from "../utils/event-manager";
import {
DatabaseAccessor,
@@ -359,7 +365,7 @@ export class FilteredSelector {
);
}
- async sorted(options: GroupOptions) {
+ async sorted(options: SortOptions) {
const items = await this.filter
.$call(this.buildSortExpression(options))
.select("id")
@@ -378,7 +384,7 @@ export class FilteredSelector {
});
}
- private buildSortExpression(options: GroupOptions) {
+ private buildSortExpression(options: SortOptions) {
return (
qb: SelectQueryBuilder
) => {
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 8b5f193ee..8ca841322 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -20,12 +20,22 @@ along with this program. If not, see .
import { Cipher } from "@notesnook/crypto";
import { TimeFormat } from "./utils/date";
-export type GroupOptions = {
- groupBy: "none" | "abc" | "year" | "month" | "week" | "default";
- sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "title";
+export type SortOptions = {
+ sortBy:
+ | "dateCreated"
+ | "dateDeleted"
+ | "dateEdited"
+ | "title"
+ | "filename"
+ | "size"
+ | "dateUploaded";
sortDirection: "desc" | "asc";
};
+export type GroupOptions = SortOptions & {
+ groupBy: "none" | "abc" | "year" | "month" | "week" | "default";
+};
+
export type GroupedItems = (T | GroupHeader)[];
export type GroupingKey =
diff --git a/packages/core/src/utils/virtualized-grouping.ts b/packages/core/src/utils/virtualized-grouping.ts
index bc8a2212b..86a90b9ba 100644
--- a/packages/core/src/utils/virtualized-grouping.ts
+++ b/packages/core/src/utils/virtualized-grouping.ts
@@ -44,6 +44,10 @@ export class VirtualizedGrouping {
return item;
}
+ get ungrouped() {
+ return this.ids.filter((i) => !isGroupHeader(i)) as string[];
+ }
+
/**
* Get item from cache or request the appropriate batch for caching
* and load it from there.