mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feat-994] Add support for alternative mod repos (#995)
* [feat] Add support for alternative mod repos * Fix lint errors * Simplify code + Fix potential issues --------- Co-authored-by: Zagrios <40181755+Zagrios@users.noreply.github.com>
This commit is contained in:
@@ -39,3 +39,17 @@ ipc.on("bs-mods.beatmods-up", (_, reply) => {
|
||||
const beatMods = BeatModsApiService.getInstance();
|
||||
reply(from(beatMods.isUp()));
|
||||
})
|
||||
|
||||
ipc.on("bs-mods.mod-repo.get-repo-list", (_, reply) => {
|
||||
const beatMods = BeatModsApiService.getInstance();
|
||||
reply(from(beatMods.getModRepoList()));
|
||||
})
|
||||
ipc.on("bs-mods.mod-repo.get-name", (_, reply) => {
|
||||
const beatMods = BeatModsApiService.getInstance();
|
||||
reply(from(beatMods.getSelectedModRepoAsync()));
|
||||
})
|
||||
|
||||
ipc.on("bs-mods.mod-repo.select-name", (args, reply) => {
|
||||
const beatMods = BeatModsApiService.getInstance();
|
||||
reply(from(beatMods.selectModRepo(args)));
|
||||
})
|
||||
@@ -3,18 +3,47 @@ import { BbmFullMod, BbmMod, BbmModVersion, BbmPlatform } from "../../../shared/
|
||||
import { RequestService } from "../request.service";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
import log from "electron-log"
|
||||
import { StaticConfigurationService } from "../static-configuration.service";
|
||||
import { ModRepo } from "shared/models/mods/repo.model";
|
||||
|
||||
export class BeatModsApiService {
|
||||
private static instance: BeatModsApiService;
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
public readonly MODS_REPO_URL = "https://beatmods.com";
|
||||
private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`;
|
||||
private static readonly MOD_REPO_LIST:ModRepo[] = [
|
||||
{
|
||||
id: "beatmods",
|
||||
mods_repo_url: "https://beatmods.com",
|
||||
mods_repo_api_url: "https://beatmods.com/api",
|
||||
display_name: "BeatMods",
|
||||
website: "https://beatmods.com"
|
||||
},
|
||||
{
|
||||
id: "beatsabercn",
|
||||
mods_repo_url: "https://beatmods.bsaber.cn",
|
||||
mods_repo_api_url: "https://beatmods.bsaber.cn/api",
|
||||
display_name: "CN中文镜像源",
|
||||
website: "https://beatmods.bsaber.cn/front/mods"
|
||||
}
|
||||
];
|
||||
|
||||
private selectedModRepo: ModRepo = BeatModsApiService.MOD_REPO_LIST.find(repo => repo.default) || BeatModsApiService.MOD_REPO_LIST[0];
|
||||
|
||||
public getSelectedModRepo(): ModRepo {
|
||||
return this.selectedModRepo;
|
||||
}
|
||||
|
||||
private readonly versionModsCache = new Map<string, BbmFullMod[]>();
|
||||
private readonly modsHashCache = new Map<string, BbmModVersion>();
|
||||
|
||||
private resetCache(){
|
||||
this.versionModsCache.clear();
|
||||
this.modsHashCache.clear();
|
||||
}
|
||||
|
||||
public static getInstance(): BeatModsApiService {
|
||||
if (!BeatModsApiService.instance) {
|
||||
BeatModsApiService.instance = new BeatModsApiService();
|
||||
@@ -23,23 +52,49 @@ export class BeatModsApiService {
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
|
||||
const repoId = this.staticConfig.get("selected-mod-repo");
|
||||
if(repoId){
|
||||
this.selectedModRepo = BeatModsApiService.MOD_REPO_LIST.find(repo => repo.id === repoId) || BeatModsApiService.MOD_REPO_LIST[0];
|
||||
}
|
||||
}
|
||||
|
||||
public async isUp(): Promise<boolean> {
|
||||
try {
|
||||
// The data in status can be dropped
|
||||
await this.requestService.getJSON<{}>(`${this.MODS_REPO_API_URL}/status`);
|
||||
await this.requestService.getJSON<{}>(`${this.getSelectedModRepo().mods_repo_api_url}/status`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("Could not connect to beatmods", error);
|
||||
log.error(`Could not connect to ${this.selectedModRepo.mods_repo_api_url}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string {
|
||||
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
|
||||
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
|
||||
return `${this.getSelectedModRepo().mods_repo_api_url}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
|
||||
}
|
||||
|
||||
public async getModRepoList():Promise<ModRepo[]>{
|
||||
return BeatModsApiService.MOD_REPO_LIST;
|
||||
}
|
||||
|
||||
public async getSelectedModRepoAsync():Promise<ModRepo>{
|
||||
return this.getSelectedModRepo();
|
||||
}
|
||||
public async selectModRepo(repoId:string): Promise<boolean>{
|
||||
const selectedRepo = BeatModsApiService.MOD_REPO_LIST.find(repo => repo.id === repoId);
|
||||
|
||||
if(!selectedRepo){
|
||||
return false;
|
||||
}
|
||||
|
||||
this.selectedModRepo = selectedRepo;
|
||||
this.staticConfig.set("selected-mod-repo", repoId);
|
||||
this.resetCache()
|
||||
return true;
|
||||
}
|
||||
|
||||
private updateModsHashCache(mods: BbmModVersion[]): void {
|
||||
@@ -76,7 +131,7 @@ export class BeatModsApiService {
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
|
||||
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`
|
||||
`${this.getSelectedModRepo().mods_repo_api_url}/hashlookup?hash=${hash}`
|
||||
).then(({ data }) => {
|
||||
this.updateModsHashCache(data?.modVersions ?? []);
|
||||
return data?.modVersions?.at(0);
|
||||
|
||||
@@ -128,7 +128,7 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
|
||||
zipUrl = new URL(zipUrl, this.beatModsApi.MODS_REPO_URL).href;
|
||||
zipUrl = new URL(zipUrl, this.beatModsApi.getSelectedModRepo().mods_repo_url).href;
|
||||
|
||||
log.info("Download mod zip", zipUrl);
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface StaticConfigKeyValues {
|
||||
"use-system-proxy": boolean;
|
||||
"last-version-launched": BSVersion;
|
||||
"auto-update": AutoUpdate;
|
||||
"selected-mod-repo": string;
|
||||
|
||||
// Linux Specific static configs
|
||||
"proton-folder": string;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Dropzone } from "renderer/components/shared/dropzone.component";
|
||||
import { ModsGridStatus } from "shared/models/mods/mod-ipc.model";
|
||||
import { BsmLink } from "renderer/components/shared/bsm-link.component";
|
||||
import { DISCORD_URL, GITHUB_URL } from "shared/constants";
|
||||
import { ModRepo } from "shared/models/mods/repo.model";
|
||||
|
||||
export type ModsSlideRef = {
|
||||
loadMods: () => Promise<void>;
|
||||
@@ -55,6 +56,8 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
const downloadRef = useRef(null);
|
||||
const [downloadWith, setDownloadWidth] = useState(0);
|
||||
|
||||
const [selectedModRepo, setSelectedModRepo] = useState(null as ModRepo)
|
||||
|
||||
const modsToCategoryMap = (mods: BbmFullMod[]): Map<BbmCategories, BbmFullMod[]> => {
|
||||
if (!mods) {
|
||||
return new Map<BbmCategories, BbmFullMod[]>();
|
||||
@@ -218,6 +221,10 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
loadMods
|
||||
}), [version]);
|
||||
|
||||
useEffect(()=>{
|
||||
modsManager.getSelectedModRepo().then(repo=>setSelectedModRepo(repo));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
@@ -284,6 +291,18 @@ export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, o
|
||||
|
||||
const renderStatus = () => {
|
||||
if (gridStatus === ModsGridStatus.BEATMODS_DOWN) {
|
||||
if(selectedModRepo && selectedModRepo.id !== "beatmods"){
|
||||
return <ModStatus image={BeatConflictImg}>
|
||||
<span className="text-xl text-center px-2 mt-3 italic">
|
||||
{
|
||||
t("pages.version-viewer.mods.notifications.third-party-mod-source-not-avaliable.description",
|
||||
{name:selectedModRepo ? selectedModRepo.display_name : "null"}
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</ModStatus>
|
||||
}
|
||||
|
||||
return <ModStatus image={BeatConflictImg}>
|
||||
<span className="text-xl text-center px-2 mt-3 italic">
|
||||
{te("pages.version-viewer.mods.status.beatmods-down", {links: (<>
|
||||
|
||||
@@ -49,6 +49,8 @@ import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
|
||||
import { DISCORD_URL } from "shared/constants";
|
||||
import { AutoUpdate } from "shared/models/config";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { ModRepo } from "shared/models/mods/repo.model";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -69,6 +71,7 @@ export function SettingsPage() {
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
const staticConfig = useService(StaticConfigurationService);
|
||||
const installationLocationService = useService(InstallationLocationService);
|
||||
const bsModManagerService = useService(BsModsManagerService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
@@ -100,6 +103,8 @@ export function SettingsPage() {
|
||||
const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false);
|
||||
const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false);
|
||||
const [hasDownloaderSession, setHasDownloaderSession] = useState(false);
|
||||
const [modRepoList, setModRepoList] = useState([] as ModRepo[]);
|
||||
const [modRepo, setModRepo] = useState("");
|
||||
const appVersion = useObservable(() => autoUpdater.getAppVersion());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -109,6 +114,15 @@ export function SettingsPage() {
|
||||
playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled));
|
||||
modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled));
|
||||
|
||||
|
||||
bsModManagerService.getModRepoList().then(list =>{
|
||||
setModRepoList(list);
|
||||
bsModManagerService.getSelectedModRepo().then(repo => {
|
||||
setModRepo(repo.id);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
staticConfig.get("proton-folder").then(setProtonFolder);
|
||||
}, []);
|
||||
|
||||
@@ -157,6 +171,13 @@ export function SettingsPage() {
|
||||
i18nService.setLanguage(item.value);
|
||||
};
|
||||
|
||||
const handleChangeModRepo = (repo: RadioItem<string>) => {
|
||||
bsModManagerService.selectModRepo(repo.value).then(result=>{
|
||||
if(result){
|
||||
setModRepo(repo.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
const setDefaultProtonFolder = async () => {
|
||||
if (!progressBarService.require()) {
|
||||
return;
|
||||
@@ -508,6 +529,17 @@ export function SettingsPage() {
|
||||
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} columnCount={2} />
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.mod-repos.title" description="pages.settings.mod-repos.description">
|
||||
<SettingRadioArray items={modRepoList.map((repo,index)=>({
|
||||
id:index,
|
||||
value: repo.id,
|
||||
text: repo.display_name,
|
||||
icon: repo.website ?
|
||||
<BsmButton onClick={()=>linkOpener.open(repo.website, false)} className="px-2 font-bold italic text-sm rounded-md" text="pages.settings.mod-repos.website" withBar={false} />
|
||||
: null
|
||||
}))} selectedItemValue={modRepo} onItemSelected={handleChangeModRepo} />
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
||||
<div className="flex gap-2">
|
||||
<BsmButton color="#ef4444" className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap !text-white" text="pages.settings.patreon.buttons.support" withBar={false} onClick={openSupportPage} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Observable, BehaviorSubject, throwError, of, lastValueFrom } from "rxjs";
|
||||
import { Observable, BehaviorSubject, throwError, of, lastValueFrom, from } from "rxjs";
|
||||
import { catchError, map, tap } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
@@ -10,6 +10,7 @@ import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
|
||||
import { logRenderError } from "renderer";
|
||||
import { ModsGridStatus } from "shared/models/mods/mod-ipc.model";
|
||||
import { LinuxService } from "./linux.service";
|
||||
import { ModRepo } from "shared/models/mods/repo.model";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -226,4 +227,18 @@ export class BsModsManagerService {
|
||||
return ModsGridStatus.OK;
|
||||
}
|
||||
|
||||
public async getModRepoList(): Promise<ModRepo[]> {
|
||||
return lastValueFrom(this.ipcService.sendV2("bs-mods.mod-repo.get-repo-list")).catch(() => [] as ModRepo[]);
|
||||
}
|
||||
|
||||
public async getSelectedModRepo(): Promise<ModRepo> {
|
||||
return lastValueFrom(this.ipcService.sendV2("bs-mods.mod-repo.get-name").pipe(
|
||||
catchError(() => from(this.getModRepoList()).pipe(map(list => list.find(repo => repo.default) || list[0])))
|
||||
));
|
||||
}
|
||||
|
||||
public async selectModRepo(name:string): Promise<boolean> {
|
||||
return lastValueFrom(this.ipcService.sendV2("bs-mods.mod-repo.select-name", name)).catch(() => false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpc
|
||||
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
|
||||
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
|
||||
import { UpdateInfo } from "electron-updater";
|
||||
import { ModRepo } from "../mods/repo.model";
|
||||
|
||||
export type IpcReplier<T> = (data: Observable<T>) => void;
|
||||
|
||||
@@ -90,6 +91,9 @@ export interface IpcChannelMapping {
|
||||
"bs-mods.uninstall-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-all-mods": { request: BSVersion, response: Progression };
|
||||
"bs-mods.beatmods-up": { request: void, response: boolean };
|
||||
"bs-mods.mod-repo.get-repo-list": { request: void, response: ModRepo[]};
|
||||
"bs-mods.mod-repo.get-name": { request: void, response: ModRepo};
|
||||
"bs-mods.mod-repo.select-name": { request: string, response: boolean};
|
||||
|
||||
/* ** bs-playlist-ipcs ** */
|
||||
"one-click-install-playlist": { request: string, response: Progression<DownloadPlaylistProgressionData> };
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ModRepo {
|
||||
id: string
|
||||
mods_repo_url: string
|
||||
mods_repo_api_url: string
|
||||
display_name: string
|
||||
website?: string
|
||||
default?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user