From 4c8b3503e63c0c6b808503a46629bcfef5e57841 Mon Sep 17 00:00:00 2001 From: Pieter Verschaffelt Date: Thu, 8 Feb 2024 14:36:19 +0100 Subject: [PATCH] Implement recent projects --- electron.vite.config.ts | 6 +- package.json | 2 +- src/common/project/RecentProjectManager.ts | 11 ++++ src/main/IPCHandler.ts | 13 ++++ ...r.ts => FileSystemRecentProjectManager.ts} | 65 +++++++++++------- src/preload/index.d.ts | 9 ++- src/preload/index.ts | 6 ++ src/renderer/components/pages/HomePage.vue | 39 +++++++++++ .../project/RecentProjectOverview.vue | 66 +++++++++++++++++++ src/renderer/env.d.ts | 8 +++ .../project/RemoteRecentProjectManager.ts | 13 ++++ src/renderer/plugins/vuetify.ts | 8 +-- tsconfig.json | 1 + tsconfig.node.json | 6 +- tsconfig.web.json | 1 + yarn.lock | 7 +- 16 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 src/common/project/RecentProjectManager.ts rename src/main/project/{RecentProjectsManager.ts => FileSystemRecentProjectManager.ts} (53%) create mode 100644 src/renderer/components/project/RecentProjectOverview.vue create mode 100644 src/renderer/logic/project/RemoteRecentProjectManager.ts diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 01bef1a..56c8201 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -8,7 +8,8 @@ export default defineConfig({ resolve: { alias: { "@common": resolve("src/common"), - "@main": resolve("src/main") + "@main": resolve("src/main"), + "@renderer": resolve("src/renderer"), } }, }, @@ -18,7 +19,8 @@ export default defineConfig({ alias: { "@common": resolve("src/common"), "@preload": resolve("src/preload"), - "@main": resolve("src/main") + "@main": resolve("src/main"), + "@renderer": resolve("src/renderer"), } }, }, diff --git a/package.json b/package.json index f254192..dacdf9c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "unipept-web-components": "^2.1.5", "uuid": "^9.0.1", "vue-router": "4", - "vuetify": "^3.4.0" + "vuetify": "^3.5.3" }, "devDependencies": { "@electron-toolkit/tsconfig": "^1.0.1", diff --git a/src/common/project/RecentProjectManager.ts b/src/common/project/RecentProjectManager.ts new file mode 100644 index 0000000..e90e4db --- /dev/null +++ b/src/common/project/RecentProjectManager.ts @@ -0,0 +1,11 @@ +import RecentProject from "@common/project/RecentProject"; + +export default interface RecentProjectManager { + /** + * @return A list of all projects that were recently opened by the user, sorted descending by date. An empty list + * is returned if the recent projects file does not exist. + */ + getRecentProjects(): Promise; + + addRecentProject(projectPath: string): Promise; +} diff --git a/src/main/IPCHandler.ts b/src/main/IPCHandler.ts index af09b4b..296db5e 100644 --- a/src/main/IPCHandler.ts +++ b/src/main/IPCHandler.ts @@ -4,6 +4,7 @@ import ConfigurationManager from "./configuration/ConfigurationManager"; import BrowserUtils from "./browser/BrowserUtils"; import DialogManager from "./dialog/DialogManager"; import AppManager from "./app/AppManager"; +import FileSystemRecentProjectManager from "@main/project/FileSystemRecentProjectManager"; export default class IPCHandler { public initializeIPC() { @@ -50,5 +51,17 @@ export default class IPCHandler { ipcMain.handle("app:get-app-version", () => appManager.getAppVersion()); ipcMain.handle("app:get-electron-version", () => appManager.getElectronVersion()); ipcMain.handle("app:get-chrome-version", () => appManager.getChromeVersion()); + + // Project actions + const recentProjectManager = new FileSystemRecentProjectManager(); + ipcMain.handle( + "recent-projects:read-recent-projects", + () => recentProjectManager.getRecentProjects() + ); + ipcMain.handle( + "recent-projects:add-recent-project", + (_, projectPath) => recentProjectManager.addRecentProject(projectPath) + ); + } } diff --git a/src/main/project/RecentProjectsManager.ts b/src/main/project/FileSystemRecentProjectManager.ts similarity index 53% rename from src/main/project/RecentProjectsManager.ts rename to src/main/project/FileSystemRecentProjectManager.ts index 276c0cb..82d90b1 100644 --- a/src/main/project/RecentProjectsManager.ts +++ b/src/main/project/FileSystemRecentProjectManager.ts @@ -2,9 +2,13 @@ import { promises as fs } from "fs"; import path from "path"; import RecentProject from "@common/project/RecentProject"; +import RecentProjectManager from "@common/project/RecentProjectManager"; +import { app } from "electron"; +import FileSystemManager from "@main/file-system/FileSystemManager"; -export default class RecentProjectsManager { +export default class FileSystemRecentProjectManager implements RecentProjectManager { public static readonly AMOUNT_OF_RECENT_PROJECTS = 15; + private static readonly RECENT_PROJECTS_FILE = "unipept_recent_projects.config"; /** * @return A list of all projects that were recently opened by the user, sorted descending by date. An empty list @@ -12,29 +16,35 @@ export default class RecentProjectsManager { * @throws {Error} If the recent projects file was corrupt, not readable or otherwise damaged. */ public async getRecentProjects(): Promise { - const storage = window.localStorage; - const readProjects = storage.getItem("recent-projects"); + const fsManager = new FileSystemManager(); - if (readProjects === null) { - return []; - } + try { + const readProjects = await fsManager.readFile(await this.getRecentProjectsPath()); + + if (readProjects === null) { + return []; + } + + const parsedProjects: RecentProject[] = JSON.parse(readProjects).map( + (obj: any) => new RecentProject(obj.name, obj.path, new Date(parseInt(obj.lastOpened))) + ); - const parsedProjects: RecentProject[] = JSON.parse(readProjects).map( - (obj: any) => new RecentProject(obj.name, obj.path, new Date(parseInt(obj.lastOpened))) - ); - - const filteredProjects: RecentProject[] = []; - for (const recentProject of parsedProjects) { - try { - await fs.stat(recentProject.path); - filteredProjects.push(recentProject); - } catch (err) { - // Do nothing, this project does not exist anymore. + const filteredProjects: RecentProject[] = []; + for (const recentProject of parsedProjects) { + try { + await fs.stat(recentProject.path); + filteredProjects.push(recentProject); + } catch (err) { + // Do nothing, this project does not exist anymore. + } } + + // We should also sort the filtered projects from newest to oldest. + return filteredProjects.sort((a, b) => b.lastOpened.getTime() - a.lastOpened.getTime()); + } catch (err) { + return []; } - // We should also sort the filtered projects from newest to oldest. - return filteredProjects.sort((a, b) => b.lastOpened.getTime() - a.lastOpened.getTime()); } /** @@ -61,7 +71,7 @@ export default class RecentProjectsManager { recentProjects.push(new RecentProject(path.basename(projectPath), projectPath, new Date())); } - this.writeRecentProjects(recentProjects); + await this.writeRecentProjects(recentProjects); } /** @@ -70,12 +80,11 @@ export default class RecentProjectsManager { * * @param projects List of projects to store. */ - private writeRecentProjects(projects: RecentProject[]): void { - const storage = window.localStorage; - - storage.setItem("recent-projects", JSON.stringify(projects + private writeRecentProjects(projects: RecentProject[]): Promise { + const fsManager = new FileSystemManager(); + return fsManager.writeFile(this.getRecentProjectsPath(), JSON.stringify(projects .sort((a, b) => b.lastOpened.getTime() - a.lastOpened.getTime()) - .slice(0, RecentProjectsManager.AMOUNT_OF_RECENT_PROJECTS) + .slice(0, FileSystemRecentProjectManager.AMOUNT_OF_RECENT_PROJECTS) .map(p => { return { name: p.name, @@ -85,4 +94,10 @@ export default class RecentProjectsManager { }) )); } + + private getRecentProjectsPath(): string { + // Get a reference to the user data folder in which configuration data will be stored. + const configurationFolder = app.getPath("userData"); + return configurationFolder + "/" + FileSystemRecentProjectManager.RECENT_PROJECTS_FILE; + } } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 80983f9..a71eed4 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,4 +1,5 @@ -import Configuration from '@common/configuration/Configuration'; +import Configuration from "@common/configuration/Configuration"; +import RecentProject from "@common/project/RecentProject"; interface ExposedAPI { config: { @@ -19,6 +20,12 @@ interface ExposedAPI { chrome: Promise, electron: Promise } + }, + project: { + recentProjects: { + getRecentProjects: () => Promise, + addRecentProject: (projectPath: string) => Promise + } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 2b03185..1f20dd7 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -21,6 +21,12 @@ const api = { chrome: ipcRenderer.invoke("app:get-chrome-version"), electron: ipcRenderer.invoke("app:get-electron-version") } + }, + project: { + recentProjects: { + getRecentProjects: () => ipcRenderer.invoke("recent-projects:read-recent-projects"), + addRecentProject: (projectPath: string) => ipcRenderer.invoke("recent-projects:add-recent-project", projectPath) + } } }; diff --git a/src/renderer/components/pages/HomePage.vue b/src/renderer/components/pages/HomePage.vue index d5203d8..c08bf1e 100644 --- a/src/renderer/components/pages/HomePage.vue +++ b/src/renderer/components/pages/HomePage.vue @@ -4,7 +4,45 @@

Project management

+ + + + + New here? Try our demo project! + +
+ If this is the first time you're using our application, we advise you to open the + demo project and discover what this application can do for you. +
+
+ + Open demo project + +
+
+
+
+ + + + Add project + +
Select an empty folder and create a new project.
+
+ + mdi-folder-plus-outline + Create project + +
+
+
+
+ +
+ + +
@@ -52,6 +90,7 @@ import GithubCommunicator from "@renderer/logic/communication/github/GithubCommu import ComparatorUtils from "@renderer/logic/utils/ComparatorUtils"; import UpdateNotesDialog from "@renderer/components/releases/UpdateNotesDialog.vue"; import ReleaseNotesCard from "@renderer/components/releases/ReleaseNotesCard.vue"; +import RecentProjectOverview from "@renderer/components/project/RecentProjectOverview.vue"; const appVersion = ref(await window.api.app.versions.app); const chromeVersion = ref(await window.api.app.versions.chrome); diff --git a/src/renderer/components/project/RecentProjectOverview.vue b/src/renderer/components/project/RecentProjectOverview.vue new file mode 100644 index 0000000..039f19d --- /dev/null +++ b/src/renderer/components/project/RecentProjectOverview.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts index d970c85..1649448 100644 --- a/src/renderer/env.d.ts +++ b/src/renderer/env.d.ts @@ -1,8 +1,16 @@ /// +import { ExposedAPI } from "src/preload"; + declare module '*.vue' { import type { DefineComponent } from 'vue'; // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types const component: DefineComponent<{}, {}, any>; export default component; } + +declare global { + interface Window { + api: ExposedAPI; + } +} diff --git a/src/renderer/logic/project/RemoteRecentProjectManager.ts b/src/renderer/logic/project/RemoteRecentProjectManager.ts new file mode 100644 index 0000000..997e981 --- /dev/null +++ b/src/renderer/logic/project/RemoteRecentProjectManager.ts @@ -0,0 +1,13 @@ +import RecentProject from "@common/project/RecentProject"; +import RecentProjectManager from "@common/project/RecentProjectManager"; + +export default class RemoteRecentProjectManager implements RecentProjectManager { + constructor() {} + + getRecentProjects(): Promise { + return window.api.project.recentProjects.getRecentProjects(); + } + addRecentProject(projectPath: string): Promise { + return window.api.project.recentProjects.addRecentProject(projectPath); + } +} diff --git a/src/renderer/plugins/vuetify.ts b/src/renderer/plugins/vuetify.ts index 2fbfc12..6832c7b 100644 --- a/src/renderer/plugins/vuetify.ts +++ b/src/renderer/plugins/vuetify.ts @@ -41,10 +41,10 @@ const vuetify = createVuetify({ }, }, defaults: { - VTooltip: { - openDelay: 500, - location: "bottom" - }, + // VTooltip: { + // openDelay: 500, + // location: "bottom" + // }, VDialog: { maxWidth: 1000 } diff --git a/tsconfig.json b/tsconfig.json index e5c2a1c..d4ebe02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "src/main/**/*", "src/main/**/*.sql", "src/common/**/*", + "src/preload/**/*", "src/preload/*.d.ts" ], "compilerOptions": { diff --git a/tsconfig.node.json b/tsconfig.node.json index 3a281b8..a9d8de4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -5,7 +5,8 @@ "src/main/**/*", "src/main/**/*.sql", "src/preload/**/*", - "src/common/**/*" + "src/common/**/*", + "src/renderer/**/*" ], "compilerOptions": { "composite": true, @@ -21,6 +22,9 @@ ], "@preload/*": [ "./src/preload/*" + ], + "@renderer/*": [ + "./src/renderer/*" ] } } diff --git a/tsconfig.web.json b/tsconfig.web.json index c0954b1..c6716d0 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,6 +5,7 @@ "src/renderer/**/*", "src/renderer/**/*.vue", "src/common/**/*", + "src/preload/**/*", "src/preload/*.d.ts" ], "compilerOptions": { diff --git a/yarn.lock b/yarn.lock index 78bf112..9e81612 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4476,12 +4476,7 @@ vue@^3.4.16, vue@^3.4.7: "@vue/server-renderer" "3.4.16" "@vue/shared" "3.4.16" -vuetify@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.4.0.tgz#8530fe0b169ed907383d102953da82aea54717f6" - integrity sha512-aW3bJGCUN3fhl62yvsb+Hv6TtMWDqiadN0PTbEB8jd9z46/X1ddzQ/fhMjkqBX69sMFtZvENl3YFGU5c88/8qw== - -vuetify@^3.4.9: +vuetify@^3.4.9, vuetify@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.3.tgz#bfb807027854db17950195659139c9abc76bf497" integrity sha512-z2H1HYEfFeqHTp47VbFOLAv6Nard/eP4+qIXY9c6Z/uUflLhq5K8cyXL6MKhfIzyUsto+KszjVTyX+bu7zT2QA==