maps managment advancement

This commit is contained in:
MathieuG-P
2022-11-07 17:09:19 +01:00
parent 5e2dd10035
commit cd8dae5976
20 changed files with 515 additions and 29 deletions
@@ -18,7 +18,6 @@ export class LocalMapsManagerService {
private readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
private readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
private readonly WIP_LEVELS_FOLDER = "CustomWIPLevels";
private readonly localVersion: BSLocalVersionService;
private readonly installLocation: InstallationLocationService;
+5 -2
View File
@@ -19,14 +19,17 @@ export default function App() {
const location = useLocation();
pageState.setLocation(location);
useEffect(() => {
themeService.theme$.subscribe(() => {
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
document.documentElement.classList.remove('dark');
});
}, []);
useEffect(() => {
pageState.setLocation(location);
}, [location])
return (
@@ -0,0 +1,68 @@
import { MapsManagerService } from "renderer/services/maps-manager.service"
import { BSVersion } from "shared/bs-version.interface"
import VisibilitySensor from "react-visibility-sensor"
import { useEffect, useState } from "react"
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
import { Subscription } from "rxjs"
import { MapItem, MapItemProps } from "./map-item.component"
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model"
type Props = {
version: BSVersion,
className?: string
}
export function LocalMapsListPanel({version, className} : Props) {
const mapsManager = MapsManagerService.getInstance();
const [isVisible, setIsVisible] = useState(false);
const [maps, setMaps] = useState([] as BsmLocalMap[]);
useEffect(() => {
const subs: Subscription[] = [];
if(isVisible && !maps?.length){
subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => localMaps)));
}
return () => { subs.forEach(s => s.unsubscribe()); }
}, [isVisible])
const extractMapDiffs = (map: BsmLocalMap): Map<BsvMapCharacteristic, BsvMapDifficultyType[]> => {
const res = new Map<BsvMapCharacteristic, BsvMapDifficultyType[]>();
map.rawInfo._difficultyBeatmapSets.forEach(set => {
set._difficultyBeatmaps.forEach(diff => {
const arr = res.get(set._beatmapCharacteristicName) || [];
arr.push(diff._difficulty);
res.set(set._beatmapCharacteristicName, arr);
})
})
return res;
}
const renderMapItem = (map: BsmLocalMap) => {
return <MapItem
key={map.hash}
hash={map.hash}
title={[map.rawInfo._songAuthorName, map.rawInfo._songName].join(" - ")}
coverUrl={map.coverUrl}
songUrl={map.songUrl}
autor={map.rawInfo._levelAuthorName}
bpm={map.rawInfo._beatsPerMinute}
duration={null}
diffs={extractMapDiffs(map)} mapId={null} qualified={null} ranked={null} autorLink={null}
/>;
}
return (
<VisibilitySensor onChange={setIsVisible}>
<ul className={className}>
{maps.map(map => renderMapItem(map))}
</ul>
</VisibilitySensor>
)
}
@@ -0,0 +1,84 @@
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
import { BsmImage } from "../shared/bsm-image.component";
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { BsmLink } from "../shared/bsm-link.component";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "../shared/bsm-button.component";
export type MapItemProps = {
hash: string,
title: string,
autor: string,
coverUrl: string,
songUrl: string,
autorLink: string,
mapId: string,
diffs: Map<BsvMapCharacteristic, BsvMapDifficultyType[]>,
qualified: boolean,
ranked: boolean,
bpm: number,
duration: number,
onDelete?: (hash: string) => void,
onDownload?: (id: string) => void,
}
export function MapItem({hash, title, autor, coverUrl, songUrl, autorLink, mapId, diffs, qualified, ranked, onDelete, onDownload}: MapItemProps) {
const color = useThemeColor("first-color");
const zipUrl = `https://r2cdn.beatsaver.com/${hash}.zip`
const previewUrl = `https://skystudioapps.com/bs-viewer/?url=${zipUrl}`
console.log(diffs)
const renderAutor = () => {
if(!autor){ null; }
if(autorLink){
return <BsmLink className="text-sm mb-1" href={autorLink} style={{color}}>{autor}</BsmLink>
}
return <span className="text-sm mb-1 text-gray-500">{autor}</span>
}
const renderDiff = (charac: BsvMapCharacteristic, diff: BsvMapDifficultyType) => {
const colorPill = diffColors[diff] ?? "";
return (
<li className="h-5 flex justify-center items-center gap-1 rounded-full px-2" style={{backgroundColor: colorPill}}>
<BsmIcon className="h-4 w-4" icon="bsMapDifficulty"/>
<span className="text-xs mb-[1.5px]">{diff === "ExpertPlus" ? "Expert+" : diff}</span>
</li>
)
}
return (
<li className="bg-main-color-1 rounded-md flex flex-row h-[120px] min-w-[400px] shrink overflow-hidden grow text-white basis-0">
<BsmImage className="" image={coverUrl}/>
<div className="h-full flex flex-col grow px-3 min-w-0 shrink">
<h1 className={`font-bold overflow-hidden text-ellipsis whitespace-nowrap ${mapId && "cursor-pointer hover:underline"}`} style={{textDecorationColor: color}}>{title}</h1>
{renderAutor()}
<ol className="flex pb-1 gap-2 scrollbar scrollbar-width-[4px] scrollbar-thumb-black scrollbar-track-transparent" style={{scrollbarGutter: "stable", }}>
{Array.from(diffs.entries()).map(([charac, diffs]) => diffs.map(diff => renderDiff(charac, diff)))}
</ol>
</div>
<div className="flex flex-col shrink-0 px-2 pt-1 gap-1">
<BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="trash" title="allo" withBar={false}/>
<BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="twitch" title="allo" withBar={false}/>
<BsmButton className="w-6 h-6 pr-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="play" title="allo" withBar={false}/>
<BsmLink href={previewUrl} internal>
<BsmButton className="w-6 h-6 px-[2px] rounded-md !bg-inherit hover:backdrop-brightness-75" iconColor={color} icon="eye" title="Preview" withBar={false}/>
</BsmLink>
</div>
</li>
)
}
const diffColors: Record<BsvMapDifficultyType, string> = {
Easy: "#008055",
Normal: "#1268a1",
Hard: "#bd5500",
Expert: "#b52a1c",
ExpertPlus: "#7646af"
}
@@ -1,8 +1,7 @@
import { useState } from "react"
import { useObservable } from "renderer/hooks/use-observable.hook"
import { MapsManagerService } from "renderer/services/maps-manager.service"
import { BSVersion } from "shared/bs-version.interface"
import { TabNavBar } from "../shared/tab-nav-bar.component"
import { LocalMapsListPanel } from "./local-maps-list-panel.component"
type Props = {
oneBlock?: boolean,
@@ -10,24 +9,17 @@ type Props = {
}
export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
const mapsManager = MapsManagerService.getInstance();
const [tabIndex, setTabIndex] = useState(0);
const maps = useObservable(mapsManager.getMaps(version));
console.log(maps);
return (
<>
{!oneBlock && <TabNavBar className="mb-8 w-72" tabsText={["Maps", "Playlists"]} onTabChange={setTabIndex}/>}
<div className="w-full h-full bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden">
<div className="w-full h-full bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
{oneBlock && <TabNavBar className="!rounded-none shadow-sm" tabsText={["Maps", "Playlists"]} onTabChange={setTabIndex}/>}
<div className="w-full h-full min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
<div className="w-full h-full grow bg-red-300 shrink-0 overflow-y-scroll">
a
</div>
<div className="w-full h-full grow bg-green-300 shrink-0">b</div>
<LocalMapsListPanel className="h-full w-full grow shrink-0 flex flex-wrap justify-center content-start gap-2 p-3" version={version}/>
<div className="w-full h-full grow shrink-0">b</div>
</div>
</div>
</>
@@ -0,0 +1,9 @@
import { ModalComponent } from "renderer/services/modale.service"
export const IframeModal: ModalComponent<void, string> = ({resolver, data}) => {
return (
<div className="w-[calc(100vw-250px)] h-[calc(100vh-250px)]">
<iframe className="w-full h-full" src={data}></iframe>
</div>
)
}
@@ -25,9 +25,10 @@ type propsType = {
typeColor?:BsmButtonType,
color?: string,
title?: string,
iconColor?: string,
}
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title}: propsType) {
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor}: propsType) {
const t = useTranslation();
const secondColor = useThemeColor("second-color");
@@ -52,7 +53,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
<div onClick={onClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
{ image && <BsmImage image={image} className={imgClassName}/> }
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"}/> }
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{color: iconColor}}/> }
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
{ withBar && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
@@ -0,0 +1,24 @@
import { LinkOpenerService } from "renderer/services/link-opener.service"
import { ModalService } from "renderer/services/modale.service";
import { IframeModal } from "../modal/modal-types/iframe-modal.component";
type Props = {
className?: string,
href?: string,
internal?: boolean
children?: React.ReactNode,
style?: React.CSSProperties
}
export function BsmLink({className, href, children, style, internal}: Props) {
const linkOpener = LinkOpenerService.getInstance();
const openLink = () => {
linkOpener.open(href, internal);
}
return (
<a className={`${className} ${href && "cursor-pointer"}`} onClick={openLink} style={style}>{children}</a>
)
}
@@ -25,9 +25,12 @@ import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
import { GitHubIcon } from "./icons/github-icon.component";
import { CloseIcon } from "./icons/close-icon.component";
import { BsMapDifficultyIcon } from "./icons/bs-map-difficulty-icon.component";
import { TwitchIcon } from "./icons/twitch-icon.component";
import EyeIcon from "./icons/eye-icon.component";
import { PlayIcon } from "./icons/play-icon.component";
export type BsmIconType = (
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
@@ -62,6 +65,9 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
if(icon === "github"){ return <GitHubIcon className={className} style={style}/> }
if(icon === "close"){ return <CloseIcon className={className} style={style}/> }
if(icon === "bsMapDifficulty"){ return <BsMapDifficultyIcon className={className} style={style}/> }
if(icon === "twitch"){ return <TwitchIcon className={className} style={style}/> }
if(icon === "eye"){ return <EyeIcon className={className} style={style}/> }
if(icon === "play"){ return <PlayIcon className={className} style={style}/> }
return <TrashIcon className={className} style={style}/>
}
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export default function EyeIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" height="40" width="40" viewBox="0 0 40 40" fill="currentColor">
<path d="M20 26.375q3 0 5.104-2.104t2.104-5.104q0-3-2.104-5.104Q23 11.958 20 11.958t-5.104 2.105q-2.104 2.104-2.104 5.104t2.104 5.104Q17 26.375 20 26.375Zm0-2.792q-1.875 0-3.146-1.291-1.271-1.292-1.271-3.125 0-1.875 1.292-3.146T20 14.75q1.875 0 3.146 1.292 1.271 1.291 1.271 3.125 0 1.875-1.292 3.145-1.292 1.271-3.125 1.271Zm0 8.334q-5.833 0-10.625-3.146T2 20.417q-.167-.25-.229-.584-.063-.333-.063-.666 0-.334.063-.688.062-.354.229-.562 2.583-5.209 7.375-8.354Q14.167 6.417 20 6.417t10.625 3.146q4.792 3.145 7.375 8.354.167.208.229.562.063.354.063.688 0 .333-.063.666-.062.334-.229.584-2.583 5.208-7.375 8.354Q25.833 31.917 20 31.917Zm0-12.75Zm0 9.708q4.917 0 9.042-2.646t6.291-7.062q-2.166-4.417-6.291-7.063T20 9.458q-4.917 0-9.042 2.646t-6.333 7.063q2.208 4.416 6.333 7.062T20 28.875Z"/>
</svg>
)
}
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function PlayIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" height="40" width="40" viewBox="0 0 40 40" fill="currentColor">
<path d="M15.458 30.542q-.791.541-1.604.083-.812-.458-.812-1.417V10.625q0-.958.812-1.417.813-.458 1.604.084l14.625 9.291q.75.5.75 1.355 0 .854-.75 1.312Z"/>
</svg>
)
}
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function TwitchIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<path d="M5.7 0L1.4 10.985V55.88h15.284V64h8.597l8.12-8.12h12.418l16.716-16.716V0H5.7zm51.104 36.3L47.25 45.85H31.967l-8.12 8.12v-8.12H10.952V5.73h45.85V36.3zM47.25 16.716v16.716h-5.73V16.716h5.73zm-15.284 0v16.716h-5.73V16.716h5.73z" fill="currentColor" fillRule="evenodd"/>
</svg>
)
}
+10 -2
View File
@@ -1,10 +1,13 @@
import { IframeModal } from "renderer/components/modal/modal-types/iframe-modal.component";
import { IpcService } from "./ipc.service";
import { ModalService } from "./modale.service";
export class LinkOpenerService{
private static instance: LinkOpenerService;
private readonly ipcService: IpcService
private readonly ipcService: IpcService;
private readonly modals: ModalService;
public static getInstance(): LinkOpenerService{
if(!LinkOpenerService.instance){ LinkOpenerService.instance = new LinkOpenerService(); }
@@ -13,9 +16,14 @@ export class LinkOpenerService{
private constructor(){
this.ipcService = IpcService.getInstance();
this.modals = ModalService.getInsance();
}
public open(url: string): void{
public open(url: string, internal?: boolean): void{
if(internal){
this.modals.openModal(IframeModal, url);
return;
}
this.ipcService.sendLazy("new-window", {args: url});
}
@@ -19,6 +19,7 @@ export class MapsManagerService {
}
public getMaps(version?: BSVersion): Observable<BsmLocalMap[]>{
console.log("get maps")
return new Observable(obs => {
this.ipcService.send<BsmLocalMap[], BSVersion>("get-version-maps", {args: version}).then(res => {
if(!res.success){ return obs.next(null);}
+2 -2
View File
@@ -24,7 +24,7 @@ export class PageStateService {
}
public getRoute(): string{
return this.location$.value.pathname;
return this.location$.value?.pathname;
}
public get state$(): Observable<unknown>{
@@ -32,7 +32,7 @@ export class PageStateService {
}
public get route$(): Observable<string>{
return this.location$.pipe(map(location => location.pathname));
return this.location$.pipe(map(location => location?.pathname));
}
}
+6 -3
View File
@@ -115,10 +115,10 @@ export interface BsvMapTestplay {
export interface BsvMapDifficulty {
bombs: number,
characteristic: string,
characteristic: BsvMapCharacteristic,
chroma: boolean,
cinema: boolean,
difficulty: string,
difficulty: BsvMapDifficultyType,
events: number,
length: number,
maxScore: number,
@@ -138,4 +138,7 @@ export interface BsvMapParitySummary {
errors: number,
resets: number,
warns: number,
}
}
export type BsvMapCharacteristic = ("Standard" | "OneSaber" | "NoArrows" | "90Degree" | "360Degree" | "Lightshow" | "Lawless")
export type BsvMapDifficultyType = ("Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus")
+4 -2
View File
@@ -1,3 +1,5 @@
import { BsvMapCharacteristic, BsvMapDifficultyType } from "./beat-saver.model"
export interface RawMapInfoData<T = unknown> {
_version: string,
_songName: string,
@@ -19,12 +21,12 @@ export interface RawMapInfoData<T = unknown> {
}
export interface RawDifficultySet {
_beatmapCharacteristicName: string,
_beatmapCharacteristicName: BsvMapCharacteristic,
_difficultyBeatmaps: RawMapDifficulty[]
}
export interface RawMapDifficulty {
_difficulty: string,
_difficulty: BsvMapDifficultyType,
_difficultyRank: string,
_beatmapFilename: string,
_noteJumpMovementSpeed: number,