mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-136] Add default image + fix not get local platforms + Add temp icon in nav bar + some style adjustements
This commit is contained in:
@@ -127,12 +127,6 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
}
|
||||
}
|
||||
|
||||
export function extname(filePath: string, withDot = true){
|
||||
const ext = path.extname(filePath);
|
||||
if(withDot){ return ext; }
|
||||
return ext.split('.').at(-1);
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown>{
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -11,8 +11,8 @@ import path from "path";
|
||||
import { RequestService } from "../request.service";
|
||||
import { copyFileSync } from "fs-extra";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, ensureFolderExist, extname } from "../../helpers/fs.helpers";
|
||||
import { MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { Progression, ensureFolderExist } from "../../helpers/fs.helpers";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { readdir } from "fs/promises";
|
||||
@@ -118,7 +118,12 @@ export class LocalModelsManagerService {
|
||||
const modelsPath = await this.getModelFolderPath(type, version);
|
||||
const files = await readdir(modelsPath, {withFileTypes: true});
|
||||
|
||||
return files.filter(file => file.isFile() && extname(file.name, false) === type).map(file => path.join(modelsPath, file.name));
|
||||
//return files.filter(file => file.isFile() && path.extname(file.name) === MODEL_FILE_EXTENSIONS[type]).map(file => path.join(modelsPath, file.name));
|
||||
return files.reduce((acc, file) => {
|
||||
if(!file.isFile() || path.extname(file.name) !== MODEL_FILE_EXTENSIONS[type]){ return acc; }
|
||||
acc.push(path.join(modelsPath, file.name));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
public getModels(type: MSModelType, version?: BSVersion): Observable<Progression<BsmLocalModel[]>>{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { memo, useState } from "react";
|
||||
import { MSModel, MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlowEffect } from "../shared/glow-effect.component";
|
||||
@@ -9,11 +9,13 @@ import { BsmLink } from "../shared/bsm-link.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { followCursor } from "tippy.js"
|
||||
import defaultImage from '../../../../assets/images/default-version-img.jpg'
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
selected?: boolean,
|
||||
hash: string,
|
||||
onClick?: React.ComponentProps<"li">["onClick"],
|
||||
} & Partial<MSModel>
|
||||
|
||||
export const ModelItem = memo(({
|
||||
@@ -25,13 +27,14 @@ export const ModelItem = memo(({
|
||||
name,
|
||||
thumbnail,
|
||||
author,
|
||||
discord,
|
||||
discordid,
|
||||
tags
|
||||
tags,
|
||||
onClick
|
||||
}: Props) => {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [idContentCopied, setIdContentCopied] = useState<string>(null);
|
||||
|
||||
const modelPageUrl = (() => {
|
||||
if(!id){ return null; }
|
||||
@@ -47,22 +50,36 @@ export const ModelItem = memo(({
|
||||
return url.toString();
|
||||
})();
|
||||
|
||||
const copyContent = (value: string, id?: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setIdContentCopied(() => id);
|
||||
setTimeout(() => {
|
||||
setIdContentCopied(() => null);
|
||||
}, 700);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.li className={`relative w-52 h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)}>
|
||||
// TODO TRANSLATE ALL TEXT
|
||||
<motion.li className={`relative w-52 h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={onClick}>
|
||||
<GlowEffect visible={selected || hovered}/>
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none">
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full" image={thumbnail} loading="lazy"/>
|
||||
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full p-2 flex flex-col gap-1.5 bg-main-color-2 z-[1] bg-opacity-60 backdrop-blur-md transition-all hover:top-0">
|
||||
<BsmLink className={`block w-full max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{name}</BsmLink>
|
||||
<BsmLink className={`block w-full max-w-full overflow-hidden whitespace-nowrap text-ellipsis brightness-200 ${authorPageUrl ? "cursor-pointer hover:underline" : ""}`} style={{color}} href={authorPageUrl}>{author ?? ''}</BsmLink>
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full object-cover" image={thumbnail} placeholder={defaultImage} loading="lazy"/>
|
||||
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full p-2 flex flex-col gap-1.5 bg-main-color-3 bg-opacity-60 backdrop-blur-md transition-all hover:top-0" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
|
||||
<BsmLink className={`block w-fit max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{name}</BsmLink>
|
||||
<BsmLink className={`block w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis brightness-200 ${authorPageUrl ? "cursor-pointer hover:underline" : ""}`} style={{color}} href={authorPageUrl}>{author ?? ''}</BsmLink>
|
||||
<ul className="flex flex-row flex-wrap gap-1">
|
||||
{tags?.map(tag => (
|
||||
<li key={tag} className="inline-block mr-1 text-xs bg-white text-black px-1 rounded-full p-0.5">{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Tippy placement="top" content="Copy hash" followCursor="horizontal" plugins={[followCursor]}>
|
||||
<div className="cursor-copy block w-full max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-2 rounded-md p-1 uppercase text-sm pointer-events-auto">{hash}</div>
|
||||
<Tippy placement="top" content={idContentCopied === "hash" ? "Copied!" : "Copy hash"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="mt-0.5 cursor-copy block w-full max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${hash}`, 'hash')}>{hash}</span>
|
||||
</Tippy>
|
||||
{id && (
|
||||
<Tippy placement="top" content={idContentCopied === "id" ? "Copied!" : "Copy id"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="cursor-copy w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${id}`, 'id')}>{id}</span>
|
||||
</Tippy>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.li>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ModelsGrid({className, version, type}: Props) {
|
||||
{isLoading && <>loading</>}
|
||||
{!hasModels && <>no models</>}
|
||||
{hasModels && !isLoading && (
|
||||
<ul className="flex w-full h-full overflow-scroll p-4 gap-4">
|
||||
<ul className="flex flex-wrap justify-center w-full h-full overflow-scroll p-4 gap-4">
|
||||
{models.extra.map(localModel => (
|
||||
<ModelItem
|
||||
key={localModel.model?.hash ?? localModel.hash}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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"
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
@@ -24,8 +25,8 @@ export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Pr
|
||||
})
|
||||
|
||||
return (
|
||||
<BsContentNavBar className={`!rounded-none shadow-sm ${className ?? ""}`} tabIndex={tabIndex} onTabChange={onTabChange} tabs={tabs} renderTab={(props, tab) => (
|
||||
<ModelTab version={version} modelType={tab.extra} {...props}/>
|
||||
<BsContentNavBar className={`!rounded-none shadow-sm ${className ?? ""}`} tabIndex={tabIndex} onTabChange={onTabChange} tabs={tabs} renderTab={(props, tab, activeTab) => (
|
||||
<ModelTab version={version} modelType={tab.extra} {...props} active={tab === activeTab}/>
|
||||
)}/>
|
||||
)
|
||||
}
|
||||
@@ -33,11 +34,12 @@ export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Pr
|
||||
type TabProps = {
|
||||
version?: BSVersion,
|
||||
modelType: MSModelType,
|
||||
active?: boolean,
|
||||
onLink?: (type: MSModelType) => void,
|
||||
onUnlink?: (type: MSModelType) => void,
|
||||
} & DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>;
|
||||
} & React.ComponentProps<"li">;
|
||||
|
||||
function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
|
||||
function ModelTab({version, modelType, active, onClick, onLink, onUnlink}: TabProps){
|
||||
|
||||
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
|
||||
|
||||
@@ -45,7 +47,7 @@ function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
|
||||
const [linkBtnDisabled, setLinkBtnDisabled] = useState(false);
|
||||
|
||||
useOnUpdate(() => {
|
||||
if(!version){ return; }
|
||||
if(!version){ return null; }
|
||||
|
||||
const sub = modelsManager.$modelsLinkingPending(version, modelType).subscribe(async (pending) => {
|
||||
if(pending){ return setLinkBtnDisabled(() => pending); }
|
||||
@@ -69,24 +71,27 @@ function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="relative w-full text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center px-6" onClick={onClick}>
|
||||
<span className="text-main-color-1 dark:text-gray-200 font-thin italic text-xs">{modelType}</span>
|
||||
<div className="flex items-center absolute top-0 left-0">
|
||||
{!!version && (
|
||||
<LinkButton
|
||||
variants={{ hover: {rotate: 22.5}, tap: {rotate: 45} }}
|
||||
disabled={linkBtnDisabled}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{rotate: 0}}
|
||||
className="block w-6 h-6 aspect-square blur-0 hover:brightness-75"
|
||||
linked={modelsAreLinked}
|
||||
title={"aaa"}
|
||||
onClick={onClickLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li className={`relative w-full text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center px-6 ${active ? "backdrop-brightness-75" : ""}`} onClick={onClick}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<BsmIcon icon="trash"/>
|
||||
<span className="text-main-color-1 dark:text-gray-200 font-thin italic text-xs">{modelType}</span>
|
||||
</div>
|
||||
<div className="flex items-center absolute top-1 left-1">
|
||||
{!!version && (
|
||||
<LinkButton
|
||||
variants={{ hover: {rotate: 22.5}, tap: {rotate: 45} }}
|
||||
disabled={linkBtnDisabled}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{rotate: 0}}
|
||||
className="block w-6 h-6 aspect-square blur-0 hover:brightness-75"
|
||||
linked={modelsAreLinked}
|
||||
title={"aaa"} //TODO ADD TRANSLATION
|
||||
onClick={onClickLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DetailedHTMLProps, Fragment } from "react";
|
||||
import { BsmIconType } from "../svgs/bsm-icon.component";
|
||||
|
||||
type Props<T> = {
|
||||
className?: string;
|
||||
@@ -10,6 +11,7 @@ type Props<T> = {
|
||||
|
||||
export type BsContentNavBarTab<T = unknown> = {
|
||||
text: string;
|
||||
icon?: BsmIconType;
|
||||
extra?: T
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useSwitchableObservable<T>(observable?: Observable<T>, clearOnSwitch = true): [T, React.Dispatch<React.SetStateAction<Observable<T>>>]{
|
||||
export function useSwitchableObservable<T>(observable?: Observable<T>, clearOnSwitch = true): [T, React.Dispatch<React.SetStateAction<Observable<T>>>, Observable<T>]{
|
||||
|
||||
const [currentObs, setCurrentObs] = useState(observable);
|
||||
const [obsValue, setObsValue] = useState<T>();
|
||||
@@ -15,5 +15,5 @@ export function useSwitchableObservable<T>(observable?: Observable<T>, clearOnSw
|
||||
}
|
||||
}, [currentObs])
|
||||
|
||||
return [obsValue, setCurrentObs];
|
||||
return [obsValue, setCurrentObs, currentObs];
|
||||
}
|
||||
@@ -14,4 +14,11 @@ export const MODELS_TYPE_MS_PAGE_ROOT = {
|
||||
[MSModelType.Bloq]: "Bloqs",
|
||||
[MSModelType.Platfrom]: "Platforms",
|
||||
[MSModelType.Saber]: "Sabers"
|
||||
}
|
||||
|
||||
export const MODEL_FILE_EXTENSIONS = {
|
||||
[MSModelType.Avatar]: ".avatar",
|
||||
[MSModelType.Bloq]: ".bloq",
|
||||
[MSModelType.Platfrom]: ".plat",
|
||||
[MSModelType.Saber]: ".saber"
|
||||
}
|
||||
Reference in New Issue
Block a user