Rework protocol handling for IPC messages

Create new UI for installing protocol addons
Update addon service to allow installing a specific addon version
Update addon providers to allow searching via protocol
Standardize the thumbnail component
This commit is contained in:
jliddev
2021-03-21 10:36:46 -05:00
parent 6f1814b001
commit 286dc355e3
50 changed files with 974 additions and 234 deletions

View File

@@ -51,6 +51,7 @@ import {
IPC_WOWUP_GET_SCAN_RESULTS,
IPC_WRITE_FILE_CHANNEL,
IPC_FOCUS_WINDOW,
IPC_IS_DEFAULT_PROTOCOL_CLIENT,
} from "./src/common/constants";
import { CurseFolderScanner } from "./src/common/curse/curse-folder-scanner";
import { CurseFolderScanResult } from "./src/common/curse/curse-folder-scan-result";
@@ -170,13 +171,17 @@ export function initializeIpcHandlers(window: BrowserWindow): void {
}
);
handle(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, () => {
app.setAsDefaultProtocolClient(APP_PROTOCOL_NAME);
handle(IPC_IS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.isDefaultProtocolClient(protocol);
});
handle(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, () => {
app.removeAsDefaultProtocolClient(APP_PROTOCOL_NAME);
})
handle(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.setAsDefaultProtocolClient(protocol);
});
handle(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
return app.removeAsDefaultProtocolClient(protocol);
});
handle(IPC_LIST_DIRECTORIES_CHANNEL, async (evt, filePath: string, scanSymlinks: boolean) => {
const files = await fs.readdir(filePath, { withFileTypes: true });

View File

@@ -1,18 +1,23 @@
import { app, BrowserWindow, BrowserWindowConstructorOptions, powerMonitor } from "electron";
import * as log from "electron-log";
import { type as osType, release as osRelease, arch as osArch } from "os";
import { find } from "lodash";
import * as minimist from "minimist";
import { arch as osArch, release as osRelease, type as osType } from "os";
import { join } from "path";
import { format as urlFormat } from "url";
import { inspect } from "util";
import * as platform from "./platform";
import * as minimist from "minimist";
import { createAppMenu } from "./app-menu";
import { initializeAppUpdateIpcHandlers, initializeAppUpdater } from "./app-updater";
import { initializeIpcHandlers } from "./ipc-events";
import * as platform from "./platform";
import {
APP_USER_MODEL_ID,
COLLAPSE_TO_TRAY_PREFERENCE_KEY,
CURRENT_THEME_KEY,
DEFAULT_BG_COLOR,
DEFAULT_LIGHT_BG_COLOR,
IPC_CUSTOM_PROTOCOL_RECEIVED,
IPC_POWER_MONITOR_LOCK,
IPC_POWER_MONITOR_RESUME,
IPC_POWER_MONITOR_SUSPEND,
@@ -27,15 +32,12 @@ import {
WINDOW_DEFAULT_WIDTH,
WINDOW_MIN_HEIGHT,
WINDOW_MIN_WIDTH,
IPC_REQUEST_INSTALL_FROM_URL,
APP_PROTOCOL_NAME,
WOWUP_LOGO_FILENAME,
} from "./src/common/constants";
import { AppOptions } from "./src/common/wowup/models";
import { windowStateManager } from "./window-state";
import { createAppMenu } from "./app-menu";
import { MainChannels } from "./src/common/wowup";
import { AppOptions } from "./src/common/wowup/models";
import { preferenceStore } from "./stores";
import { windowStateManager } from "./window-state";
// LOGGING SETUP
// Override the default log path so they aren't a pain to find on Mac
@@ -46,6 +48,9 @@ log.transports.file.resolvePath = (variables: log.PathVariables) => {
};
log.info("Main starting");
log.info(`Electron: ${process.versions.electron}`);
log.info(`BinaryPath: ${app.getPath("exe")}`);
log.info("ExecPath", process.execPath);
log.info("Args", process.argv);
// ERROR HANDLING SETUP
process.on("uncaughtException", (error) => {
@@ -59,7 +64,7 @@ process.on("unhandledRejection", (error) => {
// VARIABLES
const startedAt = Date.now();
const argv = minimist(process.argv.slice(1), {
boolean: ["serve", "hidden"]
boolean: ["serve", "hidden"],
}) as AppOptions;
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
const USER_AGENT = getUserAgent();
@@ -72,7 +77,7 @@ let win: BrowserWindow = null;
createAppMenu(win);
// Set the app ID so that our notifications work correctly on Windows
app.setAppUserModelId("io.wowup.jliddev");
app.setAppUserModelId(APP_USER_MODEL_ID);
// HARDWARE ACCELERATION SETUP
if (preferenceStore.get(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY) === "false") {
@@ -94,6 +99,7 @@ if (!singleInstanceLock) {
app.quit();
} else {
app.on("second-instance", (evt, args) => {
log.info(`Second instance detected`, args);
// Someone tried to run a second instance, we should focus our window.
if (!win) {
log.warn("Second instance launched, but no window found");
@@ -107,25 +113,28 @@ if (!singleInstanceLock) {
}
win.focus();
args.slice(1).forEach(arg => {
try {
const url = new URL(arg);
if (url && url.protocol == APP_PROTOCOL_NAME + ":") {
win.webContents.send(IPC_REQUEST_INSTALL_FROM_URL, url.searchParams.get("install"));
return;
}
} catch { log.info("Failed to load as URI: " + arg); }
});
const argv = minimist(args.slice(1), {
string: ["install"]
}) as AppOptions;
win.webContents.send(IPC_REQUEST_INSTALL_FROM_URL, argv.install);
// Find the first protocol arg if any exist
const customProtocol = find(args, (arg) => isProtocol(arg));
if (customProtocol) {
log.info(`Custom protocol detected: ${customProtocol}`);
// If we did get a custom protocol notify the app
win.webContents.send(IPC_CUSTOM_PROTOCOL_RECEIVED, customProtocol);
} else {
log.info(`No custom protocol detected`);
}
});
}
function isProtocol(arg: string) {
return getProtocol(arg) != null;
}
function getProtocol(arg: string) {
const match = /^([a-z][a-z0-9+\-.]*):/.exec(arg);
return match !== null && match.length > 1 ? match[1] : null;
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.

View File

@@ -40,6 +40,7 @@
"test": "ng test --watch=false",
"test:watch": "ng test",
"test:locales": "ng test --watch=false --include='src/locales.spec.ts'",
"test:customprotocol:": "npx electron . \"curseforge://install?addonId=3358^^^&fileId=3240590\"",
"e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts",
"version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
"lint": "ng lint",

View File

@@ -39,18 +39,6 @@ function rendererOn(channel: string, listener: (event: IpcRendererEvent, ...args
ipcRenderer.on(channel, listener);
}
function isDefaultProtocolClient(protocol: string, path?: string, args?: string[]) {
return remote.app.isDefaultProtocolClient(protocol, path, args);
}
function setAsDefaultProtocolClient(protocol: string, path?: string, args?: string[]) {
return remote.app.setAsDefaultProtocolClient(protocol, path, args);
}
function removeAsDefaultProtocolClient(protocol: string, path?: string, args?: string[]) {
return remote.app.removeAsDefaultProtocolClient(protocol, path, args);
}
function openExternal(url: string, options?: OpenExternalOptions): Promise<void> {
return shell.openExternal(url, options);
}
@@ -71,7 +59,6 @@ function showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnVal
}
if (window.opener === null) {
console.log("NO OPENER");
window.log = log;
window.libs = {
handlebars: require("handlebars"),
@@ -84,9 +71,6 @@ if (window.opener === null) {
rendererInvoke,
rendererOff,
rendererOn,
isDefaultProtocolClient,
setAsDefaultProtocolClient,
removeAsDefaultProtocolClient,
openExternal,
showOpenDialog,
openPath,

View File

@@ -6,6 +6,7 @@ import { Addon } from "../../common/entities/addon";
import { AddonChannelType } from "../../common/wowup/models";
import { AddonFolder } from "../models/wowup/addon-folder";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { ProtocolSearchResult } from "../models/wowup/protocol-search-result";
export type AddonProviderType = "Curse" | "GitHub" | "TukUI" | "WowInterface" | "WowUpHub" | "RaiderIO" | "Zip";
@@ -54,6 +55,10 @@ export abstract class AddonProvider {
return Promise.resolve(undefined);
}
public searchProtocol(protocol: string): Promise<ProtocolSearchResult | undefined> {
return Promise.resolve(undefined);
}
public searchByName(
addonName: string,
folderName: string,
@@ -75,6 +80,10 @@ export abstract class AddonProvider {
return false;
}
public isValidProtocol(protocol: string): boolean {
return false;
}
public async scan(
installation: WowInstallation,
addonChannelType: AddonChannelType,

View File

@@ -16,6 +16,7 @@ import { AddonChannelType, AddonDependencyType } from "../../common/wowup/models
import { AppConfig } from "../../environments/environment";
import { AppCurseScanResult } from "../models/curse/app-curse-scan-result";
import {
CurseAddonFileResponse,
CurseAuthor,
CurseDependency,
CurseDependencyType,
@@ -30,6 +31,7 @@ import { AddonFolder } from "../models/wowup/addon-folder";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultDependency } from "../models/wowup/addon-search-result-dependency";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { ProtocolSearchResult } from "../models/wowup/protocol-search-result";
import { WowInstallation } from "../models/wowup/wow-installation";
import { ElectronService } from "../services";
import { CachingService } from "../services/caching/caching-service";
@@ -42,6 +44,11 @@ import { AddonProvider, GetAllResult } from "./addon-provider";
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
const CHANGELOG_CACHE_TTL_SEC = 30 * 60;
interface ProtocolData {
addonId: number;
fileId: number;
}
export class CurseAddonProvider extends AddonProvider {
private readonly _circuitBreaker: CircuitBreakerWrapper;
@@ -86,6 +93,51 @@ export class CurseAddonProvider extends AddonProvider {
return "";
}
public isValidProtocol(protocol: string): boolean {
return protocol.toLowerCase().startsWith("curseforge://");
}
public async searchProtocol(protocol: string): Promise<ProtocolSearchResult | undefined> {
const protocolData = this.parseProtocol(protocol);
console.debug("protocolData", protocolData);
if (!protocolData.addonId || !protocolData.fileId) {
throw new Error("Invalid protocol data");
}
const addonResult = await this.getByIdBase(protocolData.addonId.toString()).toPromise();
console.debug("addonResult", addonResult);
if (!addonResult) {
throw new Error(`Failed to get addon data`);
}
const addonFileResponse = await this.getAddonFileById(protocolData.addonId, protocolData.fileId).toPromise();
// const targetFile = _.find(addonResult.latestFiles, (lf) => lf.id === protocolData.fileId);
console.debug("targetFile", addonFileResponse);
if (!addonFileResponse) {
throw new Error("Failed to get target file");
}
const searchResult: ProtocolSearchResult = {
protocol,
protocolAddonId: protocolData.addonId.toString(),
protocolReleaseId: protocolData.fileId.toString(),
validClientTypes: this.getValidClientTypes(addonFileResponse.gameVersionFlavor),
...this.getAddonSearchResult(addonResult, [addonFileResponse]),
};
console.debug("searchResult", searchResult);
return searchResult;
}
private parseProtocol(protocol: string): ProtocolData {
const url = new URL(protocol);
return {
addonId: +url.searchParams.get("addonId"),
fileId: +url.searchParams.get("fileId"),
};
}
public async getChangelog(
installation: WowInstallation,
externalId: string,
@@ -377,9 +429,7 @@ export class CurseAddonProvider extends AddonProvider {
}
public getById(addonId: string, installation: WowInstallation): Observable<AddonSearchResult> {
const url = `${API_URL}/addon/${addonId}`;
return from(this._circuitBreaker.getJson<CurseSearchResult>(url)).pipe(
return this.getByIdBase(addonId).pipe(
map((result) => {
if (!result) {
return null;
@@ -395,6 +445,18 @@ export class CurseAddonProvider extends AddonProvider {
);
}
private getByIdBase(addonId: string): Observable<CurseSearchResult> {
const url = `${API_URL}/addon/${addonId}`;
return from(this._circuitBreaker.getJson<CurseSearchResult>(url));
}
private getAddonFileById(addonId: string | number, fileId: string | number): Observable<CurseAddonFileResponse> {
const url = `${API_URL}/addon/${addonId}/file/${fileId}`;
return from(this._circuitBreaker.getJson<CurseAddonFileResponse>(url));
}
public isValidAddonUri(addonUri: URL): boolean {
return addonUri.host && addonUri.host.endsWith("curseforge.com") && addonUri.pathname.startsWith("/wow/addons");
}
@@ -581,6 +643,15 @@ export class CurseAddonProvider extends AddonProvider {
}
}
private getValidClientTypes(gameVersionFlavor: string): WowClientType[] {
switch (gameVersionFlavor) {
case "wow_classic":
return [WowClientType.Classic, WowClientType.ClassicPtr];
default:
return [WowClientType.Retail, WowClientType.RetailPtr, WowClientType.Beta];
}
}
private getWowUpChannel(releaseType: CurseReleaseType): AddonChannelType {
switch (releaseType) {
case CurseReleaseType.Alpha:

View File

@@ -132,7 +132,6 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
map((appOptions) => {
this.showPreLoad = false;
this.quitEnabled = appOptions.quit;
this.openInstallFromUrlDialog(appOptions.install);
this._cdRef.detectChanges();
}),
catchError((err) => {
@@ -192,8 +191,8 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
this._electronService.applyZoom(ZoomDirection.ZoomReset).catch((e) => console.error(e));
};
public onRequestInstallFromUrl = async (evt: any, path?: string): Promise<void> => {
await this.openInstallFromUrlDialog(path);
public onRequestInstallFromUrl = (evt: any, path?: string): void => {
this.openInstallFromUrlDialog(path);
};
public openDialog(): void {
@@ -209,9 +208,12 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
private async openInstallFromUrlDialog(path?: string) {
if (!path) return;
var dialogRef = await this._dialog.open(InstallFromUrlDialogComponent);
private openInstallFromUrlDialog(path?: string) {
if (!path) {
return;
}
const dialogRef = this._dialog.open(InstallFromUrlDialogComponent);
dialogRef.componentInstance.query = path;
}

View File

@@ -0,0 +1,9 @@
<div *ngIf="hasUrl() === true" class="addon-logo-container bg-secondary-3" [style.width]="size + 'px'"
[style.height]="size + 'px'">
<img [src]="url" loading="lazy" />
</div>
<div *ngIf="hasUrl() === false" class="addon-logo-container">
<div class="addon-logo-letter text-3">
{{ getLetter() }}
</div>
</div>

View File

@@ -0,0 +1,26 @@
.addon-logo-container {
width: 40px;
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
overflow: hidden;
.addon-logo {
height: 100%;
}
.addon-logo-letter {
font-size: 2em;
font-weight: 400;
}
img {
height: 100%;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddonThumbnailComponent } from './addon-thumbnail.component';
describe('AddonThumbnailComponent', () => {
let component: AddonThumbnailComponent;
let fixture: ComponentFixture<AddonThumbnailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AddonThumbnailComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddonThumbnailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
import { Component, Input, OnInit } from "@angular/core";
@Component({
selector: "app-addon-thumbnail",
templateUrl: "./addon-thumbnail.component.html",
styleUrls: ["./addon-thumbnail.component.scss"],
})
export class AddonThumbnailComponent implements OnInit {
@Input() public url = "";
@Input() public name = "";
@Input() public size = 40;
public constructor() {}
public ngOnInit(): void {}
public hasUrl(): boolean {
return !!this.url;
}
public getLetter(): string {
return this.name?.charAt(0).toUpperCase() ?? "";
}
}

View File

@@ -0,0 +1,53 @@
<h1 *ngIf="ready === true" mat-dialog-title>{{ "DIALOGS.INSTALL_FROM_PROTOCOL.TITLE" | translate:{providerName:
getProviderName()} }}</h1>
<div mat-dialog-content class="content">
<div *ngIf="ready === false" class="row justify-content-center">
<app-progress-spinner></app-progress-spinner>
</div>
<div *ngIf="ready === true">
<div class="row mb-3">
<app-addon-thumbnail [url]="getThumbnailUrl()" [name]="getName()" [size]="60" class="pt-1 mr-3"></app-addon-thumbnail>
<div>
<h3 class="m-0">{{getName()}}</h3>
<p class="m-0">{{getAuthor()}}</p>
<p class="m-0 text-2">{{getVersion()}}</p>
</div>
</div>
<mat-form-field *ngIf="error.length === 0" class="control">
<mat-label>WoW Installation</mat-label>
<mat-select multiple class="select" [formControl]="installations"
[disabled]="isInstalling === true || isComplete === true">
<mat-option *ngFor="let installation of validWowInstallations" [value]="installation.id"
[disabled]="installation.isInstalled">{{ installation.label
}}
<mat-icon *ngIf="installation.isInstalled" class="option-icon success-icon" svgIcon="fas:check-circle">
</mat-icon>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="ready === true && error.length > 0" class="error">
<h4>Error</h4>
<p>{{ error | translate:{protocol: data.protocol} }}</p>
</div>
<div *ngIf="isInstalling === true">
<p>{{ 'DIALOGS.INSTALL_FROM_PROTOCOL.ADDON_INSTALLING' | translate }}</p>
<mat-progress-bar mode="determinate" [value]="installProgress"></mat-progress-bar>
</div>
<div *ngIf="isComplete === true" class="installed">
<div class="success-icon">
<img src="assets/images/checkbox-marked-circle-green.svg" class="icon-larger" />
</div>
<p class="text-center">{{ 'DIALOGS.INSTALL_FROM_PROTOCOL.ADDON_INSTALLED' | translate }}</p>
</div>
</div>
<div *ngIf="ready === true" mat-dialog-actions>
<button mat-button [disabled]="isInstalling" (click)="onClose()">
{{ "DIALOGS.INSTALL_FROM_PROTOCOL.CANCEL_BUTTON" | translate }}
</button>
<button mat-flat-button color="primary" cdkFocusInitial (click)="onInstall()"
[disabled]="error.length > 0 || installations.value.length === 0 || isInstalling === true || isComplete === true">
{{ "DIALOGS.INSTALL_FROM_PROTOCOL.INSTALL_BUTTON" | translate }}
</button>
</div>

View File

@@ -0,0 +1,22 @@
.content {
min-width: 300px;
.control {
width: 100%;
}
.success-icon {
text-align: center;
filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%);
}
}
.error {
color: #f04747;
}
.select {
.mat-icon {
height: 17px;
width: 17px;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InstallFromProtocolDialogComponent } from './install-from-protocol-dialog.component';
describe('InstallFromProtocolDialogComponent', () => {
let component: InstallFromProtocolDialogComponent;
let fixture: ComponentFixture<InstallFromProtocolDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ InstallFromProtocolDialogComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InstallFromProtocolDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,169 @@
import { AfterViewInit, Component, Inject, OnInit } from "@angular/core";
import { FormControl } from "@angular/forms";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { ProtocolSearchResult } from "app/models/wowup/protocol-search-result";
import { WowInstallation } from "app/models/wowup/wow-installation";
import { SessionService } from "app/services/session/session.service";
import { WarcraftInstallationService } from "app/services/warcraft/warcraft-installation.service";
import * as _ from "lodash";
import { BehaviorSubject, from, of, Subject } from "rxjs";
import { catchError, delay, filter, first, map, switchMap } from "rxjs/operators";
import { AddonService } from "../../services/addons/addon.service";
export interface InstallFromProtocolDialogComponentData {
protocol: string;
}
export interface WowInstallationWrapper extends WowInstallation {
isInstalled?: boolean;
}
const ERROR_ADDON_NOT_FOUND = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.ADDON_NOT_FOUND";
const ERROR_GENERIC = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.GENERIC";
const ERROR_NO_VALID_WOW_INSTALLATIONS = "DIALOGS.INSTALL_FROM_PROTOCOL.ERRORS.NO_VALID_WOW_INSTALLATIONS";
@Component({
selector: "app-install-from-protocol-dialog",
templateUrl: "./install-from-protocol-dialog.component.html",
styleUrls: ["./install-from-protocol-dialog.component.scss"],
})
export class InstallFromProtocolDialogComponent implements OnInit, AfterViewInit {
public error = "";
public ready = false;
public addon: ProtocolSearchResult;
public installations = new FormControl();
public validWowInstallations: WowInstallationWrapper[] = [];
public installProgress = 0;
public isInstalling = false;
public isComplete = false;
public constructor(
private _addonService: AddonService,
private _sessionService: SessionService,
private _warcraftInstallationService: WarcraftInstallationService,
@Inject(MAT_DIALOG_DATA) public data: InstallFromProtocolDialogComponentData,
public dialogRef: MatDialogRef<InstallFromProtocolDialogComponent>
) {}
public ngOnInit(): void {}
public ngAfterViewInit(): void {
of(true)
.pipe(
first(),
delay(1000),
switchMap(() => from(this.loadAddon())),
catchError((e) => {
console.error(e);
return of(undefined);
})
)
.subscribe();
}
public getVersion(): string {
return _.first(this.addon?.files)?.version ?? "";
}
public getName(): string {
return this.addon?.name ?? "";
}
public getThumbnailUrl(): string {
return this.addon?.thumbnailUrl ?? "";
}
public getAuthor(): string {
return this.addon?.author ?? "'";
}
public getProviderName(): string {
return this.addon?.providerName ?? "";
}
public onClose(): void {
this.dialogRef.close();
}
public onInstall = async (): Promise<void> => {
console.debug("selectedInstallationId", this.installations.value);
const selectedInstallationIds: string[] = this.installations.value;
const selectedInstallations = this.validWowInstallations.filter((installation) =>
selectedInstallationIds.includes(installation.id)
);
const targetFile = _.first(this.addon.files);
try {
this.isInstalling = true;
const totalInstalls = selectedInstallations.length;
let installIdx = 0;
for (const installation of selectedInstallations) {
await this._addonService.installPotentialAddon(
this.addon,
installation,
(state, progress) => {
console.debug("Install Progress", progress);
this.installProgress = (installIdx * 100 + progress) / totalInstalls;
},
targetFile
);
installIdx += 1;
this._sessionService.notifyTargetFileInstallComplete();
}
this.isComplete = true;
} catch (e) {
console.error(`Failed to install addon for protocol: ${this.data.protocol}`, e);
this.error = ERROR_GENERIC;
} finally {
this.isInstalling = false;
}
};
private async loadAddon(): Promise<void> {
try {
const searchResult = await this._addonService.getAddonForProtocol(this.data.protocol);
if (!searchResult) {
this.error = ERROR_ADDON_NOT_FOUND;
return;
}
this.addon = searchResult;
this.validWowInstallations = this._warcraftInstallationService.getWowInstallationsByClientTypes(
searchResult.validClientTypes
);
if (this.validWowInstallations.length === 0) {
this.error = ERROR_NO_VALID_WOW_INSTALLATIONS;
return;
}
this.validWowInstallations.forEach((installation) => {
installation.isInstalled = this._addonService.isInstalled(this.addon.externalId, installation);
});
if (this.validWowInstallations.length === 0) {
return;
}
const allInstalled = _.every(this.validWowInstallations, (installation) => installation.isInstalled);
if (allInstalled) {
this.isComplete = true;
this.installations.setValue(this.validWowInstallations.map((installation) => installation.id));
return;
}
const installationId = _.find(this.validWowInstallations, (installation) => !installation.isInstalled)?.id;
if (!installationId) {
return;
}
this.installations.setValue([installationId]);
} catch (e) {
console.error(`Failed to load protocol addon`, e);
this.error = ERROR_GENERIC;
} finally {
this.ready = true;
}
}
}

View File

@@ -1,20 +1,7 @@
<div>
<div class="addon-column row align-items-center">
<div class="thumbnail-container">
<div *ngIf="listItem.hasThumbnail === true" class="addon-logo-container bg-secondary-3">
<img [src]="listItem.addon.thumbnailUrl" loading="lazy" />
</div>
<div *ngIf="listItem.hasThumbnail === false" class="addon-logo-container">
<div class="addon-logo-letter text-3">
{{ listItem.thumbnailLetter }}
</div>
</div>
<!-- <div *ngIf="listItem.isBetaChannel() || listItem.isAlphaChannel()" class="channel bg-secondary-3" [ngClass]="{
beta: listItem.isBetaChannel(),
alpha: listItem.isAlphaChannel()
}">
{{ listItem.isAlphaChannel() ? "Alpha" : "Beta" }}
</div> -->
<app-addon-thumbnail [url]="getThumbnailUrl()" [name]="listItem.addon.name"></app-addon-thumbnail>
</div>
<div class="version-container">
<div class="title-container">
@@ -30,9 +17,9 @@
<div class="addon-version text-2 row align-items-center" [ngClass]="{ ignored: listItem.addon.isIgnored }">
<div *ngIf="listItem.isBetaChannel() || listItem.isAlphaChannel()" class="channel bg-secondary-3 mr-2"
[ngClass]="{
beta: listItem.isBetaChannel(),
alpha: listItem.isAlphaChannel()
}">
beta: listItem.isBetaChannel(),
alpha: listItem.isAlphaChannel()
}">
{{ channelTranslationKey | translate }}
</div>
<div *ngIf="hasMultipleProviders === true" class="mr-2">

View File

@@ -63,6 +63,10 @@ export class MyAddonsAddonCellComponent implements AgRendererComponent {
this._dialogFactory.getAddonDetailsDialog(this.listItem);
}
public getThumbnailUrl(): string {
return this.listItem?.addon?.thumbnailUrl ?? "";
}
public getRequireDependencyCount(): number {
return this.listItem.getDependencies(AddonDependencyType.Required).length;
}

View File

@@ -20,6 +20,7 @@
</mat-select>
</mat-form-field>
</div>
<div class="divider"></div>
</div>
<!-- THEME -->
<div class="toggle">
@@ -41,6 +42,7 @@
</mat-select>
</mat-form-field>
</div>
<div class="divider"></div>
</div>
<!-- TELEMETRY -->
<div class="toggle">
@@ -55,6 +57,7 @@
<small class="text-2">
{{ "PAGES.OPTIONS.APPLICATION.TELEMETRY_DESCRIPTION" | translate }}
</small>
<div class="divider"></div>
</div>
<!-- MINIMIZE ON CLOSE -->
<div class="toggle">
@@ -67,6 +70,7 @@
<mat-slide-toggle [(checked)]="collapseToTray" (change)="onCollapseChange($event)"> </mat-slide-toggle>
</div>
<small class="text-2">{{ minimizeOnCloseDescription }}</small>
<div class="divider"></div>
</div>
<!-- SYSTEM NOTIFICATIONS -->
<div class="toggle">
@@ -83,6 +87,7 @@
<small class="text-2">
{{ "PAGES.OPTIONS.APPLICATION.ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION" | translate }}
</small>
<div class="divider"></div>
</div>
<!-- HARDWARE ACCELERATION -->
<div class="toggle">
@@ -95,7 +100,9 @@
<mat-slide-toggle [(checked)]="useHardwareAcceleration" (change)="onUseHardwareAccelerationChange($event)">
</mat-slide-toggle>
</div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_DESCRIPTION" | translate }}</small>
<small class="text-2"
[innerHTML]="'PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_DESCRIPTION' | translate"></small>
<div class="divider"></div>
</div>
<!-- SYMLINK SUPPORT -->
<div class="toggle">
@@ -109,6 +116,7 @@
</mat-slide-toggle>
</div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.USE_SYMLINK_SUPPORT_DESCRIPTION" | translate }}</small>
<div class="divider"></div>
</div>
<!-- START WITH SYSTEM -->
<div class="toggle">
@@ -123,6 +131,7 @@
(change)="onStartWithSystemChange($event)">
</mat-slide-toggle>
</div>
<div class="divider"></div>
</div>
<!-- START MINIMIZED -->
<div class="toggle">
@@ -137,30 +146,50 @@
(change)="onStartMinimizedChange($event)">
</mat-slide-toggle>
</div>
<div class="divider"></div>
</div>
<!-- SCALE -->
<div class="row row align-items-center">
<div class="flex-grow-1">
<div> {{ "PAGES.OPTIONS.APPLICATION.SCALE_LABEL" | translate }} </div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.SCALE_DESCRIPTION" | translate }}</small>
<div class="toggle">
<div class="row row align-items-center">
<div class="flex-grow-1">
<div> {{ "PAGES.OPTIONS.APPLICATION.SCALE_LABEL" | translate }} </div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.SCALE_DESCRIPTION" | translate }}</small>
</div>
<mat-form-field class="light-select">
<mat-select [(value)]="currentScale" (selectionChange)="onScaleChange($event)">
<mat-option *ngFor="let value of zoomScale" [value]="value">
{{ value * 100 | number:'1.0-0' }} %
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field class="light-select">
<mat-select [(value)]="currentScale" (selectionChange)="onScaleChange($event)">
<mat-option *ngFor="let value of zoomScale" [value]="value">
{{ value * 100 | number:'1.0-0' }} %
</mat-option>
</mat-select>
</mat-form-field>
<div class="divider"></div>
</div>
<!-- APP PROTOCOL -->
<div class="row row align-items-center">
<!-- <div class="row row align-items-center">
<div class="flex-grow-1">
<div>
{{ "PAGES.OPTIONS.APPLICATION.PROTOCOL_LABEL" | translate }}
</div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.PROTOCOL_DESCRIPTION" | translate }}</small>
</div>
<mat-slide-toggle [(checked)]="protocolRegistered" (change)="onProtocolRegisteredChange($event)">
<mat-slide-toggle [checked]="wowupProtocolHandled$ | async" (change)="onProtocolHandlerChange($event, wowupProtocolName)">
</mat-slide-toggle>
<div class="divider"></div>
</div> -->
<!-- CURSE PROTOCOL -->
<div class="toggle">
<div class="row align-items-center">
<div class="flex-grow-1">
<div>
{{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_LABEL" | translate }}
</div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.CURSE_PROTOCOL_DESCRIPTION" | translate }}</small>
</div>
<mat-slide-toggle [checked]="curseforgeProtocolHandled$ | async"
(change)="onProtocolHandlerChange($event, curseProtocolName)">
</mat-slide-toggle>
</div>
<div class="divider"></div>
</div>
</div>

View File

@@ -7,8 +7,16 @@
.hint {
color: $white-3;
}
}
.toggle {
margin-top: 1em;
.toggle {
margin-top: 1em;
margin-bottom: 20px;
.divider {
margin-top: 20px;
height: 1px;
border-top: thin solid $dark-1;
}
}

View File

@@ -12,6 +12,8 @@ import { ConfirmDialogComponent } from "../confirm-dialog/confirm-dialog.compone
import {
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
APP_PROTOCOL_NAME,
CURSE_PROTOCOL_NAME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
@@ -32,6 +34,9 @@ interface LocaleListItem {
styleUrls: ["./options-app-section.component.scss"],
})
export class OptionsAppSectionComponent implements OnInit {
public readonly curseProtocolName = CURSE_PROTOCOL_NAME;
public readonly wowupProtocolName = APP_PROTOCOL_NAME;
public collapseToTray = false;
public minimizeOnCloseDescription = "";
public startMinimized = false;
@@ -77,6 +82,9 @@ export class OptionsAppSectionComponent implements OnInit {
},
];
public curseforgeProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(CURSE_PROTOCOL_NAME));
public wowupProtocolHandled$ = from(this.electronService.isDefaultProtocolClient(APP_PROTOCOL_NAME));
public constructor(
private _analyticsService: AnalyticsService,
private _dialog: MatDialog,
@@ -105,7 +113,6 @@ export class OptionsAppSectionComponent implements OnInit {
this.useHardwareAcceleration = this.wowupService.useHardwareAcceleration;
this.startWithSystem = this.wowupService.getStartWithSystem();
this.startMinimized = this.wowupService.startMinimized;
this.protocolRegistered = this.wowupService.protocolRegistered;
this.currentLanguage = this.wowupService.currentLanguage;
this.useSymlinkMode = this.wowupService.useSymlinkMode;
@@ -115,6 +122,13 @@ export class OptionsAppSectionComponent implements OnInit {
this.currentScale = zoomFactor;
this._cdRef.detectChanges();
});
this.electronService
.isDefaultProtocolClient(APP_PROTOCOL_NAME)
.then((isDefault) => {
this.protocolRegistered = isDefault;
})
.catch((e) => console.error(e));
}
private async initScale() {
@@ -149,11 +163,11 @@ export class OptionsAppSectionComponent implements OnInit {
await this.wowupService.setStartMinimized(evt.checked);
};
public onProtocolRegisteredChange = async (evt: MatSlideToggleChange): Promise<void> => {
public onProtocolHandlerChange = async (evt: MatSlideToggleChange, protocol: string): Promise<void> => {
try {
await this.wowupService.setProtocolRegistered(evt.checked);
await this.setProtocolHandler(protocol, evt.checked);
} catch (e) {
console.error(`onProtocolRegisteredChange failed`, e);
console.error(`onProtocolHandlerChange failed: ${protocol}`, e);
}
};
@@ -260,4 +274,12 @@ export class OptionsAppSectionComponent implements OnInit {
private async updateScale() {
this.currentScale = await this.electronService.getZoomFactor();
}
private setProtocolHandler(protocol: string, enabled: boolean): Promise<boolean> {
if (enabled) {
return this.electronService.setAsDefaultProtocolClient(protocol);
} else {
return this.electronService.removeAsDefaultProtocolClient(protocol);
}
}
}

View File

@@ -1,15 +1,7 @@
<span>
<div class="addon-column">
<div class="thumbnail-container bg-secondary-3">
<div *ngIf="hasThumbnail === true" class="addon-logo-container ">
<img [src]="addon.thumbnailUrl" loading="lazy" />
</div>
<div *ngIf="hasThumbnail === false" class="addon-logo-container">
<div class="addon-logo-letter text-3">
{{ thumbnailLetter }}
</div>
</div>
<app-addon-thumbnail [url]="addon.thumbnailUrl" [name]="addon.name"></app-addon-thumbnail>
</div>
<div style="display: flex; flex-direction: column; justify-content: space-between; min-height: 40px">
<a class="addon-title hover-text-2 " (click)="viewDetails()">{{ addon.name }}</a>

View File

@@ -29,6 +29,9 @@ export interface CurseGetFeaturedResponse {
RecentlyUpdated: CurseSearchResult[];
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CurseAddonFileResponse extends CurseFile {}
export interface CurseSearchResult {
id: number;
name: string;

View File

@@ -0,0 +1,9 @@
import { WowClientType } from "common/warcraft/wow-client-type";
import { AddonSearchResult } from "./addon-search-result";
export interface ProtocolSearchResult extends AddonSearchResult {
protocol: string;
protocolAddonId?: string;
protocolReleaseId?: string;
validClientTypes: WowClientType[];
}

View File

@@ -1,11 +1,16 @@
import { from, interval } from "rxjs";
import { from, Subscription } from "rxjs";
import { filter, first, map, switchMap, tap } from "rxjs/operators";
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, NgZone, OnDestroy } from "@angular/core";
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from "@angular/core";
import { MatSnackBar } from "@angular/material/snack-bar";
import { TranslateService } from "@ngx-translate/core";
import { IPC_POWER_MONITOR_RESUME, IPC_POWER_MONITOR_UNLOCK } from "../../../common/constants";
import {
APP_PROTOCOL_NAME,
CURSE_PROTOCOL_NAME,
IPC_POWER_MONITOR_RESUME,
IPC_POWER_MONITOR_UNLOCK,
} from "../../../common/constants";
import { AppConfig } from "../../../environments/environment";
import { AddonScanError } from "../../errors";
import { WowInstallation } from "../../models/wowup/wow-installation";
@@ -14,6 +19,9 @@ import { AddonService, ScanUpdate, ScanUpdateType } from "../../services/addons/
import { SessionService } from "../../services/session/session.service";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { WowUpService } from "../../services/wowup/wowup.service";
import { DialogFactory } from "../../services/dialog/dialog.factory";
import { getProtocol } from "../../utils/string.utils";
import { InstallFromProtocolDialogComponent } from "app/components/install-from-protocol-dialog/install-from-protocol-dialog.component";
@Component({
selector: "app-home",
@@ -23,6 +31,7 @@ import { WowUpService } from "../../services/wowup/wowup.service";
})
export class HomeComponent implements AfterViewInit, OnDestroy {
private _appUpdateInterval?: number;
private _subscriptions: Subscription[] = [];
public selectedIndex = 0;
public hasWowClient = false;
@@ -37,22 +46,61 @@ export class HomeComponent implements AfterViewInit, OnDestroy {
private _wowupService: WowUpService,
private _snackBar: MatSnackBar,
private _cdRef: ChangeDetectorRef,
private _warcraftInstallationService: WarcraftInstallationService
private _warcraftInstallationService: WarcraftInstallationService,
private _dialogFactory: DialogFactory
) {
this._warcraftInstallationService.wowInstallations$.subscribe((installations) => {
const wowInstalledSub = this._warcraftInstallationService.wowInstallations$.subscribe((installations) => {
this.hasWowClient = installations.length > 0;
this.selectedIndex = this.hasWowClient ? 0 : 3;
});
this._addonService.scanError$.subscribe(this.onAddonScanError);
const customProtocolSub = this.electronService.customProtocol$
.pipe(
filter((protocol) => !!protocol),
switchMap((protocol) => from(this.handleCustomProtocol(protocol)))
)
.subscribe();
this._addonService.scanUpdate$
const scanErrorSub = this._addonService.scanError$.subscribe(this.onAddonScanError);
const scanUpdateSub = this._addonService.scanUpdate$
.pipe(filter((update) => update.type !== ScanUpdateType.Unknown))
.subscribe(this.onScanUpdate);
this._subscriptions.push(customProtocolSub, wowInstalledSub, scanErrorSub, scanUpdateSub);
}
private handleCustomProtocol = async (protocol: string): Promise<void> => {
console.debug("PROTOCOL RECEIEVED", protocol);
const protocolName = getProtocol(protocol);
try {
switch (protocolName) {
case APP_PROTOCOL_NAME:
case CURSE_PROTOCOL_NAME:
await this.handleAddonInstallProtocol(protocol);
break;
default:
console.warn(`Unknown protocol: ${protocol}`);
return;
}
} catch (e) {
console.error(`Failed to handle protocol`, e);
}
};
private async handleAddonInstallProtocol(protocol: string) {
const dialog = this._dialogFactory.getDialog(InstallFromProtocolDialogComponent, {
disableClose: true,
data: {
protocol,
},
});
await dialog.afterClosed().toPromise();
}
public ngAfterViewInit(): void {
this.electronService.powerMonitor$.pipe(filter((evt) => !!evt)).subscribe((evt) => {
const powerMonitorSub = this.electronService.powerMonitor$.pipe(filter((evt) => !!evt)).subscribe((evt) => {
console.log("Stopping app update check...");
this.destroyAppUpdateCheck();
@@ -61,9 +109,7 @@ export class HomeComponent implements AfterViewInit, OnDestroy {
}
});
this.initAppUpdateCheck();
this._warcraftInstallationService.wowInstallations$
const wowInstallInitialSub = this._warcraftInstallationService.wowInstallations$
.pipe(
first((installations) => installations.length > 0),
switchMap((installations) => {
@@ -74,10 +120,15 @@ export class HomeComponent implements AfterViewInit, OnDestroy {
this.appReady = true;
this.detectChanges();
});
this.initAppUpdateCheck();
this._subscriptions.push(powerMonitorSub, wowInstallInitialSub);
}
public ngOnDestroy(): void {
window.clearInterval(this._appUpdateInterval);
this._subscriptions.forEach((sub) => sub.unsubscribe());
}
private initAppUpdateCheck() {

View File

@@ -13,6 +13,7 @@ import { ConfirmDialogComponent } from "../../components/confirm-dialog/confirm-
import { FundingButtonComponent } from "../../components/funding-button/funding-button.component";
import { GetAddonStatusColumnComponent } from "../../components/get-addon-status-column/get-addon-status-column.component";
import { InstallFromUrlDialogComponent } from "../../components/install-from-url-dialog/install-from-url-dialog.component";
import { InstallFromProtocolDialogComponent } from "../../components/install-from-protocol-dialog/install-from-protocol-dialog.component";
import { MyAddonStatusColumnComponent } from "../../components/my-addon-status-column/my-addon-status-column.component";
import { MyAddonsAddonCellComponent } from "../../components/my-addons-addon-cell/my-addons-addon-cell.component";
import { OptionsAddonSectionComponent } from "../../components/options-addon-section/options-addon-section.component";
@@ -24,6 +25,7 @@ import { ProgressButtonComponent } from "../../components/progress-button/progre
import { ProgressSpinnerComponent } from "../../components/progress-spinner/progress-spinner.component";
import { TelemetryDialogComponent } from "../../components/telemetry-dialog/telemetry-dialog.component";
import { WowClientOptionsComponent } from "../../components/wow-client-options/wow-client-options.component";
import { AddonThumbnailComponent } from "../../components/addon-thumbnail/addon-thumbnail.component";
import { TableContextHeaderCellComponent } from "../../components/table-context-header-cell/table-context-header-cell.component";
import { CellWrapTextComponent } from "../../components/cell-wrap-text/cell-wrap-text.component";
import { DirectiveModule } from "../../directive.module";
@@ -59,6 +61,7 @@ import { HomeComponent } from "./home.component";
AlertDialogComponent,
WowClientOptionsComponent,
InstallFromUrlDialogComponent,
InstallFromProtocolDialogComponent,
AddonDetailComponent,
AddonInstallButtonComponent,
GetAddonStatusColumnComponent,
@@ -73,6 +76,7 @@ import { HomeComponent } from "./home.component";
CenteredSnackbarComponent,
TableContextHeaderCellComponent,
CellWrapTextComponent,
AddonThumbnailComponent,
],
imports: [
CommonModule,

View File

@@ -13,7 +13,7 @@ import {
import * as _ from "lodash";
import { join } from "path";
import { from, Observable, of, Subject, Subscription, zip } from "rxjs";
import { catchError, debounceTime, map, switchMap, tap } from "rxjs/operators";
import { catchError, debounceTime, first, map, switchMap, tap } from "rxjs/operators";
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core";
@@ -195,6 +195,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.subscriptions.push(
this._sessionService.selectedHomeTab$.subscribe(this.onSelectedTabChange),
this._sessionService.addonsChanged$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
this._sessionService.targetFileInstallComplete$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
this.addonService.addonInstalled$.subscribe(this.onAddonInstalledEvent),
this.addonService.addonRemoved$.subscribe(this.onAddonRemoved),
filterInputSub
@@ -531,6 +532,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.getRemoveAddonPrompt(addon.name)
.afterClosed()
.pipe(
first(),
switchMap((result) => {
if (!result) {
return of(undefined);
@@ -607,6 +609,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
dialogRef
.afterClosed()
.pipe(
first(),
switchMap((result) => {
if (!result) {
return of(undefined);
@@ -708,6 +711,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
dialogRef
.afterClosed()
.pipe(
first(),
switchMap((result) => {
if (!result) {
return of(undefined);
@@ -907,31 +911,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
} finally {
this._cdRef.detectChanges();
}
// return from().pipe(
// map((addons) => {
// const rowData = this.formatAddons(addons);
// this.enableControls = this.calculateControlState();
// this.rowData = rowData;
// this.gridApi.setRowData(rowData);
// this.gridApi.redrawRows();
// this.isBusy = false;
// this.setPageContextText();
// this._cdRef.detectChanges();
// }),
// catchError((e) => {
// console.error(e);
// this.isBusy = false;
// this.enableControls = this.calculateControlState();
// return of(undefined);
// }),
// tap(() => {
// this._cdRef.detectChanges();
// })
// );
};
private formatAddons(addons: Addon[]): AddonViewModel[] {
@@ -977,6 +956,10 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
private onAddonInstalledEvent = (evt: AddonUpdateEvent) => {
try {
if (evt.addon.installationId !== this.selectedInstallationId) {
return;
}
if ([AddonInstallState.Complete, AddonInstallState.Error].includes(evt.installState) === false) {
this.enableControls = false;
return;
@@ -1013,11 +996,11 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
};
private onAddonRemoved = (addonId: string) => {
const addons: AddonViewModel[] = this.rowData.slice();
const listItemIdx = addons.findIndex((li) => li.addon.id === addonId);
addons.splice(listItemIdx, 1);
const listItemIdx = this._baseRowData.findIndex((li) => li.addon.id === addonId);
this._baseRowData.splice(listItemIdx, 1);
this.rowData = addons;
this.rowData = [...this._baseRowData];
this._cdRef.detectChanges();
};
private showErrorMessage(title: string, message: string) {

View File

@@ -32,10 +32,12 @@ import { AddonSearchResult } from "../../models/wowup/addon-search-result";
import { AddonSearchResultDependency } from "../../models/wowup/addon-search-result-dependency";
import { AddonSearchResultFile } from "../../models/wowup/addon-search-result-file";
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
import { ProtocolSearchResult } from "../../models/wowup/protocol-search-result";
import { Toc } from "../../models/wowup/toc";
import { WowInstallation } from "../../models/wowup/wow-installation";
import * as AddonUtils from "../../utils/addon.utils";
import { getEnumName } from "../../utils/enum.utils";
import * as SearchResults from "../../utils/search-result.utils";
import { capitalizeString } from "../../utils/string.utils";
import { AnalyticsService } from "../analytics/analytics.service";
import { DownloadService } from "../download/download.service";
@@ -46,7 +48,6 @@ import { WarcraftInstallationService } from "../warcraft/warcraft-installation.s
import { WarcraftService } from "../warcraft/warcraft.service";
import { WowUpService } from "../wowup/wowup.service";
import { AddonProviderFactory } from "./addon.provider.factory";
import * as SearchResults from "../../utils/search-result.utils";
export enum ScanUpdateType {
Start,
@@ -312,14 +313,20 @@ export class AddonService {
public async installPotentialAddon(
potentialAddon: AddonSearchResult,
installation: WowInstallation,
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined,
targetFile?: AddonSearchResultFile
): Promise<void> {
const existingAddon = this._addonStorage.getByExternalId(potentialAddon.externalId, installation.id);
if (existingAddon) {
throw new Error("Addon already installed");
}
const addon = await this.getAddon(potentialAddon.externalId, potentialAddon.providerName, installation).toPromise();
const addon = await this.getAddon(
potentialAddon.externalId,
potentialAddon.providerName,
installation,
targetFile
).toPromise();
this._addonStorage.set(addon.id, addon);
await this.installAddon(addon.id, onUpdate);
@@ -477,8 +484,6 @@ export class AddonService {
};
this._installQueue.next(installQueueItem);
onUpdate?.call(this, AddonInstallState.Pending, 0);
return promise;
}
@@ -816,7 +821,8 @@ export class AddonService {
public getAddon(
externalId: string,
providerName: string,
installation: WowInstallation
installation: WowInstallation,
targetFile?: AddonSearchResultFile
): Observable<Addon | undefined> {
const targetAddonChannel = installation.defaultAddonChannelType;
const provider = this.getProvider(providerName);
@@ -827,7 +833,8 @@ export class AddonService {
}
const latestFile = SearchResults.getLatestFile(searchResult, targetAddonChannel);
return this.createAddon(latestFile.folders[0], searchResult, latestFile, installation);
const newAddon = this.createAddon(latestFile.folders[0], searchResult, targetFile ?? latestFile, installation);
return newAddon;
})
);
}
@@ -920,6 +927,19 @@ export class AddonService {
return addons;
}
public async getAddonForProtocol(protocol: string): Promise<ProtocolSearchResult> {
const addonProvider = this.getAddonProviderForProtocol(protocol);
if (!addonProvider) {
throw new Error(`No addon provider found for protocol ${protocol}`);
}
return await addonProvider.searchProtocol(protocol);
}
private getAddonProviderForProtocol(protocol: string): AddonProvider {
return _.find(this.getEnabledAddonProviders(), (provider) => provider.isValidProtocol(protocol));
}
public async syncAllClients(): Promise<void> {
const installations = this._warcraftInstallationService.getWowInstallations();
for (const installation of installations) {

View File

@@ -1,5 +1,6 @@
import { ComponentType } from "@angular/cdk/portal";
import { Injectable } from "@angular/core";
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
import { MatDialog, MatDialogConfig, MatDialogRef } from "@angular/material/dialog";
import { TranslateService } from "@ngx-translate/core";
import { AddonChannelType } from "../../../common/wowup/models";
@@ -62,4 +63,8 @@ export class DialogFactory {
data,
});
}
public getDialog<T, K>(component: ComponentType<T>, config?: MatDialogConfig<K>): MatDialogRef<T, any> {
return this._dialog.open(component, config);
}
}

View File

@@ -1,18 +1,28 @@
// If you import a module but never use any of the imported values other than as TypeScript types,
// the resulting javascript file will look as if you never imported the module at all.
import { IpcRendererEvent, OpenDialogOptions, OpenDialogReturnValue, OpenExternalOptions, Settings } from "electron";
import { LoginItemSettings } from "electron/main";
import { find } from "lodash";
import * as minimist from "minimist";
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";
import { Injectable } from "@angular/core";
import {
APP_UPDATE_CHECK_END,
APP_UPDATE_CHECK_START,
APP_UPDATE_DOWNLOADED,
APP_UPDATE_START_DOWNLOAD,
IPC_CLOSE_WINDOW,
IPC_CUSTOM_PROTOCOL_RECEIVED,
IPC_FOCUS_WINDOW,
IPC_GET_APP_VERSION,
IPC_GET_LAUNCH_ARGS,
IPC_GET_LOCALE,
IPC_GET_LOGIN_ITEM_SETTINGS,
IPC_GET_ZOOM_FACTOR,
IPC_SET_LOGIN_ITEM_SETTINGS,
IPC_SET_ZOOM_FACTOR,
IPC_SET_ZOOM_LIMITS,
IPC_IS_DEFAULT_PROTOCOL_CLIENT,
IPC_MAXIMIZE_WINDOW,
IPC_MINIMIZE_WINDOW,
IPC_POWER_MONITOR_LOCK,
@@ -20,33 +30,27 @@ import {
IPC_POWER_MONITOR_SUSPEND,
IPC_POWER_MONITOR_UNLOCK,
IPC_QUIT_APP,
IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT,
IPC_RESTART_APP,
IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT,
IPC_SET_LOGIN_ITEM_SETTINGS,
IPC_SET_ZOOM_FACTOR,
IPC_SET_ZOOM_LIMITS,
IPC_WINDOW_LEAVE_FULLSCREEN,
IPC_WINDOW_MAXIMIZED,
IPC_WINDOW_MINIMIZED,
IPC_WINDOW_UNMAXIMIZED,
ZOOM_FACTOR_KEY,
IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT,
IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT,
APP_PROTOCOL_NAME,
IPC_FOCUS_WINDOW,
} from "../../../common/constants";
import * as minimist from "minimist";
import * as log from "electron-log";
// If you import a module but never use any of the imported values other than as TypeScript types,
// the resulting javascript file will look as if you never imported the module at all.
import { IpcRendererEvent, OpenDialogOptions, OpenDialogReturnValue, OpenExternalOptions, Settings } from "electron";
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";
import { IpcRequest } from "../../../common/models/ipc-request";
import { IpcResponse } from "../../../common/models/ipc-response";
import { ValueRequest } from "../../../common/models/value-request";
import { ValueResponse } from "../../../common/models/value-response";
import { AppOptions } from "../../../common/wowup/models";
import { ZoomDirection, ZOOM_SCALE } from "../../utils/zoom.utils";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { MainChannels, RendererChannels } from "../../../common/wowup";
import { LoginItemSettings } from "electron/main";
import { AppOptions } from "../../../common/wowup/models";
import { isProtocol } from "../../utils/string.utils";
import { ZOOM_SCALE, ZoomDirection } from "../../utils/zoom.utils";
import { PreferenceStorageService } from "../storage/preference-storage.service";
@Injectable({
providedIn: "root",
@@ -57,6 +61,7 @@ export class ElectronService {
private readonly _ipcEventReceivedSrc = new BehaviorSubject("");
private readonly _zoomFactorChangeSrc = new BehaviorSubject(1.0);
private readonly _powerMonitorSrc = new BehaviorSubject("");
private readonly _customProtocolSrc = new BehaviorSubject("");
private _appVersion = "";
@@ -65,6 +70,7 @@ export class ElectronService {
public readonly ipcEventReceived$ = this._ipcEventReceivedSrc.asObservable();
public readonly zoomFactor$ = this._zoomFactorChangeSrc.asObservable();
public readonly powerMonitor$ = this._powerMonitorSrc.asObservable();
public readonly customProtocol$ = this._customProtocolSrc.asObservable();
public readonly isWin = process.platform === "win32";
public readonly isMac = process.platform === "darwin";
public readonly isLinux = process.platform === "linux";
@@ -121,6 +127,11 @@ export class ElectronService {
this._windowMaximizedSrc.next(false);
});
this.onRendererEvent(IPC_CUSTOM_PROTOCOL_RECEIVED, (evt, protocol: string) => {
console.debug(IPC_CUSTOM_PROTOCOL_RECEIVED, protocol);
this._customProtocolSrc.next(protocol);
});
this.onRendererEvent(IPC_POWER_MONITOR_LOCK, () => {
console.log("POWER_MONITOR_LOCK received");
this._powerMonitorSrc.next(IPC_POWER_MONITOR_LOCK);
@@ -180,33 +191,32 @@ export class ElectronService {
return this.invoke(IPC_SET_LOGIN_ITEM_SETTINGS, settings);
}
public setAsDefaultProtocolClient(): Promise<void> {
return this.invoke(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT);
public isDefaultProtocolClient(protocol: string): Promise<boolean> {
return this.invoke(IPC_IS_DEFAULT_PROTOCOL_CLIENT, protocol);
}
public async removeAsDefaultProtocolClient(): Promise<void> {
return this.invoke(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT);
public setAsDefaultProtocolClient(protocol: string): Promise<boolean> {
return this.invoke(IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT, protocol);
}
public async removeAsDefaultProtocolClient(protocol: string): Promise<boolean> {
return this.invoke(IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT, protocol);
}
public async getAppOptions(): Promise<AppOptions> {
// TODO check protocols here
const launchArgs = await this.invoke(IPC_GET_LAUNCH_ARGS);
let opts;
opts = (<any>minimist(launchArgs.slice(1), {
const opts = (<any>minimist(launchArgs.slice(1), {
boolean: ["hidden", "quit"],
string: ["install"],
})) as AppOptions;
launchArgs.slice(1).forEach((arg) => {
try {
const url = new URL(arg);
if (url && url.protocol == APP_PROTOCOL_NAME + ":") {
opts = (<any>{ install: url.searchParams.get("install") }) as AppOptions;
}
} catch (e) {
console.error(e);
}
});
// Find the first protocol arg if any exist
const customProtocol = find(launchArgs, (arg) => isProtocol(arg));
if (customProtocol) {
// If we did get a custom protocol notify the app
this._customProtocolSrc.next(customProtocol);
}
return opts;
}
@@ -256,18 +266,6 @@ export class ElectronService {
return new Notification(title, options);
}
public isHandlingProtocol(protocol: string): boolean {
return window.wowup.isDefaultProtocolClient(protocol);
}
public setHandleProtocol(protocol: string, enable: boolean): boolean {
if (enable) {
return window.wowup.setAsDefaultProtocolClient(protocol);
} else {
return window.wowup.removeAsDefaultProtocolClient(protocol);
}
}
public showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogReturnValue> {
return window.wowup.showOpenDialog(options);
}

View File

@@ -20,6 +20,7 @@ import {
faCompressArrowsAlt,
faPencilAlt,
faArrowDown,
faCheckCircle,
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faClock } from "@fortawesome/free-regular-svg-icons";
import { faDiscord, faGithub, faPatreon } from "@fortawesome/free-brands-svg-icons";
@@ -51,6 +52,7 @@ export class IconService {
this.addSvg(faCoins);
this.addSvg(faCompressArrowsAlt);
this.addSvg(faPencilAlt);
this.addSvg(faCheckCircle);
}
private addSvg(icon: IconDefinition): void {

View File

@@ -1,6 +1,6 @@
import * as _ from "lodash";
import { BehaviorSubject, Subject } from "rxjs";
import { filter, first, map } from "rxjs/operators";
import { filter } from "rxjs/operators";
import { Injectable } from "@angular/core";
@@ -8,10 +8,7 @@ import { SELECTED_DETAILS_TAB_KEY } from "../../../common/constants";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service";
import { WarcraftService } from "../warcraft/warcraft.service";
import { WowUpService } from "../wowup/wowup.service";
import { ColumnState } from "../../models/wowup/column-state";
import { TranslateService } from "@ngx-translate/core";
@Injectable({
providedIn: "root",
@@ -24,6 +21,7 @@ export class SessionService {
private readonly _autoUpdateCompleteSrc = new BehaviorSubject(0);
private readonly _addonsChangedSrc = new Subject<boolean>();
private readonly _myAddonsColumnsSrc = new BehaviorSubject<ColumnState[]>([]);
private readonly _targetFileInstallCompleteSrc = new Subject<boolean>();
private readonly _getAddonsColumnsSrc = new Subject<ColumnState>();
@@ -37,13 +35,11 @@ export class SessionService {
public readonly addonsChanged$ = this._addonsChangedSrc.asObservable();
public readonly myAddonsHiddenColumns$ = this._myAddonsColumnsSrc.asObservable();
public readonly getAddonsHiddenColumns$ = this._getAddonsColumnsSrc.asObservable();
public readonly targetFileInstallComplete$ = this._targetFileInstallCompleteSrc.asObservable();
public constructor(
private _warcraftService: WarcraftService,
private _wowUpService: WowUpService,
private _warcraftInstallationService: WarcraftInstallationService,
private _preferenceStorageService: PreferenceStorageService,
private _translateService: TranslateService
private _preferenceStorageService: PreferenceStorageService
) {
this._selectedDetailTabType =
this._preferenceStorageService.getObject<DetailsTabType>(SELECTED_DETAILS_TAB_KEY) || "description";
@@ -53,6 +49,10 @@ export class SessionService {
.subscribe((installations) => this.onWowInstallationsChange(installations));
}
public notifyTargetFileInstallComplete(): void {
this._targetFileInstallCompleteSrc.next(true);
}
public notifyAddonsChanged(): void {
this._addonsChangedSrc.next(true);
}

View File

@@ -68,6 +68,10 @@ export class WarcraftInstallationService {
return _.filter(this.getWowInstallations(), (installation) => installation.clientType === clientType);
}
public getWowInstallationsByClientTypes(clientTypes: WowClientType[]): WowInstallation[] {
return _.filter(this.getWowInstallations(), (installation) => clientTypes.includes(installation.clientType));
}
public setWowInstallations(wowInstallations: WowInstallation[]): void {
console.log(`Setting wow installations: ${wowInstallations.length}`);
this._preferenceStorageService.setObject(WOW_INSTALLATIONS_KEY, wowInstallations);

View File

@@ -235,19 +235,6 @@ export class WowUpService {
await this.setAutoStartup();
}
public get protocolRegistered(): boolean {
const preference = this._preferenceStorageService.findByKey(PROTOCOL_REGISTERED_PREFERENCE_KEY);
return preference === "true";
}
public async setProtocolRegistered(value: boolean): Promise<void> {
const key = PROTOCOL_REGISTERED_PREFERENCE_KEY;
this._preferenceStorageService.set(key, value);
this._preferenceChangeSrc.next({ key, value: value.toString() });
await this.registerProtocol();
}
public get wowUpReleaseChannel(): WowUpReleaseChannelType {
const preference = this._preferenceStorageService.findByKey(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY);
return parseInt(preference, 10) as WowUpReleaseChannelType;
@@ -479,12 +466,4 @@ export class WowUpService {
});
}
}
private registerProtocol(): Promise<void> {
if (this.protocolRegistered) {
return this._electronService.setAsDefaultProtocolClient();
} else {
return this._electronService.removeAsDefaultProtocolClient();
}
}
}

View File

@@ -19,3 +19,12 @@ export function getSha1Hash(str: string): string {
export function capitalizeString(str: string): string {
return str.charAt(0).toUpperCase() + str.toLowerCase().slice(1);
}
export function isProtocol(arg: string): boolean {
return getProtocol(arg) != null;
}
export function getProtocol(arg: string): string | null {
const match = /^([a-z][a-z0-9+\-.]*):/.exec(arg);
return match !== null && match.length > 1 ? match[1] : null;
}

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Ne",
"POSITIVE_BUTTON": "Ano"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "URL adresa addonu",
"ADDON_URL_INPUT_PLACEHOLDER": "GitHub nebo WowInterface URL adresa",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Jazyk",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Povolí různé systémové notifikace, např. po automatické aktualizaci addonů.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Povolit systémové notifikace",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Minimalizovat do lišty při uzavírání WowUp okna.",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Nein",
"POSITIVE_BUTTON": "Ja"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "Addon-URL",
"ADDON_URL_INPUT_PLACEHOLDER": "z.B. GitHub- oder WoWInterface-URL",
@@ -333,13 +345,15 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Aktuelle Sprache",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktivieren/Deaktivieren verschiedener Systembenachrichtigungen",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Systembenachrichtigungen aktivieren",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Beim Schließen WowUp in die Menüleiste minimieren",
"MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "Beim Schließen des WowUp-Fensters in den Benachrichtigungsbereich der Taskleiste minimieren.",
"MINIMIZE_ON_CLOSE_LABEL": "Minimieren beim Schließen",
"PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquries",
"PROTOCOL_LABEL": "Allow WowUp to handle wowup:// URI",
"MINIMIZE_ON_CLOSE_LABEL": "Minimieren beim Schließen",
"SCALE_DESCRIPTION": "Den Zoomfaktor für die ganze Anwendung verändern.",
"SCALE_LABEL": "Skalierung",
"SET_LANGUAGE_CONFIRMATION_DESCRIPTION": "Zum Ändern der Standardsprache muss die Anwendung neu gestartet werden.",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "No",
"POSITIVE_BUTTON": "Yes"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "Addon URL",
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub or WowInterface URL",
@@ -333,12 +345,14 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Current Language",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Enable various system notification popups, such as auto updated addons.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Enable system notifications",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "When closing the WowUp window, minimize to the menu bar.",
"MINIMIZE_ON_CLOSE_DESCRIPTION_WINDOWS": "When closing the WowUp window, minimize to the taskbar notification area.",
"MINIMIZE_ON_CLOSE_LABEL": "Minimize on Close",
"PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquries",
"PROTOCOL_DESCRIPTION": "WowUp will register a custom URI protocol in your system and handle its incoming inquiries",
"PROTOCOL_LABEL": "Enable the wowup:// URI protocol",
"SCALE_DESCRIPTION": "Change zoom factor for entire app.",
"SCALE_LABEL": "Scale",
@@ -356,7 +370,7 @@
"THEME_LABEL": "Color Theme",
"TITLE": "Application",
"USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Do you want to restart?",
"USE_HARDWARE_ACCELERATION_DESCRIPTION": "Disabling hardware acceleration might solve FPS issues and fix other rendering issues in this app. Changing this setting requires a restart.",
"USE_HARDWARE_ACCELERATION_DESCRIPTION": "Disabling hardware acceleration might solve FPS issues and fix other rendering issues in this app.<br>Changing this setting requires a restart.",
"USE_HARDWARE_ACCELERATION_DISABLE_CONFIRMATION_DESCRIPTION": "Disabling hardware acceleration requires the application to restart.",
"USE_HARDWARE_ACCELERATION_ENABLE_CONFIRMATION_DESCRIPTION": "Enabling hardware acceleration requires the application to restart.",
"USE_HARDWARE_ACCELERATION_LABEL": "Enable Hardware Acceleration",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "No",
"POSITIVE_BUTTON": "Sí"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "URL del addon",
"ADDON_URL_INPUT_PLACEHOLDER": "Ejemplo: URL de GitHub o WowInterface",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Idioma actual",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activa varios mensajes de notificación del sistema, tales como cuando los addons son actualizados automáticamente.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activar notificaciones del sistema",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Al cerrar la ventana de WowUp, minimizarla a la barra de menú",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Non",
"POSITIVE_BUTTON": "Oui"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "URL de l'Addon",
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. URL de GitHub ou WowInterface",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Langue actuelle",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Activer les fenêtres contextuelles système comme la mise à jour auto des addons.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Activer le système de notification",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Lorsque vous fermez la fenêtre WowUp, minimise dans la barre des menus",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "No",
"POSITIVE_BUTTON": "Sì"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "Addon URL",
"ADDON_URL_INPUT_PLACEHOLDER": "Esempio URL di GitHub o WowInterface",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Lingua Corrente",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Abilita i vari popup di notifica del sistema, come gli addons aggiornati automaticamente.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Notifiche di Sistema",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Alla chiusura, WowUp verrà ridotto a icona nella barra dei menu.",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "취소",
"POSITIVE_BUTTON": "확인"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "애드온 URL 주소",
"ADDON_URL_INPUT_PLACEHOLDER": "예) GitHub 또는 WowInterface URL",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "현재 언어",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "애드온 자동업데이트 등과 같은 다양한 시스템 알림 팝업 활성화 여부",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "시스템 알림 활성화",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "창을 닫으면 메뉴바로 최소화",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Nei",
"POSITIVE_BUTTON": "Ja"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "Utvidelsens URL",
"ADDON_URL_INPUT_PLACEHOLDER": "F.Eks. GitHub eller WowInterface URL",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Current Language",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Aktiver/Deaktiver forskjellige systemvarsler, foreksempel: automatisk oppdatering av addons.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Aktiver systemvarsler",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Når WowUp-vinduet lukkes, minimer til menylinjen.",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Não",
"POSITIVE_BUTTON": "Sim"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "URL do Addon ",
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub ou WowInterface URL",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Língua Atual",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Habilitar várias notificações e popups do sistema, como os de aviso de Addons atualizados automaticamente.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Habilitar notificações do sistema",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "Ao fechar a janela do WowUp, minimize para a barra de menu",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "Нет",
"POSITIVE_BUTTON": "Да"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "Ссылка на модификацию",
"ADDON_URL_INPUT_PLACEHOLDER": "Например ссылки GitHub или WowInterface",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "Текущий язык",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "Включить различные окна системных уведомлений, такие как автоматически обновлённые модификации.",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "Включить системные уведомления",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "При закрытии окна WowUp сворачивается в меню статуса.",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "否",
"POSITIVE_BUTTON": "是"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "插件 URL",
"ADDON_URL_INPUT_PLACEHOLDER": "例如GitHub 或 WowInterface URL",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "當前語言",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "啟用各種系統通知彈窗,如自動更新插件通知。",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "啟用系統通知",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "關閉 WowUp 視窗時,最小化到選單欄。",

View File

@@ -178,6 +178,18 @@
"NEGATIVE_BUTTON": "否",
"POSITIVE_BUTTON": "是"
},
"INSTALL_FROM_PROTOCOL": {
"ADDON_INSTALLED": "Addon is installed!",
"ADDON_INSTALLING": "Installing addon",
"CANCEL_BUTTON": "Close",
"ERRORS": {
"ADDON_NOT_FOUND": "No addon was found for the protocol: {protocol}",
"GENERIC": "Error fetching data for protocol: {protocol}",
"NO_VALID_WOW_INSTALLATIONS": "No WoW client installed for protocol: {protocol}"
},
"INSTALL_BUTTON": "Install",
"TITLE": "Install Addon From {providerName}"
},
"INSTALL_FROM_URL": {
"ADDON_URL_INPUT_LABEL": "插件 URL",
"ADDON_URL_INPUT_PLACEHOLDER": "例如GitHub 或 WowInterface URL",
@@ -333,6 +345,8 @@
},
"APPLICATION": {
"CURRENT_LANGUAGE_LABEL": "当前语言",
"CURSE_PROTOCOL_DESCRIPTION": "When downloading addons from the CurseForge website, WowUp will handle the install",
"CURSE_PROTOCOL_LABEL": "Handle CurseForge download links",
"ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION": "启用各种系统通知弹窗,如自动更新插件通知。",
"ENABLE_SYSTEM_NOTIFICATIONS_LABEL": "启用系统通知",
"MINIMIZE_ON_CLOSE_DESCRIPTION_MAC": "关闭 WowUp 窗口时,最小化到菜单栏。",

View File

@@ -1,3 +1,5 @@
export const APP_USER_MODEL_ID = "io.wowup.jliddev"; // Bundle ID
export const ADDON_PROVIDER_WOWINTERFACE = "WowInterface";
export const ADDON_PROVIDER_CURSEFORGE = "Curse";
export const ADDON_PROVIDER_GITHUB = "GitHub";
@@ -7,7 +9,9 @@ export const ADDON_PROVIDER_UNKNOWN = "Unknown";
export const ADDON_PROVIDER_HUB_LEGACY = "Hub";
export const ADDON_PROVIDER_HUB = "WowUpHub";
export const ADDON_PROVIDER_ZIP = "Zip";
export const APP_PROTOCOL_NAME = "wowup";
export const CURSE_PROTOCOL_NAME = "curseforge";
// IPC CHANNELS
export const IPC_DOWNLOAD_FILE_CHANNEL = "download-file";
@@ -60,9 +64,11 @@ export const IPC_GET_LOGIN_ITEM_SETTINGS = "get-login-item-settings";
export const IPC_SET_LOGIN_ITEM_SETTINGS = "set-login-item-settings";
export const IPC_LIST_ENTRIES = "list-entries";
export const IPC_READDIR = "readdir";
export const IPC_IS_DEFAULT_PROTOCOL_CLIENT = "is-default-protocol-client";
export const IPC_SET_AS_DEFAULT_PROTOCOL_CLIENT = "set-as-default-protocol-client";
export const IPC_REMOVE_AS_DEFAULT_PROTOCOL_CLIENT = "remove-as-default-protocol-client";
export const IPC_REQUEST_INSTALL_FROM_URL = "request-install-from-url";
export const IPC_CUSTOM_PROTOCOL_RECEIVED = "custom-protocol-received";
export const IPC_ADDONS_SAVE_ALL = "addons-save-all";
// PREFERENCES

View File

@@ -16,7 +16,8 @@ declare type MainChannels =
| "power-monitor-suspend"
| "power-monitor-lock"
| "power-monitor-unlock"
| "request-install-from-url";
| "request-install-from-url"
| "custom-protocol-received";
// Events that can be sent from renderer to main
declare type RendererChannels =
@@ -56,6 +57,7 @@ declare type RendererChannels =
| "list-entries"
| "list-files"
| "readdir"
| "is-default-protocol-client"
| "set-as-default-protocol-client"
| "remove-as-default-protocol-client"
| "read-file-buffer"
@@ -76,9 +78,6 @@ declare global {
rendererInvoke: (channel: string, ...args: any[]) => Promise<any>;
rendererOff: (event: string | symbol, listener: (...args: any[]) => void) => void;
rendererOn: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => void;
isDefaultProtocolClient: (protocol: string, path?: string, args?: string[]) => boolean;
setAsDefaultProtocolClient: (protocol: string, path?: string, args?: string[]) => boolean;
removeAsDefaultProtocolClient: (protocol: string, path?: string, args?: string[]) => boolean;
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>;
showOpenDialog: (options: OpenDialogOptions) => Promise<OpenDialogReturnValue>;
openPath: (path: string) => Promise<string>;

View File

@@ -24,7 +24,6 @@ export interface AppOptions {
serve?: boolean;
hidden?: boolean;
quit?: boolean;
install?: string;
}
export interface MenuConfig {

View File

@@ -80,6 +80,14 @@ img {
padding: 0 0.25em;
}
.pt-1 {
padding-top: 0.25em;
}
.pt-2 {
padding-top: 0.5em;
}
.pt-3 {
padding-top: 1em;
}
@@ -128,6 +136,14 @@ img {
margin-bottom: 0.25em !important;
}
.mb-2 {
margin-bottom: 0.5em !important;
}
.mb-3 {
margin-bottom: 1em !important;
}
.w-100 {
width: 100%;
}
@@ -164,6 +180,15 @@ img {
}
}
.option-icon {
width: 17px !important;
height: 17px !important;
}
mat-icon.success-icon {
color: green;
}
.mat-tab-label,
.mat-tab-label-content,
.mat-select-value,