diff --git a/apps/web/src/common/index.js b/apps/web/src/common/index.js index 5cbfb8ece..848abf32e 100644 --- a/apps/web/src/common/index.js +++ b/apps/web/src/common/index.js @@ -27,12 +27,11 @@ import Config from "../utils/config"; import { hashNavigate, getCurrentHash } from "../navigation"; import { db } from "./db"; import { sanitizeFilename } from "@notesnook/common"; - import { store as userstore } from "../stores/user-store"; import FileSaver from "file-saver"; import { showToast } from "../utils/toast"; import { SUBSCRIPTION_STATUS } from "./constants"; -import { showFilePicker } from "../components/editor/picker"; +import { showFilePicker } from "../utils/file-picker"; import { logger } from "../utils/logger"; import { PATHS } from "@notesnook/desktop"; import { TaskManager } from "./task-manager"; diff --git a/apps/web/src/components/editor/picker.ts b/apps/web/src/components/editor/picker.ts index 53d0c6aa5..2fd92a93e 100644 --- a/apps/web/src/components/editor/picker.ts +++ b/apps/web/src/components/editor/picker.ts @@ -25,13 +25,12 @@ import { TaskManager } from "../../common/task-manager"; import { isUserPremium } from "../../hooks/use-is-user-premium"; import fs from "../../interfaces/fs"; import { showToast } from "../../utils/toast"; +import { showFilePicker } from "../../utils/file-picker"; const FILE_SIZE_LIMIT = 500 * 1024 * 1024; const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024; -type MimeType = string; //`${string}/${string}`; - -export async function insertAttachment(type: MimeType = "*/*") { +export async function insertAttachment(type = "*/*") { if (!isUserPremium()) { await showBuyDialog(); return; @@ -58,7 +57,7 @@ export async function attachFile(selectedFile: File) { } export async function reuploadAttachment( - type: MimeType, + type: string, expectedFileHash: string ) { const selectedFile = await showFilePicker({ @@ -120,25 +119,6 @@ async function getEncryptionKey(): Promise { return key; } -type FilePickerOptions = { acceptedFileTypes: MimeType }; - -export function showFilePicker({ - acceptedFileTypes -}: FilePickerOptions): Promise { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("accept", acceptedFileTypes); - input.dispatchEvent(new MouseEvent("click")); - input.onchange = async function () { - if (!input.files) return resolve(undefined); - const file = input.files[0]; - if (!file) return resolve(undefined); - resolve(file); - }; - }); -} - async function toDataURL(file: File): Promise { const buffer = await file.arrayBuffer(); const base64 = Buffer.from(buffer).toString("base64"); diff --git a/apps/web/src/dialogs/settings/components/themes-selector.tsx b/apps/web/src/dialogs/settings/components/themes-selector.tsx index d215391ee..206f85219 100644 --- a/apps/web/src/dialogs/settings/components/themes-selector.tsx +++ b/apps/web/src/dialogs/settings/components/themes-selector.tsx @@ -21,7 +21,11 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useCallback, useState } from "react"; import { Box, Button, Flex, Input, Text } from "@theme-ui/components"; import { CheckCircleOutline, Loading } from "../../../components/icons"; -import { THEME_COMPATIBILITY_VERSION } from "@notesnook/theme"; +import { + THEME_COMPATIBILITY_VERSION, + getPreviewColors, + validateTheme +} from "@notesnook/theme"; import { debounce } from "@notesnook/common"; import { useStore as useThemeStore } from "../../../stores/theme-store"; import { useStore as useUserStore } from "../../../stores/user-store"; @@ -36,6 +40,7 @@ import { ThemePreview } from "../../../components/theme-preview"; import { VirtuosoGrid } from "react-virtuoso"; import { Loader } from "../../../components/loader"; import { showToast } from "../../../utils/toast"; +import { showFilePicker, readFile } from "../../../utils/file-picker"; const ThemesClient = ThemesTRPC.createClient({ links: [ @@ -67,15 +72,15 @@ function ThemesList() { const [colorScheme, setColorScheme] = useState<"all" | "dark" | "light">( "all" ); + const [isApplying, setIsApplying] = useState(false); + const setCurrentTheme = useThemeStore((store) => store.setTheme); + const user = useUserStore((store) => store.user); + const darkTheme = useThemeStore((store) => store.darkTheme); + const lightTheme = useThemeStore((store) => store.lightTheme); const isThemeCurrentlyApplied = useThemeStore( (store) => store.isThemeCurrentlyApplied ); - const setCurrentTheme = useThemeStore((store) => store.setTheme); - const user = useUserStore((store) => store.user); - useThemeStore((store) => store.darkTheme); - useThemeStore((store) => store.lightTheme); - const filters = []; if (searchQuery) filters.push({ type: "term" as const, value: searchQuery }); if (colorScheme !== "all") @@ -87,7 +92,19 @@ function ThemesList() { compatibilityVersion: THEME_COMPATIBILITY_VERSION, filters }, - { getNextPageParam: (lastPage) => lastPage.nextCursor } + { + keepPreviousData: true, + select: (themes) => ({ + pageParams: themes.pageParams, + pages: themes.pages.map((page) => ({ + nextCursor: page.nextCursor, + themes: page.themes.filter( + (theme) => !isThemeCurrentlyApplied(theme.id) + ) + })) + }), + getNextPageParam: (lastPage) => lastPage.nextCursor + } ); const setTheme = useCallback( @@ -120,31 +137,65 @@ function ThemesList() { sx={{ mt: 2 }} onChange={debounce((e) => setSearchQuery(e.target.value), 500)} /> - - {COLOR_SCHEMES.map((filter) => ( + + + {COLOR_SCHEMES.map((filter) => ( + + ))} + {themes.isLoading && !themes.isInitialLoading ? ( + + ) : null} + + + - ))} - {themes.isLoading && !themes.isInitialLoading ? ( - - ) : null} + + themes.hasNextPage ? themes.fetchNextPage() : null } + components={{ + Header: () => ( +
+ + +
+ ) + }} + computeItemKey={(_index, item) => item.id} itemContent={(_index, theme) => ( - { - if (await showThemeDetails(theme)) { - await setTheme(theme); - } - }} - > - - - {theme.name} - - {theme.authors[0].name} - - {theme.totalInstalls} installs - {isThemeCurrentlyApplied(theme.id) ? ( - - ) : ( - - )} - - + theme={theme} + isApplied={false} + isApplying={isApplying} + setTheme={setTheme} + /> )} /> )} @@ -224,3 +255,69 @@ function ThemesList() { ); } + +type ThemeItemProps = { + theme: ThemeMetadata; + isApplied: boolean; + isApplying: boolean; + setTheme: (theme: ThemeMetadata) => Promise; +}; +function ThemeItem(props: ThemeItemProps) { + const { theme, isApplied, isApplying, setTheme } = props; + + return ( + { + if (await showThemeDetails(theme)) { + await setTheme(theme); + } + }} + > + + + {theme.name} + + {theme.authors[0].name} + + + {theme.colorScheme === "dark" ? "Dark" : "Light"} +    + {theme.totalInstalls ? `${theme.totalInstalls} installs` : ""} + + {isApplied ? ( + + ) : ( + + )} + + + ); +} diff --git a/apps/web/src/dialogs/theme-details-dialog.tsx b/apps/web/src/dialogs/theme-details-dialog.tsx index d6cdadc0e..6bcee74e0 100644 --- a/apps/web/src/dialogs/theme-details-dialog.tsx +++ b/apps/web/src/dialogs/theme-details-dialog.tsx @@ -63,20 +63,34 @@ function ThemeDetailsDialog(props: ThemeDetailsDialogProps) { {theme.authors.map((author) => author.name).join(", ")} - - {theme.totalInstalls} installs - + {theme.totalInstalls && ( + + {theme.totalInstalls} installs + + )} Licensed under {theme.license} - - Website - + + + Website + + {theme.sourceURL && ( + + Source + + )} +
); diff --git a/apps/web/src/utils/file-picker.ts b/apps/web/src/utils/file-picker.ts new file mode 100644 index 000000000..fdb9706fd --- /dev/null +++ b/apps/web/src/utils/file-picker.ts @@ -0,0 +1,49 @@ +/* +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 . +*/ + +type FilePickerOptions = { acceptedFileTypes: string }; + +export function showFilePicker({ + acceptedFileTypes +}: FilePickerOptions): Promise { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", acceptedFileTypes); + input.dispatchEvent(new MouseEvent("click")); + input.onchange = async function () { + if (!input.files) return resolve(undefined); + const file = input.files[0]; + if (!file) return resolve(undefined); + resolve(file); + }; + }); +} + +export async function readFile(file: File): Promise { + const reader = new FileReader(); + return await new Promise((resolve, reject) => { + reader.addEventListener("load", (event) => { + const text = event.target?.result as string; + if (!text) return reject("FileReader failed to load file."); + resolve(text); + }); + reader.readAsText(file); + }); +} diff --git a/servers/themes/src/sync.ts b/servers/themes/src/sync.ts index d18e347b8..98b679378 100644 --- a/servers/themes/src/sync.ts +++ b/servers/themes/src/sync.ts @@ -35,8 +35,8 @@ import { } from "@notesnook/theme"; export type CompiledThemeDefinition = ThemeDefinition & { - sourceURL: string; - totalInstalls: number; + sourceURL?: string; + totalInstalls?: number; previewColors: PreviewColors; };