diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index e2bcf1dc..a29e3579 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "Geteilte Maps" - }, "version-viewer": { "launch-mods": { "oculus": "Oculus Modus", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "Geteilte Maps", "search-bar": { "search-placeholder": "Suche eine Map", "filters-btn": "Filter", @@ -732,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "Länge", + "nps" : "Noten Pro Sekunde", "tags": "Stichworte", "specificities": "Allgemein", "requirements": "Anforderungen" @@ -799,7 +796,13 @@ }, "map-item": { "by": "Von {songAutor}", - "mapped-by": "Gemapped von" + "mapped-by": "Gemapped von", + "delete": "Löschen", + "preview" : "Kartenvorschau", + "bsr-code" : "BSR Code", + "download" : "Karte herunterladen", + "downloading" :"Karte wird heruntergeladen", + "cancel-download" : "Download abbrechen" } }, "models": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 8713ea3b..0f25f568 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "Shared maps" - }, "version-viewer": { "launch-mods": { "oculus": "Oculus Mode", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "Shared maps", "search-bar": { "search-placeholder": "Search a map", "filters-btn": "Filters", @@ -732,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "Duration", + "nps" : "Notes Per Second", "tags": "tags", "specificities": "general", "requirements": "requirements" @@ -799,7 +796,13 @@ }, "map-item": { "by": "By {songAutor}", - "mapped-by": "mapped by" + "mapped-by": "mapped by", + "delete": "Delete", + "preview" : "Preview map", + "bsr-code" : "BSR code", + "download" : "Download map", + "downloading" :"Downloading map", + "cancel-download" : "Cancel download" } }, "models": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 137f9f70..4e8ed44a 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "Mapas comunes" - }, "version-viewer": { "launch-mods": { "oculus": "Modo Oculus", @@ -731,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "Duración", + "nps" : "Notas Por Segundo", "tags": "tags", "specificities": "general", "requirements": "requisitos" @@ -798,7 +796,13 @@ }, "map-item": { "by": "Por {songAutor}", - "mapped-by": "mapeado por" + "mapped-by": "mapeado por", + "delete": "Borrar", + "preview" : "Vista previa del mapa", + "bsr-code" : "Código BSR", + "download" : "Descargar mapa", + "downloading" :"Descargando mapa", + "cancel-download" : "Cancelar descarga" } }, "models": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 2ce8cd08..2daec164 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "Maps partagées" - }, "version-viewer": { "launch-mods": { "oculus": "Mode Oculus", @@ -234,8 +231,11 @@ "steam-download": { "warnings": { "msg": { + "ManifestChecksum": "Le manifeste précédemment téléchargé ne correspond pas au nouveau 🤔", "ConnectionTimeout": "Ta connexion internet semble instable 🥶", - "Unknown": "Quelque chose d'étrange s'est produit 🤔 Votre connexion est surement instable." + "ConnectionLost": "La connexion a été perdue, essayez à nouveau...", + "ConnectionError": "Impossible de se connecter à Steam, essayez à nouveau...", + "Unknown": "Quelque chose d'étrange s'est produit 🤔 Votre connexion est probablement instable." } }, "errors": { @@ -728,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "Durée", + "nps" : "Notes Par Seconde", "tags": "tags", "specificities": "général", "requirements": "requis" @@ -795,7 +796,13 @@ }, "map-item": { "by": "Par {songAutor}", - "mapped-by": "mappée par" + "mapped-by": "mappée par", + "delete": "Supprimer", + "preview" : "Aperçu de la carte", + "bsr-code" : "Code BSR", + "download" : "Télécharger la carte", + "downloading" :"Téléchargement de la carte", + "cancel-download" : "Annuler le téléchargement" } }, "models": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 6791eb20..2e6aa5d9 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "共有マップ" - }, "version-viewer": { "launch-mods": { "oculus": "Oculusモード", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "共有マップ", "search-bar": { "search-placeholder": "マップを検索", "filters-btn": "絞り込み", @@ -732,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "尺", + "nps" : "秒間ノート数", "tags": "タグ", "specificities": "一般", "requirements": "要Mod" @@ -799,7 +796,13 @@ }, "map-item": { "by": "By {songAutor}", - "mapped-by": "作成者 " + "mapped-by": "作成者 ", + "delete": "削除", + "preview" : "マッププレビュー", + "bsr-code" : "BSRコード", + "download" : "マップをダウンロード", + "downloading" :"マップをダウンロード中", + "cancel-download" : "ダウンロードをキャンセル" } }, "models": { diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 54cba86a..a40d8c28 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "Общие карты" - }, "version-viewer": { "launch-mods": { "oculus": "Режим Oculus", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "Общие карты", "search-bar": { "search-placeholder": "Поиск среди карт", "filters-btn": "Фильтр", @@ -732,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "Длительность", + "nps" : "Нот в Секунду", "tags": "тэги", "specificities": "основное", "requirements": "требуемые моды" @@ -799,7 +796,13 @@ }, "map-item": { "by": "От {songAutor}", - "mapped-by": "автор карты " + "mapped-by": "автор карты ", + "delete": "Удалить", + "preview" : "Предпросмотр карты", + "bsr-code" : "Код BSR", + "download" : "Скачать карту", + "downloading" :"Загрузка карты", + "cancel-download" : "Отменить загрузку" } }, "models": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 90d59cba..93970058 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "共享譜面 " - }, "version-viewer": { "launch-mods": { "oculus": "Oculus 模式", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "Shared maps", "search-bar": { "search-placeholder": "譜面搜尋", "filters-btn": "篩選", @@ -657,7 +653,6 @@ "valid-btn": "取消關聯譜面" } }, - "download-maps": { "search-btn": "搜尋", "loading-maps": "譜面載入中...", @@ -733,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "時長", + "nps" : "每秒音符數", "tags": "標籤", "specificities": "general", "requirements": "要求" @@ -800,7 +796,13 @@ }, "map-item": { "by": "By {songAutor}", - "mapped-by": "mapped by" + "mapped-by": "mapped by", + "delete": "刪除", + "preview" : "預覽地圖", + "bsr-code" : "BSR代碼", + "download" : "下載地圖", + "downloading" :"正在下載地圖", + "cancel-download" : "取消下載" } }, "models": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 3d5bf74c..192d9bed 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -25,9 +25,6 @@ } }, "pages": { - "shared-maps": { - "title": "共享谱面" - }, "version-viewer": { "launch-mods": { "oculus": "Oculus 模式", @@ -42,7 +39,6 @@ } }, "maps": { - "title": "Shared maps", "search-bar": { "search-placeholder": "谱面搜索", "filters-btn": "筛选", @@ -657,7 +653,6 @@ "valid-btn": "取消关联谱面" } }, - "download-maps": { "search-btn": "搜索", "loading-maps": "谱面加载中...", @@ -733,6 +728,7 @@ "maps": { "map-filter-panel": { "duration": "时长", + "nps" : "每秒音符数", "tags": "标签", "specificities": "general", "requirements": "要求" @@ -800,7 +796,13 @@ }, "map-item": { "by": "By {songAutor}", - "mapped-by": "mapped by" + "mapped-by": "mapped by", + "delete": "删除", + "preview" : "预览地图", + "bsr-code" : "BSR代码", + "download" : "下载地图", + "downloading" :"正在下载地图", + "cancel-download" : "取消下载" } }, "models": { diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index ec64701e..7fa474f7 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -5,6 +5,8 @@ import { Observable, concatMap, from } from "rxjs"; import log from "electron-log"; import { BsmException } from "shared/models/bsm-exception.model"; import crypto from "crypto"; +import { execSync } from "child_process"; +import { tryit } from "../../shared/helpers/error.helpers"; export async function pathExist(path: string): Promise { try { @@ -232,6 +234,16 @@ export async function isJunction(path: string): Promise{ return lstats.isSymbolicLink() && stats.isDirectory(); } +export function resolveGUIDPath(guidPath: string): string { + const guidVolume = path.parse(guidPath).root; + const command = `powershell -command "(Get-WmiObject -Class Win32_Volume | Where-Object { $_.DeviceID -like '${guidVolume}' }).DriveLetter"`; + const {result: driveLetter, error} = tryit(() => execSync(command).toString().trim()); + if (!driveLetter || error) { + throw new Error("Unable to resolve GUID path", error); + } + return path.join(driveLetter, path.relative(guidVolume, guidPath)); +} + export interface Progression { total: number; current: number; 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 0fdf040c..b30349aa 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -7,7 +7,7 @@ import { InstallationLocationService } from "../installation-location.service"; import { UtilsService } from "../utils.service"; import crypto from "crypto"; import { lstatSync } from "fs"; -import { copy, createReadStream, ensureDir, realpath, unlink } from "fs-extra"; +import { copy, createReadStream, ensureDir, pathExists, realpath, unlink } from "fs-extra"; import StreamZip from "node-stream-zip"; import { RequestService } from "../request.service"; import sanitize from "sanitize-filename"; @@ -255,10 +255,12 @@ export class LocalMapsManagerService { const mapFolderName = sanitize(`${map.id}-${map.name}`); const mapsFolder = await this.getMapsFolderPath(version); - const installedMap = await this.loadMapInfoFromPath(path.join(mapsFolder, mapFolderName)).catch(e => { - log.error("loadMapInfoFromPath", e); - return null; - }); + const mapPath = path.join(mapsFolder, mapFolderName); + + const installedMap = await pathExists(mapPath).then(exists => { + if(!exists){ return null; } + return this.loadMapInfoFromPath(mapPath); + }).catch(() => null); if(map.versions.every(version => version.hash === installedMap?.hash)) { return installedMap; @@ -270,8 +272,6 @@ export class LocalMapsManagerService { throw `Cannot download ${zipUrl}`; } - const mapPath = path.join(mapsFolder, mapFolderName); - await ensureFolderExist(mapPath); await zip.extract(null, mapPath); diff --git a/src/main/services/bs-launcher/oculus-launcher.service.ts b/src/main/services/bs-launcher/oculus-launcher.service.ts index 2cf7f7e8..e07232b9 100644 --- a/src/main/services/bs-launcher/oculus-launcher.service.ts +++ b/src/main/services/bs-launcher/oculus-launcher.service.ts @@ -34,7 +34,9 @@ export class OculusLauncherService extends AbstractLauncherService implements St this.pathsService = InstallationLocationService.getInstance(); this.oculus.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]).then(async dirPath => { - if(dirPath){ return this.oculusLib$.next( path.join(dirPath, "..") ); } + if(dirPath){ + return this.oculusLib$.next( path.join(dirPath, "..") ); + } const defaultLib = ((await this.oculus.getOculusLibs()) || []).find(lib => lib.isDefault); if(defaultLib?.path){ return this.oculusLib$.next(path.join(defaultLib.path, "Software")); } this.oculusLib$.next(null); diff --git a/src/main/services/oculus.service.ts b/src/main/services/oculus.service.ts index 349e96c0..c0b0622b 100644 --- a/src/main/services/oculus.service.ts +++ b/src/main/services/oculus.service.ts @@ -1,8 +1,9 @@ import { list } from "regedit-rs"; import path from "path"; -import { pathExist } from "../helpers/fs.helpers"; +import { pathExist, resolveGUIDPath } from "../helpers/fs.helpers"; import log from "electron-log"; import { lstat } from "fs-extra"; +import { tryit } from "../../shared/helpers/error.helpers"; export class OculusService { private static instance: OculusService; @@ -42,11 +43,16 @@ export class OculusService { await Promise.all(libsRegData.keys.map(async key => { const originalPath = await list([`${oculusLibsRegKey}\\${key}`]).then(res => res[`${oculusLibsRegKey}\\${key}`]); - if (!originalPath.values?.OriginalPath) { - return null; + if (originalPath.values?.OriginalPath) { + return { id: key, path: originalPath.values.OriginalPath.value, isDefault: defaultLibraryId === key } as OculusLibrary; + } + + if(originalPath.values?.Path) { + const { result } = tryit(() => resolveGUIDPath(originalPath.values.Path.value as string)); + return result ? { id: key, path: result, isDefault: defaultLibraryId === key } as OculusLibrary : null; } - return { id: key, path: originalPath.values.OriginalPath.value, isDefault: defaultLibraryId === key } as OculusLibrary + return null; }, [])) ).filter(Boolean); diff --git a/src/renderer/components/available-versions/available-versions-slider.component.tsx b/src/renderer/components/available-versions/available-versions-slider.component.tsx index 89255959..02f45b9f 100644 --- a/src/renderer/components/available-versions/available-versions-slider.component.tsx +++ b/src/renderer/components/available-versions/available-versions-slider.component.tsx @@ -8,7 +8,7 @@ import { useObservable } from "renderer/hooks/use-observable.hook"; export function AvailableVersionsSlider() { const versionManagerService = useService(BSVersionManagerService); - const availableVersions = useObservable(versionManagerService.availableVersions$); + const availableVersions = useObservable(() => versionManagerService.availableVersions$); const [yearIndex, setYearIndex] = useState(0); const availableYears = (() => { diff --git a/src/renderer/components/maps-mangement-components/filter-panel.component.tsx b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx index 5332c5ce..7962b23e 100644 --- a/src/renderer/components/maps-mangement-components/filter-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx @@ -56,7 +56,7 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange const renderDurationLabel = (sec: number): JSX.Element => { const textValue = (() => { if (sec === MIN_DURATION) { - return t("maps.map-filter-panel.duration"); + return MIN_DURATION; } if (sec === MAX_DURATION) { return "∞"; @@ -72,7 +72,7 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange const renderNpsLabel = (nps: number): JSX.Element => { const textValue = (() => { if (nps === MIN_NPS) { - return "NPS"; + return MIN_NPS; } if (nps === MAX_NPS) { return "∞"; @@ -167,9 +167,11 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange return !playlist ? ( -
- - +
+ + + {t("maps.map-filter-panel.nps")} + {t("maps.map-filter-panel.duration")}
@@ -180,7 +182,6 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange {translateMapSpecificity(specificity)}
))} -

{t("maps.map-filter-panel.requirements")}

{MAP_REQUIREMENTS.map(requirement => (
handleCheckbox(requirement)}> diff --git a/src/renderer/components/maps-mangement-components/link-button.component.tsx b/src/renderer/components/maps-mangement-components/link-button.component.tsx index bc8d1d6c..9526fceb 100644 --- a/src/renderer/components/maps-mangement-components/link-button.component.tsx +++ b/src/renderer/components/maps-mangement-components/link-button.component.tsx @@ -1,48 +1,56 @@ import { motion } from "framer-motion"; -import { forwardRef } from "react"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { BsmIcon } from "../svgs/bsm-icon.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { FolderLinkState } from "renderer/services/version-folder-linker.service"; -type Props = { +export type LinkBtnProps = { className?: string; title?: string; - linked?: boolean; - disabled?: boolean; - onClick?: () => void; + state: FolderLinkState; + onClick?: () => unknown; }; -export const LinkButton = motion( - forwardRef((props: Props, ref) => { - const t = useTranslation(); - const color = useThemeColor("first-color"); +export const LinkButton = ({className, title, state, onClick}: LinkBtnProps) => { + const t = useTranslation(); + + const color = useThemeColor("first-color"); + const disabled = state === FolderLinkState.Processing || state === FolderLinkState.Pending; - const linkedColor = (() => { - if (props.disabled) { - return "orange"; - } - if (props.linked) { + const btnColor = () => { + switch (state) { + case FolderLinkState.Linked: return color; - } - return "red"; - })(); + case FolderLinkState.Pending: + case FolderLinkState.Processing: + return "orange"; + case FolderLinkState.Unlinked: + return "red"; + default: + return "red"; + } + } - return ( -
{ - e.preventDefault(); - e.stopPropagation(); - !props.disabled && props.onClick?.(); - }} - style={{ pointerEvents: props.disabled ? "none" : "auto" }} - tabIndex={-1} - > - - -
- ); - }) -); + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + !disabled && onClick?.(); + } + + return ( + + + + + ); +} diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index e7af8ece..a6f66cf9 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -429,7 +429,7 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear return (
- 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}> + 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}> {props => }
diff --git a/src/renderer/components/maps-mangement-components/map-item.component.tsx b/src/renderer/components/maps-mangement-components/map-item.component.tsx index b71ccd33..617776d7 100644 --- a/src/renderer/components/maps-mangement-components/map-item.component.tsx +++ b/src/renderer/components/maps-mangement-components/map-item.component.tsx @@ -21,6 +21,7 @@ import useDoubleClick from "use-double-click"; import { GlowEffect } from "../shared/glow-effect.component"; import { useDelayedState } from "renderer/hooks/use-delayed-state.hook"; import { useService } from "renderer/hooks/use-service.hook"; +import Tippy from "@tippyjs/react"; export type ParsedMapDiff = { type: BsvMapDifficultyType; name: string; stars: number }; @@ -51,7 +52,6 @@ export type MapItemProps = { }; export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemProps) => { - const linkOpener = useService(LinkOpenerService); const audioPlayer = useService(AudioPlayerService); @@ -70,7 +70,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, onDoubleClick: () => onDoubleClick?.(callBackParam), }); - const songPlaying = useObservable(audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); + const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); const previewUrl = mapId ? `https://allpoland.github.io/ArcViewer/?id=${mapId}` : null; const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; @@ -156,25 +156,25 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, return ( setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> - - {(diffsPanelHovered || bottomBarHovered) && ( - - {Array.from(diffs.entries()).map(([charac, diffSet]) => ( -
    - {diffSet.map(({ type, name, stars }) => ( -
  1. - - - {stars ? ★ {stars} : {parseDiffLabel(type)}} - - {parseDiffLabel(name)} -
  2. - ))} -
- ))} -
- )} -
+ + {(diffsPanelHovered || bottomBarHovered) && ( + + {Array.from(diffs.entries()).map(([charac, diffSet]) => ( +
    + {diffSet.map(({ type, name, stars }) => ( +
  1. + + + {stars ? ★ {stars} : {parseDiffLabel(type)}} + + {parseDiffLabel(name)} +
  2. + ))} +
+ ))} +
+ )} +
@@ -191,8 +191,8 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
- -
+ +

{title} @@ -240,52 +240,72 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,

-
+
{onDelete && !downloading && ( - { - e.stopPropagation(); - onDelete(callBackParam); - }} - /> + +
+ { + e.stopPropagation(); + onDelete(callBackParam); + }} + /> +
+
)} {onDownload && !downloading && ( - { - e.stopPropagation(); - onDownload(callBackParam); - }} - /> + +
+ { + e.stopPropagation(); + onDownload(callBackParam); + }} + /> +
+
)} {onCancelDownload && !downloading && ( - { - e.stopPropagation(); - onCancelDownload(callBackParam); - }} - /> + +
+ { + e.stopPropagation(); + onCancelDownload(callBackParam); + }} + /> +
+
)} - {downloading && } + {downloading && + +
+ +
+
+ } {previewUrl && ( + +
+
+
)} {mapId && ( + +
+
+
)}
diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx index db26ff29..3705f055 100644 --- a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -1,24 +1,22 @@ -import { DetailedHTMLProps, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { BSVersion } from "shared/bs-version.interface"; -import { TabNavBar } from "../shared/tab-nav-bar.component"; import { LocalMapsListPanel } from "./local-maps-list-panel.component"; import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"; import { FilterPanel } from "./filter-panel.component"; import { MapFilter } from "shared/models/maps/beat-saver.model"; -import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { MapsManagerService } from "renderer/services/maps-manager.service"; -import { motion, Variants } from "framer-motion"; import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; import { BsmImage } from "../shared/bsm-image.component"; import wipGif from "../../../../assets/images/gifs/wip.gif"; -import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; -import { useObservable } from "renderer/hooks/use-observable.hook"; -import { BsmIcon } from "../svgs/bsm-icon.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; -import { LinkButton } from "./link-button.component"; -import { debounceTime } from "rxjs/operators"; -import { VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service"; +import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service"; import { useService } from "renderer/hooks/use-service.hook"; +import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component"; +import { BsmButton } from "../shared/bsm-button.component"; +import { MapIcon } from "../svgs/icons/map-icon.component"; +import { PlaylistIcon } from "../svgs/icons/playlist-icon.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { of } from "rxjs"; type Props = { version?: BSVersion; @@ -29,7 +27,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { const mapsService = useService(MapsManagerService); const mapsDownloader = useService(MapsDownloaderService); - const osDiagnostic = useService(OsDiagnosticService); const linker = useService(VersionFolderLinkerService); const [tabIndex, setTabIndex] = useState(0); @@ -37,9 +34,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { const [mapSearch, setMapSearch] = useState(""); const [playlistSearch, setPlaylistSearch] = useState(""); const [mapsLinked, setMapsLinked] = useState(false); - const [linkingPending, setLinkingPending] = useState(false); - const isOnline = useObservable(osDiagnostic.isOnline$); - const color = useThemeColor("first-color"); + const mapsLinkedState = useObservable(() => version ? mapsService.$mapsFolderLinkState(version) : of(null), FolderLinkState.Unlinked, [version]); const t = useTranslation(); const mapsRef = useRef(); @@ -50,8 +45,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { loadMapIsLinked(); - const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending); - const onMapsLinked: VersionLinkerActionListener = action => { if (!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)) { return; @@ -63,7 +56,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { linker.onVersionFolderUnlinked(onMapsLinked); return () => { - sub.unsubscribe(); linker.removeVersionFolderLinkedListener(onMapsLinked); linker.removeVersionFolderUnlinkedListener(onMapsLinked); }; @@ -80,6 +72,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return setPlaylistSearch(() => value); }; + const handleMapsAddClick = () => { + mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.()); + }; + const handleMapsLinkClick = () => { if (!mapsLinked) { return mapsService.linkVersion(version); @@ -87,56 +83,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return mapsService.unlinkVersion(version); }; - const handleMapsAddClick = () => { - mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.()); - }; - - const renderTab = (props: DetailedHTMLProps, HTMLLIElement>, text: string, index: number): JSX.Element => { - const onClickLink = (index: number) => { - if (index === 0) { - handleMapsLinkClick(); - } - }; - - const onClickAdd = (index: number) => { - if (index === 0) { - handleMapsAddClick(); - } - }; - - const variants: Variants = { hover: { rotate: 22.5 }, tap: { rotate: 45 } }; - - return ( -
  • - {text} - {index === 0 && ( -
    - {isOnline && ( - { - e.stopPropagation(); - onClickAdd(index); - }} - tabIndex={-1} - > - - - - - {t("misc.add")} - - )} - {!!version && onClickLink(index)} />} -
    - )} -
  • - ); - }; - const dropDownItems = ((): DropDownItem[] => { if (tabIndex === 1) { return []; @@ -150,6 +96,15 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return (
    -
    - -
    + setTabIndex(index)} + tabs={[ + { + text: "misc.maps", + icon: MapIcon, + onClick: () => setTabIndex(0), + linkProps: version ? { + state: mapsLinkedState, + onClick: handleMapsLinkClick, + } : null, + }, + { + text: "misc.playlists", + icon: PlaylistIcon, + onClick: () => setTabIndex(1), + }, + ]} + > + <>
    Coming soon
    -
    -
    + +
    ); } diff --git a/src/renderer/components/maps-mangement-components/maps-row.component.tsx b/src/renderer/components/maps-mangement-components/maps-row.component.tsx index e92d4ac2..0e99fd27 100644 --- a/src/renderer/components/maps-mangement-components/maps-row.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-row.component.tsx @@ -16,13 +16,10 @@ type Props = { }; export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDelete }: Props) => { - const selectedMaps = useObservable( - selectedMaps$.pipe( + const selectedMaps = useObservable(() => selectedMaps$.pipe( map(selectedMaps => selectedMaps.filter(selected => maps.some(map => map.hash === selected.hash))), - distinctUntilChanged(equal) - ), - [] - ); + distinctUntilChanged(equal), + ), []); const extractMapDiffs = (map: BsmLocalMap): Map => { const res = new Map(); @@ -48,11 +45,30 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel }; const renderMapItem = (map: BsmLocalMap) => { - return selected.hash === map.hash)} diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} ranked={map.bsaverInfo?.ranked} autorId={map.bsaverInfo?.uploader?.id} likes={map.bsaverInfo?.stats?.upvotes} createdAt={map.bsaverInfo?.createdAt} onDelete={onMapDelete} onSelected={onMapSelect} callBackParam={map} />; + return selected.hash === map.hash)} + diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} + ranked={map.bsaverInfo?.ranked} + autorId={map.bsaverInfo?.uploader?.id} + likes={map.bsaverInfo?.stats?.upvotes} + createdAt={map.bsaverInfo?.createdAt} + onDelete={onMapDelete} + onSelected={onMapSelect} + callBackParam={map} + />; }; return ( -
      +
        {maps?.map(renderMapItem)}
      ); diff --git a/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx b/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx index ef511bf6..5d4169d2 100644 --- a/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx @@ -21,7 +21,7 @@ export const LoginToSteamModal: ModalComponent< const [password, setPassword] = useState(""); const [stay, setStay] = useState(false); const [showPassword, setShowPassword] = useState(false); - const qrCodeUrl = useObservable(data.qrCode$); + const qrCodeUrl = useObservable(() => data.qrCode$); const t = useTranslation(); useEffect(() => { diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index a42e710f..04fa513b 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -27,8 +27,8 @@ export const DownloadMapsModal: ModalComponent mapsDownloader.currentMapDownload$); + const mapsInQueue = useObservable(() => mapsDownloader.mapsInQueue$); const t = useTranslation(); const filterContainerRef = useRef(null); const [filter, setFilter] = useState({}); @@ -37,7 +37,7 @@ export const DownloadMapsModal: ModalComponent(BSV_SORT_ORDER.at(0)); const [ownedMapHashs, setOwnedMapHashs] = useState(ownedMaps?.map(map => map.hash) ?? []); const [loading, setLoading] = useState(false); - const isOnline = useObservable(os.isOnline$); + const isOnline = useObservable(() => os.isOnline$); const [searchParams, setSearchParams] = useState({ sortOrder, filter, diff --git a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx index 0d0b1a76..d6982bd3 100644 --- a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx @@ -44,10 +44,10 @@ export const DownloadModelsModal: ModalComponent modelsDownloader.currentDownload$(), null); + const downloadQueue = useObservable(() => modelsDownloader.getQueue$(), []); const [msModels, msModels$] = useBehaviorSubject([]); - const isOnline = useObservable(os.isOnline$, true); + const isOnline = useObservable(() => os.isOnline$, true); const [error, error$] = useBehaviorSubject(false); const [isLoading, isLoading$] = useBehaviorSubject(false); const [ownedModels, setOwnedModels] = useState(owned ?? []); 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 75907e77..c04865d7 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,5 +1,4 @@ import Tippy from "@tippyjs/react"; -import { Variants } from "framer-motion"; 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"; @@ -12,7 +11,7 @@ import { BSVersionManagerService } from "renderer/services/bs-version-manager.se import { ConfigurationService } from "renderer/services/configuration.service"; import { IpcService } from "renderer/services/ipc.service"; import { ModalComponent } from "renderer/services/modale.service"; -import { VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service"; +import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service"; import { BSVersion } from "shared/bs-version.interface"; export const ShareFoldersModal: ModalComponent = ({ data }) => { @@ -107,33 +106,12 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => { const t = useTranslation(); - const name = relativeFolder.split(window.electron.path.sep).at(-1); const color = useThemeColor("first-color"); - - const variants: Variants = { - hover: { rotate: 22.5 }, - tap: { rotate: 45 }, - }; - - const pending = useObservable(linker.isPending(version, relativeFolder)); - const processing = useObservable(linker.isProcessing(version, relativeFolder)); - const linkDisabled = pending || processing; - - const [linked, setLinked] = useState(false); - - useEffect(() => { - if (pending) { - return; - } - loadFolderIsLinked(); - }, [version, relativeFolder, pending]); - - const loadFolderIsLinked = () => { - linker.isVersionFolderLinked(version, relativeFolder).toPromise().then(setLinked); - }; + const state = useObservable(() => linker.$folderLinkedState(version, relativeFolder), FolderLinkState.Unlinked, [version, relativeFolder]); + const name = relativeFolder.split(window.electron.path.sep).at(-1); const onClickLink = () => { - if (linked) { + if (state === FolderLinkState.Linked) { return linker.unlinkVersionFolder({ version, relativeFolder, @@ -150,7 +128,6 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => { const cancelLink = () => { linker.cancelAction(version, relativeFolder); - loadFolderIsLinked(); }; return ( @@ -159,14 +136,18 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => { {name}
      - - + + {(() => { - if (processing) { + if (state === FolderLinkState.Processing) { return ; } - if (pending) { + if (state === FolderLinkState.Pending) { return ( modalSevice.getModalToShow()); const modalData = modalSevice.getModalData(); const resolver = modalSevice.getResolver(); diff --git a/src/renderer/components/models-management/models-panel.component.tsx b/src/renderer/components/models-management/models-panel.component.tsx index 1bf18a2f..1b1f96fe 100644 --- a/src/renderer/components/models-management/models-panel.component.tsx +++ b/src/renderer/components/models-management/models-panel.component.tsx @@ -1,6 +1,5 @@ import { BSVersion } from "shared/bs-version.interface"; import { useRef, useState } from "react"; -import { ModelsTabsNavbar } from "./models-tabs-navbar.component"; import { ModelsGrid } from "./models-grid.component"; import { MSModelType } from "shared/models/models/model-saber.model"; import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"; @@ -14,6 +13,16 @@ import { NotificationService } from "renderer/services/notification.service"; import { ConfigurationService } from "renderer/services/configuration.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { lt } from "semver"; +import { lastValueFrom, of, take } from "rxjs"; +import { FolderLinkState } from "renderer/services/version-folder-linker.service"; +import { BsContentTabItemProps } from "../shared/bs-content-tab-panel/bs-content-tab-item.component"; +import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component"; +import { ModelTypeAvatarIcon } from "../svgs/icons/model-type-avatar-icon.component"; +import { SvgIcon } from "../svgs/svg-icon.type"; +import { ModelTypeSaberIcon } from "../svgs/icons/model-type-saber-icon.component"; +import { ModelTypePlatformIcon } from "../svgs/icons/model-type-platform-icon.component"; +import { ModelTypeBloqIcon } from "../svgs/icons/model-type-bloq-icon.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersion; isActive: boolean; goToMods?: () => void }) { const modelsManager = useService(ModelsManagerService); @@ -29,9 +38,15 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi const [modelTypeTab, setModelTypeTab] = useState(MSModelType.Avatar); const [currentTabIndex, setCurrentTabIndex] = useState(0); - const [search, setSearch] = useState(""); + const modelsLinkStats = { + [MSModelType.Avatar]: useObservable(() => version ? modelsManager.$modelsLinkingState(version, MSModelType.Avatar) : of(null), FolderLinkState.Unlinked, [version]), + [MSModelType.Saber]: useObservable(() => version ? modelsManager.$modelsLinkingState(version, MSModelType.Saber) : of(null), FolderLinkState.Unlinked, [version]), + [MSModelType.Platfrom]: useObservable(() => version ? modelsManager.$modelsLinkingState(version, MSModelType.Platfrom) : of(null), FolderLinkState.Unlinked, [version]), + [MSModelType.Bloq]: useObservable(() => version ? modelsManager.$modelsLinkingState(version, MSModelType.Bloq) : of(null), FolderLinkState.Unlinked, [version]), + } + useOnUpdate(() => { if (!isActive || !goToMods) { return; @@ -93,6 +108,44 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi { text: "models.panel.actions.drop-down.delete", onClick: deleteModels, icon: "trash" }, ]; + const getModelTabProps = (model: MSModelType): BsContentTabItemProps => { + + const onClick = async () => { + const state = await lastValueFrom(modelsManager.$modelsLinkingState(version, model).pipe(take(1))); + if (state === FolderLinkState.Pending || state === FolderLinkState.Processing) { return; } + if (state === FolderLinkState.Linked) { + modelsManager.unlinkModels(model, version); + } else { + modelsManager.linkModels(model, version); + } + } + + const getModelIcon = (model: MSModelType): SvgIcon => { + switch (model) { + case MSModelType.Avatar: + return ModelTypeAvatarIcon; + case MSModelType.Saber: + return ModelTypeSaberIcon; + case MSModelType.Platfrom: + return ModelTypePlatformIcon; + case MSModelType.Bloq: + return ModelTypeBloqIcon; + default: + return ModelTypeAvatarIcon; + } + } + + return { + text: `models.types.plural.${model}`, + icon: getModelIcon(model), + onClick: () => setModelTypeTab(model), + linkProps: version ? { + onClick, + state: modelsLinkStats[model], + } : null + } + }; + return (
      @@ -112,24 +165,23 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
      -
      - { - setModelTypeTab(() => tab.extra); - setCurrentTabIndex(() => index); - }} - /> - -
      + setCurrentTabIndex(index)} + tabs={[ + getModelTabProps(MSModelType.Avatar), + getModelTabProps(MSModelType.Saber), + getModelTabProps(MSModelType.Platfrom), + getModelTabProps(MSModelType.Bloq), + ]} + > + <> -
      -
      + +
      ); } diff --git a/src/renderer/components/models-management/models-tabs-navbar.component.tsx b/src/renderer/components/models-management/models-tabs-navbar.component.tsx deleted file mode 100644 index 71d59464..00000000 --- a/src/renderer/components/models-management/models-tabs-navbar.component.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { BSVersion } from "shared/bs-version.interface"; -import { MSModelType } from "../../../shared/models/models/model-saber.model"; -import { LinkButton } from "../maps-mangement-components/link-button.component"; -import { useState } from "react"; -import { useOnUpdate } from "renderer/hooks/use-on-update.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"; -import { BsmIcon } from "../svgs/bsm-icon.component"; -import { MODEL_TYPES } from "../../../shared/models/models/constants"; -import { useService } from "renderer/hooks/use-service.hook"; -import { useTranslation } from "renderer/hooks/use-translation.hook"; - -type Props = { - className?: string; - version?: BSVersion; - tabIndex: number; - onTabChange: (index: number, tab: BsContentNavBarTab) => void; -}; - -export function ModelsTabsNavbar({ className, version, tabIndex, onTabChange }: Props) { - const tabs = useConstant[]>(() => { - return MODEL_TYPES.map(type => ({ - text: type, - extra: type, - })); - }); - - return } />; -} - -type TabProps = { - version?: BSVersion; - modelType: MSModelType; - active?: boolean; -} & React.ComponentProps<"li">; - -function ModelTab({ version, modelType, active, onClick }: TabProps) { - const modelsManager = useService(ModelsManagerService); - - const t = useTranslation(); - - 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); - const unlinkModels = () => modelsManager.unlinkModels(modelType, version); - - const onClickLink = () => { - if (!version) { - return Promise.resolve(); - } - if (modelsAreLinked) { - return unlinkModels(); - } - return linkModels(); - }; - - return ( -
    • -
      - - {t(`models.types.plural.${modelType}`)} -
      -
      {!!version && }
      -
    • - ); -} diff --git a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx index 0a0ddcda..b4e1355d 100644 --- a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx +++ b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx @@ -12,7 +12,7 @@ export const BsManagerIcon = memo(({ className }: { className?: string }) => { const audioPlayer = useService(AudioPlayerService); const { firstColor, secondColor } = useThemeColor(); - const playing = useObservable(audioPlayer.playing$); + const playing = useObservable(() => audioPlayer.playing$); const { bpm } = audioPlayer; diff --git a/src/renderer/components/nav-bar/nav-bar-items/shared-nav-bar-item.component.tsx b/src/renderer/components/nav-bar/nav-bar-items/shared-nav-bar-item.component.tsx index 2b24dfb5..a820cdfc 100644 --- a/src/renderer/components/nav-bar/nav-bar-items/shared-nav-bar-item.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar-items/shared-nav-bar-item.component.tsx @@ -12,7 +12,7 @@ export function SharedNavBarItem() { const pageState = useService(PageStateService); const t = useTranslation(); - const route = useObservable(pageState.route$); + const route = useObservable(() => pageState.route$); const color = useThemeColor("first-color"); return ( diff --git a/src/renderer/components/nav-bar/nav-bar.component.tsx b/src/renderer/components/nav-bar/nav-bar.component.tsx index adb87168..f3fcc58b 100644 --- a/src/renderer/components/nav-bar/nav-bar.component.tsx +++ b/src/renderer/components/nav-bar/nav-bar.component.tsx @@ -19,8 +19,8 @@ export function NavBar() { const versionManager = useService(BSVersionManagerService); const versionDownloader = useService(BsDownloaderService); - const downloadingVersion = useObservable(versionDownloader.downloadingVersion$.pipe(distinctUntilChanged(equal))); - const installedVersions = useObservable(versionManager.installedVersions$); + const downloadingVersion = useObservable(() => versionDownloader.downloadingVersion$.pipe(distinctUntilChanged(equal))); + const installedVersions = useObservable(() => versionManager.installedVersions$); const color = useThemeColor("first-color"); const t = useTranslation(); diff --git a/src/renderer/components/notification/notification-overlay.component.tsx b/src/renderer/components/notification/notification-overlay.component.tsx index 4709235a..eea04f13 100644 --- a/src/renderer/components/notification/notification-overlay.component.tsx +++ b/src/renderer/components/notification/notification-overlay.component.tsx @@ -7,7 +7,7 @@ import { useService } from "renderer/hooks/use-service.hook"; export function NotificationOverlay() { const notificationService = useService(NotificationService); - const notifications = useObservable(notificationService.notifications$); + const notifications = useObservable(() => notificationService.notifications$); return (
        diff --git a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx index 13eea1d6..1b005c79 100644 --- a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx +++ b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx @@ -9,9 +9,9 @@ import { useService } from "renderer/hooks/use-service.hook"; export function BsmProgressBar() { const progressBarService = useService(ProgressBarService); - const progressData = useObservable(progressBarService.progressData$); - const visible = useObservable(progressBarService.visible$); - const style = useObservable(progressBarService.style$); + const progressData = useObservable(() => progressBarService.progressData$); + const visible = useObservable(() => progressBarService.visible$); + const style = useObservable(() => progressBarService.style$); const progressLabel = (() => { if (!progressData) { diff --git a/src/renderer/components/progress-bar/text-progress-bar.component.tsx b/src/renderer/components/progress-bar/text-progress-bar.component.tsx index 3179f2a2..81964e87 100644 --- a/src/renderer/components/progress-bar/text-progress-bar.component.tsx +++ b/src/renderer/components/progress-bar/text-progress-bar.component.tsx @@ -9,7 +9,7 @@ type Props = { }; export default function TextProgressBar({ value$, className, style }: Props) { - const value = useObservable(value$); + const value = useObservable(() => value$); const prefix = typeof value === "number" ? "%" : ""; diff --git a/src/renderer/components/settings/setting-radio-array.component.tsx b/src/renderer/components/settings/setting-radio-array.component.tsx index 60eb44a1..1a7f2e60 100644 --- a/src/renderer/components/settings/setting-radio-array.component.tsx +++ b/src/renderer/components/settings/setting-radio-array.component.tsx @@ -21,7 +21,7 @@ export function SettingRadioArray({ id, items, selectedItemId, selectedItemVa return (
        {items.map(i => ( -
        onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-200 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}> +
        onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
        diff --git a/src/renderer/components/shared/bs-content-nav-bar.component.tsx b/src/renderer/components/shared/bs-content-nav-bar.component.tsx deleted file mode 100644 index e747df83..00000000 --- a/src/renderer/components/shared/bs-content-nav-bar.component.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { DetailedHTMLProps, Fragment } from "react"; -import { BsmIconType } from "../svgs/bsm-icon.component"; - -type Props = { - className?: string; - tabIndex: number; - tabs: BsContentNavBarTab[]; - renderTab: (props: DetailedHTMLProps, any>, tab?: BsContentNavBarTab, activeTab?: BsContentNavBarTab) => JSX.Element; - onTabChange?: (index: number, tab?: BsContentNavBarTab) => void; -}; - -export type BsContentNavBarTab = { - text: string; - icon?: BsmIconType; - extra?: T; -}; - -export function BsContentNavBar({ className, tabIndex, tabs, renderTab, onTabChange }: Props) { - const handleTabClick = (index: number) => { - onTabChange?.(index, tabs[index]); - }; - - return ( - - ); -} diff --git a/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-item.component.tsx b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-item.component.tsx new file mode 100644 index 00000000..336dc3df --- /dev/null +++ b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-item.component.tsx @@ -0,0 +1,44 @@ +import { LinkBtnProps, LinkButton } from "renderer/components/maps-mangement-components/link-button.component"; +import { SvgIcon } from "renderer/components/svgs/svg-icon.type"; +import { useTranslation } from "renderer/hooks/use-translation.hook"; + +export type BsContentTabItemProps = { + text: string; + icon: SvgIcon; + active?: boolean; + value?: T; + linkProps?: LinkBtnProps; + onClick: (value?: T) => void; +}; + +type BsContentTabItemComponent = ({text, icon, active, value, linkProps, onClick}: BsContentTabItemProps) => JSX.Element; + +export const BsContentTabItem: BsContentTabItemComponent = ({ text, icon: Icon, active, value, linkProps, onClick }) => { + + const t = useTranslation(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onClick(value); + } + + return ( +
      • +
        + + {t(text)} +
        + {linkProps && ( +
        + +
        + )} +
      • + ); +} \ No newline at end of file diff --git a/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx new file mode 100644 index 00000000..6cc1257e --- /dev/null +++ b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx @@ -0,0 +1,35 @@ +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { LaserSlider } from "../laser-slider.component"; +import { BsContentTabItem, BsContentTabItemProps } from "./bs-content-tab-item.component"; + +type Props = Readonly<{ + className?: string; + tabs: BsContentTabItemProps[]; + tabIndex: number; + onTabChange: (index: number, tab: BsContentTabItemProps) => void; + children: JSX.Element; +}>; + +export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, children}: Props) { + + const sliderColor = useThemeColor("second-color"); + + return ( +
        + + +
        + {children} +
        +
        + ) +} diff --git a/src/renderer/components/shared/iframe-view.component.tsx b/src/renderer/components/shared/iframe-view.component.tsx index 2ca5ebd6..c378d846 100644 --- a/src/renderer/components/shared/iframe-view.component.tsx +++ b/src/renderer/components/shared/iframe-view.component.tsx @@ -8,7 +8,7 @@ import { LinkOpenerService } from "renderer/services/link-opener.service"; export function BsmIframeView() { const linkService = useService(LinkOpenerService); - const iframeUrl = useObservable(linkService.iframeLink$); + const iframeUrl = useObservable(() => linkService.iframeLink$); const ref = useRef(null); useClickOutside(ref, () => linkService.closeIframe()); diff --git a/src/renderer/components/shared/laser-slider.component.tsx b/src/renderer/components/shared/laser-slider.component.tsx new file mode 100644 index 00000000..ce35107a --- /dev/null +++ b/src/renderer/components/shared/laser-slider.component.tsx @@ -0,0 +1,35 @@ +import { CSSProperties } from "react"; + +type Props = Readonly<{ + mode: "horizontal" | "vertical"; + color: string; + className?: string; + nbSteps: number; + step: number; +}> + +export function LaserSlider({mode, color, className, nbSteps, step}: Props) { + + const sliderStyle = ((): CSSProperties => { + if(mode === "vertical"){ + return { + transform: `translate(0, ${step * 100}%)`, + height: `calc(100% / ${nbSteps})`, + width: "100%" + } + } + + return { + transform: `translate(${step * 100}%, 0)`, + width: `calc(100% / ${nbSteps})`, + height: "100%" + } + })(); + + return ( +
        + + +
        + ) +} diff --git a/src/renderer/components/shared/tab-nav-bar.component.tsx b/src/renderer/components/shared/tab-nav-bar.component.tsx index 0484ec58..960ab97e 100644 --- a/src/renderer/components/shared/tab-nav-bar.component.tsx +++ b/src/renderer/components/shared/tab-nav-bar.component.tsx @@ -1,6 +1,7 @@ import { DetailedHTMLProps, Fragment } from "react"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { LaserSlider } from "./laser-slider.component"; type Props = { tabIndex: number; @@ -22,10 +23,7 @@ export function TabNavBar(props: Props) { return (
        - + diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index aacf9cb6..4c227074 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -13,7 +13,7 @@ import { ConfigurationService } from "./configuration.service"; import { ArchiveProgress } from "shared/models/archive.interface"; import { map, last, catchError } from "rxjs/operators"; import { ProgressionInterface } from "shared/models/progress-bar"; -import { VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service"; +import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service"; export class MapsManagerService { private static instance: MapsManagerService; @@ -176,6 +176,10 @@ export class MapsManagerService { } public $mapsLinkingPending(version: BSVersion): Observable { - return this.linker.$isVersionFolderPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER); + return this.linker.$isPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER); + } + + public $mapsFolderLinkState(version: BSVersion): Observable { + return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER); } } diff --git a/src/renderer/services/models-management/models-manager.service.ts b/src/renderer/services/models-management/models-manager.service.ts index 3dd5f111..0c1c230e 100644 --- a/src/renderer/services/models-management/models-manager.service.ts +++ b/src/renderer/services/models-management/models-manager.service.ts @@ -1,6 +1,6 @@ import { MSModelType } from "shared/models/models/model-saber.model"; import { IpcService } from "../ipc.service"; -import { VersionFolderLinkerService, VersionLinkerActionListener, VersionLinkerActionType } from "../version-folder-linker.service"; +import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionListener, VersionLinkerActionType } from "../version-folder-linker.service"; import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants"; import { Observable, lastValueFrom, map } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; @@ -49,7 +49,11 @@ export class ModelsManagerService { } public $modelsLinkingPending(version: BSVersion, type: MSModelType): Observable { - return this.versionFolderLinked.$isVersionFolderPending(version, MODEL_TYPE_FOLDERS[type]); + return this.versionFolderLinked.$isPending(version, MODEL_TYPE_FOLDERS[type]); + } + + public $modelsLinkingState(version: BSVersion, type: MSModelType): Observable { + return this.versionFolderLinked.$folderLinkedState(version, MODEL_TYPE_FOLDERS[type]); } public onModelsFolderLinked(callback: VersionLinkerActionListener): void { diff --git a/src/renderer/services/version-folder-linker.service.ts b/src/renderer/services/version-folder-linker.service.ts index 5b7c6b33..936de9d6 100644 --- a/src/renderer/services/version-folder-linker.service.ts +++ b/src/renderer/services/version-folder-linker.service.ts @@ -1,6 +1,6 @@ import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service"; import { map, distinctUntilChanged, filter, mergeMap, shareReplay } from "rxjs/operators"; -import { BehaviorSubject, Observable } from "rxjs"; +import { BehaviorSubject, Observable, of } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { IpcService } from "./ipc.service"; import { ProgressBarService } from "./progress-bar.service"; @@ -62,6 +62,13 @@ export class VersionFolderLinkerService { return this.ipcService.sendV2("link-version-folder-action", { args: action }); } + private get currentAction$(): Observable { + return this._queue$.pipe( + map(actions => actions.at(0)), + distinctUntilChanged() + ); + } + public linkVersionFolder(action: VersionLinkFolderAction): Promise { const promise = new Promise(resolve => { const callBack: VersionLinkerActionListener = (performedAction, linked) => { @@ -122,12 +129,33 @@ export class VersionFolderLinkerService { return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } }); } - public isPending(version: BSVersion, relativeFolder: string): Observable { - return this.queue$.pipe(map(queue => queue.length > 0 && queue.some(action => action.version === version && action.relativeFolder === relativeFolder), distinctUntilChanged())); + public $folderLinkedState(version: BSVersion, relativeFolder: string): Observable { + return this._queue$.pipe( + mergeMap(queue => { + const currentAction = queue.at(0); + if(currentAction && equal(currentAction.version, version) && currentAction.relativeFolder === relativeFolder) { + return of(FolderLinkState.Processing) + } + + if(queue.some(action => equal(action.version, version) && action.relativeFolder === relativeFolder)) { + return of(FolderLinkState.Pending); + } + + return this.isVersionFolderLinked(version, relativeFolder).pipe( + map(linked => linked ? FolderLinkState.Linked : FolderLinkState.Unlinked) + ); + }), + distinctUntilChanged(), + shareReplay(1) + ); } - public isProcessing(version: BSVersion, relativeFolder: string): Observable { - return this.currentAction$.pipe(map(action => action && action.version === version && action.relativeFolder === relativeFolder)); + public $isPending(version: BSVersion, relativeFolder: string): Observable { + return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Pending)); + } + + public $isProcessing(version: BSVersion, relativeFolder: string): Observable { + return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Processing)); } public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable { @@ -137,27 +165,6 @@ export class VersionFolderLinkerService { public relinkAllVersionsFolders(): Observable { 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(), - shareReplay(1) - ); - } - - public get currentAction$(): Observable { - return this._queue$.pipe( - map(actions => actions.at(0)), - distinctUntilChanged() - ); - } - - public get queue$(): Observable { - return this._queue$.asObservable(); - } } export const enum VersionLinkerActionType { @@ -182,3 +189,10 @@ export interface VersionUnlinkFolderAction extends VersionLinkerAction { } export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void; + +export enum FolderLinkState { + Linked, + Unlinked, + Pending, + Processing +} diff --git a/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx b/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx index 8dab7d1b..a21bf590 100644 --- a/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx +++ b/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx @@ -24,8 +24,8 @@ export default function OneClickDownloadPlaylist() { const mapsContainer = useRef(null); const playlistUrl = useConstant(() => new URLSearchParams(window.location.search).get("playlistUrl")); const download$ = useConstant(() => playlistDownloader.oneClickInstallPlaylist(playlistUrl)); - const playlistInfos = useObservable(download$.pipe(filter(progress => !!progress.data?.playlistInfos), map(progress => progress.data.playlistInfos), take(1))); - const downloadedMaps = useObservable(download$.pipe(filter(progress => !!progress.data?.downloadedMaps), map(progress => progress.data.downloadedMaps))); + const playlistInfos = useObservable(() => download$.pipe(filter(progress => !!progress.data?.playlistInfos), map(progress => progress.data.playlistInfos), take(1))); + const downloadedMaps = useObservable(() => download$.pipe(filter(progress => !!progress.data?.downloadedMaps), map(progress => progress.data.downloadedMaps))); useEffect(() => { diff --git a/src/renderer/windows/ShortcutLaunch.tsx b/src/renderer/windows/ShortcutLaunch.tsx index ca2f2a87..1049676a 100644 --- a/src/renderer/windows/ShortcutLaunch.tsx +++ b/src/renderer/windows/ShortcutLaunch.tsx @@ -26,7 +26,7 @@ export default function ShortcutLaunch() { const t = useTranslation(); const color = useThemeColor("second-color"); - const launchOptions = useObservable(ipc.sendV2("shortcut-launch-options").pipe(take(1)), null) + const launchOptions = useObservable(() => ipc.sendV2("shortcut-launch-options").pipe(take(1)), null) const [rotation, setRotation] = useState(0); const [status, setStatus] = useState();