[feature-136] fix some bugs + prepare models download + use color libs for font contrast

This commit is contained in:
MathieuG-P
2023-05-14 12:55:37 +02:00
parent 86efb9293b
commit eb9775c89f
20 changed files with 237 additions and 95 deletions
+1
View File
@@ -1,6 +1,7 @@
{
"misc": {
"download": "Laden",
"add": "Hinzufügen",
"verify": "Prüfen",
"launch": "Starten",
"mods": "Mods",
+1
View File
@@ -1,6 +1,7 @@
{
"misc": {
"download": "Download",
"add": "Add",
"verify": "Verify",
"launch": "Launch",
"mods": "Mods",
+1
View File
@@ -1,6 +1,7 @@
{
"misc": {
"download": "Descargar",
"add": "Añadir",
"verify": "Verificar",
"launch": "Lanzar",
"mods": "Mods",
+1
View File
@@ -1,6 +1,7 @@
{
"misc": {
"download": "Télécharger",
"add": "Ajouter",
"verify": "Vérifier",
"launch": "Lancer",
"mods": "Mods",
+46 -12
View File
@@ -10,7 +10,9 @@
"dependencies": {
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"@types/color": "^3.0.3",
"archiver": "^5.3.1",
"color": "^4.2.3",
"dateformat": "^5.0.3",
"dot-prop": "^8.0.0",
"electron-debug": "^3.2.0",
@@ -2273,6 +2275,27 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
},
"node_modules/@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
"dependencies": {
"@types/color-convert": "*"
}
},
"node_modules/@types/color-convert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
"dependencies": {
"@types/color-name": "*"
}
},
"node_modules/@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -4922,7 +4945,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
@@ -4935,7 +4957,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -4952,7 +4973,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
@@ -16267,7 +16287,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dev": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
@@ -16275,8 +16294,7 @@
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"dev": true
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/simple-update-notifier": {
"version": "1.0.7",
@@ -20432,6 +20450,27 @@
}
}
},
"@types/color": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
"requires": {
"@types/color-convert": "*"
}
},
"@types/color-convert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
"requires": {
"@types/color-name": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -22549,7 +22588,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
@@ -22559,7 +22597,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -22573,7 +22610,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
@@ -31032,7 +31068,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dev": true,
"requires": {
"is-arrayish": "^0.3.1"
},
@@ -31040,8 +31075,7 @@
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"dev": true
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
}
}
},
+2
View File
@@ -229,7 +229,9 @@
"dependencies": {
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"@types/color": "^3.0.3",
"archiver": "^5.3.1",
"color": "^4.2.3",
"dateformat": "^5.0.3",
"dot-prop": "^8.0.0",
"electron-debug": "^3.2.0",
+1 -1
View File
@@ -75,7 +75,7 @@ ipc.on<{version: BSVersion, type: MSModelType}>("get-version-models", async (req
ipc.on<{version: BSVersion, models: BsmLocalModel[], outPath: string}>("export-models", async (req, reply) => {
const models = LocalModelsManagerService.getInstance();
reply(await models.exportModels(req.args.outPath, req.args.version, req.args.models));
reply(models.exportModels(req.args.outPath, req.args.version, req.args.models));
});
ipc.on<BsmLocalModel[]>("delete-models", async (req, reply) => {
@@ -167,19 +167,30 @@ export class LocalModelsManagerService {
}
public async exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Promise<Observable<ArchiveProgress>>{
public exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Observable<ArchiveProgress>{
// TOTO NOT ASYNC
const archive = new Archive(output);
if(models?.length){
models.forEach(model => archive.addFile(model.path, path.join(MODEL_TYPE_FOLDERS[model.type], path.basename(model.path))));
}
else{
for(const type of MODEL_TYPES){
archive.addDirectory(await this.getModelFolderPath(type, version));
}
}
return archive.finalize();
return new Observable<ArchiveProgress>(subscriber => {
const archive = new Archive(output);
(async () => {
if(models?.length){
models.forEach(model => archive.addFile(model.path, path.join(MODEL_TYPE_FOLDERS[model.type], path.basename(model.path))));
}
else{
for(const type of MODEL_TYPES){
archive.addDirectory(await this.getModelFolderPath(type, version));
}
}
})().catch(e => subscriber.error(e)).then(() => (
archive.finalize().subscribe(subscriber)
));
});
}
public deleteModels(models: BsmLocalModel[]): Observable<Progression>{
@@ -108,7 +108,7 @@ export function MapsPlaylistsPanel({version}: Props) {
<motion.div className="h-full p-0.5" variants={variants}>
<BsmIcon className="block h-full aspect-square brightness-150" icon="add"/>
</motion.div>
<span className="text-sm brightness-150">{t("pages.version-viewer.maps.tabs.maps.actions.add-maps.text")}</span>
<span className="text-sm brightness-150">{t("misc.add")}</span>
</motion.div>
)}
{(!!version) && (
@@ -11,47 +11,60 @@ import Tippy from "@tippyjs/react";
import { followCursor } from "tippy.js"
import defaultImage from '../../../../assets/images/default-version-img.jpg'
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
import { BsmIconType } from "../svgs/bsm-icon.component";
import { Observable, of } from "rxjs";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BsmButton } from "../shared/bsm-button.component";
type Props = {
className?: string,
selected?: boolean,
hash: string,
onClick?: React.ComponentProps<"li">["onClick"],
} & Partial<MSModel> & Partial<BsmLocalModel>
id: number,
isDownloading?: Observable<boolean>
onDelete?: () => void,
onDownload?: () => void,
onCancelDownload?: () => void
} & Partial<MSModel> & Partial<BsmLocalModel> & Omit<React.ComponentProps<"li">, "id">
export const ModelItem = memo(({
className,
selected,
path,
hash,
id,
type,
name,
thumbnail,
author,
discordid,
tags,
onClick
}: Props) => {
export const ModelItem = memo((props: Props) => {
const color = useThemeColor("first-color");
const [hovered, setHovered] = useState(false);
const [idContentCopied, setIdContentCopied] = useState<string>(null);
const isDownloading = useObservable(props.isDownloading ?? of(false), false);
const modelPageUrl = (() => {
if(!id){ return null; }
const url = new URL(MODELS_TYPE_MS_PAGE_ROOT[type], MODEL_SABER_URL);
url.searchParams.set("id", id.toString());
if(!props.id){ return null; }
const url = new URL(MODELS_TYPE_MS_PAGE_ROOT[props.type], MODEL_SABER_URL);
url.searchParams.set("id", props.id.toString());
return url.toString();
})();
const authorPageUrl = (() => {
if(!discordid){ return null; }
if(!props.discordid){ return null; }
const url = new URL("Profile", MODEL_SABER_URL);
url.searchParams.set("user", discordid.toString());
url.searchParams.set("user", props.discord.toString());
return url.toString();
})();
const actionButtons = (): {text: string, icon: BsmIconType, action: () => void}[] => {
const buttons: {text: string, icon: BsmIconType, action: () => void}[] = [];
if(props.onDownload){
buttons.push({ text: "Download", icon: "download", action: props.onDownload }); // TODO TRANSLATE
}
if(props.onDelete){
buttons.push({ text: "Delete", icon: "trash", action: props.onDelete }); // TODO TRANSLATE
}
if(props.onCancelDownload){
buttons.push({ text: "Cancel download", icon: "cross", action: props.onCancelDownload }); // TODO TRANSLATE
}
return buttons;
}
const copyContent = (value: string, id?: string) => {
navigator.clipboard.writeText(value);
setIdContentCopied(() => id);
@@ -62,30 +75,45 @@ export const ModelItem = memo(({
return (
// TODO TRANSLATE ALL TEXT
<motion.li className={`relative flex-grow min-w-[13rem] h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={onClick}>
<GlowEffect visible={selected || hovered}/>
<motion.li className={`relative flex-grow min-w-[13rem] h-52 cursor-pointer ${props.className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={props.onClick}>
<GlowEffect visible={props.selected || hovered}/>
<div>
{!isDownloading ? (
<div>
{actionButtons().map((button, index) => (
<Tippy key={index} placement="top" content={button.text} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
<BsmButton className="absolute top-0 right-0 w-6 h-6 p-1 bg-main-color-3 bg-opacity-60 backdrop-blur-md rounded-md text-white" onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}}/>
</Tippy>
))}
</div>
) : (
<div>
</div>
)}
</div>
<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 object-cover ${type === MSModelType.Avatar ? "object-top" : ""}`} image={thumbnail} placeholder={defaultImage} loading="lazy"/>
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover ${props.type === MSModelType.Avatar ? "object-top" : ""}`} image={props.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>
<BsmLink className={`block w-fit max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${props.id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{props.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}>{props.author ?? ''}</BsmLink>
<ul className="flex flex-row flex-wrap gap-1">
{tags?.map(tag => (
{props.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={idContentCopied === "hash" ? "Copied!" : "Copy hash"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
<span className="mt-0.5 cursor-copy block w-full max-w-fit overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${hash}`, 'hash')}>{hash}</span>
<span className="mt-0.5 cursor-copy block w-full max-w-fit overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${props.hash}`, 'hash')}>{props.hash}</span>
</Tippy>
<div className="flex gap-1">
{id && (
{props.id && (
<Tippy placement="top" content={idContentCopied === "id" ? "Copied!" : "Copy id"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
<span className="cursor-copy w-fit shrink-0 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>
<span className="cursor-copy w-fit shrink-0 max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${props.id}`, 'id')}>{props.id}</span>
</Tippy>
)}
{path && (
{props.path && (
<Tippy placement="top" content={idContentCopied === "path" ? "Copied!" : "Copy path"} 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 text-xs" onClick={() => copyContent(`${path}`, 'path')}>{path}</span>
<span className="cursor-copy w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 text-xs" onClick={() => copyContent(`${props.path}`, 'path')}>{props.path}</span>
</Tippy>
)}
</div>
@@ -52,7 +52,7 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active}
loadModels();
},
deleteSelectedModels: () => {
modelsManager.deleteModels(modelsSelected, version).then(deleted => {
modelsManager.deleteModels(!modelsSelected?.length ? models : modelsSelected, version).then(deleted => {
if(!deleted){ return; }
const newModels = models.filter(m => !modelsSelected.some(d => d.hash === m.hash));
setModels(() => newModels);
@@ -83,6 +83,7 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active}
const loadModels = () => {
const modelsObs$ = modelsManager.$getModels(type, version);
setModels(() => null);
setModelsLoadObservable(() => modelsObs$.pipe(map(models => models?.extra), distinctUntilChanged()));
setProgress$(modelsObs$.pipe(map(models => Math.floor((models.current / models.total) * 100)), startWith(0), distinctUntilChanged()))
}
@@ -7,35 +7,23 @@ import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.c
import { useConstant } from "renderer/hooks/use-constant.hook";
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
import { BsmButton } from "../shared/bsm-button.component";
export function ModelsPanel({version}: {version?: BSVersion}) {
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
const [avatarsRef, sabersRef, platformsRef, bloqsRef] = [useRef(null), useRef(null), useRef(null), useRef(null)];
const [modelTypeTab, setModelTypeTab] = useState<ModelTabType>(ModelTabType.Avatars);
const [search, setSearch] = useState<string>("");
const getSelectedModels = (ref?: MutableRefObject<any>) => {
if(ref){ return (ref.current?.getSelectedModels() as BsmLocalModel[]); }
return [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat()
};
const getAllModels = (ref?: MutableRefObject<any>) => {
if(ref){ return (ref.current?.getModels() as BsmLocalModel[]); }
return [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getModels() as BsmLocalModel[])).flat();
}
const reloadModels = (ref?: MutableRefObject<any>) => {
if(ref){ return (ref.current?.reloadModels()); }
return [avatarsRef, sabersRef, platformsRef, bloqsRef].forEach(ref => ref.current?.reloadModels())
};
const getActiveTabRef = (activeTab: ModelTabType) => [avatarsRef, sabersRef, platformsRef, bloqsRef][modelTypeTab];
const getActiveTabRef = () => [avatarsRef, sabersRef, platformsRef, bloqsRef][modelTypeTab];
const exportModels = () => {
modelsManager.exportModels(getSelectedModels(), version);
const selectedModels = [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat()
modelsManager.exportModels(selectedModels, version);
}
const deleteModels = () => {
const activeTab = getActiveTabRef(modelTypeTab);
const activeTab = getActiveTabRef();
activeTab.current?.deleteSelectedModels();
}
@@ -47,6 +35,7 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 mb-3 text-main-color-1 dark:text-white">
<BsmButton className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold" icon="add" text="misc.add" typeColor="primary" withBar={false}/>
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={"TODO TRANSLATE"} onChange={e => setSearch(e.target.value)} tabIndex={-1}/>
</div>
@@ -36,11 +36,9 @@ type TabProps = {
version?: BSVersion,
modelType: MSModelType,
active?: boolean,
onLink?: (type: MSModelType) => void,
onUnlink?: (type: MSModelType) => void,
} & React.ComponentProps<"li">;
function ModelTab({version, modelType, active, onClick, onLink, onUnlink}: TabProps){
function ModelTab({version, modelType, active, onClick}: TabProps){
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
@@ -62,8 +60,8 @@ function ModelTab({version, modelType, active, onClick, onLink, onUnlink}: TabPr
}
}, [version]);
const linkModels = () => modelsManager.linkModels(modelType, version).then(() => onLink?.(modelType));
const unlinkModels = () => modelsManager.unlinkModels(modelType, version).then(() => onUnlink?.(modelType));
const linkModels = () => modelsManager.linkModels(modelType, version)
const unlinkModels = () => modelsManager.unlinkModels(modelType, version)
const onClickLink = () => {
if(!version){ return Promise.resolve(); }
@@ -11,7 +11,7 @@ export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
const [hovered, setHovered] = useState(false);
const checkedColor = useThemeColor("first-color");
const iconColor = getCorrectTextColor(checkedColor);
const iconColor = checkedColor && getCorrectTextColor(checkedColor);
const handleClick = () => {
if(disabled){ return; }
@@ -17,7 +17,7 @@ type Props = {
export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1, colors} : Props) {
const color = useThemeColor(colorType);
const labelTextColor = getCorrectTextColor(color);
const labelTextColor = color && getCorrectTextColor(color);
return (
<Range
+5 -8
View File
@@ -1,15 +1,12 @@
import Color from "color";
/**
* Return text color for best constrast with background color
* @param {string} bgHex
* @returns {string}
*/
export function getCorrectTextColor(bgHex: string): string{
const hex = bgHex.replaceAll("#", "");
const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
const c = uicolors.map(col => {
if (col <= 0.03928) { return col / 12.92; }
return (col + 0.055) / 1.055 ** 2.4;
});
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
return (L > 0.128) ? "#000" : "fff";
if(!bgHex){ return "#000"; }
const color = Color(bgHex);
return color.isLight() ? "#000" : "fff";
}
@@ -1,4 +1,4 @@
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, Observable, shareReplay } from "rxjs";
import { DefaultConfigKey, defaultConfiguration } from "renderer/config/default-configuration.config";
export class ConfigurationService {
@@ -40,9 +40,9 @@ export class ConfigurationService {
this.emitChange(key);
}
public watch<T>(key: DefaultConfigKey | string): BehaviorSubject<T>{
public watch<T>(key: DefaultConfigKey | string): Observable<T>{
if(this.observers.has(key)){ return this.observers.get(key); }
this.observers.set(key, new BehaviorSubject(this.get(key)));
return this.observers.get(key);
return this.observers.get(key).asObservable();
}
}
@@ -51,7 +51,7 @@ export class MapsDownloaderService {
while(this.mapsQueue$.value.at(0)){
const toDownload = this.mapsQueue$.value.at(0);
this.currentDownload$.next(toDownload);
const downloaded = await this.downloadMap(toDownload.map, toDownload.version).toPromise();
const downloaded = await this.downloadMap(toDownload.map, toDownload.version)?.toPromise();
if(downloaded){
this.downloadedListerners.forEach(func => func(downloaded, toDownload.version));
@@ -0,0 +1,76 @@
import { BehaviorSubject, Observable, Subscription, distinctUntilChanged, filter, map, shareReplay } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
import { MSModel, MSModelType } from "shared/models/models/model-saber.model";
import { ModalResponse, ModalService } from "../modale.service";
import { IpcService } from "../ipc.service";
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
export class ModelsDownloaderService {
private static instance: ModelsDownloaderService;
public static getInstance(): ModelsDownloaderService{
if(!ModelsDownloaderService.instance){ ModelsDownloaderService.instance = new ModelsDownloaderService(); }
return ModelsDownloaderService.instance;
}
private readonly ipc: IpcService;
private readonly modal: ModalService
private readonly lastDownloadedModel$ = new BehaviorSubject<ModelDownload>(null);
private readonly queue$ = new BehaviorSubject<ModelDownload[]>([]);
private readonly currentDownload$ = this.queue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(), shareReplay(1));
private constructor(){
this.ipc = IpcService.getInstance();
this.modal = ModalService.getInsance();
this.currentDownload$.pipe(filter(v => !!v)).subscribe(model => this.downloadModel(model));
}
private downloadModel(model: ModelDownload){
// TODO: Download model
}
public addModelToDownload(model: ModelDownload): ModelDownload{
const queue = this.queue$.value;
queue.push(model);
this.queue$.next(queue);
return model;
}
public removeFromDownloadQueue(model: ModelDownload){
const queue = this.queue$.value;
const index = queue.findIndex(m => m.hash === model.hash);
if(index === -1){ return; }
queue.splice(index, 1);
this.queue$.next(queue);
}
public onModelsDownloaded$(cb: (model: ModelDownload) => void): Subscription{
return this.lastDownloadedModel$.pipe(filter(v => !!v)).subscribe(cb);
}
public isDownloading$(model: ModelDownload): Observable<boolean>{
return this.currentDownload$.pipe(map(current => {
if(!current){ return false; }
return current.hash === model.hash && current.version === model.version
}), distinctUntilChanged());
}
public isPending$(model: ModelDownload): Observable<boolean>{
return this.queue$.pipe(map(queue => queue.at(0).hash !== model.hash && queue.at(0).version !== model.version && queue.some(m => m.hash === model.hash && m.version === model.version)), distinctUntilChanged());
}
public openDownloadModelsModal(version: BSVersion, type?: MSModelType): Promise<ModalResponse<void>>{
return this.modal.openModal(DownloadMapsModal, {version: null, ownedMaps: []}); // TODO MODELS
}
}
export type ModelDownload = Partial<MSModel> & Pick<MSModel, "hash"> & {version: BSVersion};
export type ModelDownloaded = BsmLocalModel & {version: BSVersion};
@@ -131,6 +131,8 @@ export class ModelsManagerService {
public async deleteModels(models: BsmLocalModel[], version?: BSVersion): Promise<boolean>{
if(!models){ return Promise.resolve(false); }
const askModal = models.length > 1 || !this.config.get<boolean>(ModelsManagerService.REMEMBER_CHOICE_DELETE_MODEL_KEY);
if(askModal){