From c6174ba03b660b4046fbfa67591d9455b93e78d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81J?= Date: Wed, 31 Jul 2024 17:32:43 +0800 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit bdba67752ff6110fc4d957d7920952e0252b82f0 Author: 老J Date: Wed Jul 31 13:27:04 2024 +0800 v1.4.0 commit c7e656b95adcb3b3285e20ed35ac18562bae723d Author: 老J Date: Wed Jul 31 13:24:39 2024 +0800 Catch the error when using the items commit 9c18d82c86bb015e140699dd93503fc2bd1b5e8d Author: 老J Date: Thu Jul 25 14:25:27 2024 +0800 Fix error on Safari: Failed to load resource: The operation couldn’t be completed. (WebKitBlobResource error 1.) commit 69838c2fa6c0355bbeb99c8c7a166a9bbe89494e Author: 老J Date: Tue Jul 23 19:48:32 2024 +0800 Empty history. commit fe3ead9352248158634825ab9da1d40b2b03c16c Author: 老J Date: Sun Jul 21 17:48:12 2024 +0800 Set the history item. to file link area. commit d95e6543847c866c7ea1dc3d3c4a1e4fe2679923 Author: 老J Date: Sun Jul 21 11:11:36 2024 +0800 Write history items to clipboard. commit 1b0d054500692e6e7279fefce43515b55400a0f1 Author: 老J Date: Fri Jul 19 10:32:09 2024 +0800 Pin/unpin history items. commit 07ee495a7b2dce16f02088e935006f5a8b81a6fe Author: 老J Date: Thu Jul 18 19:53:34 2024 +0800 Delete item from history IndexedDB commit 592d72ef52e88179f287e9f4b2e7a83111bba419 Author: 老J Date: Thu Jul 18 08:57:28 2024 +0800 Replace the group buttons with the ellipsis button. commit d72f1fefc60b78647813524b6aac2311fea5ca12 Author: 老J Date: Wed Jul 17 18:42:17 2024 +0800 Display history items from IndexedDB commit 9eb3cf266905311258acf05ea9f8504a44d756d8 Author: 老J Date: Tue Jul 16 20:10:22 2024 +0800 Add history item to IndexedDB commit 698831f69668b0529f44c0f877ef23e6caee7c10 Author: 老J Date: Fri Jul 12 15:55:54 2024 +0800 Put clipboard data into indexedDB. commit 05cb4f5dee2c8a4c45c96c663abe1e748d5de30e Author: 老J Date: Tue Jul 9 17:24:11 2024 +0800 Made some adjustments to the page. commit 5510b8dd0af0ad8169524a5e5ffd047cbd53e5c5 Author: 老J Date: Tue Jul 9 16:27:27 2024 +0800 Add static pages of clipboard history feature. --- frontend/app/[locale]/page.tsx | 2 + frontend/app/globals.css | 29 ++++ frontend/components/history-item.tsx | 190 ++++++++++++++++++++++ frontend/components/history.tsx | 58 +++++++ frontend/components/log-box.tsx | 8 +- frontend/components/quick-input.tsx | 2 +- frontend/components/sync-clipboard.tsx | 213 +++++++++++++----------- frontend/lib/clipboard.ts | 19 ++- frontend/lib/log.ts | 57 ++++++- frontend/messages/en.json | 12 +- frontend/messages/zh.json | 12 +- frontend/models/db.ts | 13 ++ frontend/models/history.ts | 10 ++ frontend/package-lock.json | 214 ++++++++++++++----------- frontend/package.json | 9 +- version.txt | 2 +- 16 files changed, 640 insertions(+), 210 deletions(-) create mode 100644 frontend/components/history-item.tsx create mode 100644 frontend/components/history.tsx create mode 100644 frontend/models/db.ts create mode 100644 frontend/models/history.ts diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx index cf902612..4509dd0d 100644 --- a/frontend/app/[locale]/page.tsx +++ b/frontend/app/[locale]/page.tsx @@ -3,12 +3,14 @@ import SyncClipboard from "@/components/sync-clipboard"; import Notice from "@/components/notice"; import Footer from "@/components/footer"; import { NextIntlClientProvider, useMessages } from "next-intl"; +import moment from "moment/min/moment-with-locales"; export default function Home({ params: { locale }, }: { params: { locale: string }; }) { + moment.locale(locale == "zh" ? "zh-cn" : "en"); const messages = useMessages(); return (
diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 081f0cab..438e2042 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -66,3 +66,32 @@ body { padding-right: 0.3rem; margin-left: -1.75rem; } + +.logbox ::-webkit-scrollbar-track { + background: var(--fallback-n, oklch(var(--n))); +} + +.logbox ::-webkit-scrollbar-thumb { + background: var(--fallback-b3, oklch(var(--b3))); +} + +.logbox ::-webkit-scrollbar-thumb:hover { + background: var(--fallback-b1, oklch(var(--b1))); +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: var(--fallback-b2, oklch(var(--b2))); +} + +::-webkit-scrollbar-thumb { + background: var(--fallback-b3, oklch(var(--b3))); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--fallback-bc, oklch(var(--bc))); +} diff --git a/frontend/components/history-item.tsx b/frontend/components/history-item.tsx new file mode 100644 index 00000000..942521c2 --- /dev/null +++ b/frontend/components/history-item.tsx @@ -0,0 +1,190 @@ +import { useTranslations } from "next-intl"; +import { + EllipsisHorizontalIcon, + LockClosedIcon, +} from "@heroicons/react/24/solid"; +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import { HistoryItemEntity } from "@/models/history"; +import moment from "moment/min/moment-with-locales"; +import { db } from "@/models/db"; +import { Log, LogLevel } from "@/lib/log"; +import { clipboardWriteBlob, clipboardWriteBlobPromise } from "@/lib/clipboard"; +import { browserName } from "react-device-detect"; +import { FileInfo } from "@/lib/clipboard"; + +export default function HistoryItem({ + item, + addLog, + updateFileLink, +}: { + item: HistoryItemEntity; + addLog: (log: Log) => void; + updateFileLink: (fileInfo: FileInfo) => void; +}) { + const t = useTranslations("SyncClipboard"); + const ulRef = useRef(null); + + const [text, setText] = useState(""); + useEffect(() => { + setText(""); + if (item.type == "text") { + const parseText = async () => { + if (item.dataArrayBuffer) { + setText( + await new Blob([item.dataArrayBuffer], { + type: item.dataType, + }).text(), + ); + } + }; + parseText(); + } + }, [item]); + + return ( + + + {item.pin == "true" ? ( + + ) : ( + {item.index} + )} + + + {item.type == "text" && ( +

{text}

+ )} + {item.type == "screenshot" && ( +
+ user's screenshot +
+ )} + {item.type == "file" && ( +

+ {"[" + t("file") + "]" + item.fileName} +

+ )} + + + {moment(item.createdAt).fromNow()} + + +
+
+ +
+ +
+ + + ); +} diff --git a/frontend/components/history.tsx b/frontend/components/history.tsx new file mode 100644 index 00000000..7827c70a --- /dev/null +++ b/frontend/components/history.tsx @@ -0,0 +1,58 @@ +import { useTranslations } from "next-intl"; +import HistoryItem from "./history-item"; +import { useLiveQuery } from "dexie-react-hooks"; +import { db } from "@/models/db"; +import { Log } from "@/lib/log"; +import { FileInfo } from "@/lib/clipboard"; + +export default function History({ + addLog, + updateFileLink, +}: { + addLog: (log: Log) => void; + updateFileLink: (fileInfo: FileInfo) => void; +}) { + const t = useTranslations("SyncClipboard.history"); + const items = useLiveQuery(() => db.history.reverse().toArray()); + if (!items) return null; + + return ( +
+
{t("title")}
+
{t("subTitle")}
+
+ + + {items.length == 0 && ( + + + + )} + {items.map( + (item) => + item.pin == "true" && ( + + ), + )} + {items.map( + (item) => + item.pin == "false" && ( + + ), + )} + +
{t("empty")}
+
+
+ ); +} diff --git a/frontend/components/log-box.tsx b/frontend/components/log-box.tsx index 689b36dc..73a0e8ba 100644 --- a/frontend/components/log-box.tsx +++ b/frontend/components/log-box.tsx @@ -1,4 +1,4 @@ -import { Log, Level } from "@/lib/log"; +import { Log, LogLevel } from "@/lib/log"; import { useRef, useEffect } from "react"; export default function LogBox({ logs }: { logs: Log[] }) { @@ -13,13 +13,13 @@ export default function LogBox({ logs }: { logs: Log[] }) { const listItems = logs.map((log, index) => { let level; switch (log.level) { - case Level.Warn: + case LogLevel.Warn: level = "text-warning"; break; - case Level.Success: + case LogLevel.Success: level = "text-green-600"; break; - case Level.Error: + case LogLevel.Error: level = "text-rose-600"; break; default: diff --git a/frontend/components/quick-input.tsx b/frontend/components/quick-input.tsx index b8a58b92..9b5cb78d 100644 --- a/frontend/components/quick-input.tsx +++ b/frontend/components/quick-input.tsx @@ -29,7 +29,7 @@ export default function QuickInput({ />
- {t("quickInput.title")}: + {t("quickInput.title")} {" " + t("quickInput.subTitle")} {isDesktop && ( diff --git a/frontend/components/sync-clipboard.tsx b/frontend/components/sync-clipboard.tsx index c3de450b..92e74399 100644 --- a/frontend/components/sync-clipboard.tsx +++ b/frontend/components/sync-clipboard.tsx @@ -3,37 +3,39 @@ import LogBox from "@/components/log-box"; import FileLink from "@/components/file-link"; import useAuth from "@/lib/auth"; -import { Log, Level } from "@/lib/log"; +import { LogLevel, useLog } from "@/lib/log"; import { DragEvent, useRef, useState } from "react"; import clsx from "clsx"; import { useRouter, usePathname } from "next/navigation"; -import { useLocale, useTranslations, useFormatter } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { - TmpClipboard, initTmpClipboard, clipboardWriteBlob, clipboardWriteBlobPromise, hashBlob, toTextBlob, + Clipboard, FileInfo, initFileInfo, clipboardRead, } from "@/lib/clipboard"; // Chrome | Safari | Mobile Safari -import { osName, browserName, isAndroid } from "react-device-detect"; +import { browserName, isAndroid } from "react-device-detect"; import SyncButton from "@/components/sync-button"; import SyncShortcut from "@/components/sync-shortcut"; import QuickInput from "@/components/quick-input"; +import History from "@/components/history"; +import { db } from "@/models/db"; +import { HistoryItemEntity } from "@/models/history"; +import moment from "moment"; // route: /locale?ci=123&cbi=abc // - ci: clipboard index // - cbi: clipboard blob id export default function SyncClipboard() { const t = useTranslations("SyncClipboard"); - const format = useFormatter(); const [fileInfo, setFileInfo] = useState(initFileInfo); - const [tmpClipboard, setTmpClipboard] = - useState(initTmpClipboard); + const [tmpClipboard, setTmpClipboard] = useState(initTmpClipboard); // "" | interrupted-[r|w] | finished const [status, setStatus] = useState(""); const [dragging, setDragging] = useState(false); @@ -44,34 +46,7 @@ export default function SyncClipboard() { const { isLoading, loggedIn } = useAuth(); const router = useRouter(); const pathname = usePathname(); - - let initLogs = [ - { - level: Level.Warn, - message: t("logs.pressToSync"), - }, - ]; - const recommendedBrowsers = [ - { osName: "Windows", browsers: ["Chrome", "Edge", "Opera"] }, - { osName: "iOS", browsers: ["Mobile Safari"] }, - { osName: "Android", browsers: ["Chrome", "Edge"] }, - { osName: "Mac OS", browsers: ["Chrome", "Opera", "Safari"] }, - ]; - recommendedBrowsers.map((item) => { - if (osName == item.osName && !item.browsers.includes(browserName)) { - initLogs = [ - { - level: Level.Info, - message: t("logs.recommendedBrowsers", { - os: item.osName, - browsers: format.list(item.browsers), - }), - }, - ...initLogs, - ]; - } - }); - const [logs, setLogs] = useState(initLogs); + const { logs, addLog, resetLog } = useLog(); if (isLoading) { return ( @@ -81,6 +56,26 @@ export default function SyncClipboard() { ); } + const updateFileLink = (fileInfo: FileInfo) => { + setFileInfo(fileInfo); + }; + + const addHistoryItem = async (history: HistoryItemEntity) => { + if (history.pin != "true") history.pin = "false"; + history.createdAt = moment().format(); + history.dataArrayBuffer = await history.data.arrayBuffer(); + history.dataType = history.data.type; + await db.history.put(history); + const items = await db.history + .where("pin") + .equals("false") + .reverse() + .primaryKeys(); + items.map((item, idx) => { + if (idx > 19) db.history.where("createdAt").equals(item).delete(); + }); + }; + const ensureLoggedIn = () => { if (!loggedIn) { router.push(`/${locale}/user/email-code`); @@ -89,24 +84,13 @@ export default function SyncClipboard() { return true; }; - const resetLog = () => { - setLogs([]); - }; - const sleep = function (ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }; - const addLog = (message: string, level?: Level) => { - setLogs((current) => [ - ...current, - { level: level ?? Level.Info, message: message }, - ]); - }; - const pullClipboard = async () => { resetLog(); - addLog(t("logs.fetching")); + addLog({ message: t("logs.fetching") }); const searchParams = new URLSearchParams(window.location.search); const response = await fetch("/api/v1/clipboard", { headers: { @@ -121,7 +105,7 @@ export default function SyncClipboard() { if (response.status != 200) { const body = await response.json(); - addLog(body.message, Level.Error); + addLog({ message: body.message, level: LogLevel.Error }); return; } const xindex = response.headers.get("x-index"); @@ -133,12 +117,12 @@ export default function SyncClipboard() { xtype == "" || xtype == null ) { - addLog(t("logs.upToDate")); + addLog({ message: t("logs.upToDate") }); if (browserName.includes("Safari")) { setStatus("interrupted-r"); - addLog(t("logs.pressAgain"), Level.Warn); - addLog(t("logs.pressPaste"), Level.Warn); + addLog({ message: t("logs.pressAgain"), level: LogLevel.Warn }); + addLog({ message: t("logs.pressPaste"), level: LogLevel.Warn }); return; } @@ -146,26 +130,22 @@ export default function SyncClipboard() { return; } const xclientname = response.headers.get("x-clientname"); - addLog( - t("logs.received", { + addLog({ + message: t("logs.received", { type: t(xtype), index: xindex, clientname: xclientname ?? "UNKNOWN", }), - ); - - if (xtype == "file") { - addLog(t("logs.autoDownload"), Level.Success); - } + }); let blob = await response.blob(); - // Format or rebuild blob - if (xtype == "text") { - blob = await toTextBlob(blob); - } - if (xtype == "text" || xtype == "screenshot") { + // Format or rebuild blob + if (xtype == "text") { + blob = await toTextBlob(blob); + } + const nextBlobId: string = await hashBlob(blob); if (nextBlobId == searchParams.get("cbi")) { return; @@ -175,16 +155,17 @@ export default function SyncClipboard() { setTmpClipboard({ blobId: nextBlobId, index: xindex, - blob: blob, + data: blob, + type: xtype, }); setStatus("interrupted-w"); - addLog(t("logs.pressAgain"), Level.Warn); + addLog({ message: t("logs.pressAgain"), level: LogLevel.Warn }); return; } await clipboardWriteBlob(blob); searchParams.set("ci", xindex); - addLog(t("logs.writeSuccess"), Level.Success); + addLog({ message: t("logs.writeSuccess"), level: LogLevel.Success }); // Cannot read the clipboard after writing immediately on Chrome for Android, Edge for Android, Edge for HarmonyOS. // So, after writing, you need to wait for a while before you can read it. @@ -209,6 +190,14 @@ export default function SyncClipboard() { document.title, "?" + searchParams.toString(), ); + + await addHistoryItem({ + index: xindex, + blobId: searchParams.get("cbi") ?? "", + data: blob, + type: xtype, + }); + return; } @@ -217,10 +206,11 @@ export default function SyncClipboard() { if (xfilename == null || xfilename == "") { return; } - setFileInfo({ + updateFileLink({ fileName: decodeURI(xfilename), fileURL: URL.createObjectURL(blob), }); + addLog({ message: t("logs.autoDownload"), level: LogLevel.Success }); // The file did not enter the clipboard, // so only update the index. searchParams.set("ci", xindex); @@ -229,6 +219,14 @@ export default function SyncClipboard() { document.title, "?" + searchParams.toString(), ); + + await addHistoryItem({ + index: xindex, + data: blob, + type: xtype, + fileName: xfilename, + }); + return; } }; @@ -237,7 +235,7 @@ export default function SyncClipboard() { let blob = null; if (textareaRef.current && textareaRef.current.value != "") { blob = new Blob([textareaRef.current.value], { type: "text/plain" }); - addLog(t("logs.readQuickInputSuccess")); + addLog({ message: t("logs.readQuickInputSuccess") }); } if (textareaRef.current?.value == "") { @@ -245,11 +243,11 @@ export default function SyncClipboard() { return; } blob = await clipboardRead(); - addLog(t("logs.readClipboardSuccess")); + addLog({ message: t("logs.readClipboardSuccess") }); } if (!blob) { - addLog(t("logs.emptyClipboard")); + addLog({ message: t("logs.emptyClipboard") }); return; } let xtype; @@ -264,7 +262,10 @@ export default function SyncClipboard() { xtype = "screenshot"; break; default: - addLog(t("logs.unsupportedFormat", { format: blob.type }), Level.Error); + addLog({ + message: t("logs.unsupportedFormat", { format: blob.type }), + level: LogLevel.Error, + }); xtype = ""; } if (xtype == "") { @@ -274,10 +275,10 @@ export default function SyncClipboard() { const nextBlobId = await hashBlob(blob); const searchParams = new URLSearchParams(window.location.search); if (nextBlobId == searchParams.get("cbi")) { - addLog(t("logs.unchanged")); + addLog({ message: t("logs.unchanged") }); return; } - addLog(t("logs.uploading")); + addLog({ message: t("logs.uploading") }); const response = await fetch("/api/v1/clipboard", { method: "POST", @@ -296,7 +297,7 @@ export default function SyncClipboard() { if (response.status != 200) { const body = await response.json(); - addLog(body.message, Level.Error); + addLog({ message: body.message, level: LogLevel.Error }); return; } @@ -314,16 +315,23 @@ export default function SyncClipboard() { document.title, `${pathname}?ci=${xindex}&cbi=${nextBlobId}`, ); - addLog( - t("logs.uploaded", { type: t(xtype), index: xindex }), - Level.Success, - ); + + await addHistoryItem({ + index: xindex, + blobId: nextBlobId, + data: blob, + type: xtype, + }); + + addLog({ + message: t("logs.uploaded", { type: t(xtype), index: xindex }), + level: LogLevel.Success, + }); }; const uploadFileHandler = async (file: File) => { resetLog(); - - addLog(t("logs.uploading")); + addLog({ message: t("logs.uploading") }); const response = await fetch("/api/v1/clipboard", { method: "POST", headers: { @@ -341,7 +349,7 @@ export default function SyncClipboard() { if (response.status != 200) { const body = await response.json(); - addLog(body.message, Level.Error); + addLog({ message: body.message, level: LogLevel.Error }); return; } const xindex = response.headers.get("x-index"); @@ -349,7 +357,7 @@ export default function SyncClipboard() { return; } - setFileInfo({ + updateFileLink({ fileName: file.name, fileURL: "", }); @@ -361,10 +369,18 @@ export default function SyncClipboard() { document.title, `${pathname}?ci=${xindex}&cbi=${searchParams.get("cbi") ?? ""}`, ); - addLog( - t("logs.uploaded", { type: t("file"), index: xindex }), - Level.Success, - ); + + await addHistoryItem({ + index: xindex, + data: file, + type: "file", + fileName: file.name, + }); + + addLog({ + message: t("logs.uploaded", { type: t("file"), index: xindex }), + level: LogLevel.Success, + }); setStatus("finished"); }; @@ -383,7 +399,7 @@ export default function SyncClipboard() { name: permissionClipboardRead, }); if (permission.state === "denied") { - addLog(t("logs.denyRead"), Level.Error); + addLog({ message: t("logs.denyRead"), level: LogLevel.Error }); return; } } @@ -391,7 +407,7 @@ export default function SyncClipboard() { // Safari requires that every call to the clipboard API must be triggered by the user. // So we have to interrupt before the next call to the clipboard API. if (status == "interrupted-w") { - await clipboardWriteBlobPromise(tmpClipboard.blob); + await clipboardWriteBlobPromise(tmpClipboard.data); // Known bug: // This blobId is hashed by the fetched blob // which is different from the blob read from clipboard. @@ -401,7 +417,15 @@ export default function SyncClipboard() { document.title, `${pathname}?ci=${tmpClipboard.index}&cbi=${tmpClipboard.blobId}`, ); - addLog(t("logs.writeSuccess"), Level.Success); + + await addHistoryItem({ + index: tmpClipboard.index, + blobId: tmpClipboard.blobId, + data: tmpClipboard.data, + type: tmpClipboard.type, + }); + + addLog({ message: t("logs.writeSuccess"), level: LogLevel.Success }); setTmpClipboard(initTmpClipboard); setStatus("finished"); @@ -419,7 +443,7 @@ export default function SyncClipboard() { current.startsWith("interrupted") ? current : "finished", ); } catch (err) { - addLog(String(err), Level.Error); + addLog({ message: String(err), level: LogLevel.Error }); setStatus("finished"); } }; @@ -440,12 +464,12 @@ export default function SyncClipboard() {
- {t("syncFile.title") + ": "} - {t("syncFile.subTitle")} + {t("syncFile.title")} + {" " + t("syncFile.subTitle")}
-
+
{t("syncFile.dragDropTip")}
+ ); } diff --git a/frontend/lib/clipboard.ts b/frontend/lib/clipboard.ts index 84f6b2d0..f08ca4dc 100644 --- a/frontend/lib/clipboard.ts +++ b/frontend/lib/clipboard.ts @@ -1,3 +1,12 @@ +export interface Clipboard { + index: string; + data: Blob; + blobId?: string; + type?: string; + fileName?: string; + clientName?: string; +} + export interface FileInfo { fileName: string; fileURL: string; @@ -8,16 +17,10 @@ export const initFileInfo: FileInfo = { fileURL: "", }; -export interface TmpClipboard { - blobId: string; - index: string; - blob: Blob; -} - -export const initTmpClipboard: TmpClipboard = { +export const initTmpClipboard: Clipboard = { blobId: "", index: "", - blob: new Blob([]), + data: new Blob([]), }; export function clipboardWriteBlob(blob: Blob) { diff --git a/frontend/lib/log.ts b/frontend/lib/log.ts index 50621898..bf388ea4 100644 --- a/frontend/lib/log.ts +++ b/frontend/lib/log.ts @@ -1,4 +1,8 @@ -export enum Level { +import { useState } from "react"; +import { osName, browserName } from "react-device-detect"; +import { useTranslations, useFormatter } from "next-intl"; + +export enum LogLevel { Warn = "warn", Error = "error", Success = "success", @@ -6,6 +10,55 @@ export enum Level { } export interface Log { - level: Level; + level?: LogLevel; message: string; } + +const recommendedBrowsers = [ + { osName: "Windows", browsers: ["Chrome", "Edge", "Opera"] }, + { osName: "iOS", browsers: ["Mobile Safari"] }, + { osName: "Android", browsers: ["Chrome", "Edge"] }, + { osName: "Mac OS", browsers: ["Chrome", "Opera", "Safari"] }, +]; + +export function useLog() { + const t = useTranslations("SyncClipboard"); + const format = useFormatter(); + + let initLogs = [ + { + level: LogLevel.Warn, + message: t("logs.pressToSync"), + }, + ]; + + recommendedBrowsers.map((item) => { + if (osName == item.osName && !item.browsers.includes(browserName)) { + initLogs = [ + { + level: LogLevel.Info, + message: t("logs.recommendedBrowsers", { + os: item.osName, + browsers: format.list(item.browsers), + }), + }, + ...initLogs, + ]; + } + }); + + const [logs, setLogs] = useState(initLogs); + + const addLog = (log: Log) => { + setLogs((current) => [ + ...current, + { level: log.level ?? LogLevel.Info, message: log.message }, + ]); + }; + + const resetLog = () => { + setLogs([]); + }; + + return { logs, addLog, resetLog }; +} diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 8c8cc7f3..1dd9750b 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -47,7 +47,7 @@ "file": "FILE", "syncFile": { "title": "Sync the files", - "subTitle": "Drag and drop a file here to sync it to different devices.", + "subTitle": "Choose a file and sync it between different devices.", "dragDropTip": "Drag and drop a file here", "fileInputText": "Choose a file" }, @@ -56,6 +56,13 @@ "subTitle": "Type text, press the button and the text synced between devices.", "newline": "Newline" }, + "history": { + "title": "History", + "subTitle": "Stored only locally on the browser and can be deleted at any time.", + "delete": "Delete", + "use": "Use", + "empty": "Empty" + }, "logs": { "pressToSync": "Press the button to sync clipboard 👉", "fetching": "Fetching...", @@ -74,7 +81,8 @@ "unsupportedFormat": "Unsupported format: {format}.", "emptyClipboard": "Clipboard is empty.", "recommendedBrowsers": "On {os}, recommend using {browsers} browser.", - "readQuickInputSuccess": "Read text from quick input area successfully." + "readQuickInputSuccess": "Read text from quick input area successfully.", + "pinLimit": "Up to 10 items can be pinned." } } } diff --git a/frontend/messages/zh.json b/frontend/messages/zh.json index 7ff7e5c1..49a6893a 100644 --- a/frontend/messages/zh.json +++ b/frontend/messages/zh.json @@ -47,7 +47,7 @@ "file": "文件", "syncFile": { "title": "同步文件", - "subTitle": "把文件拖拽到这里就能在不同设备间同步它.", + "subTitle": "选择一个文件然后在不同设备间同步它.", "dragDropTip": "把文件拖拽到这里", "fileInputText": "选择一个文件" }, @@ -56,6 +56,13 @@ "subTitle": "输入文字后按下同步按钮就能在不同设备间同步.", "newline": "换行" }, + "history": { + "title": "历史记录", + "subTitle": "仅存储在本地浏览器并且可以随时删除.", + "delete": "删除", + "use": "使用", + "empty": "空" + }, "logs": { "pressToSync": "按下按钮同步剪切板 👉", "fetching": "正在获取...", @@ -74,7 +81,8 @@ "unsupportedFormat": "不支持的格式: {format}.", "emptyClipboard": "剪切板是空的.", "recommendedBrowsers": "在{os}上, 推荐使用{browsers}浏览器.", - "readQuickInputSuccess": "成功读取快捷输入区文字." + "readQuickInputSuccess": "成功读取快捷输入区文字.", + "pinLimit": "最多可以Pin 10条记录." } } } diff --git a/frontend/models/db.ts b/frontend/models/db.ts new file mode 100644 index 00000000..5a40f94e --- /dev/null +++ b/frontend/models/db.ts @@ -0,0 +1,13 @@ +import Dexie, { Table } from "dexie"; +import { HistoryItemEntity } from "./history"; + +export class DB extends Dexie { + history!: Table; + constructor() { + super("GCopyDB"); + this.version(1).stores({ + history: "createdAt, pin", + }); + } +} +export const db = new DB(); diff --git a/frontend/models/history.ts b/frontend/models/history.ts new file mode 100644 index 00000000..a299cf60 --- /dev/null +++ b/frontend/models/history.ts @@ -0,0 +1,10 @@ +import { Clipboard } from "@/lib/clipboard"; + +export interface HistoryItemEntity extends Clipboard { + createdAt?: string; + pin?: string; + // Fix error on Safari: Failed to load resource: The operation couldn’t be completed. (WebKitBlobResource error 1.) + // https://stackoverflow.com/questions/68386273/error-loading-blob-to-img-in-safari-webkitblobresource-error-1 + dataArrayBuffer?: ArrayBuffer; + dataType?: string; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 774dda46..47f776b9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,20 +1,23 @@ { "name": "gcopy", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gcopy", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "@heroicons/react": "^2.1.1", "clsx": "^2.1.0", + "dexie": "^4.0.8", + "dexie-react-hooks": "^1.1.7", + "moment": "^2.30.1", "negotiator": "^0.6.3", - "next": "14.0.4", + "next": "^14.2.5", "next-intl": "^3.4.5", - "react": "^18", + "react": "^18.3.1", "react-device-detect": "^2.2.3", "react-dom": "^18", "swr": "^2.2.4" @@ -283,9 +286,9 @@ } }, "node_modules/@next/env": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/env/-/env-14.0.4.tgz", - "integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==" + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.0.4", @@ -297,9 +300,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz", - "integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", "cpu": [ "arm64" ], @@ -312,9 +315,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", - "integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", "cpu": [ "x64" ], @@ -327,9 +330,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", - "integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", "cpu": [ "arm64" ], @@ -342,9 +345,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", - "integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", "cpu": [ "arm64" ], @@ -357,9 +360,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", - "integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", "cpu": [ "x64" ], @@ -372,9 +375,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", - "integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", "cpu": [ "x64" ], @@ -387,9 +390,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", - "integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", "cpu": [ "arm64" ], @@ -402,9 +405,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", - "integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", "cpu": [ "ia32" ], @@ -417,9 +420,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", - "integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", "cpu": [ "x64" ], @@ -472,11 +475,17 @@ "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==", "dev": true }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -532,14 +541,12 @@ "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.45", "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.45.tgz", "integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -558,8 +565,7 @@ "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@typescript-eslint/parser": { "version": "6.15.0", @@ -946,12 +952,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1016,9 +1022,23 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001570", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", - "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==" + "version": "1.0.30001641", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz", + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -1152,8 +1172,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/culori": { "version": "3.3.0", @@ -1245,6 +1264,21 @@ "node": ">=6" } }, + "node_modules/dexie": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", + "integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==" + }, + "node_modules/dexie-react-hooks": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz", + "integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==", + "peerDependencies": { + "@types/react": ">=16", + "dexie": "^3.2 || ^4.0.1-alpha", + "react": ">=16" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1899,9 +1933,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2071,11 +2105,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", @@ -2438,7 +2467,7 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "engines": { @@ -2799,6 +2828,14 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", @@ -2842,18 +2879,17 @@ } }, "node_modules/next": { - "version": "14.0.4", - "resolved": "https://registry.npmmirror.com/next/-/next-14.0.4.tgz", - "integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", "dependencies": { - "@next/env": "14.0.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" @@ -2862,18 +2898,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.0.4", - "@next/swc-darwin-x64": "14.0.4", - "@next/swc-linux-arm64-gnu": "14.0.4", - "@next/swc-linux-arm64-musl": "14.0.4", - "@next/swc-linux-x64-gnu": "14.0.4", - "@next/swc-linux-x64-musl": "14.0.4", - "@next/swc-win32-arm64-msvc": "14.0.4", - "@next/swc-win32-ia32-msvc": "14.0.4", - "@next/swc-win32-x64-msvc": "14.0.4" + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -2882,6 +2919,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -3362,9 +3402,9 @@ "dev": true }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -3908,7 +3948,7 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { @@ -4135,18 +4175,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 61dbdafd..2a7c353d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "gcopy", - "version": "1.3.0", + "version": "1.4.0", "private": true, "scripts": { "dev": "next dev -p 3375 --experimental-https", @@ -14,10 +14,13 @@ "@formatjs/intl-localematcher": "^0.5.4", "@heroicons/react": "^2.1.1", "clsx": "^2.1.0", + "dexie": "^4.0.8", + "dexie-react-hooks": "^1.1.7", + "moment": "^2.30.1", "negotiator": "^0.6.3", - "next": "14.0.4", + "next": "^14.2.5", "next-intl": "^3.4.5", - "react": "^18", + "react": "^18.3.1", "react-device-detect": "^2.2.3", "react-dom": "^18", "swr": "^2.2.4" diff --git a/version.txt b/version.txt index 8b3a0227..ec7b9678 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v1.3.0 \ No newline at end of file +v1.4.0 \ No newline at end of file