diff --git a/apps/electron/package.json b/apps/electron/package.json index c79a2cd8..11a6ad02 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -27,6 +27,7 @@ "dependencies": { "@electron-toolkit/preload": "^2.0.0", "@electron-toolkit/utils": "^1.0.2", + "@heroicons/react": "^2.0.18", "@rao-pics/api": "workspace:^", "@rao-pics/constant": "workspace:^", "@rao-pics/db": "workspace:*", diff --git a/apps/electron/src/renderer/src/components/Svg.tsx b/apps/electron/src/renderer/src/components/Svg.tsx deleted file mode 100644 index 459fada5..00000000 --- a/apps/electron/src/renderer/src/components/Svg.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 右侧箭头 - */ -export const ArrowRightSvg = () => ( - - - -); diff --git a/apps/electron/src/renderer/src/index.css b/apps/electron/src/renderer/src/index.css index a6cc4370..b38173d4 100644 --- a/apps/electron/src/renderer/src/index.css +++ b/apps/electron/src/renderer/src/index.css @@ -25,6 +25,10 @@ .card-row > div { @apply flex items-center; } + + .card-row .right-svg { + @apply ml-1 h-4 w-4 text-base-content/30; + } } /* windows 窗口透明 */ diff --git a/apps/electron/src/renderer/src/pages/basic/index.tsx b/apps/electron/src/renderer/src/pages/basic/index.tsx index c69d30ab..05e4c700 100644 --- a/apps/electron/src/renderer/src/pages/basic/index.tsx +++ b/apps/electron/src/renderer/src/pages/basic/index.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Content from "@renderer/components/Content"; -import { ArrowRightSvg } from "@renderer/components/Svg"; import Title from "@renderer/components/Title"; import { useLanguage } from "@renderer/hooks"; import { trpc } from "@renderer/utils/trpc"; @@ -141,7 +141,7 @@ const BasicPage = () => { }} > {library?.path} - + @@ -234,7 +234,7 @@ const BasicPage = () => { {config && ( {`http://${config.ip}:${config.clientPort}`} )} - + diff --git a/apps/electron/src/renderer/src/pages/setting/index.css b/apps/electron/src/renderer/src/pages/setting/index.css new file mode 100644 index 00000000..4bcd21c9 --- /dev/null +++ b/apps/electron/src/renderer/src/pages/setting/index.css @@ -0,0 +1,3 @@ +.custom-select { + @apply select select-xs w-full rounded bg-base-100/50 text-right text-sm font-normal transition-all duration-300 hover:bg-base-100 hover:shadow focus:outline-none; +} diff --git a/apps/electron/src/renderer/src/pages/setting/index.tsx b/apps/electron/src/renderer/src/pages/setting/index.tsx index 53d565cf..6c90d2cf 100644 --- a/apps/electron/src/renderer/src/pages/setting/index.tsx +++ b/apps/electron/src/renderer/src/pages/setting/index.tsx @@ -1,28 +1,58 @@ +import { + FolderMinusIcon, + LanguageIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; import Content from "@renderer/components/Content"; import Title from "@renderer/components/Title"; import { useLanguage } from "@renderer/hooks"; +import { trpc } from "@renderer/utils/trpc"; import { LANGUAGE } from "@rao-pics/constant"; +import "./index.css"; + const languages = { "zh-cn": { title: "通用", language_title: "语言", language_desc: "选择语言", + trash: "回收站素材", + pwd_folder: "加密文件夹素材", + show: "显示", + hide: "不显示", }, "en-us": { title: "General", language_title: "Language", language_desc: "Select language", + trash: "Trash Material", + pwd_folder: "Password Folder Material", + show: "Show", + hide: "Hide", }, "zh-tw": { title: "通用", language_title: "語言", language_desc: "選擇語言", + trash: "回收站素材", + pwd_folder: "加密文件夾素材", + show: "顯示", + hide: "不顯示", }, }; const SettingPage = () => { + const utils = trpc.useContext(); + + const configUpsert = trpc.config.upsert.useMutation({ + onSuccess() { + void utils.config.invalidate(); + }, + }); + + const { data: config } = trpc.config.findUnique.useQuery(); + const { lang, language, setLanguage } = useLanguage(languages); @@ -37,20 +67,7 @@ const SettingPage = () => { - - - + {lang.language_title} @@ -64,7 +81,7 @@ const SettingPage = () => { } }} value={language} - className="select select-xs w-full bg-transparent text-right text-sm font-normal focus:outline-none" + className="custom-select" > {items.map((item) => ( @@ -75,6 +92,53 @@ const SettingPage = () => { + + + + + + + {lang.trash} + + + + { + configUpsert.mutate({ + trash: Number(e.target.value) === 1, + }); + }} + > + {lang.hide} + {lang.show} + + + + + + + + {lang.pwd_folder} + + + + { + configUpsert.mutate({ + pwdFolder: Number(e.target.value) === 1, + }); + }} + > + {lang.hide} + {lang.show} + + + + ); diff --git a/apps/electron/src/renderer/src/pages/unsync.tsx b/apps/electron/src/renderer/src/pages/unsync.tsx index 9f66862b..fa362563 100644 --- a/apps/electron/src/renderer/src/pages/unsync.tsx +++ b/apps/electron/src/renderer/src/pages/unsync.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; import Content from "@renderer/components/Content"; -import { ArrowRightSvg } from "@renderer/components/Svg"; import Title from "@renderer/components/Title"; import { useDebounce, useLanguage } from "@renderer/hooks"; import { trpc } from "@renderer/utils/trpc"; @@ -75,7 +75,7 @@ const UnsyncPage = () => { .replace(libPath, "") .replace("/metadata.json", "")} - + diff --git a/packages/api/index.ts b/packages/api/index.ts index e4a9735b..33e65854 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -1,6 +1,6 @@ import { color } from "./src/color"; -import { config } from "./src/config"; -import { folder } from "./src/folder"; +import { config, configCore } from "./src/config"; +import { folder, folderCore } from "./src/folder"; import { image } from "./src/image"; import { library } from "./src/library"; import { log } from "./src/log"; @@ -21,6 +21,25 @@ export const router = t.router({ log, }); +export const routerCore = { + config: configCore, + folder: folderCore, +}; + export type AppRouter = typeof router; export { t } from "./src/utils"; export * from "./src/express"; + +let caller: ReturnType; + +/** + * router.createCaller() + * @returns + */ +export const getCaller = () => { + if (!caller) { + caller = router.createCaller({}); + } + + return caller; +}; diff --git a/packages/api/mocks/folder.json b/packages/api/mocks/folder.json index 5c60e2a7..131e01b2 100644 --- a/packages/api/mocks/folder.json +++ b/packages/api/mocks/folder.json @@ -12,8 +12,8 @@ "children": [], "modificationTime": 1693138821077, "tags": [], - "password": "", - "passwordTips": "" + "password": "1234", + "passwordTips": "1234" } ], "modificationTime": 1690869001589, diff --git a/packages/api/src/config.ts b/packages/api/src/config.ts index 21fd44df..03142fda 100644 --- a/packages/api/src/config.ts +++ b/packages/api/src/config.ts @@ -3,44 +3,56 @@ import { z } from "zod"; import { DEFAULT_THEME } from "@rao-pics/constant"; import { prisma } from "@rao-pics/db"; +import { folderCore } from "./folder"; import { t } from "./utils"; +export const configInput = { + upsert: z.object({ + language: z.enum(["zh-cn", "en-us", "zh-tw"]).optional(), + theme: z.string().optional(), + color: z.string().optional(), + serverPort: z.number().optional(), + clientPort: z.number().optional(), + ip: z.string().optional(), + pwdFolder: z.boolean().optional(), + trash: z.boolean().optional(), + }), +}; + +export const configCore = { + findUnique: async () => + await prisma.config.findFirst({ where: { name: "config" } }), + + // pwdFolder 更新逻辑 + // 1. config.pwdFolder = true + // 2. 修改 folder.password != null 的 folder.show = true + // 2.1 如果修改的是父级 folder, 则需要同步修改子级 folder + // 3. 查询 Folder 时,只需要返回 folder.show = true 的 folder + upsert: async (input: z.infer) => { + const { pwdFolder } = input; + + if (pwdFolder != undefined) { + await folderCore.setPwdFolderShow(pwdFolder); + } + + return await prisma.config.upsert({ + where: { name: "config" }, + update: input, + create: { + ...input, + name: "config", + language: input.language ?? "zh-cn", + color: input.color ?? "light", + theme: input.theme ?? DEFAULT_THEME, + }, + }); + }, +}; + export const config = t.router({ upsert: t.procedure - .input( - z.object({ - language: z.enum(["zh-cn", "en-us", "zh-tw"]).optional(), - theme: z.string().optional(), - color: z.string().optional(), - serverPort: z.number().optional(), - clientPort: z.number().optional(), - ip: z.string().optional(), - }), - ) - .mutation(async ({ input }) => { - return await prisma.config.upsert({ - where: { name: "config" }, - update: { - language: input.language ?? undefined, - color: input.color ?? undefined, - theme: input.theme ?? undefined, - ip: input.ip ?? undefined, - serverPort: input.serverPort ?? undefined, - clientPort: input.clientPort ?? undefined, - }, - create: { - name: "config", - language: input.language ?? "zh-cn", - color: input.color ?? "light", - theme: input.theme ?? DEFAULT_THEME, - ip: input.ip, - serverPort: input.serverPort, - clientPort: input.clientPort, - }, - }); - }), - - findUnique: t.procedure.query(async () => { - return await prisma.config.findFirst({ where: { name: "config" } }); - }), + .input(configInput.upsert) + .mutation(({ input }) => configCore.upsert(input)), + + findUnique: t.procedure.query(configCore.findUnique), }); diff --git a/packages/api/src/folder.ts b/packages/api/src/folder.ts index 8a2560f3..6f90a6e7 100644 --- a/packages/api/src/folder.ts +++ b/packages/api/src/folder.ts @@ -2,48 +2,124 @@ import { z } from "zod"; import { prisma } from "@rao-pics/db"; +import { configCore } from "./config"; import { t } from "./utils"; +export const folderInput = { + find: z + .object({ + id: z.string().optional(), + pid: z.string().optional(), + }) + .optional(), + upsert: z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + pid: z.string().optional(), + password: z.string().optional(), + passwordTips: z.string().optional(), + show: z.boolean().optional().default(true), + }), + + setPwdFolderShow: z.boolean(), +}; + +export const folderCore = { + find: async (input: z.infer) => { + if (input?.id) { + return await prisma.folder.findUnique({ + where: { id: input.id, show: true }, + }); + } + + if (input?.pid) { + return await prisma.folder.findMany({ + where: { pid: input.pid, show: true }, + }); + } + + return await prisma.folder.findMany({ + where: { show: true }, + }); + }, + + upsert: async (input: z.infer) => { + const { password } = input; + const config = await configCore.findUnique(); + + if (password) { + input.show = config?.pwdFolder ?? false; + } + + // 更新 show, 需要同步更新子文件夹中的 show + await prisma.folder.updateMany({ + where: { + OR: [ + // 父级在 upsert 中更新 + // { id: input.id }, + { pid: input.id }, + ], + }, + data: { + show: input.show, + }, + }); + + return await prisma.folder.upsert({ + where: { id: input.id }, + create: input, + update: input, + }); + }, + + /** + * 修改所有有密码的文件夹的 show + * 子级的 show 保持和父级一致 + * 父级有密码,子项没有,子项也会设置为 父级 相同的 show + */ + setPwdFolderShow: async ( + input: z.infer, + ) => { + // 所有有密码的文件夹 + const pFolders = await prisma.folder.findMany({ + where: { password: { not: "" } }, + }); + + // 父级文件有密码、子级没有密码的文件夹 id + const pids = pFolders.filter((f) => !f.pid).map((f) => f.id); + + const childFolders = await prisma.folder.findMany({ + where: { + pid: { + in: pids, + }, + }, + }); + + return await prisma.folder.updateMany({ + where: { + id: { + in: pFolders.concat(childFolders).map((item) => item.id), + }, + }, + data: { + show: input, + }, + }); + }, +}; + export const folder = t.router({ upsert: t.procedure - .input( - z.object({ - id: z.string(), - name: z.string(), - description: z.string().optional(), - pid: z.string().optional(), - }), - ) - .mutation(async ({ input }) => { - return await prisma.folder.upsert({ - where: { id: input.id }, - create: input, - update: input, - }); - }), + .input(folderInput.upsert) + .mutation(async ({ input }) => folderCore.upsert(input)), find: t.procedure - .input( - z - .object({ - id: z.string().optional(), - pid: z.string().optional(), - }) - .optional(), - ) - .query(async ({ input }) => { - if (input?.id) { - return await prisma.folder.findUnique({ - where: { id: input.id }, - }); - } - - if (input?.pid) { - return await prisma.folder.findMany({ - where: { pid: input.pid }, - }); - } - - return await prisma.folder.findMany(); - }), + .input(folderInput.find) + .query(async ({ input }) => folderCore.find(input)), + + setPwdFolderShow: t.procedure + .input(folderInput.setPwdFolderShow) + .mutation(async ({ input }) => folderCore.setPwdFolderShow(input)), }); diff --git a/packages/api/src/image.ts b/packages/api/src/image.ts index a183631e..7a11f5a1 100644 --- a/packages/api/src/image.ts +++ b/packages/api/src/image.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import type { Color, Folder, Image, Prisma, Tag } from "@rao-pics/db"; +import type { Prisma } from "@rao-pics/db"; import { prisma } from "@rao-pics/db"; -import { router } from ".."; +import { getCaller, routerCore } from ".."; import { t } from "./utils"; export const image = t.router({ @@ -18,10 +18,16 @@ export const image = t.router({ .query(async ({ input }) => { const { includes } = input; + const config = await routerCore.config.findUnique(); + const image = await prisma.image.findUnique({ where: { id: input.id, path: input.path, + // 回收站需要显示 => isDeleted: undefined + // 回收站不需要显示 => isDeleted: false + isDeleted: config?.trash ? undefined : false, + folders: config?.pwdFolder ? undefined : { every: { show: true } }, }, include: { tags: includes?.includes("tags"), @@ -30,17 +36,7 @@ export const image = t.router({ }, }); - return image - ? { - ...image, - thumbnailPath: image.noThumbnail - ? image.path - : image.path.replace( - "metadata.json", - `${image.name}_thumbnail.png`, - ), - } - : null; + return image; }), upsert: t.procedure @@ -81,7 +77,6 @@ export const image = t.router({ }), ) .mutation(async ({ input }) => { - const caller = router.createCaller({}); const data: Prisma.ImageUpdateInput = { path: input.path, name: input.name, @@ -146,58 +141,11 @@ export const image = t.router({ } // 清除日志中的错误信息 - await caller.log.delete(res.path); + await getCaller().log.delete(res.path); return res; }), - update: t.procedure - .input( - z - .object({ - id: z.number().optional(), - path: z.string().optional(), - name: z.string().optional(), - size: z.number().optional(), - ext: z.string().optional(), - width: z.number().optional(), - height: z.number().optional(), - mtime: z.date().optional(), - duration: z.number().optional(), - annotation: z.string().optional(), - url: z.string().optional(), - isDeleted: z.boolean().optional(), - blurDataURL: z.string().optional(), - noThumbnail: z.boolean().optional(), - }) - .partial() - .refine( - (data) => !!data.id || !!data.path, - "id or path either one is required", - ), - ) - .mutation(async ({ input }) => { - const { id, path, ...data } = input; - if (id) { - return prisma.image.update({ - where: { id }, - data: { - ...data, - path, - }, - }); - } - - if (path) { - return prisma.image.update({ - where: { path }, - data, - }); - } - - return null; - }), - deleteByUnique: t.procedure .input( z @@ -218,10 +166,11 @@ export const image = t.router({ if (path) { return prisma.image.delete({ where: { path } }); } - - return null; }), + /** + * 查询时不返回 回收站 和 不显示文件夹中的素材 + */ find: t.procedure .input( z @@ -234,10 +183,17 @@ export const image = t.router({ ) .query(async ({ input }) => { const limit = input?.limit ?? 50; - const { cursor, includes } = input ?? {}; - let images = await prisma.image.findMany({ + const images = await prisma.image.findMany({ + where: { + // 回收站的素材不显示 + isDeleted: false, + // 文件夹显示的素材不显示 + folders: { + every: { show: true }, + }, + }, take: limit + 1, cursor: cursor ? { path: cursor } : undefined, orderBy: { createdTime: "desc" }, @@ -248,27 +204,15 @@ export const image = t.router({ }, }); - type ResultImage = Image & { tags: Tag[] } & { colors: Color[] } & { - folders: Folder[]; - } & { thumbnailPath: string }; - - images = images.map((item) => { - const _item = item as ResultImage; - _item.thumbnailPath = _item.noThumbnail - ? _item.path - : _item.path.replace("metadata.json", `${_item.name}_thumbnail.png`); - - return _item; - }); - let nextCursor: typeof cursor | undefined = undefined; + if (images.length > limit) { const nextImage = images.pop(); nextCursor = nextImage!.path; } return { - data: images as ResultImage[], + data: images, nextCursor, }; }), diff --git a/packages/api/src/sync/folder.ts b/packages/api/src/sync/folder.ts index 8e35fa16..00f8994f 100644 --- a/packages/api/src/sync/folder.ts +++ b/packages/api/src/sync/folder.ts @@ -14,6 +14,8 @@ interface FolderTree { description?: string; pid?: string; children: FolderTree[]; + password?: string; + passwordTips?: string; } export const treeToFlat = (folderTree: FolderTree[]) => { @@ -22,6 +24,8 @@ export const treeToFlat = (folderTree: FolderTree[]) => { id: string; description?: string; pid?: string; + password?: string; + passwordTips?: string; }[] = []; const callback = (folderTree: FolderTree[]) => { @@ -31,6 +35,8 @@ export const treeToFlat = (folderTree: FolderTree[]) => { id: folder.id, description: folder.description, pid: folder.pid, + password: folder.password ?? undefined, + passwordTips: folder.passwordTips ?? undefined, }); if (folder.children.length > 0) { callback( diff --git a/packages/api/src/sync/index.ts b/packages/api/src/sync/index.ts index 8780c2d6..b3ec33c3 100644 --- a/packages/api/src/sync/index.ts +++ b/packages/api/src/sync/index.ts @@ -6,6 +6,8 @@ import { z } from "zod"; import type { Pending } from "@rao-pics/db"; import { router } from "../.."; +import { configCore } from "../config"; +import { folderCore } from "../folder"; import { t } from "../utils"; import { handleFolder } from "./folder"; import { deleteImage, upsertImage } from "./image"; @@ -53,6 +55,8 @@ export const sync = t.router({ // 同步文件夹 const folders = handleFolder(join(input.libraryPath, "metadata.json")); await syncFolder(folders, caller); + const config = await configCore.findUnique(); + await folderCore.setPwdFolderShow(config?.pwdFolder ?? false); } catch (e) { // 每次同步首先同步文件夹,如果文件夹同步失败,直接返回 return false; diff --git a/packages/api/test/config.test.ts b/packages/api/test/config.test.ts index addeed67..be3b9864 100644 --- a/packages/api/test/config.test.ts +++ b/packages/api/test/config.test.ts @@ -6,6 +6,18 @@ import { router } from ".."; const caller = router.createCaller({}); +const defaultConfig = { + name: "config", + language: "zh-cn", + color: "light", + theme: "gallery", + ip: null, + serverPort: null, + clientPort: null, + pwdFolder: false, + trash: false, +}; + describe("config module", () => { beforeEach(async () => { await prisma.config.deleteMany(); @@ -24,13 +36,8 @@ describe("config module", () => { const res = await caller.config.findUnique(); expect(res).toEqual({ - name: "config", + ...defaultConfig, language: "zh-cn", - color: "light", - theme: "gallery", - ip: null, - serverPort: null, - clientPort: null, }); }); @@ -43,13 +50,9 @@ describe("config module", () => { const res = await caller.config.findUnique(); expect(res).toEqual({ - name: "config", - language: "zh-cn", - color: "light", - theme: "gallery", + ...defaultConfig, ip: "0.0.0.0", serverPort: 8080, - clientPort: null, }); expect( @@ -57,17 +60,13 @@ describe("config module", () => { serverPort: 8081, }), ).toEqual({ - name: "config", - language: "zh-cn", - color: "light", - theme: "gallery", + ...defaultConfig, ip: "0.0.0.0", serverPort: 8081, - clientPort: null, }); }); - it("should update the color field in the config table", async () => { + it("should update the color and theme field in the config table", async () => { await caller.config.upsert({ color: "senven", }); @@ -75,13 +74,8 @@ describe("config module", () => { const res = await caller.config.findUnique(); expect(res).toEqual({ - name: "config", - language: "zh-cn", + ...defaultConfig, color: "senven", - theme: "gallery", - ip: null, - serverPort: null, - clientPort: null, }); }); @@ -93,13 +87,8 @@ describe("config module", () => { const res = await caller.config.findUnique(); expect(res).toEqual({ - name: "config", - language: "zh-cn", - color: "light", + ...defaultConfig, theme: "dark", - ip: null, - serverPort: null, - clientPort: null, }); }); @@ -113,4 +102,20 @@ describe("config module", () => { }); }); }); + + describe("findUnique procedure", () => { + it("should return the default config", async () => { + await caller.config.upsert({ + language: "zh-cn", + color: "light", + theme: "gallery", + pwdFolder: false, + trash: false, + }); + + const res = await caller.config.findUnique(); + + expect(res).toEqual(defaultConfig); + }); + }); }); diff --git a/packages/api/test/folder.test.ts b/packages/api/test/folder.test.ts index cbfb1015..db4eac37 100644 --- a/packages/api/test/folder.test.ts +++ b/packages/api/test/folder.test.ts @@ -69,7 +69,7 @@ describe("folder module", () => { }); }); - describe("get", () => { + describe("find", () => { it("returns all folders if no input is provided", async () => { const input = { id: "folder-1", @@ -128,5 +128,45 @@ describe("folder module", () => { expect(result).toHaveLength(2); expect(result).toMatchObject([input1, input2]); }); + + it("returns an folder.show=false is null", async () => { + const input = { + id: "folder-1", + name: "Folder 1", + description: "This is folder 1", + password: "123", + }; + + await caller.folder.upsert(input); + + const result = await caller.folder.find({ id: input.id }); + + expect(result).toBeNull(); + }); + }); + + describe("setPwdFolderShow", () => { + it("sets config.pwdFolder to true", async () => { + const input = { + id: "folder-1", + name: "Folder 1", + description: "This is folder 1", + password: "123", + }; + + await caller.folder.upsert(input); + + await caller.folder.setPwdFolderShow(true); + + const result = await caller.folder.find({ id: input.id }); + + expect(result).toMatchObject({ + id: input.id, + name: input.name, + description: input.description, + password: input.password, + show: true, + }); + }); }); }); diff --git a/packages/api/test/image.test.ts b/packages/api/test/image.test.ts index 53abee33..ddd3d4e9 100644 --- a/packages/api/test/image.test.ts +++ b/packages/api/test/image.test.ts @@ -12,6 +12,7 @@ describe("image module", () => { await prisma.image.deleteMany(); await prisma.folder.deleteMany(); await prisma.tag.deleteMany(); + await prisma.config.deleteMany(); }); describe("findUnique", () => { @@ -31,12 +32,8 @@ describe("image module", () => { }); const result = await caller.image.findUnique({ id: testImage.id }); - console.log(result); - expect(result).toEqual({ - ...testImage, - thumbnailPath: "/path/to/image_thumbnail.png", - }); + expect(result).toEqual(testImage); }); it("should find an image by id", async () => { @@ -52,10 +49,7 @@ describe("image module", () => { const result = await caller.image.findUnique({ id: testImage.id }); - expect(result).toEqual({ - ...testImage, - thumbnailPath: "/path/to/image.jpg", - }); + expect(result).toEqual(testImage); }); it("should find an image by path", async () => { @@ -71,10 +65,7 @@ describe("image module", () => { const result = await caller.image.findUnique({ path: testImage.path }); - expect(result).toEqual({ - ...testImage, - thumbnailPath: "/path/to/1image.jpg", - }); + expect(result).toEqual(testImage); }); it("should return null if no image is found", async () => { @@ -84,6 +75,179 @@ describe("image module", () => { }); }); + describe("findUnique, config.trash", () => { + beforeEach(async () => { + await prisma.image.deleteMany(); + await prisma.config.deleteMany(); + }); + + it("config.trash = true, isDeleted = true should find an image by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + isDeleted: true, + mtime: new Date(), + }); + await caller.config.upsert({ trash: true }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toEqual(testImage); + }); + + it("config.trash = true, isDeleted = false should find an image by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + isDeleted: false, + mtime: new Date(), + }); + await caller.config.upsert({ trash: true }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toEqual(testImage); + }); + + it("config.trash = false, isDeleted = true should find an image by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + isDeleted: true, + mtime: new Date(), + }); + await caller.config.upsert({ trash: false }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toBeNull(); + }); + + it("config.trash = false, isDeleted = false should find an image by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + isDeleted: false, + mtime: new Date(), + }); + await caller.config.upsert({ trash: false }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toEqual(testImage); + }); + }); + + describe("findUnique, config.pwdFolder", () => { + beforeEach(async () => { + await prisma.image.deleteMany(); + await prisma.config.deleteMany(); + await prisma.folder.deleteMany(); + }); + + it("config.pwdFolder = true, folder.show = true should find an image by path", async () => { + await caller.folder.upsert({ name: "folder1", id: "1", show: true }); + + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + folders: { + connect: ["1"], + }, + }); + await caller.config.upsert({ pwdFolder: true }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toEqual(testImage); + }); + + it("config.pwdFolder = true, folder.show = false should find an image by path", async () => { + await caller.folder.upsert({ name: "folder1", id: "1", show: false }); + + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + folders: { + connect: ["1"], + }, + }); + await caller.config.upsert({ pwdFolder: true }); + + const result = await caller.image.findUnique({ path: testImage.path }); + expect(result).toEqual(testImage); + }); + + it("config.pwdFolder = false, folder.show = false should find an image by path", async () => { + await caller.folder.upsert({ name: "folder1", id: "1", show: false }); + + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + folders: { + connect: ["1"], + }, + }); + await caller.config.upsert({ pwdFolder: false }); + + const result = await caller.image.findUnique({ + path: testImage.path, + includes: ["folders"], + }); + expect(result).toBeNull(); + }); + + it("config.pwdFolder = false, folder.show = true should find an image by path", async () => { + await caller.folder.upsert({ name: "folder1", id: "1", show: true }); + + const testImage = await caller.image.upsert({ + path: "/path/to/metadata.json", + name: "image", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + folders: { + connect: ["1"], + }, + }); + await caller.config.upsert({ pwdFolder: false }); + + const result = await caller.image.findUnique({ + path: testImage.path, + }); + + expect(result).toEqual(testImage); + }); + }); + describe("upsert", () => { beforeEach(async () => { await prisma.image.deleteMany(); @@ -468,4 +632,100 @@ describe("image module", () => { await caller.image.findUnique({ path: "/path/to/image.jpg" }), ).toHaveProperty("path", "/path/to/image.jpg"); }); + + describe("deleteByUnique", () => { + beforeEach(async () => { + await prisma.image.deleteMany(); + }); + + it("delete by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/image.jpg", + name: "image.jpg", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + }); + + expect( + await caller.image.deleteByUnique({ path: "/path/to/image.jpg" }), + ).toEqual(testImage); + + expect( + await caller.image.findUnique({ path: "/path/to/image.jpg" }), + ).toBeNull(); + }); + + it("delete by path", async () => { + const testImage = await caller.image.upsert({ + path: "/path/to/image.jpg", + name: "image.jpg", + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + }); + + expect(await caller.image.deleteByUnique({ id: testImage.id })).toEqual( + testImage, + ); + + expect(await caller.image.findUnique({ id: testImage.id })).toBeNull(); + }); + }); + + describe("find", () => { + beforeEach(async () => { + await prisma.image.deleteMany(); + }); + + it("should find all images and nextCursor", async () => { + for (let i = 0; i < 25; i++) { + await caller.image.upsert({ + path: `/path/to/image${i}.jpg`, + name: `image${i}.jpg`, + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + }); + } + + const res = await caller.image.find({ limit: 24 }); + expect(res.data).toHaveLength(24); + expect(res.nextCursor).toEqual(`/path/to/image0.jpg`); + + const res2 = await caller.image.find({ + limit: 24, + cursor: res.nextCursor, + }); + expect(res2.data).toHaveLength(1); + expect(res2.nextCursor).toEqual(undefined); + }); + + it("includes tags, colors, folders", async () => { + await caller.image.upsert({ + path: `/path/to/image.jpg`, + name: `image.jpg`, + size: 1024, + ext: "jpg", + width: 800, + height: 600, + mtime: new Date(), + }); + + const res = await caller.image.find({ + includes: ["folders", "tags", "colors"], + }); + + expect(res.data).toHaveLength(1); + expect(res.data[0]).toHaveProperty("folders"); + expect(res.data[0]).toHaveProperty("tags"); + expect(res.data[0]).toHaveProperty("colors"); + }); + }); }); diff --git a/packages/api/test/sync.folder.test.ts b/packages/api/test/sync.folder.test.ts index 51b46ea9..d79494a1 100644 --- a/packages/api/test/sync.folder.test.ts +++ b/packages/api/test/sync.folder.test.ts @@ -10,19 +10,37 @@ describe("handle folder by path", () => { it("should return a folder tree", () => { const flat = handleFolder(mockjson); expect(flat).toEqual([ - { name: "期刊", id: "LKRVQVLHGERAX", description: "", pid: undefined }, + { + name: "期刊", + id: "LKRVQVLHGERAX", + description: "", + pid: undefined, + password: "", + passwordTips: "", + }, { name: "04", id: "LLTF4ZITJSWSS", description: "", pid: "LKRVQVLHGERAX", + password: "1234", + passwordTips: "1234", + }, + { + name: "套图", + id: "LLNP0K49I63ZA", + description: "", + pid: undefined, + password: "", + passwordTips: "", }, - { name: "套图", id: "LLNP0K49I63ZA", description: "", pid: undefined }, { name: "08", id: "LLWCB0OZM5GZ3", description: "", pid: "LLNP0K49I63ZA", + password: "", + passwordTips: "", }, ]); }); @@ -38,19 +56,37 @@ describe("folder tree to flat", () => { it("should return a flat array", () => { const flat = treeToFlat(folderMock.folders as never); expect(flat).toEqual([ - { name: "期刊", id: "LKRVQVLHGERAX", description: "", pid: undefined }, + { + name: "期刊", + id: "LKRVQVLHGERAX", + description: "", + pid: undefined, + password: "", + passwordTips: "", + }, { name: "04", id: "LLTF4ZITJSWSS", description: "", pid: "LKRVQVLHGERAX", + password: "1234", + passwordTips: "1234", + }, + { + name: "套图", + id: "LLNP0K49I63ZA", + description: "", + pid: undefined, + password: "", + passwordTips: "", }, - { name: "套图", id: "LLNP0K49I63ZA", description: "", pid: undefined }, { name: "08", id: "LLWCB0OZM5GZ3", description: "", pid: "LLNP0K49I63ZA", + password: "", + passwordTips: "", }, ]); }); diff --git a/packages/db/package.json b/packages/db/package.json index c1a5b624..e45e1df3 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -24,7 +24,7 @@ ] }, "dependencies": { - "@prisma/client": "5.3.1", + "@prisma/client": "5.4.1", "@rao-pics/constant": "workspace:^", "fs-extra": "^11.1.1" }, @@ -35,7 +35,7 @@ "@types/node": "^18.17.6", "cross-env": "^7.0.3", "eslint": "^8.47.0", - "prisma": "^5.3.1", + "prisma": "^5.4.1", "typescript": "^5.1.6", "vitest": "^0.34.2" } diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 5c4b6c99..f345b3ea 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -23,6 +23,10 @@ model Config { clientPort Int? // 当前 IP ip String? + // 显示回收站 + trash Boolean @default(false) + // 显示加密文件夹 + pwdFolder Boolean @default(false) } model Library { @@ -97,11 +101,14 @@ model Color { } model Folder { - id String @id @unique @default(uuid()) - name String - pid String? - description String? - images Image[] + id String @id @unique @default(uuid()) + name String + pid String? + description String? + images Image[] + password String? + passwordTips String? + show Boolean @default(true) } model Log { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f53df151..3739407b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: '@electron-toolkit/utils': specifier: ^1.0.2 version: 1.0.2(electron@24.6.5) + '@heroicons/react': + specifier: ^2.0.18 + version: 2.0.18(react@18.2.0) '@rao-pics/api': specifier: workspace:^ version: link:../../packages/api @@ -256,8 +259,8 @@ importers: packages/db: dependencies: '@prisma/client': - specifier: 5.3.1 - version: 5.3.1(prisma@5.3.1) + specifier: 5.4.1 + version: 5.4.1(prisma@5.4.1) '@rao-pics/constant': specifier: workspace:^ version: link:../constant @@ -284,8 +287,8 @@ importers: specifier: ^8.47.0 version: 8.47.0 prisma: - specifier: ^5.3.1 - version: 5.3.1 + specifier: ^5.4.1 + version: 5.4.1 typescript: specifier: ^5.1.6 version: 5.1.6 @@ -1111,6 +1114,14 @@ packages: resolution: {integrity: sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@heroicons/react@2.0.18(react@18.2.0): + resolution: {integrity: sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==} + peerDependencies: + react: '>= 16' + dependencies: + react: 18.2.0 + dev: false + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -1332,8 +1343,8 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true - /@prisma/client@5.3.1(prisma@5.3.1): - resolution: {integrity: sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==} + /@prisma/client@5.4.1(prisma@5.4.1): + resolution: {integrity: sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ==} engines: {node: '>=16.13'} requiresBuild: true peerDependencies: @@ -1342,16 +1353,16 @@ packages: prisma: optional: true dependencies: - '@prisma/engines-version': 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59 - prisma: 5.3.1 + '@prisma/engines-version': 5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f + prisma: 5.4.1 dev: false - /@prisma/engines-version@5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59: - resolution: {integrity: sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==} + /@prisma/engines-version@5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f: + resolution: {integrity: sha512-+nUQM/y8C+1GG5Ioeqcu6itFslCfxvQSAUVSMC9XM2G2Fcq0F4Afnp6m0pXF6X6iUBWen7jZBPmM9Qlq4Nr3/A==} dev: false - /@prisma/engines@5.3.1: - resolution: {integrity: sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==} + /@prisma/engines@5.4.1: + resolution: {integrity: sha512-vJTdY4la/5V3N7SFvWRmSMUh4mIQnyb/MNoDjzVbh9iLmEC+uEykj/1GPviVsorvfz7DbYSQC4RiwmlEpTEvGA==} requiresBuild: true /@sentry-internal/tracing@7.70.0: @@ -5593,13 +5604,13 @@ packages: react-is: 18.2.0 dev: true - /prisma@5.3.1: - resolution: {integrity: sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==} + /prisma@5.4.1: + resolution: {integrity: sha512-op9PmU8Bcw5dNAas82wBYTG0yHnpq9/O3bhxbDBrNzwZTwBqsVCxxYRLf6wHNh9HVaDGhgjjHlu1+BcW8qdnBg==} engines: {node: '>=16.13'} hasBin: true requiresBuild: true dependencies: - '@prisma/engines': 5.3.1 + '@prisma/engines': 5.4.1 /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}