mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] Advancement on playlist creation
This commit is contained in:
@@ -2,6 +2,7 @@ import { shell, dialog, app, BrowserWindow } from "electron";
|
||||
import { NotificationService } from "../services/notification.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, of } from "rxjs";
|
||||
import { readFileSync } from "fs-extra";
|
||||
|
||||
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
|
||||
|
||||
@@ -45,3 +46,15 @@ ipc.on("notify-system", (args, reply) => {
|
||||
ipc.on("view-path-in-explorer", (args, reply) => {
|
||||
reply(of(shell.showItemInFolder(args)));
|
||||
});
|
||||
|
||||
ipc.on("choose-image", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openFile", "multiSelections"], filters: [{ name: "Images", extensions: ["jpg", "png", "jpeg"] }] }).then(res => {
|
||||
if (res.canceled || !res.filePaths) {
|
||||
return [];
|
||||
}
|
||||
if(args.base64){
|
||||
return res.filePaths.map(path => Buffer.from(readFileSync(path)).toString("base64"));
|
||||
}
|
||||
return res.filePaths;
|
||||
})));
|
||||
});
|
||||
|
||||
+2
-4
@@ -6,7 +6,7 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BehaviorSubject, combineAll, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
@@ -37,7 +37,7 @@ import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { enumerate } from "shared/helpers/array.helpers";
|
||||
import { SyncPlaylistModal } from "renderer/components/modal/modal-types/playlist/sync-playlist-modal.component";
|
||||
import { ExportPlaylistModal } from "renderer/components/modal/modal-types/playlist/export-playlist-modal.component";
|
||||
import { EditPlaylistModal } from "renderer/components/modal/modal-types/playlist/edit-playlist-modal.component";
|
||||
import { EditPlaylistModal } from "renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -77,8 +77,6 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
const playlists = useObservable(() => playlists$, []);
|
||||
|
||||
console.log(playlists);
|
||||
|
||||
const [playlistsLoading, setPlaylistsLoading] = useState(false);
|
||||
const loadPercent$ = useConstant(() => new BehaviorSubject<number>(0));
|
||||
const linked = useStateMap(linkedState, (newState, precMapped) => (newState === FolderLinkState.Pending || newState === FolderLinkState.Processing) ? precMapped : newState === FolderLinkState.Linked, false);
|
||||
|
||||
@@ -75,9 +75,7 @@ export function PlaylistItem({ title,
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(0);
|
||||
date.setSeconds(duration);
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
return duration > 3600 ? dateFormat(duration * 1000, "h:MM:ss") : dateFormat(duration * 1000, "MM:ss");
|
||||
})();
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
+2
-2
@@ -59,14 +59,14 @@ export const LoginToSteamModal: ModalComponent<
|
||||
<label className="block font-bold cursor-pointer tracking-wide" htmlFor="username">
|
||||
{t("modals.steam-login.inputs.username.label")}
|
||||
</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none h-9" onChange={e => setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")} />
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-0.5 rounded-md outline-none h-9" onChange={e => setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-bold cursor-pointer tracking-wide" htmlFor="password">
|
||||
{t("modals.steam-login.inputs.password.label")}
|
||||
</label>
|
||||
<div className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md flex box-border h-9">
|
||||
<input className="grow px-1 py-[2px] outline-none bg-transparent" onChange={e => setPassword(e.target.value)} value={password} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")} />
|
||||
<input className="grow px-1 py-0.5 outline-none bg-transparent" onChange={e => setPassword(e.target.value)} value={password} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")} />
|
||||
<BsmButton className="shrink-0 m-1 rounded-md p-0.5 !bg-light-main-color-3 dark:!bg-main-color-3" icon={showPassword ? "eye-cross" : "eye"} withBar={false} onClick={() => setShowPassword(prev => !prev)} />
|
||||
</div>
|
||||
{password?.length > 64 && <span className="text-orange-700 dark:text-orange-400 text-xs whitespace-normal min-w-0">{t("modals.steam-login.inputs.password.max-length-warning")}</span>}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DeleteModelsModal: ModalComponent<void, { models: BsmLocalModel[];
|
||||
const isMultiple = data.models.length > 1;
|
||||
|
||||
const title = useConstant(() => (isMultiple ? t("models.modals.delete-models.title") : t("models.modals.delete-model.title")));
|
||||
const desc = useConstant(() => (isMultiple ? t("models.modals.delete-models.desc", { nb: `${data.models.length}` }) : t("models.modals.delete-model.desc", { modelName: data.models[0].model?.name ?? data.models[0].fileName })));
|
||||
const desc = useConstant(() => (isMultiple ? t("models.modals.delete-models.desc", { nb: `${data.models.length}` }) : t("models.modals.delete-model.desc", { modelName: data.models[0]?.model?.name ?? data.models[0]?.fileName })));
|
||||
const linkedAnnotation = useConstant(() =>
|
||||
(() => {
|
||||
if (!data.linked) return null;
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useState } from "react";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { SteamDownloaderService } from "renderer/services/bs-version-download/steam-downloader.service";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
type Props = {
|
||||
playlistTitle: string;
|
||||
playlistDescription: string;
|
||||
playlistAuthor: string;
|
||||
base64Image: string;
|
||||
}
|
||||
|
||||
export const EditPlaylistInfosModal: ModalComponent<Props, Props> = ({ resolver, options: { data: {
|
||||
playlistTitle,
|
||||
playlistDescription,
|
||||
playlistAuthor,
|
||||
base64Image
|
||||
}}}) => {
|
||||
|
||||
const steamDownloader = useService(SteamDownloaderService);
|
||||
const ipc = useService(IpcService);
|
||||
|
||||
const [title, setTitle] = useState(playlistTitle);
|
||||
const [description, setDescription] = useState(playlistDescription);
|
||||
const [author, setAuthor] = useState(playlistAuthor ?? steamDownloader.getSteamUsername());
|
||||
const [base64, setBase64] = useState(base64Image);
|
||||
|
||||
const handleClickImage = async () => {
|
||||
const res = await lastValueFrom(ipc.sendV2("choose-image", { base64: true })).catch(logRenderError) as string[];
|
||||
setBase64(prev => res?.at(0) ?? prev);
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
resolver({
|
||||
exitCode: ModalExitCode.COMPLETED,
|
||||
data: {
|
||||
playlistTitle: title,
|
||||
playlistDescription: description,
|
||||
playlistAuthor: author,
|
||||
base64Image: base64
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center mb-4 px-4">Créer une playlist</h1>
|
||||
<div className="w-full flex flex-col justify-center items-center">
|
||||
<button className="flex justify-center items-center relative size-36 border-2 border-gray-400 bg-theme-1 rounded-md overflow-hidden" onClick={handleClickImage}>
|
||||
{base64 ? (
|
||||
<BsmImage className="absolute size-full cursor-pointer" base64={base64} />
|
||||
) : (
|
||||
<span className="absolute size-full flex justify-center items-center p-2">Choisir une image</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="w-full">
|
||||
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-title">Titre</label>
|
||||
<input id="playlist-title" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={title} placeholder="Titre de la playlist" onChange={e => setTitle(e.target.value)}/>
|
||||
</div>
|
||||
<div className="w-full mt-1.5">
|
||||
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-desc">Description</label>
|
||||
<textarea id="playlist-desc" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none max-h-40 min-h-8" value={description} placeholder="Description de la playlist" onChange={e => setDescription(e.target.value)} />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-author">Auteur</label>
|
||||
<input id="playlist-author" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={author} placeholder="Auteur de la playlist" onChange={e => setAuthor(e.target.value)}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all h-full" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all h-full" onClick={submit} withBar={false} text="Enregistrer" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+113
-17
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { FilterPanel, isLocalMapFitMapFilter, isMapFitFilter } from "renderer/components/maps-playlists-panel/maps/filter-panel.component";
|
||||
import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { FilterPanel, isMapFitFilter } from "renderer/components/maps-playlists-panel/maps/filter-panel.component";
|
||||
import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
|
||||
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
|
||||
import { VirtualScroll, VirtualScrollEndHandler } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
@@ -8,8 +8,8 @@ import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { BehaviorSubject, Observable, Subject, filter, lastValueFrom, map, of, take } from "rxjs";
|
||||
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service"
|
||||
import { BehaviorSubject, Observable, lastValueFrom, map, take } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
|
||||
@@ -24,6 +24,15 @@ import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { MapIcon } from "renderer/components/svgs/icons/map-icon.component";
|
||||
import { PersonIcon } from "renderer/components/svgs/icons/person-icon.component";
|
||||
import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
|
||||
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
|
||||
import dateFormat from 'dateformat';
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
import { DeleteModelsModal } from "../../models/delete-models-modal.component";
|
||||
import { BPList } from "shared/models/playlists/playlist.interface";
|
||||
import { EditPlaylistInfosModal } from "./edit-playlist-infos-modal.component";
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion;
|
||||
@@ -31,13 +40,14 @@ type Props = {
|
||||
playlist?: LocalBPList;
|
||||
}
|
||||
|
||||
export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({ resolver, options: { data: { version, maps$, playlist } } }) => {
|
||||
export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, options: { data: { version, maps$, playlist } } }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const mapsService = useService(MapsManagerService);
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const modals = useService(ModalService);
|
||||
|
||||
const keyPressed$ = useConstant(() => new BehaviorSubject<string|undefined>(undefined));
|
||||
const filterContainerRef = useRef(null);
|
||||
@@ -72,9 +82,10 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
|
||||
const [availableMapsSource, setAvailableMapsSource] = useState<number>(0);
|
||||
|
||||
|
||||
const displayablePlaylistMaps = useMemo(() => playlistMaps ? Object.values(playlistMaps).filter(Boolean) : [], [playlistMaps]);
|
||||
|
||||
const [base64Cover, setBase64Cover] = useState<string>(undefined);
|
||||
|
||||
useOnUpdate(() => {
|
||||
const keyDown = (e: KeyboardEvent) => keyPressed$.next(e.key);
|
||||
document.addEventListener("keydown", keyDown);
|
||||
@@ -281,6 +292,72 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
setBsvSearchParams(() => ({ page: 0, filter: availableMapsFilter, q: availableMapsSearch, sortOrder: bsvSearchOrder}));
|
||||
}
|
||||
|
||||
const playlistNbMappers = useMemo(() => {
|
||||
const mappersSet = new Set<string>();
|
||||
Object.values(playlistMaps ?? {}).forEach(map => {
|
||||
if((map as BsmLocalMap).rawInfo?._levelAuthorName){
|
||||
mappersSet.add((map as BsmLocalMap).rawInfo._levelAuthorName);
|
||||
}
|
||||
else if((map as SongDetails).uploader?.name){
|
||||
mappersSet.add((map as SongDetails).uploader.name);
|
||||
}
|
||||
else if((map as BsvMapDetail).uploader?.name){
|
||||
mappersSet.add((map as BsvMapDetail).uploader.name);
|
||||
}
|
||||
});
|
||||
return Intl.NumberFormat(undefined, { notation: "compact" }).format(mappersSet.size).trim();
|
||||
}, [playlistMaps]);
|
||||
|
||||
const playlistDuration = useMemo(() => {
|
||||
const durations = Object.values(playlistMaps ?? {}).map(map => {
|
||||
if((map as BsmLocalMap)?.songDetails?.duration){
|
||||
return (map as BsmLocalMap).songDetails.duration;
|
||||
}
|
||||
else if((map as SongDetails)?.duration){
|
||||
return (map as SongDetails).duration;
|
||||
}
|
||||
else if((map as BsvMapDetail)?.metadata?.duration){
|
||||
return (map as BsvMapDetail).metadata.duration;
|
||||
}
|
||||
return 0;
|
||||
}).filter(duration => !isNaN(duration));
|
||||
|
||||
const totalDuration = durations.reduce((acc, duration) => acc + duration, 0);
|
||||
return totalDuration > 3600 ? dateFormat(totalDuration * 1000, "h:MM:ss") : dateFormat(totalDuration * 1000, "MM:ss");
|
||||
}, [playlistMaps]);
|
||||
|
||||
const [playlistMinNps, playlistMaxNps] = useMemo(() => {
|
||||
const nps = Object.values(playlistMaps ?? {}).reduce((acc, map) => {
|
||||
if(Array.isArray((map as BsmLocalMap)?.songDetails?.difficulties)){
|
||||
acc.push(...(map as BsmLocalMap).songDetails.difficulties.map(diff => diff.nps));
|
||||
}
|
||||
else if(Array.isArray((map as SongDetails)?.difficulties)){
|
||||
acc.push(...(map as SongDetails).difficulties.map(diff => diff.nps));
|
||||
}
|
||||
else if(Array.isArray((map as BsvMapDetail)?.versions?.at(0)?.diffs)){
|
||||
acc.push(...(map as BsvMapDetail).versions.flatMap(version => version.diffs.map(diff => diff.nps)));
|
||||
}
|
||||
return acc;
|
||||
}, [] as number[]).filter(n => !isNaN(n));
|
||||
|
||||
const minNps = Math.min(...nps);
|
||||
const maxNps = Math.max(...nps);
|
||||
|
||||
const minMaxNps = [minNps === Infinity ? 0 : minNps, maxNps === -Infinity ? 0 : maxNps];
|
||||
|
||||
return minMaxNps.map(n => Math.round(n * 10) / 10);
|
||||
}, [playlistMaps]);
|
||||
|
||||
const handleContinue = async () => {
|
||||
const res = await modals.openModal(EditPlaylistInfosModal, { data: {
|
||||
playlistTitle: playlist?.playlistTitle ?? "",
|
||||
playlistDescription: playlist?.playlistDescription ?? "",
|
||||
base64Image: playlist?.image ?? "",
|
||||
playlistAuthor: playlist?.playlistAuthor ?? "",
|
||||
}});
|
||||
resolver({ exitCode: ModalExitCode.CANCELED });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen max-h-[calc(100vh-2rem)] max-w-[55rem] lg:max-w-[66rem] xl:max-w-[77rem] bg-theme-3 p-4 rounded-md">
|
||||
{(() => {
|
||||
@@ -289,15 +366,14 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
}
|
||||
else{
|
||||
return (
|
||||
<div className="size-full flex flex-col justify-between gap-3">
|
||||
<header>header</header>
|
||||
<div className="size-full flex flex-col justify-between">
|
||||
<div className="grow flex flex-row min-h-0 gap-2.5">
|
||||
<div className="flex flex-col grow basis-0 min-w-0">
|
||||
<div className="h-8 flex flex-row gap-2 w-full mb-1.5 min-w-0">
|
||||
<form className="h-8 flex flex-row gap-2 w-full mb-1.5 min-w-0" onSubmit={e => {e.preventDefault(); handleNewSearch()}}>
|
||||
<BsmSelect className="bg-theme-1 h-full rounded-full text-center pb-0.5" options={[{ text: "Installée(s)", value: 0 }, { text: "BeatSaver", value: 1 }]} onChange={setAvailableMapsSource}/>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5 min-w-0" type="text" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={availableMapsSearch} onChange={e => setAvailableMapsSearch(() => e.target.value)} />
|
||||
{availableMapsSource === 1 && (
|
||||
<BsmButton className="h-full aspect-square z-[1] flex justify-center p-1 rounded-full min-w-0 shrink-0" icon="search" withBar={false}/>
|
||||
<BsmButton className="h-full aspect-square z-[1] flex justify-center p-1 rounded-full min-w-0 shrink-0 !bg-light-main-color-1 dark:!bg-main-color-1" icon="search" onClick={handleNewSearch} withBar={false}/>
|
||||
)}
|
||||
<BsmDropdownButton ref={filterContainerRef} className="h-full aspect-square relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full p-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" textClassName="whitespace-nowrap" withBar={false}>
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black -translate-x-1/4 lg:translate-x-0" filter={availableMapsFilter} onChange={setAvailableMapsFilter} onApply={availableMapsSource === 1 && handleNewSearch} onClose={() => filterContainerRef.current.close()}/>
|
||||
@@ -305,7 +381,7 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
{availableMapsSource === 1 && (
|
||||
<BsmSelect className="bg-theme-1 h-full rounded-full text-center pb-0.5 min-w-0 lg:min-w-fit" options={sortOptions} onChange={handleSortChange}/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{availableMapsSource === 0 ? (
|
||||
renderList(localMaps.filter(map => {
|
||||
@@ -315,16 +391,18 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
) : (
|
||||
renderList(bsvMaps, renderBsvMapItem, { onScrollEnd: loadMoreBsvMaps })
|
||||
)}
|
||||
<div className="w-full h-4 flex justify-end"/>
|
||||
<div className="w-full h-4 flex justify-start">
|
||||
<span className="text-xs italic leading-4">Hold shift or ctrl to select multiples maps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex flex-col gap-2.5 pb-4 pt-10">
|
||||
<Tippy content="Ajouter à la playlist" theme="default" placement="left">
|
||||
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color}} type="button" onClick={addMapsToPlaylist}>
|
||||
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color, color: getCorrectTextColor(color)}} type="button" onClick={addMapsToPlaylist}>
|
||||
<ChevronTopIcon className="origin-center rotate-90"/>
|
||||
</button>
|
||||
</Tippy>
|
||||
<Tippy content="Enlever de la playlist" theme="default" placement="right">
|
||||
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color}} type="button" onClick={removeMapsFromPlaylist}>
|
||||
<button className="grow w-9 rounded-md cursor-pointer transition-transform duration-150 hover:brightness-110 active:scale-95" style={{backgroundColor: color, color: getCorrectTextColor(color)}} type="button" onClick={removeMapsFromPlaylist}>
|
||||
<ChevronTopIcon className="origin-center -rotate-90"/>
|
||||
</button>
|
||||
</Tippy>
|
||||
@@ -339,12 +417,30 @@ export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({
|
||||
{renderList(displayablePlaylistMaps.filter(map => {
|
||||
return isMapFitFilter({ map, filter: playlistMapsFilter, search: playlistMapsSearch });
|
||||
}), renderPlaylistMapItem)}
|
||||
<div className="w-full h-4 flex justify-end">
|
||||
<span className="text-xs italic leading-4">{Object.keys(playlistMaps ?? {}).length} Maps</span>
|
||||
<div className="w-full h-4 flex justify-end gap-3 mt-px">
|
||||
<div className="h-full flex justify-center items-center gap-0.5">
|
||||
<MapIcon className='h-full aspect-square mt-0.5'/>
|
||||
<span className="text-xs italic leading-4">{Object.keys(playlistMaps ?? {}).length}</span>
|
||||
</div>
|
||||
<div className="h-full flex justify-center items-center gap-0.5">
|
||||
<PersonIcon className='h-full aspect-square mt-0.5'/>
|
||||
<span className="text-xs italic leading-4">{playlistNbMappers}</span>
|
||||
</div>
|
||||
<div className="h-full flex justify-center items-center gap-0.5">
|
||||
<ClockIcon className='h-full aspect-square mt-0.5'/>
|
||||
<span className="text-xs italic leading-4">{playlistDuration}</span>
|
||||
</div>
|
||||
<div className="h-full flex justify-center items-center gap-0.5">
|
||||
<NpsIcon className='h-full aspect-square mt-0.5'/>
|
||||
<span className="text-xs italic leading-4">{`${playlistMinNps} - ${playlistMaxNps}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>footer</footer>
|
||||
<footer className="flex justify-center items-center gap-2 h-8 mt-2.5">
|
||||
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="cancel" text="Annuler" withBar={false} onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })}/>
|
||||
<BsmButton className="rounded-md text-center h-full grow basis-0 flex justify-center items-center" typeColor="primary" text="Continuer" withBar={false} onClick={handleContinue}/>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export const BsmImage = forwardRef<HTMLImageElement, Props>(({ className, image,
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const getBase64Url = () => {
|
||||
|
||||
if(base64?.startsWith("data:image")){ return base64; }
|
||||
return base64 ? `data:image/png;base64,${base64}` : undefined;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
}
|
||||
|
||||
private setSteamSession(username: string): void { localStorage.setItem(this.STEAM_SESSION_USERNAME_KEY, username); }
|
||||
private getSteamUsername(): string { return localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
|
||||
public getSteamUsername(): string { return localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
|
||||
public deleteSteamSession(): void { localStorage.removeItem(this.STEAM_SESSION_USERNAME_KEY); }
|
||||
public sessionExist(): boolean { return !!localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ export interface IpcChannelMapping {
|
||||
/* ** os-controls-ipcs ** */
|
||||
"new-window": { request: string, response: void };
|
||||
"choose-folder": { request: string, response: OpenDialogReturnValue };
|
||||
"choose-image": { request: { multiple?: boolean, base64?: boolean }, response: string[] }
|
||||
"window.progression": { request: number, response: void };
|
||||
"save-file": { request: { filename?: string; filters?: FileFilter[] }, response: string };
|
||||
"current-version": { request: void, response: string };
|
||||
|
||||
Reference in New Issue
Block a user