mirror of
https://github.com/WowUp/WowUp.git
synced 2026-04-23 07:17:00 -04:00
#743 Improvements to the installtion management flow
This commit is contained in:
@@ -7,29 +7,20 @@
|
||||
<div class="flex-grow-1">
|
||||
{{ "PAGES.OPTIONS.WOW.RESCAN_CLIENTS_LABEL" | translate }}
|
||||
</div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="onReScan()"
|
||||
appUserActionTracker
|
||||
category="Options"
|
||||
action="ScanInstalls"
|
||||
class="mr-3"
|
||||
>
|
||||
<button mat-stroked-button (click)="onReScan()" class="mr-3">
|
||||
{{ "PAGES.OPTIONS.WOW.RESCAN_CLIENTS_BUTTON" | translate }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="onReScan()"
|
||||
appUserActionTracker
|
||||
category="Options"
|
||||
action="ScanInstalls"
|
||||
>
|
||||
<button mat-flat-button color="primary" (click)="onAddNew()">
|
||||
{{ "PAGES.OPTIONS.WOW.ADD_CLIENT_BUTTON" | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<app-wow-client-options class="client-section" *ngFor="let installation of wowInstallations$ | async" [installationId]="installation.id">
|
||||
<h2 *ngIf="(wowInstallations$ | async).length === 0" class="pt-3">
|
||||
No World of Warcraft installations found, please make sure your Battle.net client is up to date or add a client
|
||||
manually
|
||||
</h2>
|
||||
<app-wow-client-options class="client-section" *ngFor="let installation of wowInstallations$ | async"
|
||||
[installationId]="installation.id">
|
||||
</app-wow-client-options>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { MatSelectChange } from "@angular/material/select";
|
||||
import { WowInstallation } from "app/models/wowup/wow-installation";
|
||||
import { ElectronService } from "app/services";
|
||||
import { WarcraftInstallationService } from "app/services/warcraft/warcraft-installation.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { catchError } from "rxjs/operators";
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { WowUpReleaseChannelType } from "../../models/wowup/wowup-release-channel-type";
|
||||
import { WarcraftService } from "../../services/warcraft/warcraft.service";
|
||||
import { WowUpService } from "../../services/wowup/wowup.service";
|
||||
import { getEnumList, getEnumName } from "../../utils/enum.utils";
|
||||
import * as _ from "lodash";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { AlertDialogComponent } from "../alert-dialog/alert-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-options-wow-section",
|
||||
@@ -31,9 +36,12 @@ export class OptionsWowSectionComponent implements OnInit {
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private _dialog: MatDialog,
|
||||
private _electronService: ElectronService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowupService: WowUpService,
|
||||
private _warcraftInstallationService: WarcraftInstallationService
|
||||
private _warcraftInstallationService: WarcraftInstallationService,
|
||||
private _translateService: TranslateService
|
||||
) {
|
||||
this.wowInstallations$ = _warcraftInstallationService.wowInstallations$;
|
||||
}
|
||||
@@ -42,11 +50,61 @@ export class OptionsWowSectionComponent implements OnInit {
|
||||
this.wowUpReleaseChannel = this._wowupService.wowUpReleaseChannel;
|
||||
}
|
||||
|
||||
public onReScan = () => {
|
||||
this._warcraftService.scanProducts().catch((e) => console.error(e));
|
||||
public onReScan = (): void => {
|
||||
this._warcraftInstallationService
|
||||
.importWowInstallations(this._warcraftInstallationService.blizzardAgentPath)
|
||||
.catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
public onWowUpChannelChange(evt: MatSelectChange) {
|
||||
this._wowupService.wowUpReleaseChannel = evt.value;
|
||||
public onAddNew(): void {
|
||||
from(this.addNewClient())
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
return of(undefined);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private async addNewClient() {
|
||||
const selectedPath = await this._warcraftInstallationService.selectWowClientPath();
|
||||
if (!selectedPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("dialogResult", selectedPath);
|
||||
|
||||
const isWowApplication = await this._warcraftService.isWowApplication(selectedPath);
|
||||
|
||||
if (!isWowApplication) {
|
||||
this.showInvalidWowApplication(selectedPath);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("isWowApplication", isWowApplication);
|
||||
|
||||
const wowInstallation = await this._warcraftInstallationService.createWowInstallationForPath(selectedPath);
|
||||
console.log("wowInstallation", wowInstallation);
|
||||
|
||||
this._warcraftInstallationService.addInstallation(wowInstallation);
|
||||
}
|
||||
|
||||
private showInvalidWowApplication(selectedPath: string) {
|
||||
const dialogMessage = this._translateService.instant("DIALOGS.SELECT_INSTALLATION.INVALID_INSTALLATION_PATH", {
|
||||
selectedPath,
|
||||
});
|
||||
|
||||
this.showError(dialogMessage);
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const title = this._translateService.instant("DIALOGS.ALERT.ERROR_TITLE");
|
||||
this._dialog.open(AlertDialogComponent, {
|
||||
data: {
|
||||
title,
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<mat-card>
|
||||
<mat-card-title>{{ clientTypeName | translate }}</mat-card-title>
|
||||
<mat-card-title class="row">
|
||||
<div *ngIf="editMode === false" class="flex-grow-1">{{ installationModel.label }}</div>
|
||||
<mat-form-field *ngIf="editMode === true" class="example-full-width">
|
||||
<input matInput [(ngModel)]="installationModel.label" />
|
||||
</mat-form-field>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<mat-form-field class="grow folder-input">
|
||||
<mat-label>{{
|
||||
"PAGES.OPTIONS.WOW.CLIENT_TYPE_PATH_LABEL" | translate: { clientTypeName: (clientTypeName | translate) }
|
||||
}}</mat-label>
|
||||
<input matInput disabled [value]="clientLocation" />
|
||||
<input matInput disabled [value]="installationModel.location" />
|
||||
<mat-hint class="text-2">
|
||||
{{
|
||||
"PAGES.OPTIONS.WOW.CLIENT_TYPE_INPUT_HINT"
|
||||
@@ -41,9 +46,21 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button mat-button color="warn" (click)="clearInstallPath()">Remove</button>
|
||||
<button mat-button color="primary" (click)="onSelectClientPath()">{{
|
||||
"PAGES.OPTIONS.WOW.OPEN_WOW_DIRECTORY_SELECT_BUTTON" | translate }}</button>
|
||||
<mat-card-actions class="row">
|
||||
<button *ngIf="editMode === true" mat-button color="warn" (click)="onClickRemove()">{{
|
||||
"PAGES.OPTIONS.WOW.REMOVE_WOW_DIRECTORY_SELECT_BUTTON"
|
||||
| translate }}</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div *ngIf="editMode === true">
|
||||
<button mat-button (click)="onClickCancel()">{{
|
||||
"PAGES.OPTIONS.WOW.CANCEL_WOW_DIRECTORY_SELECT_BUTTON" | translate }}</button>
|
||||
<button mat-button color="primary" (click)="onClickSave()">{{
|
||||
"PAGES.OPTIONS.WOW.SAVE_WOW_DIRECTORY_SELECT_BUTTON" | translate }}</button>
|
||||
</div>
|
||||
<div *ngIf="editMode === false">
|
||||
<button mat-button color="primary" (click)="editMode = true">{{
|
||||
"PAGES.OPTIONS.WOW.EDIT_WOW_DIRECTORY_SELECT_BUTTON" | translate }}</button>
|
||||
|
||||
</div>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
@@ -32,7 +32,7 @@ p {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
// padding: 0.5em;
|
||||
&.np {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { WarcraftInstallationService } from "app/services/warcraft/warcraft-inst
|
||||
import * as _ from "lodash";
|
||||
import * as path from "path";
|
||||
import { Subscription } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { AddonChannelType } from "../../models/wowup/addon-channel-type";
|
||||
import { ElectronService } from "../../services";
|
||||
@@ -26,6 +27,7 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
@Input("installationId") installationId: string;
|
||||
|
||||
private installation: WowInstallation;
|
||||
private installationModel: WowInstallation;
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
public readonly addonChannelInfos: {
|
||||
@@ -39,6 +41,17 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
public selectedAddonChannelType: AddonChannelType;
|
||||
|
||||
public clientAutoUpdate: boolean;
|
||||
public editMode = false;
|
||||
|
||||
public get installationLabel(): string {
|
||||
return this.installation?.label ?? "";
|
||||
}
|
||||
|
||||
public set installationLabel(input: string) {
|
||||
if (this.installation) {
|
||||
this.installation.label = input;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _dialog: MatDialog,
|
||||
@@ -64,6 +77,7 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.installation = this._warcraftInstallationService.getWowInstallation(this.installationId);
|
||||
this.installationModel = { ...this.installation };
|
||||
this.selectedAddonChannelType = this.installation.defaultAddonChannelType;
|
||||
this.clientAutoUpdate = this.installation.defaultAutoUpdate;
|
||||
this.clientTypeName = `COMMON.CLIENT_TYPES.${getEnumName(
|
||||
@@ -88,6 +102,41 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
this._warcraftInstallationService.updateWowInstallation(this.installation);
|
||||
}
|
||||
|
||||
onClickCancel(): void {
|
||||
this.installationModel = { ...this.installation };
|
||||
this.editMode = false;
|
||||
}
|
||||
|
||||
onClickSave(): void {
|
||||
this.installation = { ...this.installationModel };
|
||||
this._warcraftInstallationService.updateWowInstallation(this.installation);
|
||||
this.editMode = false;
|
||||
}
|
||||
|
||||
onClickRemove(): void {
|
||||
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: this._translateService.instant("PAGES.OPTIONS.WOW.CLEAR_INSTALL_LOCATION_DIALOG.TITLE"),
|
||||
message: this._translateService.instant("PAGES.OPTIONS.WOW.CLEAR_INSTALL_LOCATION_DIALOG.MESSAGE", {
|
||||
location: this._translateService.instant(this.installation.location),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
map((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._warcraftInstallationService.removeWowInstallation(this.installation);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public async clearInstallPath(): Promise<void> {
|
||||
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
@@ -114,15 +163,6 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async onSelectClientPath(): Promise<void> {
|
||||
const selectedPath = await this.selectWowClientPath(this.installation.clientType);
|
||||
if (selectedPath) {
|
||||
console.debug("selectedPath", selectedPath);
|
||||
this.clientLocation = selectedPath;
|
||||
this._cdRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private getAddonChannelInfos() {
|
||||
return getEnumList(AddonChannelType).map((type: AddonChannelType) => {
|
||||
const channelName = getEnumName(AddonChannelType, type).toUpperCase();
|
||||
@@ -132,46 +172,4 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async selectWowClientPath(clientType: WowClientType): Promise<string> {
|
||||
const dialogResult = await this._electronService.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
|
||||
if (dialogResult.canceled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const selectedPath = _.first(dialogResult.filePaths);
|
||||
if (!selectedPath) {
|
||||
console.warn("No path selected");
|
||||
return "";
|
||||
}
|
||||
|
||||
console.log("dialogResult", selectedPath);
|
||||
|
||||
const clientRelativePath = this._warcraftService.getClientRelativePath(clientType, selectedPath);
|
||||
|
||||
const didSetWowPath = await this._warcraftService.setWowFolderPath(clientType, clientRelativePath);
|
||||
if (didSetWowPath) {
|
||||
return clientRelativePath;
|
||||
}
|
||||
|
||||
const clientFolderName = this._warcraftService.getClientFolderName(clientType);
|
||||
const clientExecutableName = this._warcraftService.getExecutableName(clientType);
|
||||
const clientExecutablePath = path.join(clientRelativePath, clientFolderName, clientExecutableName);
|
||||
const dialogRef = this._dialog.open(AlertDialogComponent, {
|
||||
data: {
|
||||
title: `Alert`,
|
||||
message: `Unable to set "${clientRelativePath}" as your ${getEnumName(
|
||||
WowClientType,
|
||||
clientType
|
||||
)} folder.\nPath not found: "${clientExecutablePath}".`,
|
||||
},
|
||||
});
|
||||
|
||||
await dialogRef.afterClosed().toPromise();
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
faCode,
|
||||
faCoins,
|
||||
faCompressArrowsAlt,
|
||||
faPencilAlt,
|
||||
} 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";
|
||||
@@ -47,10 +48,13 @@ export class IconService {
|
||||
this.addSvg(faPatreon);
|
||||
this.addSvg(faCoins);
|
||||
this.addSvg(faCompressArrowsAlt);
|
||||
this.addSvg(faPencilAlt);
|
||||
}
|
||||
|
||||
async addSvg(icon: IconDefinition) {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${icon.icon[0]} ${icon.icon[1]}"><path d="${icon.icon[4]}" /></svg>`;
|
||||
addSvg(icon: IconDefinition): void {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${icon.icon[0]} ${
|
||||
icon.icon[1]
|
||||
}"><path d="${icon.icon[4].toString()}" /></svg>`;
|
||||
|
||||
this._matIconRegistry.addSvgIconLiteralInNamespace(
|
||||
icon.prefix,
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import * as _ from "lodash";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { filter, map, switchMap, tap } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as path from "path";
|
||||
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { WOW_INSTALLATIONS_KEY } from "../../../common/constants";
|
||||
import {
|
||||
DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX,
|
||||
DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX,
|
||||
WOW_INSTALLATIONS_KEY,
|
||||
} from "../../../common/constants";
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { WowInstallation } from "../../models/wowup/wow-installation";
|
||||
import { PreferenceStorageService } from "../storage/preference-storage.service";
|
||||
import { WarcraftService } from "./warcraft.service";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { getEnumName } from "app/utils/enum.utils";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
|
||||
const CLIENT_RETAIL_FOLDER = "_retail_";
|
||||
const CLIENT_RETAIL_PTR_FOLDER = "_ptr_";
|
||||
@@ -27,12 +35,28 @@ const INTERFACE_FOLDER_NAME = "Interface";
|
||||
export class WarcraftInstallationService {
|
||||
private readonly _wowInstallationsSrc = new BehaviorSubject<WowInstallation[]>([]);
|
||||
|
||||
private _blizzardAgentPath = "";
|
||||
|
||||
public readonly wowInstallations$ = this._wowInstallationsSrc.asObservable();
|
||||
|
||||
constructor(private _preferenceStorageService: PreferenceStorageService, private _warcraftService: WarcraftService) {
|
||||
public get blizzardAgentPath(): string {
|
||||
return `${this._blizzardAgentPath}`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _preferenceStorageService: PreferenceStorageService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _translateService: TranslateService,
|
||||
private _fileService: FileService,
|
||||
private _electronService: ElectronService
|
||||
) {
|
||||
this._warcraftService.blizzardAgent$
|
||||
.pipe(
|
||||
filter((blizzardAgentPath) => !!blizzardAgentPath),
|
||||
tap((blizzardAgentPath) => {
|
||||
this._blizzardAgentPath = blizzardAgentPath;
|
||||
}),
|
||||
switchMap((blizzardAgentPath) => this.migrateAllLegacyInstallations(blizzardAgentPath)),
|
||||
map((blizzardAgentPath) => this.importWowInstallations(blizzardAgentPath))
|
||||
)
|
||||
.subscribe();
|
||||
@@ -80,38 +104,192 @@ export class WarcraftInstallationService {
|
||||
this.setWowInstallations(storedInstallations);
|
||||
}
|
||||
|
||||
public async selectWowClientPath(): Promise<string> {
|
||||
const selectionName = this._translateService.instant("COMMON.WOW_EXE_SELECTION_NAME");
|
||||
const extensionFilter = this._warcraftService.getExecutableExtension();
|
||||
let dialogResult: Electron.OpenDialogReturnValue;
|
||||
if (extensionFilter) {
|
||||
dialogResult = await this._electronService.showOpenDialog({
|
||||
filters: [{ extensions: [extensionFilter], name: selectionName }],
|
||||
properties: ["openFile"],
|
||||
});
|
||||
} else {
|
||||
// platforms like linux don't really fit the rule
|
||||
dialogResult = await this._electronService.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
});
|
||||
}
|
||||
|
||||
if (dialogResult.canceled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const selectedPath = _.first(dialogResult.filePaths);
|
||||
if (!selectedPath || !selectedPath.endsWith("")) {
|
||||
console.warn("No path selected");
|
||||
return "";
|
||||
}
|
||||
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
public addInstallation(installation: WowInstallation): void {
|
||||
const existingInstallations = this.getWowInstallations();
|
||||
const exists = _.findIndex(existingInstallations, (inst) => inst.location === installation.location) !== -1;
|
||||
if (exists) {
|
||||
throw new Error(`Installation already exists: ${installation.location}`);
|
||||
}
|
||||
|
||||
existingInstallations.push(installation);
|
||||
|
||||
console.debug("ADDED INSTALL");
|
||||
this.setWowInstallations(existingInstallations);
|
||||
this._wowInstallationsSrc.next(existingInstallations);
|
||||
}
|
||||
|
||||
public removeWowInstallation(installation: WowInstallation): void {
|
||||
const installations = this.getWowInstallations();
|
||||
const installationExists = _.findIndex(installations, (inst) => inst.id === installation.id) !== -1;
|
||||
if (!installationExists) {
|
||||
throw new Error(`Installation does not exist: ${installation.id}`);
|
||||
}
|
||||
|
||||
_.remove(installations, (inst) => inst.id === installation.id);
|
||||
|
||||
this.setWowInstallations(installations);
|
||||
this._wowInstallationsSrc.next(installations);
|
||||
}
|
||||
|
||||
public async createWowInstallationForPath(applicationPath: string): Promise<WowInstallation> {
|
||||
const clientDir = path.dirname(applicationPath);
|
||||
const clientType = this._warcraftService.getClientTypeForFolderName(clientDir);
|
||||
const typeName = getEnumName(WowClientType, clientType);
|
||||
const currentInstallations = this.getWowInstallationsByClientType(clientType);
|
||||
|
||||
let label = await this._translateService.get(`COMMON.CLIENT_TYPES.${typeName.toUpperCase()}`).toPromise();
|
||||
if (currentInstallations.length > 0) {
|
||||
label += `${currentInstallations.length + 1}`;
|
||||
}
|
||||
|
||||
const installation: WowInstallation = {
|
||||
id: uuidv4(),
|
||||
clientType: clientType,
|
||||
defaultAddonChannelType: AddonChannelType.Stable,
|
||||
defaultAutoUpdate: false,
|
||||
label: label,
|
||||
location: applicationPath,
|
||||
selected: false,
|
||||
};
|
||||
|
||||
return installation;
|
||||
}
|
||||
|
||||
public async importWowInstallations(blizzardAgentPath: string): Promise<void> {
|
||||
const wowInstallationPreferences = this.getWowInstallations();
|
||||
const installedProducts = await this._warcraftService.getInstalledProducts(blizzardAgentPath);
|
||||
|
||||
const installations = _.map(Array.from(installedProducts.values()), (product) => {
|
||||
for (const product of Array.from(installedProducts.values())) {
|
||||
const typeName = getEnumName(WowClientType, product.clientType);
|
||||
const currentInstallations = this.getWowInstallationsByClientType(product.clientType);
|
||||
|
||||
let label = await this._translateService.get(`COMMON.CLIENT_TYPES.${typeName.toUpperCase()}`).toPromise();
|
||||
if (currentInstallations.length > 0) {
|
||||
label += `${currentInstallations.length + 1}`;
|
||||
}
|
||||
|
||||
const fullProductPath = this.getFullProductPath(product.location, product.clientType);
|
||||
const wowInstallation: WowInstallation = {
|
||||
id: uuidv4(),
|
||||
clientType: product.clientType,
|
||||
label: product.name,
|
||||
location: product.location,
|
||||
label,
|
||||
location: fullProductPath,
|
||||
selected: false,
|
||||
defaultAddonChannelType: AddonChannelType.Stable,
|
||||
defaultAutoUpdate: false,
|
||||
};
|
||||
return wowInstallation;
|
||||
});
|
||||
console.debug("db installations", installations);
|
||||
|
||||
_.remove(
|
||||
installations,
|
||||
(installation) =>
|
||||
_.findIndex(wowInstallationPreferences, (pref) => pref.location === installation.location) !== -1
|
||||
);
|
||||
|
||||
const allInstallations = [...wowInstallationPreferences, ...installations];
|
||||
|
||||
if (allInstallations.length !== wowInstallationPreferences.length) {
|
||||
this.setWowInstallations(allInstallations);
|
||||
try {
|
||||
this.addInstallation(wowInstallation);
|
||||
} catch (e) {
|
||||
// Ignore duplicate error
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("WOWINSTALLS", allInstallations);
|
||||
this._wowInstallationsSrc.next(allInstallations);
|
||||
this._wowInstallationsSrc.next(this.getWowInstallations());
|
||||
}
|
||||
|
||||
private async migrateAllLegacyInstallations(blizzardAgentPath: string): Promise<string> {
|
||||
for (const clientType of this._warcraftService.getAllClientTypes()) {
|
||||
try {
|
||||
await this.migrateLegacyInstallations(clientType);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return blizzardAgentPath;
|
||||
}
|
||||
|
||||
private async migrateLegacyInstallations(clientType: WowClientType) {
|
||||
if (clientType === WowClientType.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeName = getEnumName(WowClientType, clientType);
|
||||
|
||||
const existingInstallations = this.getWowInstallationsByClientType(clientType);
|
||||
if (existingInstallations.length > 0) {
|
||||
console.debug(`Existing install exists for: ${typeName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyLocationKey = this._warcraftService.getClientLocationKey(clientType);
|
||||
const legacyLocation = this._preferenceStorageService.findByKey(legacyLocationKey);
|
||||
if (!legacyLocation) {
|
||||
console.debug(`Legacy ${typeName}: nothing to migrate`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`Migrating legacy ${typeName} installation`);
|
||||
|
||||
const legacyDefaultChannel = this.getLegacyDefaultAddonChannel(typeName);
|
||||
const legacyDefaultAutoUpdate = this.getLegacyDefaultAutoUpdate(typeName);
|
||||
|
||||
const label = await this._translateService.get(`COMMON.CLIENT_TYPES.${typeName.toUpperCase()}`).toPromise();
|
||||
|
||||
const newLocation = this.getFullProductPath(legacyLocation, clientType);
|
||||
|
||||
const newLocationExists = await this._fileService.pathExists(newLocation);
|
||||
if (!newLocationExists) {
|
||||
throw new Error(`Could not migrate legacy installation, path does not exist: ${newLocation}`);
|
||||
}
|
||||
|
||||
const installation: WowInstallation = {
|
||||
id: uuidv4(),
|
||||
clientType,
|
||||
defaultAddonChannelType: legacyDefaultChannel,
|
||||
defaultAutoUpdate: legacyDefaultAutoUpdate,
|
||||
label,
|
||||
location: newLocation,
|
||||
selected: false,
|
||||
};
|
||||
|
||||
this.addInstallation(installation);
|
||||
}
|
||||
|
||||
private getFullProductPath(location: string, clientType: WowClientType): string {
|
||||
const clientFolderName = this._warcraftService.getClientFolderName(clientType);
|
||||
const executableName = this._warcraftService.getExecutableName(clientType);
|
||||
return path.join(location, clientFolderName, executableName);
|
||||
}
|
||||
|
||||
private getLegacyDefaultAddonChannel(typeName: string): AddonChannelType {
|
||||
const legacyDefaultChannelKey = `${typeName}${DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX}`.toLowerCase();
|
||||
return parseInt(this._preferenceStorageService.findByKey(legacyDefaultChannelKey), 10) as AddonChannelType;
|
||||
}
|
||||
|
||||
private getLegacyDefaultAutoUpdate(typeName: string): boolean {
|
||||
const legacyDefaultAutoUpdateKey = `${typeName}${DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX}`.toLowerCase();
|
||||
return this._preferenceStorageService.findByKey(legacyDefaultAutoUpdateKey) === true.toString();
|
||||
}
|
||||
|
||||
// public getClientFolderName(clientType: WowClientType): string {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
|
||||
export interface WarcraftServiceImpl {
|
||||
getExecutableExtension(): string;
|
||||
isWowApplication(appName: string): boolean;
|
||||
getBlizzardAgentPath(): Promise<string>;
|
||||
getExecutableName(clientType: WowClientType): string;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { WarcraftServiceImpl } from "./warcraft.service.impl";
|
||||
|
||||
const WOW_RETAIL_NAME = "Wow.exe";
|
||||
const WOW_RETAIL_PTR_NAME = "WowT.exe";
|
||||
const WOW_CLASSIC_NAME = "WowClassic.exe";
|
||||
const WOW_CLASSIC_PTR_NAME = "WowClassicT.exe";
|
||||
const WOW_RETAIL_BETA_NAME = "WowB.exe";
|
||||
|
||||
const WOW_APP_NAMES = [
|
||||
WOW_RETAIL_NAME,
|
||||
WOW_RETAIL_PTR_NAME,
|
||||
WOW_CLASSIC_NAME,
|
||||
WOW_CLASSIC_PTR_NAME,
|
||||
WOW_RETAIL_BETA_NAME,
|
||||
];
|
||||
|
||||
export class WarcraftServiceLinux implements WarcraftServiceImpl {
|
||||
public getExecutableExtension(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
public getBlizzardAgentPath(): Promise<string> {
|
||||
return Promise.resolve("");
|
||||
}
|
||||
|
||||
public isWowApplication(appName: string): boolean {
|
||||
return WOW_APP_NAMES.includes(appName);
|
||||
}
|
||||
|
||||
public getExecutableName(clientType: WowClientType): string {
|
||||
switch (clientType) {
|
||||
case WowClientType.Retail:
|
||||
return "Wow.exe";
|
||||
return WOW_RETAIL_NAME;
|
||||
case WowClientType.Classic:
|
||||
return "WowClassic.exe";
|
||||
return WOW_CLASSIC_NAME;
|
||||
case WowClientType.RetailPtr:
|
||||
return "WowT.exe";
|
||||
return WOW_RETAIL_PTR_NAME;
|
||||
case WowClientType.ClassicPtr:
|
||||
return "WowClassicT.exe";
|
||||
return WOW_CLASSIC_PTR_NAME;
|
||||
case WowClientType.Beta:
|
||||
return "WowB.exe";
|
||||
return WOW_RETAIL_BETA_NAME;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3,12 +3,34 @@ import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { WarcraftServiceImpl } from "./warcraft.service.impl";
|
||||
|
||||
const WOW_RETAIL_NAME = "World of Warcraft.app";
|
||||
const WOW_RETAIL_PTR_NAME = "World of Warcraft Test.app";
|
||||
const WOW_CLASSIC_NAME = "World of Warcraft Classic.app";
|
||||
const WOW_CLASSIC_PTR_NAME = "World of Warcraft Classic Test.app";
|
||||
const WOW_RETAIL_BETA_NAME = "World of Warcraft Beta.app";
|
||||
|
||||
const WOW_APP_NAMES = [
|
||||
WOW_RETAIL_NAME,
|
||||
WOW_RETAIL_PTR_NAME,
|
||||
WOW_CLASSIC_NAME,
|
||||
WOW_CLASSIC_PTR_NAME,
|
||||
WOW_RETAIL_BETA_NAME,
|
||||
];
|
||||
|
||||
const BLIZZARD_AGENT_PATH = "/Users/Shared/Battle.net/Agent";
|
||||
const BLIZZARD_PRODUCT_DB_NAME = "product.db";
|
||||
|
||||
export class WarcraftServiceMac implements WarcraftServiceImpl {
|
||||
constructor(private _fileService: FileService) {}
|
||||
|
||||
public getExecutableExtension(): string {
|
||||
return "app";
|
||||
}
|
||||
|
||||
public isWowApplication(appName: string): boolean {
|
||||
return WOW_APP_NAMES.includes(appName);
|
||||
}
|
||||
|
||||
public async getBlizzardAgentPath(): Promise<string> {
|
||||
const agentPath = join(BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
|
||||
const exists = await this._fileService.pathExists(agentPath);
|
||||
|
||||
@@ -95,6 +95,20 @@ export class WarcraftService {
|
||||
return this._impl.getExecutableName(clientType);
|
||||
}
|
||||
|
||||
public getExecutableExtension(): string {
|
||||
return this._impl.getExecutableExtension();
|
||||
}
|
||||
|
||||
public async isWowApplication(appPath: string): Promise<boolean> {
|
||||
const pathExists = await this._fileService.pathExists(appPath);
|
||||
if (!pathExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileName = path.basename(appPath);
|
||||
return this._impl.isWowApplication(fileName);
|
||||
}
|
||||
|
||||
public getFullExecutablePath(clientType: WowClientType): string {
|
||||
const clientLocation = this.getClientLocation(clientType);
|
||||
const clientFolder = this.getClientFolderName(clientType);
|
||||
|
||||
@@ -5,6 +5,20 @@ import { FileService } from "../files/file.service";
|
||||
import { WarcraftServiceImpl } from "./warcraft.service.impl";
|
||||
import { IPC_LIST_DISKS_WIN32 } from "../../../common/constants";
|
||||
|
||||
const WOW_RETAIL_NAME = "Wow.exe";
|
||||
const WOW_RETAIL_PTR_NAME = "WowT.exe";
|
||||
const WOW_CLASSIC_NAME = "WowClassic.exe";
|
||||
const WOW_CLASSIC_PTR_NAME = "WowClassicT.exe";
|
||||
const WOW_RETAIL_BETA_NAME = "WowB.exe";
|
||||
|
||||
const WOW_APP_NAMES = [
|
||||
WOW_RETAIL_NAME,
|
||||
WOW_RETAIL_PTR_NAME,
|
||||
WOW_CLASSIC_NAME,
|
||||
WOW_CLASSIC_PTR_NAME,
|
||||
WOW_RETAIL_BETA_NAME,
|
||||
];
|
||||
|
||||
// BLIZZARD STRINGS
|
||||
const WINDOWS_BLIZZARD_AGENT_PATH = "ProgramData/Battle.net/Agent";
|
||||
const BLIZZARD_PRODUCT_DB_NAME = "product.db";
|
||||
@@ -12,6 +26,14 @@ const BLIZZARD_PRODUCT_DB_NAME = "product.db";
|
||||
export class WarcraftServiceWin implements WarcraftServiceImpl {
|
||||
constructor(private _electronService: ElectronService, private _fileService: FileService) {}
|
||||
|
||||
public getExecutableExtension(): string {
|
||||
return "exe";
|
||||
}
|
||||
|
||||
public isWowApplication(appName: string): boolean {
|
||||
return WOW_APP_NAMES.includes(appName);
|
||||
}
|
||||
|
||||
async getBlizzardAgentPath(): Promise<string> {
|
||||
try {
|
||||
const diskInfo = await this._electronService.invoke(IPC_LIST_DISKS_WIN32);
|
||||
|
||||
@@ -151,9 +151,13 @@
|
||||
"PROVIDER_ERROR": "Error contacting {providerName}",
|
||||
"SEARCH": {
|
||||
"NO_ADDONS": "No addons found"
|
||||
}
|
||||
},
|
||||
"WOW_EXE_SELECTION_NAME": "WoW Executable"
|
||||
},
|
||||
"DIALOGS": {
|
||||
"SELECT_INSTALLATION": {
|
||||
"INVALID_INSTALLATION_PATH": "This does not appear to be a valid World of Warcraft application:\n{selectedPath}"
|
||||
},
|
||||
"ADDON_DETAILS": {
|
||||
"ADDON_ID_PREFIX": "Addon ID:",
|
||||
"BY_AUTHOR": "By {authorName}",
|
||||
@@ -377,14 +381,18 @@
|
||||
"AUTO_UPDATE_DESCRIPTION": "Newly installed addons will be set to auto update by default",
|
||||
"AUTO_UPDATE_LABEL": "Auto Update",
|
||||
"CLEAR_INSTALL_LOCATION_DIALOG": {
|
||||
"MESSAGE": "Are you sure you want to clear the install path for {clientName}? This will remove all stored addon information for this client.\n\nYour addon folders will not be removed.",
|
||||
"TITLE": "Clear Install Location?"
|
||||
"MESSAGE": "Are you sure you want to remove the installation at \"{location}\"? This will remove all stored addon information for this client.\n\nYour addon folders will not be removed.",
|
||||
"TITLE": "Remove World of Warcraft Installation?"
|
||||
},
|
||||
"CLIENT_TYPE_INPUT_HINT": "The folder that contains the {clientTypeName} client folder \"{clientFolderName}\"",
|
||||
"CLIENT_TYPE_PATH_LABEL": "{clientTypeName} path",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Default Addon Channel",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Addon Channel",
|
||||
"REMOVE_WOW_DIRECTORY_SELECT_BUTTON": "Remove",
|
||||
"OPEN_WOW_DIRECTORY_SELECT_BUTTON": "Select",
|
||||
"EDIT_WOW_DIRECTORY_SELECT_BUTTON": "Edit",
|
||||
"CANCEL_WOW_DIRECTORY_SELECT_BUTTON": "Cancel",
|
||||
"SAVE_WOW_DIRECTORY_SELECT_BUTTON": "Save",
|
||||
"RESCAN_CLIENTS_BUTTON": "Re-Scan",
|
||||
"ADD_CLIENT_BUTTON": "Add New",
|
||||
"RESCAN_CLIENTS_LABEL": "Rescan installed World of Warcraft products",
|
||||
|
||||
@@ -79,6 +79,10 @@ img {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.pt-3 {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.m-0 {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user