#743 Improvements to the installtion management flow

This commit is contained in:
jliddev
2021-02-23 22:04:28 -06:00
parent 85bd5b0427
commit a3c865c88c
14 changed files with 455 additions and 115 deletions

View File

@@ -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>

View File

@@ -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,
},
});
}
}

View File

@@ -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>

View File

@@ -32,7 +32,7 @@ p {
flex-direction: row;
align-items: center;
// margin: 1em 0;
padding: 0.5em;
// padding: 0.5em;
&.np {
padding: 0;
}

View File

@@ -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 "";
}
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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 "";
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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",

View File

@@ -79,6 +79,10 @@ img {
padding: 0 0.25em;
}
.pt-3 {
padding-top: 1em;
}
.m-0 {
margin: 0 !important;
}