advancement on oneClick model AND lazy loading react windows

This commit is contained in:
MathieuG-P
2022-12-24 18:19:59 +01:00
parent 6c5190eacb
commit 2aed523480
19 changed files with 440 additions and 47 deletions
@@ -168,6 +168,7 @@ const configuration: webpack.Configuration = {
new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
],
@@ -150,6 +150,7 @@ const configuration: webpack.Configuration = {
new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
],
};
+103 -5
View File
@@ -2202,6 +2202,17 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/plist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz",
"integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==",
"dev": true,
"optional": true,
"requires": {
"@types/node": "*",
"xmlbuilder": ">=11.0.1"
}
},
"@types/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
@@ -2238,12 +2249,12 @@
}
},
"@types/react-dom": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz",
"integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==",
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
"dev": true,
"requires": {
"@types/react": "^17"
"@types/react": "*"
}
},
"@types/react-outside-click-handler": {
@@ -2363,6 +2374,13 @@
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/verror": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz",
"integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==",
"dev": true,
"optional": true
},
"@types/webpack-bundle-analyzer": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.6.0.tgz",
@@ -3168,6 +3186,13 @@
}
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true,
"optional": true
},
"ast-metadata-inferer": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.7.0.tgz",
@@ -4257,6 +4282,16 @@
"yaml": "^1.10.0"
}
},
"crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"dev": true,
"optional": true,
"requires": {
"buffer": "^5.1.0"
}
},
"crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -4850,8 +4885,36 @@
"dev": true,
"optional": true,
"requires": {
"@types/plist": "^3.0.1",
"@types/verror": "^1.10.3",
"ajv": "^6.10.0",
"crc": "^3.8.0",
"iconv-corefoundation": "^1.1.7",
"plist": "^3.0.4",
"smart-buffer": "^4.0.2"
"smart-buffer": "^4.0.2",
"verror": "^1.10.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"optional": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"optional": true
}
}
},
"dns-equal": {
@@ -6581,6 +6644,13 @@
"yauzl": "^2.10.0"
}
},
"extsprintf": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
"dev": true,
"optional": true
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -7485,6 +7555,13 @@
"ms": "^2.0.0"
}
},
"iconv-corefoundation": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -13577,6 +13654,27 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true
},
"verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true,
"optional": true,
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
},
"dependencies": {
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"optional": true
}
}
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+1 -1
View File
@@ -153,7 +153,7 @@
"@types/jest": "^27.5.2",
"@types/node": "17.0.23",
"@types/react": "^17.0.48",
"@types/react-dom": "^17.0.17",
"@types/react-dom": "^18.0.10",
"@types/react-outside-click-handler": "^1.3.1",
"@types/react-test-renderer": "^17.0.2",
"@types/terser-webpack-plugin": "^5.0.4",
+2 -1
View File
@@ -9,4 +9,5 @@ import './window-manager-ipcs';
import './bs-mods-ipcs';
import './bs-maps-ipcs';
import './beat-saver-ipcs';
import './bs-playlist-ipcs';
import './bs-playlist-ipcs';
import './model-saber.ipcs';
+15
View File
@@ -0,0 +1,15 @@
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { ModelSaberService } from "../services/thrid-party/model-saber/model-saber.service";
import { UtilsService } from "../services/utils.service";
ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest<string|number>) => {
const utils = UtilsService.getInstance();
const ms = ModelSaberService.getInstance();
ms.getModelById(request.args).then(model => {
utils.ipcSend(request.responceChannel, {success: true, data: model});
}).catch(e => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
})
});
+4 -1
View File
@@ -18,6 +18,8 @@ import { DeepLinkService } from './services/deep-link.service';
import { AppWindow } from 'shared/models/window-manager/app-window.model';
import { LocalMapsManagerService } from './services/additional-content/local-maps-manager.service';
import { LocalPlaylistsManagerService } from './services/additional-content/local-playlists-manager.service';
import { ModelSaberService } from './services/thrid-party/model-saber/model-saber.service';
import { LocalModelsManagerService } from './services/additional-content/local-models-manager.service';
export const PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js')
@@ -59,6 +61,7 @@ const createWindow = async (window: AppWindow = "launcher.html") => {
const initServicesMustBeInitialized = () => {
LocalMapsManagerService.getInstance();
LocalPlaylistsManagerService.getInstance();
LocalModelsManagerService.getInstance();
// Model
}
@@ -89,7 +92,7 @@ else{
app.whenReady().then(() => {
process.argv.push("bsplaylist://playlist/https://api.beatsaver.com/playlists/id/11199/download/beatsaver-11199.bplist"); // to force deep-link (oneClick map)
process.argv.push("modelsaber://saber/1670981586/r_Happy Crimbo!.saber"); // to force deep-link (oneClick map)
initServicesMustBeInitialized();
@@ -0,0 +1,54 @@
import { DeepLinkService } from "../deep-link.service";
import log from "electron-log"
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { UtilsService } from "../utils.service";
import { WindowManagerService } from "../window-manager.service";
export class LocalModelsManagerService {
private static instance: LocalModelsManagerService;
public static getInstance(): LocalModelsManagerService{
if(!LocalModelsManagerService.instance){ LocalModelsManagerService.instance = new LocalModelsManagerService(); }
return LocalModelsManagerService.instance;
}
private readonly DEEP_LINKS = {
ModelSaber: "modelsaber",
};
private readonly deepLink: DeepLinkService;
private readonly utils: UtilsService;
private readonly windows: WindowManagerService;
private constructor(){
this.deepLink = DeepLinkService.getInstance();
this.utils = UtilsService.getInstance();
this.windows = WindowManagerService.getInstance();
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, (link) => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link);
const url = new URL(link);
const type = url.host
const id = url.pathname.replace("/", '').split("/").at(0);
this.openOneClickDownloadModelWindow(id, type);
});
}
private openOneClickDownloadModelWindow(id: string, type: string){
// TODO make once
ipcMain.on("one-click-model-info", async (event, req: IpcRequest<void>) => {
this.utils.ipcSend(req.responceChannel, {success: true, data: {id, type}});
});
this.windows.openWindow("oneclick-download-model.html");
}
}
@@ -1,8 +1,7 @@
import { splitIntoChunk } from "../../../helpers/array-tools";
import { BsvMapDetail } from "shared/models/maps";
import { BsvPlaylist, BsvPlaylistPage, SearchParams } from "shared/models/maps/beat-saver.model";
import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model";
import { BeatSaverApiService } from "./beat-saver-api.service";
import { red } from "tailwindcss/colors";
export class BeatSaverService {
@@ -0,0 +1,70 @@
import fetch from "node-fetch";
import { ApiResult } from "renderer/models/api/api.model";
import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "shared/models/model-saber/model-saber.model";
export class ModelSaberApiService {
private static instance: ModelSaberApiService;
public static getInstance(): ModelSaberApiService{
if(!ModelSaberApiService.instance){ ModelSaberApiService.instance = new ModelSaberApiService(); }
return ModelSaberApiService.instance;
}
private readonly API_URL = "https://modelsaber.com/api/v2/";
private readonly ENDPOINTS = {get: "get.php", types: "types.php"};
private constructor(){}
private parseFilters(filters: MSGetQueryFilter[]): string{
if(!filters){ return null; }
const parsed = filters.map(filter => {
const stringFilter = filter.type === "searchName" ? filter.value : `${filter.type}:${filter.value}`;
return filter.isNegative ? `-${stringFilter}` : stringFilter;
});
return parsed.join(",");
}
private buildUrlQuery(query: MSGetQuery): URLSearchParams{
if(!query){ return new URLSearchParams(); }
const filterQuery = this.parseFilters(query.filter);
const searchParams = {
...(query.type && {type: query.type}),
...(query.platform && {platform: query.platform}),
...(query.start && {start: `${query.start}`}),
...(query.end && {end: `${query.end}`}),
...(query.sort && {sort: query.sort}),
...(query.sortDirection && {sortDirection: query.sortDirection}),
...(filterQuery && {filter: filterQuery}),
}
return new URLSearchParams(searchParams);
}
public async searchModel(query: MSGetQuery): Promise<ApiResult<MSGetResponse>>{
const url = new URL(this.ENDPOINTS.get, this.API_URL);
url.search = this.buildUrlQuery(query).toString();
const res = await fetch(url.toString());
if(!res.ok){
return {data: null, status: res.status};
}
const data = await res.json() as MSGetResponse;
return {data, status: res.status};
}
}
@@ -1,3 +1,6 @@
import { MSGetQuery, MSGetQueryFilter, MSModel } from "shared/models/model-saber/model-saber.model";
import { ModelSaberApiService } from "./model-saber-api.service";
export class ModelSaberService {
private static instance: ModelSaberService;
@@ -7,6 +10,36 @@ export class ModelSaberService {
return ModelSaberService.instance;
}
private constructor(){}
private readonly modelSaberApi: ModelSaberApiService;
private constructor(){
this.modelSaberApi = ModelSaberApiService.getInstance();
}
public async getModelById(id: number|string): Promise<MSModel>{
const query: MSGetQuery = {
start: 0,
end: 1,
platform: "pc",
filter: [{type: "id", value: id}]
}
try{
const res = await this.modelSaberApi.searchModel(query);
if(res.status !== 200){ return null; }
if(Object.keys(res.data).length === 0){
return null;
}
return res.data[`${id}`];
}
catch(e){
return null;
}
}
}
@@ -1,19 +0,0 @@
export class ModelSaberApiService {
private static instance: ModelSaberApiService;
public static getInstance(): ModelSaberApiService{
if(!ModelSaberApiService.instance){ ModelSaberApiService.instance = new ModelSaberApiService(); }
return ModelSaberApiService.instance;
}
private readonly API_URL = "https://modelsaber.com/api/v2";
private readonly ENDPOINTS = {get: "get.php", types: "types.php"};
private constructor(){}
public searchModel(search: any): any{
}
}
@@ -15,6 +15,7 @@ export class WindowManagerService{
"index.html": {width: 1080, height: 720, minWidth: 900, minHeight: 500},
"oneclick-download-map.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
"oneclick-download-playlist.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
"oneclick-download-model.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
}
private readonly baseWindowOption: BrowserWindowConstructorOptions = {
+25 -16
View File
@@ -1,31 +1,40 @@
//@ts-ignore
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import App from './windows/App';
import Launcher from './windows/Launcher';
import 'tailwindcss/tailwind.css';
import './index.css'
import OneClickDownloadMap from './windows/OneClick/OneClickDownloadMap';
import OneClickDownloadPlaylist from './windows/OneClick/OneClickDownloadPlaylist';
const launcherContainer = document.getElementById('launcher');
const oneclickDownloadMapContainer = document.getElementById('oneclick-download-map');
const oneclickDownloadPlaylistContainer = document.getElementById('oneclick-download-playlist');
const oneclickDownloadModelContainer = document.getElementById('oneclick-download-model');
if(!!launcherContainer){
createRoot(launcherContainer).render(<Launcher/>);
if(launcherContainer){
import("./windows/Launcher").then(reactWindow => {
createRoot(launcherContainer).render(<reactWindow.default/>);
});
}
else if(!!oneclickDownloadMapContainer){
createRoot(oneclickDownloadMapContainer).render(<OneClickDownloadMap/>);
else if(oneclickDownloadMapContainer){
import("./windows/OneClick/OneClickDownloadMap").then(reactWindow => {
createRoot(oneclickDownloadMapContainer).render(<reactWindow.default/>);
});
}
else if(!!oneclickDownloadPlaylistContainer){
createRoot(oneclickDownloadPlaylistContainer).render(<OneClickDownloadPlaylist/>);
else if(oneclickDownloadPlaylistContainer){
import("./windows/OneClick/OneClickDownloadPlaylist").then(reactWindow => {
createRoot(oneclickDownloadPlaylistContainer).render(<reactWindow.default/>);
});
}
else if(oneclickDownloadModelContainer){
import("./windows/OneClick/OneClickDownloadModel").then(reactWindow => {
createRoot(oneclickDownloadModelContainer).render(<reactWindow.default/>);
});
}
else{
const root = document.getElementById('root');
createRoot(root).render(
<HashRouter>
<App />
</HashRouter>
);
import("./windows/App").then(reactWindow => {
createRoot(root).render(
<HashRouter>
<reactWindow.default/>
</HashRouter>
);
});
}
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://use.typekit.net/okr8ven.css">
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
</head>
<body>
<div id="oneclick-download-model"></div>
</body>
</html>
@@ -0,0 +1,25 @@
import { MSModel } from "shared/models/model-saber/model-saber.model";
import { IpcService } from "../ipc.service";
export class ModelSaberService {
private static instance: ModelSaberService;
public static getInstance(): ModelSaberService{
if(!ModelSaberService.instance){ ModelSaberService.instance = new ModelSaberService(); }
return ModelSaberService.instance;
}
private readonly ipc: IpcService;
private constructor(){
this.ipc = IpcService.getInstance();
}
public async getModelById(id: number|string): Promise<MSModel>{
const res = await this.ipc.send<MSModel>("ms-get-model-by-id", {args: id});
if(!res.success){ return null; }
return res.data;
}
}
@@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
import { IpcService } from "renderer/services/ipc.service";
import { ThemeService } from "renderer/services/theme.service";
import { ModelSaberService } from "renderer/services/thrird-partys/model-saber.service";
import { MSModel } from "shared/models/model-saber/model-saber.model";
export default function OneClickDownloadModel() {
const ipc = IpcService.getInstance();
const themeService = ThemeService.getInstance();
const modelSaber = ModelSaberService.getInstance();
const [model, setModel] = useState<MSModel>(null);
useEffect(() => {
const sub = themeService.theme$.subscribe(() => {
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ document.documentElement.classList.add('dark'); }
else { document.documentElement.classList.remove('dark'); }
});
const promise = new Promise(async (resolve, reject) => {
const infos = await ipc.send<{id: string, type: string}>("one-click-model-info");
if(!infos.success){ reject(); }
const model = await modelSaber.getModelById(infos.data.id);
setModel(() => model);
// Download model
});
return () => {
sub.unsubscribe();
}
}, [])
return (
<div>OneClickDownloadModel</div>
)
}
@@ -0,0 +1,42 @@
export interface MSModel {
tags: string[],
type: MSModelType,
name: string,
author: string,
thumbnail: string,
id: number,
hash: string,
bsaber: string,
status: string,
discordid: string,
discord?: string,
variationid?: number,
platform: MSModelPlatform,
download: string,
install_link: string,
date: string
}
export type MSModelType = "saber"|"platform"|"bloq"|"misc"|"avatar";
export type MSModelPlatform = "pc"|"quest"|"all";
export interface MSGetQuery {
type?: MSModelType;
platform?: MSModelPlatform,
start?: number,
end?: number,
sort?: MSGetSort,
sortDirection?: MSGetSortDirection,
filter?: MSGetQueryFilter[]
}
export interface MSGetQueryFilter {
type:MSGetQueryFilterType,
value: string|number,
isNegative?: boolean
}
export type MSGetResponse<T extends string = string> = Record<T, MSModel>;
export type MSGetSort = "date"|"name"|"author"
export type MSGetSortDirection = "asc"|"desc"
export type MSGetQueryFilterType = "author"|"name"|"tag"|"hash"|"discordid"|"id"|"searchName"
@@ -2,5 +2,6 @@ export type AppWindow = (
"index.html" |
"launcher.html" |
"oneclick-download-map.html"|
"oneclick-download-playlist.html"
"oneclick-download-playlist.html"|
"oneclick-download-model.html"
);