From bdbdcfb1b4d68ca70f328bb71469bd1022c43b87 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 29 Apr 2023 15:24:45 +0200 Subject: [PATCH 01/27] [feature-136] Models panel + models linkage + models linkage modals + some cleaning --- assets/jsons/translations/de.json | 82 +++++++++++++++++ assets/jsons/translations/en.json | 82 +++++++++++++++++ assets/jsons/translations/es.json | 82 +++++++++++++++++ assets/jsons/translations/fr.json | 82 +++++++++++++++++ .../local-models-manager.service.ts | 13 +-- .../models/link-models-modal.component.tsx | 32 +++++++ .../models/unlink-models-modal.component.tsx | 30 ++++++ .../share-folders-modal.component.tsx | 2 - .../models-panel.component.tsx | 26 ++++++ .../models-tabs-navbar.component.tsx | 92 +++++++++++++++++++ src/renderer/hooks/use-constant.hook.ts | 13 +++ src/renderer/hooks/use-on-update.hook.ts | 7 ++ .../pages/settings-page.component.tsx | 2 +- .../pages/version-viewer.component.tsx | 6 +- src/renderer/services/maps-manager.service.ts | 4 +- .../models-manager.service.ts | 82 +++++++++++++++++ .../services/models-manager.service.ts | 34 ------- .../services/version-folder-linker.service.ts | 9 +- src/renderer/windows/App.tsx | 2 +- .../models/model-saber/model-saber.model.ts | 14 ++- .../models/model-saber/models-constants.ts | 8 ++ 21 files changed, 649 insertions(+), 55 deletions(-) create mode 100644 src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx create mode 100644 src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx create mode 100644 src/renderer/components/models-management/models-panel.component.tsx create mode 100644 src/renderer/components/models-management/models-tabs-navbar.component.tsx create mode 100644 src/renderer/hooks/use-constant.hook.ts create mode 100644 src/renderer/hooks/use-on-update.hook.ts create mode 100644 src/renderer/services/models-management/models-manager.service.ts delete mode 100644 src/renderer/services/models-manager.service.ts create mode 100644 src/shared/models/model-saber/models-constants.ts diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index f7e211b3..36a61941 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -597,6 +597,88 @@ "unlink-folder": "Ordner Verknüpfung Aufheben", "link-all": "Alle verlinken" } + }, + "models": { + "link-models": { + "avatar": { + "title": "Avatare verbinden", + "desc": "Das Verbinden von Avataren ermöglicht das Teilen von Avataren zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Avataren profitieren", + "info": "Das Hinzufügen und Entfernen von Avataren wird ebenfalls geteilt", + "keep-models": { + "label": "Avatare behalten", + "title": "Das Behalten der Avatare wird die Avatare der aktuellen Version in den Ordner der geteilten Avatare verschieben. Andernfalls gehen sie verloren" + }, + "valid-btn": "Avatare verbinden" + }, + "saber": { + "title": "Säbel verbinden", + "desc": "Das Verbinden von Säbeln ermöglicht das Teilen von Säbeln zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Säbeln profitieren", + "info": "Das Hinzufügen und Entfernen von Säbeln wird ebenfalls geteilt", + "keep-models": { + "label": "Säbel behalten", + "title": "Das Behalten der Säbel wird die Säbel der aktuellen Version in den Ordner der geteilten Säbel verschieben. Andernfalls gehen sie verloren" + }, + "valid-btn": "Säbel verbinden" + }, + "platform": { + "title": "Plattformen verbinden", + "desc": "Das Verbinden von Plattformen ermöglicht das Teilen von Plattformen zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Plattformen profitieren", + "info": "Das Hinzufügen und Entfernen von Plattformen wird ebenfalls geteilt", + "keep-models": { + "label": "Plattformen behalten", + "title": "Das Behalten der Plattformen wird die Plattformen der aktuellen Version in den Ordner der geteilten Plattformen verschieben. Andernfalls gehen sie verloren" + }, + "valid-btn": "Plattformen verbinden" + }, + "bloq": { + "title": "Bloqs verbinden", + "desc": "Das Verbinden von Bloqs ermöglicht das Teilen von Bloqs zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Bloqs profitieren", + "info": "Das Hinzufügen und Entfernen von Bloqs wird ebenfalls geteilt", + "keep-models": { + "label": "Bloqs behalten", + "title": "Das Behalten der Bloqs wird die Bloqs der aktuellen Version in den Ordner der geteilten Bloqs verschieben. Andernfalls gehen sie verloren" + }, + "valid-btn": "Bloqs verbinden" + } + }, + "unlink-models": { + "avatar": { + "title": "Avatare trennen", + "desc": "Achtung, das Trennen der Avatare wird die Nutzung der geteilten Avatare für diese Version nicht mehr ermöglichen.", + "keep-models": { + "label": "Avatare behalten", + "title": "Das Behalten der Avatare wird eine Kopie der geteilten Avatare für die aktuelle Version erstellen. Andernfalls wird kein Avatar für diese Version aufbewahrt." + }, + "valid-btn": "Avatare trennen" + }, + "saber": { + "title": "Säbel trennen", + "desc": "Achtung, das Trennen der Säbel wird die Nutzung der geteilten Säbel für diese Version nicht mehr ermöglichen.", + "keep-models": { + "label": "Säbel behalten", + "title": "Das Behalten der Säbel wird eine Kopie der geteilten Säbel für die aktuelle Version erstellen. Andernfalls wird kein Säbel für diese Version aufbewahrt." + }, + "valid-btn": "Säbel trennen" + }, + "platform": { + "title": "Plattformen trennen", + "desc": "Achtung, das Trennen der Plattformen wird die Nutzung der geteilten Plattformen für diese Version nicht mehr ermöglichen.", + "keep-models": { + "label": "Plattformen behalten", + "title": "Das Behalten der Plattformen wird eine Kopie der geteilten Plattformen für die aktuelle Version erstellen. Andernfalls wird keine Plattform für diese Version aufbewahrt." + }, + "valid-btn": "Plattformen trennen" + }, + "bloq": { + "title": "Bloqs trennen", + "desc": "Achtung, das Trennen der Bloqs wird die Nutzung der geteilten Bloqs für diese Version nicht mehr ermöglichen.", + "keep-models": { + "label": "Bloqs behalten", + "title": "Das Behalten der Bloqs wird eine Kopie der geteilten Bloqs für die aktuelle Version erstellen. Andernfalls wird kein Bloq für diese Version aufbewahrt." + }, + "valid-btn": "Bloqs trennen" + } + } } }, "maps": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index f8cd62ee..5e9fe0b5 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -597,6 +597,88 @@ "unlink-folder": "Unlink Folder", "link-all": "Link all" } + }, + "models": { + "link-models": { + "avatar": { + "title": "Link Avatars", + "desc": "Linking avatars allows sharing avatars between all versions. Once linked, this version will benefit from the shared avatars", + "info": "Adding and deleting avatars will also be shared", + "keep-models": { + "label": "Keep Avatars", + "title": "Keeping the avatars will move the avatars from the current version to the shared avatars folder. Otherwise, they will be lost" + }, + "valid-btn": "Link Avatars" + }, + "saber": { + "title": "Link Sabers", + "desc": "Linking sabers allows sharing sabers between all versions. Once linked, this version will benefit from the shared sabers", + "info": "Adding and deleting sabers will also be shared", + "keep-models": { + "label": "Keep Sabers", + "title": "Keeping the sabers will move the sabers from the current version to the shared sabers folder. Otherwise, they will be lost" + }, + "valid-btn": "Link Sabers" + }, + "platform": { + "title": "Link Platforms", + "desc": "Linking platforms allows sharing platforms between all versions. Once linked, this version will benefit from the shared platforms", + "info": "Adding and deleting platforms will also be shared", + "keep-models": { + "label": "Keep Platforms", + "title": "Keeping the platforms will move the platforms from the current version to the shared platforms folder. Otherwise, they will be lost" + }, + "valid-btn": "Link Platforms" + }, + "bloq": { + "title": "Link Bloqs", + "desc": "Linking bloqs allows sharing bloqs between all versions. Once linked, this version will benefit from the shared bloqs", + "info": "Adding and deleting bloqs will also be shared", + "keep-models": { + "label": "Keep Bloqs", + "title": "Keeping the bloqs will move the bloqs from the current version to the shared bloqs folder. Otherwise, they will be lost" + }, + "valid-btn": "Link Bloqs" + } + }, + "unlink-models": { + "avatar": { + "title": "Unlink Avatars", + "desc": "Be careful, unlinking avatars will no longer allow the use of shared avatars for this version.", + "keep-models": { + "label": "Keep Avatars", + "title": "Keeping the avatars will create a copy of the shared avatars for the current version. Otherwise, no avatars will be kept for this version." + }, + "valid-btn": "Unlink Avatars" + }, + "saber": { + "title": "Unlink Sabers", + "desc": "Be careful, unlinking sabers will no longer allow the use of shared sabers for this version.", + "keep-models": { + "label": "Keep Sabers", + "title": "Keeping the sabers will create a copy of the shared sabers for the current version. Otherwise, no sabers will be kept for this version." + }, + "valid-btn": "Unlink Sabers" + }, + "platform": { + "title": "Unlink Platforms", + "desc": "Be careful, unlinking platforms will no longer allow the use of shared platforms for this version.", + "keep-models": { + "label": "Keep Platforms", + "title": "Keeping the platforms will create a copy of the shared platforms for the current version. Otherwise, no platforms will be kept for this version." + }, + "valid-btn": "Unlink Platforms" + }, + "bloq": { + "title": "Unlink Bloqs", + "desc": "Be careful, unlinking bloqs will no longer allow the use of shared bloqs for this version.", + "keep-models": { + "label": "Keep Bloqs", + "title": "Keeping the bloqs will create a copy of the shared bloqs for the current version. Otherwise, no bloqs will be kept for this version." + }, + "valid-btn": "Unlink Bloqs" + } + } } }, "maps": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index d66c78bc..d2265272 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -596,6 +596,88 @@ "unlink-folder": "Desenlazar Carpeta", "link-all": "Enlazar todo" } + }, + "models": { + "link-models": { + "avatar": { + "title": "Vincular avatares", + "desc": "La vinculación de avatares permite compartir avatares entre todas las versiones. Una vez vinculada, esta versión se beneficiará de los avatares compartidos", + "info": "La adición y eliminación de avatares también se compartirá", + "keep-models": { + "label": "Conservar los avatares", + "title": "Conservar los avatares moverá los avatares de la versión actual a la carpeta de avatares compartidos. De lo contrario, se perderán" + }, + "valid-btn": "Vincular avatares" + }, + "saber": { + "title": "Vincular sables", + "desc": "La vinculación de sables permite compartir sables entre todas las versiones. Una vez vinculada, esta versión se beneficiará de los sables compartidos", + "info": "La adición y eliminación de sables también se compartirá", + "keep-models": { + "label": "Conservar los sables", + "title": "Conservar los sables moverá los sables de la versión actual a la carpeta de sables compartidos. De lo contrario, se perderán" + }, + "valid-btn": "Vincular sables" + }, + "platform": { + "title": "Vincular plataformas", + "desc": "La vinculación de plataformas permite compartir plataformas entre todas las versiones. Una vez vinculada, esta versión se beneficiará de las plataformas compartidas", + "info": "La adición y eliminación de plataformas también se compartirá", + "keep-models": { + "label": "Conservar las plataformas", + "title": "Conservar las plataformas moverá las plataformas de la versión actual a la carpeta de plataformas compartidas. De lo contrario, se perderán" + }, + "valid-btn": "Vincular plataformas" + }, + "bloq": { + "title": "Vincular bloqs", + "desc": "La vinculación de bloqs permite compartir bloqs entre todas las versiones. Una vez vinculada, esta versión se beneficiará de los bloqs compartidos", + "info": "La adición y eliminación de bloqs también se compartirá", + "keep-models": { + "label": "Conservar los bloqs", + "title": "Conservar los bloqs moverá los bloqs de la versión actual a la carpeta de bloqs compartidos. De lo contrario, se perderán" + }, + "valid-btn": "Vincular bloqs" + } + }, + "unlink-models": { + "avatar": { + "title": "Desvincular avatares", + "desc": "Atención, desvincular los avatares ya no permitirá el uso de avatares compartidos para esta versión.", + "keep-models": { + "label": "Conservar los avatares", + "title": "Conservar los avatares creará una copia de los avatares compartidos para la versión actual. En caso contrario, ningún avatar se conservará para esta versión." + }, + "valid-btn": "Desvincular avatares" + }, + "saber": { + "title": "Desvincular sables", + "desc": "Atención, desvincular los sables ya no permitirá el uso de sables compartidos para esta versión.", + "keep-models": { + "label": "Conservar los sables", + "title": "Conservar los sables creará una copia de los sables compartidos para la versión actual. En caso contrario, ningún sable se conservará para esta versión." + }, + "valid-btn": "Desvincular sables" + }, + "platform": { + "title": "Desvincular plataformas", + "desc": "Atención, desvincular las plataformas ya no permitirá el uso de plataformas compartidas para esta versión.", + "keep-models": { + "label": "Conservar las plataformas", + "title": "Conservar las plataformas creará una copia de las plataformas compartidas para la versión actual. En caso contrario, ninguna plataforma se conservará para esta versión." + }, + "valid-btn": "Desvincular plataformas" + }, + "bloq": { + "title": "Desvincular bloqs", + "desc": "Atención, desvincular los bloqs ya no permitirá el uso de bloqs compartidos para esta versión.", + "keep-models": { + "label": "Conservar los bloqs", + "title": "Conservar los bloqs creará una copia de los bloqs compartidos para la versión actual. En caso contrario, ningún bloq se conservará para esta versión." + }, + "valid-btn": "Desvincular bloqs" + } + } } }, "maps": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 0dd59d84..079a564b 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -596,6 +596,88 @@ "unlink-folder": "Délier le dossier", "link-all": "Tout lier" } + }, + "models": { + "link-models":{ + "avatar": { + "title": "Lier les avatars", + "desc": "La liaison des avatars permet de partager les avatars entre toute les version. Une fois liée, cette version profitera des avatars partagés", + "info": "L'ajout et la suppression d'avatars sera également partagé", + "keep-models": { + "label": "Conserver les avatars", + "title": "Conserver les avatars déplacera les avatars de la version actuelle dans le dossier des avatars partagés. Dans le cas contraire ils seront perdus" + }, + "valid-btn": "Lier les avatars" + }, + "saber": { + "title": "Lier les sabres", + "desc": "La liaison des sabres permet de partager les sabres entre toute les version. Une fois liée, cette version profitera des sabres partagés", + "info": "L'ajout et la suppression de sabres sera également partagé", + "keep-models": { + "label": "Conserver les sabres", + "title": "Conserver les sabres déplacera les sabres de la version actuelle dans le dossier des sabres partagés. Dans le cas contraire ils seront perdus" + }, + "valid-btn": "Lier les sabres" + }, + "platform": { + "title": "Lier les plateformes", + "desc": "La liaison des plateformes permet de partager les plateformes entre toute les version. Une fois liée, cette version profitera des plateformes partagées", + "info": "L'ajout et la suppression de plateformes sera également partagé", + "keep-models": { + "label": "Conserver les plateformes", + "title": "Conserver les plateformes déplacera les plateformes de la version actuelle dans le dossier des plateformes partagées. Dans le cas contraire ils seront perdus" + }, + "valid-btn": "Lier les plateformes" + }, + "bloq": { + "title": "Lier les bloqs", + "desc": "La liaison des bloqs permet de partager les bloqs entre toute les version. Une fois liée, cette version profitera des bloqs partagés", + "info": "L'ajout et la suppression de bloqs sera également partagé", + "keep-models": { + "label": "Conserver les bloqs", + "title": "Conserver les bloqs déplacera les bloqs de la version actuelle dans le dossier des bloqs partagés. Dans le cas contraire ils seront perdus" + }, + "valid-btn": "Lier les bloqs" + } + }, + "unlink-models":{ + "avatar": { + "title": "Délier les avatars", + "desc": "Attention, délier les avatars ne permettra plus l'utilisation des avatars paratagés pour cette version.", + "keep-models": { + "label": "Conserver les avatars", + "title": "Conserver les avatars créera une copie des avatars partagés pour la version actuelle. Dans le cas contraire, aucun avatar ne sera conservé pour cette version." + }, + "valid-btn": "Délier les avatars" + }, + "saber": { + "title": "Délier les sabres", + "desc": "Attention, délier les sabres ne permettra plus l'utilisation des sabres paratagés pour cette version.", + "keep-models": { + "label": "Conserver les sabres", + "title": "Conserver les sabres créera une copie des sabres partagés pour la version actuelle. Dans le cas contraire, aucun sabre ne sera conservé pour cette version." + }, + "valid-btn": "Délier les sabres" + }, + "platform": { + "title": "Délier les plateformes", + "desc": "Attention, délier les plateformes ne permettra plus l'utilisation des plateformes paratagées pour cette version.", + "keep-models": { + "label": "Conserver les plateformes", + "title": "Conserver les plateformes créera une copie des plateformes partagées pour la version actuelle. Dans le cas contraire, aucune plateforme ne sera conservée pour cette version." + }, + "valid-btn": "Délier les plateformes" + }, + "bloq": { + "title": "Délier les bloqs", + "desc": "Attention, délier les bloqs ne permettra plus l'utilisation des bloqs paratagés pour cette version.", + "keep-models": { + "label": "Conserver les bloqs", + "title": "Conserver les bloqs créera une copie des bloqs partagés pour la version actuelle. Dans le cas contraire, aucun bloq ne sera conservé pour cette version." + }, + "valid-btn": "Délier les bloqs" + } + } } }, "maps": { diff --git a/src/main/services/additional-content/local-models-manager.service.ts b/src/main/services/additional-content/local-models-manager.service.ts index df329b9b..212732fe 100644 --- a/src/main/services/additional-content/local-models-manager.service.ts +++ b/src/main/services/additional-content/local-models-manager.service.ts @@ -4,7 +4,7 @@ import { ipcMain } from "electron"; import { IpcRequest } from "shared/models/ipc"; import { UtilsService } from "../utils.service"; import { WindowManagerService } from "../window-manager.service"; -import { MSModel, MSModelType } from "shared/models/model-saber/model-saber.model"; +import { MSModel, MSModelType } from "../../../shared/models/model-saber/model-saber.model"; import { BSVersion } from "shared/bs-version.interface"; import { BSLocalVersionService } from "../bs-local-version.service"; import path from "path"; @@ -12,6 +12,7 @@ import { RequestService } from "../request.service"; import { copyFileSync } from "fs-extra"; import sanitize from "sanitize-filename"; import { ensureFolderExist } from "../../helpers/fs.helpers"; +import { MODEL_TYPE_FOLDERS } from "../../../shared/models/model-saber/models-constants"; export class LocalModelsManagerService { @@ -26,13 +27,6 @@ export class LocalModelsManagerService { ModelSaber: "modelsaber", }; - private readonly MODEL_TYPE_FOLDER: Record, string> = { - avatar: "CustomAvatars", - bloq: "CustomNotes", - platform: "CustomPlatforms", - saber: "CustomSabers" - } - private readonly deepLink: DeepLinkService; private readonly utils: UtilsService; private readonly windows: WindowManagerService; @@ -70,10 +64,9 @@ export class LocalModelsManagerService { private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise{ if(!version){ throw "will be implemented whith models management" } - if(type === "misc"){ throw "model type not supported"; } const versionPath = await this.localVersion.getVersionPath(version); - const modelFolderPath = path.join(versionPath, this.MODEL_TYPE_FOLDER[type]); + const modelFolderPath = path.join(versionPath, MODEL_TYPE_FOLDERS[type]); await ensureFolderExist(modelFolderPath); diff --git a/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx new file mode 100644 index 00000000..ac57ada9 --- /dev/null +++ b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx @@ -0,0 +1,32 @@ +import { useState } from "react" +import { BsmButton } from "renderer/components/shared/bsm-button.component" +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component" +import { BsmImage } from "renderer/components/shared/bsm-image.component" +import { useTranslation } from "renderer/hooks/use-translation.hook" +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" +import { MSModelType } from "shared/models/model-saber/model-saber.model" +import BeatRunning from '../../../../../../assets/images/apngs/beat-running.png' + +export const LinkModelsModal: ModalComponent = ({resolver, data}) => { + + const t = useTranslation(); + + const [keepMaps, setKeepMaps] = useState(true) + + return ( +
+

{t(`modals.models.link-models.${data}.title`)}

+ +

{t(`modals.models.link-models.${data}.desc`)}

+

{t(`modals.models.link-models.${data}.info`)}

+
+ + {t(`modals.models.link-models.${data}.keep-models.label`)} +
+
+ resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/> + resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text={`modals.models.link-models.${data}.valid-btn`}/> +
+ + ) +} diff --git a/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx new file mode 100644 index 00000000..ad3266eb --- /dev/null +++ b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx @@ -0,0 +1,30 @@ +import { useState } from "react"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; +import BeatConflict from '../../../../../../assets/images/apngs/beat-conflict.png' +import { MSModelType } from "shared/models/model-saber/model-saber.model"; + +export const UnlinkModelsModal: ModalComponent = ({resolver, data}) => { + + const t = useTranslation(); + const [keepMaps, setKeepMaps] = useState(true); + + return ( +
+

{t(`modals.models.unlink-models.${data}.title`)}

+ +

{t(`modals.models.unlink-models.${data}.desc`)}

+
+ + {t(`modals.models.unlink-models.${data}.keep-models.label`)} +
+
+ resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/> + resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text={`modals.models.unlink-models.${data}.valid-btn`}/> +
+ + ) +} \ No newline at end of file diff --git a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx index bed4e578..8fb18622 100644 --- a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx @@ -1,11 +1,9 @@ import Tippy from "@tippyjs/react"; import { Variants } from "framer-motion"; -import { version } from "os"; import { useEffect, useState } from "react"; import { LinkButton } from "renderer/components/maps-mangement-components/link-button.component"; import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; -import { BsmIcon } from "renderer/components/svgs/bsm-icon.component"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { useTranslation } from "renderer/hooks/use-translation.hook"; diff --git a/src/renderer/components/models-management/models-panel.component.tsx b/src/renderer/components/models-management/models-panel.component.tsx new file mode 100644 index 00000000..758e818a --- /dev/null +++ b/src/renderer/components/models-management/models-panel.component.tsx @@ -0,0 +1,26 @@ +import { BSVersion } from "shared/bs-version.interface"; +import { useState } from "react"; +import { ModelsTabsNavbar } from "./models-tabs-navbar.component"; + +export function ModelsPanel({version}: {version?: BSVersion}) { + + const [modelTypeTab, setModelTypeTab] = useState(ModelTabType.Avatars); + + return ( +
+
+ +
+
+ +
+
+ ) +} + +enum ModelTabType { + Avatars = 0, + Sabers = 1, + Platforms = 2, + Bloqs = 3, +} diff --git a/src/renderer/components/models-management/models-tabs-navbar.component.tsx b/src/renderer/components/models-management/models-tabs-navbar.component.tsx new file mode 100644 index 00000000..1d682abd --- /dev/null +++ b/src/renderer/components/models-management/models-tabs-navbar.component.tsx @@ -0,0 +1,92 @@ +import { BSVersion } from "shared/bs-version.interface" +import { TabNavBar } from "../shared/tab-nav-bar.component" +import { MSModelType } from "shared/models/model-saber/model-saber.model" +import { LinkButton } from "../maps-mangement-components/link-button.component" +import { Variants } from "framer-motion" +import { DetailedHTMLProps, useEffect, useState } from "react" +import { useTranslation } from "renderer/hooks/use-translation.hook" +import { useThemeColor } from "renderer/hooks/use-theme-color.hook" +import { useOnUpdate } from "renderer/hooks/use-on-update.hook" +import { useConstant } from "renderer/hooks/use-constant.hook" +import { ModelsManagerService } from "renderer/services/models-management/models-manager.service" + +type Props = { + version?: BSVersion + tabIndex: number, + onTabChange: (index: number) => void +} + +export function ModelsTabsNavbar({version, tabIndex, onTabChange}: Props) { + + const renderTab = (props: DetailedHTMLProps, any>, text: string, index?: number) => { + return + } + + return ( + + ) +} + +type TabProps = { + version?: BSVersion, + modelType: MSModelType, + index: number, + onLink?: (type: MSModelType) => void, + onUnlink?: (type: MSModelType) => void, +} & DetailedHTMLProps, HTMLLIElement>; + +function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){ + + const modelsManager = useConstant(() => ModelsManagerService.getInstance()); + + const [modelsAreLinked, setModelsAreLinked] = useState(false); + const [linkBtnDisabled, setLinkBtnDisabled] = useState(false); + + useOnUpdate(() => { + if(!version){ return; } + + const sub = modelsManager.$modelsLinkingPending(version, modelType).subscribe(async (pending) => { + if(pending){ return setLinkBtnDisabled(() => pending); } + const modelsLinked = await modelsManager.isModelsLinked(version, modelType); + setModelsAreLinked(() => modelsLinked); + setLinkBtnDisabled(() => pending); + }); + + return () => { + sub.unsubscribe(); + } + }, [version]); + + const linkModels = () => modelsManager.linkModels(modelType, version).then(() => onLink?.(modelType)); + const unlinkModels = () => modelsManager.unlinkModels(modelType, version).then(() => onUnlink?.(modelType)); + + const onClickLink = () => { + if(!version){ return Promise.resolve(); } + if(modelsAreLinked){ return unlinkModels(); } + return linkModels(); + } + + return ( +
  • + {modelType} +
    + {!!version && ( + + )} +
    +
  • + ) + +} diff --git a/src/renderer/hooks/use-constant.hook.ts b/src/renderer/hooks/use-constant.hook.ts new file mode 100644 index 00000000..5f851c4b --- /dev/null +++ b/src/renderer/hooks/use-constant.hook.ts @@ -0,0 +1,13 @@ +import { useRef } from "react" + +type ResultBox = { v: T } + +export function useConstant(fn: () => T): T { + const ref = useRef>() + + if (!ref.current) { + ref.current = { v: fn() } + } + + return ref.current.v +} \ No newline at end of file diff --git a/src/renderer/hooks/use-on-update.hook.ts b/src/renderer/hooks/use-on-update.hook.ts new file mode 100644 index 00000000..a4ef1160 --- /dev/null +++ b/src/renderer/hooks/use-on-update.hook.ts @@ -0,0 +1,7 @@ +import { useEffect, EffectCallback, DependencyList } from "react"; + +export function useOnUpdate(func: EffectCallback, deps: DependencyList = []) { + useEffect(() => { + func(); + }, deps); +} diff --git a/src/renderer/pages/settings-page.component.tsx b/src/renderer/pages/settings-page.component.tsx index 6859d442..2162d917 100644 --- a/src/renderer/pages/settings-page.component.tsx +++ b/src/renderer/pages/settings-page.component.tsx @@ -28,7 +28,7 @@ import beatleaderIcon from "../../../assets/images/third-party-icons/beat-leader import Tippy from '@tippyjs/react'; import { MapsManagerService } from "renderer/services/maps-manager.service"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; -import { ModelsManagerService } from "renderer/services/models-manager.service"; +import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { VersionFolderLinkerService } from "renderer/services/version-folder-linker.service"; diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index b8dbd722..41925830 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -16,6 +16,7 @@ import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-s import { UninstallModal } from 'renderer/components/modal/modal-types/uninstall-modal.component'; import { MapsPlaylistsPanel } from 'renderer/components/maps-mangement-components/maps-playlists-panel.component'; import { ShareFoldersModal } from 'renderer/components/modal/modal-types/share-folders-modal.component'; +import { ModelsPanel } from 'renderer/components/models-management/models-panel.component'; export function VersionViewer() { @@ -78,12 +79,15 @@ export function VersionViewer() {

    {state.name ? `${state.BSVersion} - ${state.name}` : state.BSVersion}

    - setCurrentTabIndex(i)}/> + setCurrentTabIndex(i)}/>
    +
    + +
    diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index 45e3f56a..f49c9cde 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -159,9 +159,7 @@ export class MapsManagerService { } public $mapsLinkingPending(version: BSVersion): Observable{ - return this.linker.queue$.pipe(mergeMap(async queue => { - return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version)); - }), distinctUntilChanged()); + return this.linker.$isVersionFolderPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER); } } \ No newline at end of file diff --git a/src/renderer/services/models-management/models-manager.service.ts b/src/renderer/services/models-management/models-manager.service.ts new file mode 100644 index 00000000..fddfc55e --- /dev/null +++ b/src/renderer/services/models-management/models-manager.service.ts @@ -0,0 +1,82 @@ +import { MSModelType } from "shared/models/model-saber/model-saber.model"; +import { IpcService } from "../ipc.service"; +import { VersionFolderLinkerService, VersionLinkerActionType } from "../version-folder-linker.service"; +import { MODEL_TYPE_FOLDERS } from "shared/models/model-saber/models-constants"; +import { Observable, distinctUntilChanged, lastValueFrom, mergeMap, share } from "rxjs"; +import { BSVersion } from "shared/bs-version.interface"; +import { ModalExitCode, ModalService } from "../modale.service"; +import { LinkModelsModal } from "renderer/components/modal/modal-types/models/link-models-modal.component"; +import { UnlinkModelsModal } from "renderer/components/modal/modal-types/models/unlink-models-modal.component"; + +export class ModelsManagerService { + + private static instance: ModelsManagerService; + + public static getInstance(): ModelsManagerService{ + if(!ModelsManagerService.instance){ ModelsManagerService.instance = new ModelsManagerService(); } + return ModelsManagerService.instance; + } + + private readonly ipc: IpcService; + private readonly versionFolderLinked: VersionFolderLinkerService; + private readonly modalService: ModalService; + + private constructor(){ + this.ipc = IpcService.getInstance(); + this.versionFolderLinked = VersionFolderLinkerService.getInstance(); + this.modalService = ModalService.getInsance(); + } + + public isModelsLinked(version: BSVersion, type: MSModelType): Promise{ + return lastValueFrom(this.versionFolderLinked.isVersionFolderLinked(version, MODEL_TYPE_FOLDERS[type])); + } + + public $modelsLinkingPending(version: BSVersion, type: MSModelType): Observable{ + return this.versionFolderLinked.$isVersionFolderPending(version, MODEL_TYPE_FOLDERS[type]); + } + + public async linkModels(type: MSModelType, version?: BSVersion): Promise{ + + const res = await this.modalService.openModal(LinkModelsModal, type); + + if(res.exitCode !== ModalExitCode.COMPLETED){ return null; } + + return this.versionFolderLinked.linkVersionFolder({ + version, + relativeFolder: MODEL_TYPE_FOLDERS[type], + type: VersionLinkerActionType.Link, + options: { keepContents: res.data !== false } + }); + } + + public async unlinkModels(type: MSModelType, version?: BSVersion): Promise{ + + const res = await this.modalService.openModal(UnlinkModelsModal, type); + + if(res.exitCode !== ModalExitCode.COMPLETED){ return null; } + + return this.versionFolderLinked.unlinkVersionFolder({ + version, + relativeFolder: MODEL_TYPE_FOLDERS[type], + type: VersionLinkerActionType.Unlink, + options: { keepContents: res.data !== false } + }); + } + + public isDeepLinksEnabled(): Promise{ + return this.ipc.send("is-models-deep-links-enabled").then(res => ( + res.success ? res.data : false + )); + } + + public async enableDeepLink(): Promise{ + const res = await this.ipc.send("register-models-deep-link"); + return res.success ? res.data : false; + } + + public async disableDeepLink(): Promise{ + const res = await this.ipc.send("unregister-models-deep-link"); + return res.success ? res.data : false; + } + +} \ No newline at end of file diff --git a/src/renderer/services/models-manager.service.ts b/src/renderer/services/models-manager.service.ts deleted file mode 100644 index d70942f0..00000000 --- a/src/renderer/services/models-manager.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IpcService } from "./ipc.service"; - -export class ModelsManagerService { - - private static instance: ModelsManagerService; - - public static getInstance(): ModelsManagerService{ - if(!ModelsManagerService.instance){ ModelsManagerService.instance = new ModelsManagerService(); } - return ModelsManagerService.instance; - } - - private readonly ipc: IpcService; - - private constructor(){ - this.ipc = IpcService.getInstance(); - } - - public isDeepLinksEnabled(): Promise{ - return this.ipc.send("is-models-deep-links-enabled").then(res => ( - res.success ? res.data : false - )); - } - - public async enableDeepLink(): Promise{ - const res = await this.ipc.send("register-models-deep-link"); - return res.success ? res.data : false; - } - - public async disableDeepLink(): Promise{ - const res = await this.ipc.send("unregister-models-deep-link"); - return res.success ? res.data : false; - } - -} \ No newline at end of file diff --git a/src/renderer/services/version-folder-linker.service.ts b/src/renderer/services/version-folder-linker.service.ts index fd14fb2b..f4dcfd1b 100644 --- a/src/renderer/services/version-folder-linker.service.ts +++ b/src/renderer/services/version-folder-linker.service.ts @@ -1,9 +1,10 @@ import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service"; -import { map, distinctUntilChanged, filter } from "rxjs/operators"; +import { map, distinctUntilChanged, filter, mergeMap, share } from "rxjs/operators"; import { BehaviorSubject, Observable } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { IpcService } from "./ipc.service"; import { ProgressBarService } from "./progress-bar.service"; +import equal from "fast-deep-equal"; export class VersionFolderLinkerService{ @@ -137,6 +138,12 @@ export class VersionFolderLinkerService{ return this.ipcService.sendV2("relink-all-versions-folders"); } + public $isVersionFolderPending(version: BSVersion, relativeFolder: string): Observable{ + return this.queue$.pipe(mergeMap(async queue => { + return queue.some(q => q.relativeFolder.includes(relativeFolder) && equal(q.version, version)); + }), distinctUntilChanged(), share()); + } + public get currentAction$(): Observable{ return this._queue$.pipe(map(actions => actions.at(0)), distinctUntilChanged()); } diff --git a/src/renderer/windows/App.tsx b/src/renderer/windows/App.tsx index 964a82a1..71e8d748 100644 --- a/src/renderer/windows/App.tsx +++ b/src/renderer/windows/App.tsx @@ -17,7 +17,7 @@ import 'tippy.js/dist/tippy.css'; import 'tippy.js/animations/shift-away-subtle.css'; import { MapsManagerService } from "renderer/services/maps-manager.service"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; -import { ModelsManagerService } from "renderer/services/models-manager.service"; +import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { NotificationService } from "renderer/services/notification.service"; import { timer } from "rxjs"; import { ConfigurationService } from "renderer/services/configuration.service"; diff --git a/src/shared/models/model-saber/model-saber.model.ts b/src/shared/models/model-saber/model-saber.model.ts index e5c40bc4..a44d3b01 100644 --- a/src/shared/models/model-saber/model-saber.model.ts +++ b/src/shared/models/model-saber/model-saber.model.ts @@ -17,8 +17,18 @@ export interface MSModel { date: string } -export type MSModelType = "saber"|"platform"|"bloq"|"misc"|"avatar"; -export type MSModelPlatform = "pc"|"quest"|"all"; +export enum MSModelType { + Avatar = "avatar", + Saber = "saber", + Platfrom = "platform", + Bloq = "bloq" +}; + +export enum MSModelPlatform { + PC = "pc", + QUEST = "quest", + ALL = "all" +}; export interface MSGetQuery { type?: MSModelType; diff --git a/src/shared/models/model-saber/models-constants.ts b/src/shared/models/model-saber/models-constants.ts new file mode 100644 index 00000000..d504ec87 --- /dev/null +++ b/src/shared/models/model-saber/models-constants.ts @@ -0,0 +1,8 @@ +import { MSModelType } from "./model-saber.model"; + +export const MODEL_TYPE_FOLDERS = { + [MSModelType.Avatar]: "CustomAvatars", + [MSModelType.Bloq]: "CustomNotes", + [MSModelType.Platfrom]: "CustomPlatforms", + [MSModelType.Saber]: "CustomSabers" +} \ No newline at end of file From ab798437d5e2ab42478d3ad6f29741e43878975e Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Mon, 1 May 2023 23:00:13 +0200 Subject: [PATCH 02/27] [feature-136] init show local models --- package-lock.json | 12 +-- src/main/helpers/fs.helpers.ts | 6 ++ src/main/ipcs/bs-model-ipcs.ts | 12 ++- src/main/main.ts | 3 + .../local-maps-manager.service.ts | 2 +- .../local-models-manager.service.ts | 77 +++++++++++++++++-- src/main/services/bs-version-lib.service.ts | 2 +- .../beat-saver/beat-saver.service.ts | 2 +- .../model-saber/model-saber-api.service.ts | 2 +- .../model-saber/model-saber.service.ts | 45 ++++++++++- .../services/version-folder-linker.service.ts | 2 +- .../models/link-models-modal.component.tsx | 2 +- .../models/unlink-models-modal.component.tsx | 2 +- .../model-item.component.tsx | 58 ++++++++++++++ .../models-grid.component.tsx | 63 +++++++++++++++ .../models-panel.component.tsx | 14 +++- .../models-tabs-navbar.component.tsx | 38 ++++----- .../components/nav-bar/nav-bar.component.tsx | 2 +- .../shared/bs-content-nav-bar.component.tsx | 31 ++++++++ .../svgs/flags/german-icon.component.tsx | 4 +- .../title-bar/title-bar.component.tsx | 2 +- src/renderer/hooks/use-observable.hook.ts | 4 +- src/renderer/hooks/use-on-update.hook.ts | 2 +- .../hooks/use-switchable-observable.hook.ts | 19 +++++ src/renderer/services/ipc.service.ts | 4 +- .../services/model-downloader.service.ts | 2 +- .../models-manager.service.ts | 10 ++- .../thrird-partys/model-saber.service.ts | 2 +- src/renderer/windows/App.tsx | 5 ++ .../OneClick/OneClickDownloadModel.tsx | 2 +- src/{main => shared}/helpers/array.helpers.ts | 0 .../helpers/promise.helpers.ts | 0 .../models/bsm-local-model.interface.ts | 9 +++ .../constants.ts} | 9 +++ .../model-saber.model.ts | 27 ++++++- .../models/optionnal/optionnal.class.ts | 44 +++++++++++ 36 files changed, 456 insertions(+), 64 deletions(-) create mode 100644 src/renderer/components/models-management/model-item.component.tsx create mode 100644 src/renderer/components/models-management/models-grid.component.tsx create mode 100644 src/renderer/components/shared/bs-content-nav-bar.component.tsx create mode 100644 src/renderer/hooks/use-switchable-observable.hook.ts rename src/{main => shared}/helpers/array.helpers.ts (100%) rename src/{main => shared}/helpers/promise.helpers.ts (100%) create mode 100644 src/shared/models/models/bsm-local-model.interface.ts rename src/shared/models/{model-saber/models-constants.ts => models/constants.ts} (50%) rename src/shared/models/{model-saber => models}/model-saber.model.ts (69%) create mode 100644 src/shared/models/optionnal/optionnal.class.ts diff --git a/package-lock.json b/package-lock.json index eabab76c..45022c87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16076,9 +16076,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -30859,9 +30859,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "dev": true, "requires": { "randombytes": "^2.1.0" diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index 4dc75ebf..a3a629c2 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -127,6 +127,12 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti } } +export function extname(filePath: string, withDot = true){ + const ext = path.extname(filePath); + if(withDot){ return ext; } + return ext.split('.').at(-1); +} + export interface Progression{ total: number; current: number; diff --git a/src/main/ipcs/bs-model-ipcs.ts b/src/main/ipcs/bs-model-ipcs.ts index a56c9a81..8fa746d8 100644 --- a/src/main/ipcs/bs-model-ipcs.ts +++ b/src/main/ipcs/bs-model-ipcs.ts @@ -1,8 +1,12 @@ import { ipcMain } from "electron"; import { UtilsService } from "../services/utils.service"; import { IpcRequest } from "shared/models/ipc"; -import { MSModel } from "shared/models/model-saber/model-saber.model"; +import { MSModel, MSModelType } from "shared/models/models/model-saber.model"; import { LocalModelsManagerService } from "../services/additional-content/local-models-manager.service"; +import { IpcService } from "../services/ipc.service"; +import { BSVersion } from "shared/bs-version.interface"; + +const ipc = IpcService.getInstance(); ipcMain.on("one-click-install-model", async (event, request: IpcRequest) => { const utils = UtilsService.getInstance(); @@ -58,4 +62,10 @@ ipcMain.on("is-models-deep-links-enabled", async (event, request: IpcRequest("get-version-models", async (req, reply) => { + const models = LocalModelsManagerService.getInstance(); + const res = await models.getModels(req.args.type, req.args.version); + reply(res); }); \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 07bd8328..d140e8e6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,6 +21,7 @@ import { LocalModelsManagerService } from './services/additional-content/local-m import { APP_NAME } from './constants'; import { BSLauncherService } from './services/bs-launcher.service'; import { IpcRequest } from 'shared/models/ipc'; +import { MSModelType } from '../shared/models/models/model-saber.model'; const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; @@ -105,5 +106,7 @@ else{ log.error(args?.args); }); + LocalModelsManagerService.getInstance().getModels(MSModelType.Saber, null).toPromise().catch(console.error).then(console.log); + }).catch(log.error); } \ No newline at end of file diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts index f94add4e..308157ad 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -21,7 +21,7 @@ import { Archive } from "../../models/archive.class"; import { deleteFolder, ensureFolderExist, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers"; import { readFile } from "fs/promises"; import { FolderLinkerService } from "../folder-linker.service"; -import { allSettled } from "../../helpers/promise.helpers"; +import { allSettled } from "../../../shared/helpers/promise.helpers"; export class LocalMapsManagerService { diff --git a/src/main/services/additional-content/local-models-manager.service.ts b/src/main/services/additional-content/local-models-manager.service.ts index 212732fe..0ca6b6a5 100644 --- a/src/main/services/additional-content/local-models-manager.service.ts +++ b/src/main/services/additional-content/local-models-manager.service.ts @@ -4,15 +4,22 @@ import { ipcMain } from "electron"; import { IpcRequest } from "shared/models/ipc"; import { UtilsService } from "../utils.service"; import { WindowManagerService } from "../window-manager.service"; -import { MSModel, MSModelType } from "../../../shared/models/model-saber/model-saber.model"; +import { MSModel, MSModelType } from "../../../shared/models/models/model-saber.model"; import { BSVersion } from "shared/bs-version.interface"; import { BSLocalVersionService } from "../bs-local-version.service"; import path from "path"; import { RequestService } from "../request.service"; import { copyFileSync } from "fs-extra"; import sanitize from "sanitize-filename"; -import { ensureFolderExist } from "../../helpers/fs.helpers"; -import { MODEL_TYPE_FOLDERS } from "../../../shared/models/model-saber/models-constants"; +import { Progression, ensureFolderExist, extname } from "../../helpers/fs.helpers"; +import { MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants"; +import { InstallationLocationService } from "../installation-location.service"; +import { Observable } from "rxjs"; +import { readdir } from "fs/promises"; +import md5File from "md5-file"; +import { allSettled } from "../../../shared/helpers/promise.helpers"; +import { ModelSaberService } from "../thrid-party/model-saber/model-saber.service"; +import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; export class LocalModelsManagerService { @@ -31,7 +38,9 @@ export class LocalModelsManagerService { private readonly utils: UtilsService; private readonly windows: WindowManagerService; private readonly localVersion: BSLocalVersionService; + private readonly installPaths: InstallationLocationService; private readonly request: RequestService; + private readonly modelSaber: ModelSaberService; private constructor(){ this.deepLink = DeepLinkService.getInstance(); @@ -39,6 +48,8 @@ export class LocalModelsManagerService { this.windows = WindowManagerService.getInstance(); this.localVersion = BSLocalVersionService.getInstance(); this.request = RequestService.getInstance(); + this.installPaths = InstallationLocationService.getInstance(); + this.modelSaber = ModelSaberService.getInstance(); this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, (link) => { log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link); @@ -63,10 +74,8 @@ export class LocalModelsManagerService { private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise{ - if(!version){ throw "will be implemented whith models management" } - - const versionPath = await this.localVersion.getVersionPath(version); - const modelFolderPath = path.join(versionPath, MODEL_TYPE_FOLDERS[type]); + const rootPath = !!version ? await this.localVersion.getVersionPath(version) : this.installPaths.sharedContentPath; + const modelFolderPath = path.join(rootPath, MODEL_TYPE_FOLDERS[type]); await ensureFolderExist(modelFolderPath); @@ -105,6 +114,54 @@ export class LocalModelsManagerService { } + private async getModelsPaths(type: MSModelType, version?: BSVersion): Promise{ + const modelsPath = await this.getModelFolderPath(type, version); + const files = await readdir(modelsPath, {withFileTypes: true}); + + return files.filter(file => file.isFile() && extname(file.name, false) === type).map(file => path.join(modelsPath, file.name)); + } + + public getModels(type: MSModelType, version?: BSVersion): Observable>{ + + const progression: Progression = { + total: 0, + current: 0, + extra: null + }; + + return new Observable>(subscriber => { + (async () => { + const modelsPaths = await this.getModelsPaths(type, version); + progression.total = modelsPaths.length; + + const models = await allSettled(modelsPaths.map(async modelPath => { + + const hash = await md5File(modelPath) + + const localModel: BsmLocalModel = { + path: modelPath, + fileName: path.basename(modelPath), + model: await this.modelSaber.getModelByHash(hash), + type, hash + } + + progression.current++; + + subscriber.next(progression); + + return localModel; + })); + + progression.extra = models; + subscriber.next(progression); + + })().catch(e => subscriber.error(e)).finally(() => subscriber.complete()); + }); + + } + + + public enableDeepLinks(): boolean{ return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link)); } @@ -117,4 +174,10 @@ export class LocalModelsManagerService { return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link)); } +} + +export interface BsmLocalModelsProgress { + total: number; + loaded: number; + models: MSModel[]; } \ No newline at end of file diff --git a/src/main/services/bs-version-lib.service.ts b/src/main/services/bs-version-lib.service.ts index 6e573eb7..8265a81b 100644 --- a/src/main/services/bs-version-lib.service.ts +++ b/src/main/services/bs-version-lib.service.ts @@ -4,7 +4,7 @@ import { writeFileSync } from 'fs'; import { BSVersion } from 'shared/bs-version.interface'; import { RequestService } from "./request.service" import { readJSON } from 'fs-extra'; -import { allSettled } from '../helpers/promise.helpers'; +import { allSettled } from '../../shared/helpers/promise.helpers'; export class BSVersionLibService{ diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts index d46735d5..dcdf6e72 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -1,4 +1,4 @@ -import { splitIntoChunk } from "../../../helpers/array.helpers"; +import { splitIntoChunk } from "../../../../shared/helpers/array.helpers"; import { BsvMapDetail } from "shared/models/maps"; import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model"; import { BeatSaverApiService } from "./beat-saver-api.service"; diff --git a/src/main/services/thrid-party/model-saber/model-saber-api.service.ts b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts index f24ed97c..e44d8976 100644 --- a/src/main/services/thrid-party/model-saber/model-saber-api.service.ts +++ b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts @@ -1,6 +1,6 @@ import fetch from "node-fetch"; import { ApiResult } from "renderer/models/api/api.model"; -import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "shared/models/model-saber/model-saber.model"; +import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "../../../../shared/models/models/model-saber.model"; export class ModelSaberApiService { diff --git a/src/main/services/thrid-party/model-saber/model-saber.service.ts b/src/main/services/thrid-party/model-saber/model-saber.service.ts index c694987e..099d85b0 100644 --- a/src/main/services/thrid-party/model-saber/model-saber.service.ts +++ b/src/main/services/thrid-party/model-saber/model-saber.service.ts @@ -1,5 +1,6 @@ -import { MSGetQuery, MSGetQueryFilter, MSModel } from "shared/models/model-saber/model-saber.model"; +import { MSGetQuery, MSGetQueryFilterType, MSModel, MSModelPlatform } from "../../../../shared/models/models/model-saber.model"; import { ModelSaberApiService } from "./model-saber-api.service"; +import log from "electron-log"; export class ModelSaberService { @@ -10,6 +11,8 @@ export class ModelSaberService { return ModelSaberService.instance; } + private readonly modelsHashCache: Map = new Map(); // key: hash, value: model + private readonly modelSaberApi: ModelSaberApiService; private constructor(){ @@ -21,8 +24,8 @@ export class ModelSaberService { const query: MSGetQuery = { start: 0, end: 1, - platform: "pc", - filter: [{type: "id", value: id}] + platform: MSModelPlatform.PC, + filter: [{type: MSGetQueryFilterType.ID, value: id}] } try{ @@ -37,6 +40,42 @@ export class ModelSaberService { return res.data[`${id}`]; } catch(e){ + log.error(e); + return null; + } + + } + + public async getModelByHash(hash: string): Promise{ + + if(this.modelsHashCache.has(hash)){ + return this.modelsHashCache.get(hash); + } + + const query: MSGetQuery = { + start: 0, + end: 1, + platform: MSModelPlatform.PC, + filter: [{type: MSGetQueryFilterType.Hash, value: hash}] + } + + try{ + const res = await this.modelSaberApi.searchModel(query); + + if(res.status !== 200){ return null;} + + if(Object.keys(res.data).length === 0){ + return null; + } + + const model = Array.from(Object.values(res.data)).at(0); + + this.modelsHashCache.set(hash, model); + + return model + } + catch(e){ + log.error(e); return null; } diff --git a/src/main/services/version-folder-linker.service.ts b/src/main/services/version-folder-linker.service.ts index 169be195..cb24a279 100644 --- a/src/main/services/version-folder-linker.service.ts +++ b/src/main/services/version-folder-linker.service.ts @@ -5,7 +5,7 @@ import { BSVersion } from "shared/bs-version.interface"; import { LocalMapsManagerService } from "./additional-content/local-maps-manager.service"; import { BSLocalVersionService } from "./bs-local-version.service"; import { FolderLinkerService, LinkOptions } from "./folder-linker.service"; -import { allSettled } from "../helpers/promise.helpers"; +import { allSettled } from "../../shared/helpers/promise.helpers"; export class VersionFolderLinkerService { diff --git a/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx index ac57ada9..727a14ee 100644 --- a/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx @@ -4,7 +4,7 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component" import { BsmImage } from "renderer/components/shared/bsm-image.component" import { useTranslation } from "renderer/hooks/use-translation.hook" import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" -import { MSModelType } from "shared/models/model-saber/model-saber.model" +import { MSModelType } from "shared/models/models/model-saber.model" import BeatRunning from '../../../../../../assets/images/apngs/beat-running.png' export const LinkModelsModal: ModalComponent = ({resolver, data}) => { diff --git a/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx index ad3266eb..0a7d1d75 100644 --- a/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx @@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; import BeatConflict from '../../../../../../assets/images/apngs/beat-conflict.png' -import { MSModelType } from "shared/models/model-saber/model-saber.model"; +import { MSModelType } from "shared/models/models/model-saber.model"; export const UnlinkModelsModal: ModalComponent = ({resolver, data}) => { diff --git a/src/renderer/components/models-management/model-item.component.tsx b/src/renderer/components/models-management/model-item.component.tsx new file mode 100644 index 00000000..df10ded5 --- /dev/null +++ b/src/renderer/components/models-management/model-item.component.tsx @@ -0,0 +1,58 @@ +import equal from "fast-deep-equal"; +import { memo, useState } from "react"; +import { MSModelType } from "shared/models/models/model-saber.model"; +import { BsmImage } from "../shared/bsm-image.component"; +import { motion } from "framer-motion"; +import { GlowEffect } from "../shared/glow-effect.component"; +import { useConstant } from "renderer/hooks/use-constant.hook"; +import { LinkOpenerService } from "renderer/services/link-opener.service"; +import { MODELS_TYPE_MS_PAGE_ROOT, MODEL_SABER_URL } from "shared/models/models/constants"; + +type Props = { + className?: string, + selected?: boolean, + // Model props + modelHash: string, + modelType: MSModelType, + modelId?: number, + modelName: string, + modelImage?: string, + modelAuthor?: string, + modelTags?: string[], +} + +export const ModelItem = memo(({ + className, + selected, + modelHash, + modelType, + modelId, + modelName, + modelImage, + modelAuthor, + modelTags, +}: Props) => { + + const linkOpener = useConstant(() => LinkOpenerService.getInstance()); + + const [hovered, setHovered] = useState(false); + + const openModelPage = () => { + if(!modelId){ return; } + const url = new URL(MODELS_TYPE_MS_PAGE_ROOT[modelType], MODEL_SABER_URL); + url.searchParams.set("id", modelId.toString()); + linkOpener.open(url.toString()); + }; + + return ( + setHovered(() => true)} onHoverEnd={() => setHovered(() => false)}> + +
    + + +

    {modelName}

    +
    +
    +
    + ) +}, equal); diff --git a/src/renderer/components/models-management/models-grid.component.tsx b/src/renderer/components/models-management/models-grid.component.tsx new file mode 100644 index 00000000..f6fa0355 --- /dev/null +++ b/src/renderer/components/models-management/models-grid.component.tsx @@ -0,0 +1,63 @@ +import { useInView } from "framer-motion"; +import { BSVersion } from "shared/bs-version.interface"; +import { MutableRefObject, useRef, useState } from "react"; +import { MSModelType } from "shared/models/models/model-saber.model"; +import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; +import { useConstant } from "renderer/hooks/use-constant.hook"; +import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; +import { useSwitchableObservable } from "renderer/hooks/use-switchable-observable.hook"; +import { Progression } from "main/helpers/fs.helpers"; +import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; +import { ModelItem } from "./model-item.component"; + +type Props = { + className?: string, + version?: BSVersion, + type: MSModelType +} + +export function ModelsGrid({className, version, type}: Props) { + + const modelsManager = useConstant(() => ModelsManagerService.getInstance()); + + const ref = useRef(); + const isVisible = useInView(ref, {once: true, amount: .1}); + + const [models, setModelsLoadObservable] = useSwitchableObservable>(); + + console.log(models); + + useOnUpdate(() => { + + if(!isVisible){ return; } + + setModelsLoadObservable(() => modelsManager.$getModels(type, version)); + + }, [version, isVisible, type]); + + return ( +
    + {!models || !models?.extra ? ( + <>loading + ) : !models?.extra.length ? ( + <>no models + ) : ( +
      + {models.extra.map(localModel => ( + + ))} +
    + )} +
    + ) +} diff --git a/src/renderer/components/models-management/models-panel.component.tsx b/src/renderer/components/models-management/models-panel.component.tsx index 758e818a..21e68386 100644 --- a/src/renderer/components/models-management/models-panel.component.tsx +++ b/src/renderer/components/models-management/models-panel.component.tsx @@ -1,6 +1,8 @@ import { BSVersion } from "shared/bs-version.interface"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { ModelsTabsNavbar } from "./models-tabs-navbar.component"; +import { ModelsGrid } from "./models-grid.component"; +import { MSModelType } from "shared/models/models/model-saber.model"; export function ModelsPanel({version}: {version?: BSVersion}) { @@ -11,8 +13,14 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
    -
    - +
    + +
    + + + + +
    ) diff --git a/src/renderer/components/models-management/models-tabs-navbar.component.tsx b/src/renderer/components/models-management/models-tabs-navbar.component.tsx index 1d682abd..3de28129 100644 --- a/src/renderer/components/models-management/models-tabs-navbar.component.tsx +++ b/src/renderer/components/models-management/models-tabs-navbar.component.tsx @@ -1,38 +1,38 @@ import { BSVersion } from "shared/bs-version.interface" -import { TabNavBar } from "../shared/tab-nav-bar.component" -import { MSModelType } from "shared/models/model-saber/model-saber.model" +import { MSModelType } from "../../../shared/models/models/model-saber.model" import { LinkButton } from "../maps-mangement-components/link-button.component" -import { Variants } from "framer-motion" -import { DetailedHTMLProps, useEffect, useState } from "react" -import { useTranslation } from "renderer/hooks/use-translation.hook" -import { useThemeColor } from "renderer/hooks/use-theme-color.hook" +import { DetailedHTMLProps, useState } from "react" import { useOnUpdate } from "renderer/hooks/use-on-update.hook" -import { useConstant } from "renderer/hooks/use-constant.hook" import { ModelsManagerService } from "renderer/services/models-management/models-manager.service" +import { BsContentNavBar, BsContentNavBarTab } from "../shared/bs-content-nav-bar.component" +import { useConstant } from "renderer/hooks/use-constant.hook" type Props = { + className?: string, version?: BSVersion tabIndex: number, onTabChange: (index: number) => void } -export function ModelsTabsNavbar({version, tabIndex, onTabChange}: Props) { +export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Props) { - const renderTab = (props: DetailedHTMLProps, any>, text: string, index?: number) => { - return - } + const tabs = useConstant[]>(() => { + return Array.from(Object.values(MSModelType)).map(type => ({ + text: type, // <= TODO: Translate + extra: type + })) + }) return ( - + ( + + )}/> ) } type TabProps = { version?: BSVersion, modelType: MSModelType, - index: number, onLink?: (type: MSModelType) => void, onUnlink?: (type: MSModelType) => void, } & DetailedHTMLProps, HTMLLIElement>; @@ -69,9 +69,9 @@ function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){ } return ( -
  • - {modelType} -
    +
  • + {modelType} +
    {!!version && ( +