[feature-107] Advancement on playlist creation

This commit is contained in:
MathieuG-P
2024-06-18 21:03:13 +02:00
parent 5e1a021c3a
commit d299553d1e
10 changed files with 215 additions and 28 deletions
+13
View File
@@ -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;
})));
});
@@ -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
@@ -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;
@@ -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>
)
}
@@ -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); }
+1
View File
@@ -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 };