Merge branch 'develop' into pr/1010

This commit is contained in:
jliddev
2021-09-07 18:16:10 -05:00
20 changed files with 2190 additions and 2292 deletions

View File

@@ -103,8 +103,6 @@ if (preferenceStore.get(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY) === "false") {
log.info("Hardware acceleration enabled");
}
app.allowRendererProcessReuse = false;
// Some servers don't supply good CORS headers for us, so we ignore them.
app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
@@ -261,8 +259,11 @@ function createWindow(): BrowserWindow {
allowRunningInsecureContent: argv.serve,
webSecurity: false,
nativeWindowOpen: true,
enableRemoteModule: false, // This is only required for electron store https://github.com/sindresorhus/electron-store/issues/152,
additionalArguments: [`--log-path=${LOG_PATH}`, `--user-data-path=${app.getPath("userData")}`],
additionalArguments: [
`--log-path=${LOG_PATH}`,
`--user-data-path=${app.getPath("userData")}`,
`--base-bg-color=${getBackgroundColor()}`,
],
},
show: false,
};

View File

@@ -36,6 +36,7 @@ function getArg(argKey: string): string {
const LOG_PATH = getArg("log-path");
const USER_DATA_PATH = getArg("user-data-path");
const BASE_BG_COLOR = getArg("base-bg-color");
log.transports.file.resolvePath = (variables: log.PathVariables) => {
return join(LOG_PATH, variables.fileName);
@@ -73,25 +74,30 @@ function openPath(path: string): Promise<string> {
return shell.openPath(path);
}
if (window.opener === null) {
window.log = log;
window.libs = {
handlebars: require("handlebars"),
autoLaunch: require("auto-launch"),
};
window.userDataPath = USER_DATA_PATH;
window.logPath = LOG_PATH;
window.platform = process.platform;
window.wowup = {
onRendererEvent,
onceRendererEvent,
rendererSend,
rendererInvoke,
rendererOff,
rendererOn,
openExternal,
openPath,
};
} else {
console.log("HAS OPENER");
try {
if (window.opener === null) {
window.log = log;
window.baseBgColor = BASE_BG_COLOR;
window.libs = {
handlebars: require("handlebars"),
autoLaunch: require("auto-launch"),
};
window.userDataPath = USER_DATA_PATH;
window.logPath = LOG_PATH;
window.platform = process.platform;
window.wowup = {
onRendererEvent,
onceRendererEvent,
rendererSend,
rendererInvoke,
rendererOff,
rendererOn,
openExternal,
openPath,
};
} else {
console.log("HAS OPENER");
}
} catch (e) {
log.error(e);
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "wowup",
"productName": "WowUp",
"version": "2.5.0-beta.13",
"version": "2.5.0-beta.14",
"description": "World of Warcraft addon updater",
"homepage": "https://wowup.io",
"author": {
@@ -49,60 +49,61 @@
"pretty": "npx prettier --write . && ng lint --fix"
},
"devDependencies": {
"@angular-builders/custom-webpack": "12.1.0",
"@angular-devkit/build-angular": "12.1.3",
"@angular-builders/custom-webpack": "12.1.1",
"@angular-devkit/build-angular": "12.2.4",
"@angular-eslint/builder": "12.3.1",
"@angular-eslint/eslint-plugin": "12.3.1",
"@angular-eslint/eslint-plugin-template": "12.3.1",
"@angular-eslint/schematics": "12.3.1",
"@angular-eslint/template-parser": "12.3.1",
"@angular/cli": "12.1.3",
"@angular/cli": "12.2.4",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/globrex": "0.1.1",
"@types/jasmine": "3.8.2",
"@types/jasmine": "3.9.0",
"@types/jasminewd2": "2.0.10",
"@types/lodash": "4.14.172",
"@types/markdown-it": "12.2.0",
"@types/markdown-it": "12.2.1",
"@types/mocha": "9.0.0",
"@types/node": "16.6.2",
"@types/opossum": "4.1.2",
"@types/node": "14.17.15",
"@types/object-hash": "2.1.1",
"@types/opossum": "6.2.0",
"@types/slug": "5.0.2",
"@types/string-similarity": "4.0.0",
"@types/uuid": "8.3.1",
"@typescript-eslint/eslint-plugin": "4.29.2",
"@typescript-eslint/eslint-plugin-tslint": "4.26.0",
"@typescript-eslint/parser": "4.29.2",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/eslint-plugin-tslint": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"chai": "4.3.4",
"conventional-changelog-cli": "2.1.1",
"core-js": "3.17.2",
"cross-env": "7.0.3",
"dotenv": "10.0.0",
"electron": "13.2.1",
"electron": "14.0.0",
"electron-builder": "22.11.7",
"electron-notarize": "1.1.0",
"electron-reload": "1.5.0",
"electron-notarize": "1.1.1",
"electron-reload": "2.0.0-alpha.1",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-import": "2.23.4",
"eslint-plugin-jsdoc": "35.1.3",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-jsdoc": "36.1.0",
"eslint-plugin-prefer-arrow": "1.2.3",
"i18next-json-sync": "2.3.1",
"jasmine-core": "3.8.0",
"jasmine-core": "3.9.0",
"jasmine-spec-reporter": "7.0.0",
"karma": "6.3.4",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-electron": "7.0.0",
"karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.7.0",
"mocha": "9.0.3",
"nan": "2.14.2",
"node-addon-api": "4.0.0",
"node-gyp": "8.1.0",
"mocha": "9.1.1",
"node-addon-api": "4.1.0",
"node-gyp": "8.2.0",
"npm-run-all": "4.1.5",
"prettier": "2.3.2",
"spectron": "15.0.0",
"ts-node": "10.2.1",
"typescript": "4.2.4",
"typescript": "4.3.5",
"wait-on": "6.0.0",
"webdriver-manager": "12.1.8"
},
@@ -110,17 +111,17 @@
"node": ">=14.0.0"
},
"dependencies": {
"@angular/animations": "12.1.3",
"@angular/cdk": "12.1.3",
"@angular/common": "12.1.3",
"@angular/compiler": "12.1.3",
"@angular/compiler-cli": "12.1.3",
"@angular/core": "12.1.3",
"@angular/forms": "12.1.3",
"@angular/material": "12.1.3",
"@angular/platform-browser": "12.1.3",
"@angular/platform-browser-dynamic": "12.1.3",
"@angular/router": "12.1.3",
"@angular/animations": "12.2.4",
"@angular/cdk": "12.2.4",
"@angular/common": "12.2.4",
"@angular/compiler": "12.2.4",
"@angular/compiler-cli": "12.2.4",
"@angular/core": "12.2.4",
"@angular/forms": "12.2.4",
"@angular/material": "12.2.4",
"@angular/platform-browser": "12.2.4",
"@angular/platform-browser-dynamic": "12.2.4",
"@angular/router": "12.2.4",
"@bbob/core": "2.7.0",
"@bbob/html": "2.7.0",
"@bbob/preset-html5": "2.7.0",
@@ -133,9 +134,8 @@
"ag-grid-angular": "26.0.0",
"ag-grid-community": "26.0.0",
"auto-launch": "5.0.5",
"axios": "0.21.1",
"axios": "0.21.4",
"compare-versions": "3.6.0",
"core-js": "3.13.1",
"electron-log": "4.4.1",
"electron-store": "8.0.0",
"electron-updater": "4.3.9",
@@ -150,7 +150,8 @@
"ngx-translate-messageformat-compiler": "4.10.0",
"node-cache": "5.1.2",
"node-disk-info": "1.3.0",
"opossum": "6.2.0",
"object-hash": "2.2.0",
"opossum": "6.2.1",
"p-limit": "3.1.0",
"protobufjs": "6.11.2",
"pushy-electron": "1.0.8",

View File

@@ -5,6 +5,7 @@ import { AddonInstallState } from "../models/wowup/addon-install-state";
import { AddonStatusSortOrder } from "../models/wowup/addon-status-sort-order";
import * as AddonUtils from "../utils/addon.utils";
import { ADDON_PROVIDER_UNKNOWN } from "../../common/constants";
import * as objectHash from "object-hash";
export class AddonViewModel {
public addon: Addon | undefined;
@@ -19,9 +20,12 @@ export class AddonViewModel {
public isLoadOnDemand = false;
public hasThumbnail = false;
public thumbnailLetter = "";
public showUpdate = false;
public canonicalName = "";
public get isIgnored(): boolean {
return this.addon?.isIgnored ?? false;
}
public get name(): string {
return this.addon?.name ?? "";
}
@@ -46,6 +50,10 @@ export class AddonViewModel {
return this.addon?.author ?? "";
}
public get hash(): string {
return objectHash(this.addon);
}
public constructor(addon: Addon | undefined) {
this.addon = addon;
this.installedAt = addon?.installedAt ? new Date(addon?.installedAt).getTime() : 0;

View File

@@ -1,70 +1,64 @@
<div>
<div class="addon-column row align-items-center">
<div class="thumbnail-container">
<app-addon-thumbnail [url]="getThumbnailUrl()" [name]="listItem.addon?.name ?? ''"></app-addon-thumbnail>
<app-addon-thumbnail [url]="thumbnailUrl$ | async" [name]="name$ | async"></app-addon-thumbnail>
</div>
<div class="version-container">
<div class="title-container">
<a class="addon-title hover-text-2" (click)="viewDetails()"
[ngClass]="{ 'text-3': listItem.addon?.isIgnored, 'text-warning': hasWarning() }">{{
listItem.addon?.name
[ngClass]="{ 'text-3': isIgnored$ | async, 'text-warning': hasWarning$ | async }">{{
name$ | async
}}</a>
</div>
<div class="addon-version text-2 row align-items-center" [ngClass]="{ ignored: listItem.addon?.isIgnored }">
<div *ngIf="listItem.addon?.fundingLinks" class="addon-funding mr-2">
<app-funding-button *ngFor="let link of listItem.addon?.fundingLinks ?? []" [funding]="link" size="small">
<div class="addon-version text-2 row align-items-center" [ngClass]="{ 'ignored': isIgnored$ | async }">
<div *ngIf="hasFundingLinks$ | async" class="addon-funding mr-2">
<app-funding-button *ngFor="let link of fundingLinks$ | async" [funding]="link" size="small">
</app-funding-button>
</div>
<div *ngIf="listItem.isBetaChannel() || listItem.isAlphaChannel()" class="channel bg-secondary-3 mr-2"
[ngClass]="{
beta: listItem.isBetaChannel(),
alpha: listItem.isAlphaChannel()
}">
{{ channelTranslationKey | translate }}
<div *ngIf="showChannel$ | async" class="channel bg-secondary-3 mr-2" [ngClass]="channelClass$ | async">
{{ channelTranslationKey$ | async | translate }}
</div>
<div *ngIf="hasMultipleProviders === true" class="mr-2">
<div *ngIf="hasMultipleProviders$ | async" class="mr-2">
<mat-icon class="auto-update-icon" svgIcon="fas:code-branch"
[matTooltip]="'PAGES.MY_ADDONS.MULTIPLE_PROVIDERS_TOOLTIP' | translate">
</mat-icon>
</div>
<div *ngIf="listItem.addon?.autoUpdateEnabled === true" class="mr-2">
<div *ngIf="autoUpdateEnabled$ | async" class="mr-2">
<mat-icon class="auto-update-icon text-2"
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate" svgIcon="far:clock">
</mat-icon>
</div>
<div *ngIf="hasRequiredDependencies()" class="mr-2"
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate: dependencyTooltip">
<div *ngIf="hasRequiredDependencies$ | async" class="mr-2"
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate: (dependencyTooltip$ | async)">
<mat-icon class="auto-update-icon" svgIcon="fas:link"></mat-icon>
</div>
<div *ngIf="listItem.isLoadOnDemand === true" class="mr-2">
<div *ngIf="isLoadOnDemand$ | async" class="mr-2">
<mat-icon class="auto-update-icon text-warning"
[matTooltip]="'PAGES.MY_ADDONS.REQUIRED_DEPENDENCY_MISSING_TOOLTIP' | translate"
svgIcon="fas:exclamation-triangle">
</mat-icon>
</div>
<div *ngIf="hasWarning() === true" class="mr-2">
<mat-icon class="auto-update-icon text-warning" [matTooltip]="getWarningText()"
<div *ngIf="hasWarning$ | async" class="mr-2">
<mat-icon class="auto-update-icon text-warning" [matTooltip]="getWarningText(listItem)"
svgIcon="fas:exclamation-triangle">
</mat-icon>
</div>
<div *ngIf="hasIgnoreReason() === true" class="mr-2">
<mat-icon class="auto-update-icon" [matTooltip]="getIgnoreTooltipKey() | translate" [style.color]="'#ff9800'"
[svgIcon]="getIgnoreIcon()">
<div *ngIf="hasIgnoreReason$ | async" class="mr-2">
<mat-icon class="auto-update-icon" [matTooltip]="ignoreTooltipKey$ | async | translate"
[style.color]="'#ff9800'" [svgIcon]="ignoreIcon$ | async">
</mat-icon>
</div>
<!-- If no warning and not ignored for some specific reason, default to this -->
<div
*ngIf="listItem.isLoadOnDemand === false && hasIgnoreReason() === false && hasWarning() === false && listItem.addon?.providerName === unknownProviderName"
class="mr-2">
<div *ngIf="isUnknownAddon$ | async" class="mr-2">
<mat-icon class="auto-update-icon" [matTooltip]="'PAGES.MY_ADDONS.UNKNOWN_ADDON_INFO_TOOLTIP' | translate"
[matTooltipClass]="['text-center']" [style.color]="'#ff9800'" svgIcon="fas:exclamation-triangle">
</mat-icon>
</div>
{{ listItem.addon?.installedVersion }}
<div *ngIf="showUpdateToVersion && listItem.needsUpdate()" class="text-1 row">
{{ installedVersion$ | async }}
<div *ngIf="showUpdateVersion$ | async" class="text-1 row">
<mat-icon class="upgrade-icon" svgIcon="fas:play"></mat-icon>
<div class="bg-secondary-4 text-2 rounded px-1">{{ listItem.addon?.latestVersion }}</div>
<div class="bg-secondary-4 text-2 rounded px-1">{{ latestVersion$ | async }}</div>
</div>
</div>
</div>

View File

@@ -8,12 +8,16 @@ import { AddonViewModel } from "../../business-objects/addon-view-model";
import { Addon } from "../../../common/entities/addon";
import { MatModule } from "../../mat-module";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { SessionService } from "../../services/session/session.service";
describe("MyAddonsAddonCellComponent", () => {
let component: MyAddonsAddonCellComponent;
let fixture: ComponentFixture<MyAddonsAddonCellComponent>;
let sessionService: SessionService;
beforeEach(async () => {
sessionService = jasmine.createSpyObj("SessionService", [""], {});
await TestBed.configureTestingModule({
declarations: [MyAddonsAddonCellComponent],
imports: [
@@ -32,7 +36,13 @@ describe("MyAddonsAddonCellComponent", () => {
},
}),
],
}).compileComponents();
})
.overrideComponent(MyAddonsAddonCellComponent, {
set: {
providers: [{ provide: SessionService, useValue: sessionService }],
},
})
.compileComponents();
fixture = TestBed.createComponent(MyAddonsAddonCellComponent);
component = fixture.componentInstance;

View File

@@ -1,19 +1,17 @@
import { AgRendererComponent } from "ag-grid-angular";
import { ICellRendererParams } from "ag-grid-community";
import { BehaviorSubject, combineLatest } from "rxjs";
import { filter, map } from "rxjs/operators";
import { Component, Input } from "@angular/core";
import { Component } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { ADDON_PROVIDER_UNKNOWN } from "../../../common/constants";
import { AddonChannelType, AddonDependencyType, AddonWarningType } from "../../../common/wowup/models";
import { AddonViewModel } from "../../business-objects/addon-view-model";
import { DialogFactory } from "../../services/dialog/dialog.factory";
import { SessionService } from "../../services/session/session.service";
import * as AddonUtils from "../../utils/addon.utils";
import { capitalizeString } from "../../utils/string.utils";
interface MyAddonsAddonCellComponentParams extends ICellRendererParams {
showUpdateToVersion: boolean;
}
@Component({
selector: "app-my-addons-addon-cell",
@@ -21,40 +19,114 @@ interface MyAddonsAddonCellComponentParams extends ICellRendererParams {
styleUrls: ["./my-addons-addon-cell.component.scss"],
})
export class MyAddonsAddonCellComponent implements AgRendererComponent {
@Input("addon") public listItem!: AddonViewModel;
private readonly _listItemSrc = new BehaviorSubject<AddonViewModel | undefined>(undefined);
public readonly capitalizeString = capitalizeString;
public readonly unknownProviderName = ADDON_PROVIDER_UNKNOWN;
public readonly listItem$ = this._listItemSrc.asObservable().pipe(filter((item) => item !== undefined));
public showUpdateToVersion = false;
public warningType?: AddonWarningType;
public warningText?: string;
public hasMultipleProviders = false;
public readonly name$ = this.listItem$.pipe(map((item) => item.name));
public get dependencyTooltip(): any {
return {
dependencyCount: this.getRequireDependencyCount(),
};
public readonly isIgnored$ = this.listItem$.pipe(map((item) => item.isIgnored));
public readonly hasWarning$ = this.listItem$.pipe(map((item) => this.hasWarning(item)));
public readonly hasFundingLinks$ = this.listItem$.pipe(
map((item) => Array.isArray(item.addon?.fundingLinks) && item.addon.fundingLinks.length > 0)
);
public readonly fundingLinks$ = this.listItem$.pipe(map((item) => item.addon?.fundingLinks ?? []));
public readonly showChannel$ = this.listItem$.pipe(map((item) => item.isBetaChannel() || item.isAlphaChannel()));
public readonly channelClass$ = this.listItem$.pipe(
map((item) => {
if (item.isBetaChannel()) {
return "beta";
}
if (item.isAlphaChannel()) {
return "alpha";
}
return "";
})
);
public readonly channelTranslationKey$ = this.listItem$.pipe(
map((item) => {
const channelType = item.addon?.channelType ?? AddonChannelType.Stable;
return channelType === AddonChannelType.Alpha
? "COMMON.ENUM.ADDON_CHANNEL_TYPE.ALPHA"
: "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA";
})
);
public readonly hasMultipleProviders$ = this.listItem$.pipe(
map((item) => (item.addon === undefined ? false : AddonUtils.hasMultipleProviders(item.addon)))
);
public readonly autoUpdateEnabled$ = this.listItem$.pipe(map((item) => item.addon?.autoUpdateEnabled ?? false));
public readonly hasIgnoreReason$ = this.listItem$.pipe(map((item) => this.hasIgnoreReason(item)));
public readonly hasRequiredDependencies$ = this.listItem$.pipe(
map((item) => this.getRequireDependencyCount(item) > 0)
);
public readonly dependencyTooltip$ = this.listItem$.pipe(
map((item) => {
return {
dependencyCount: this.getRequireDependencyCount(item),
};
})
);
public readonly isLoadOnDemand$ = this.listItem$.pipe(map((item) => item.isLoadOnDemand));
public readonly ignoreTooltipKey$ = this.listItem$.pipe(map((item) => this.getIgnoreTooltipKey(item)));
public readonly ignoreIcon$ = this.listItem$.pipe(map((item) => this.getIgnoreIcon(item)));
public readonly warningText$ = this.listItem$.pipe(
filter((item) => this.hasWarning(item)),
map((item) => this.getWarningText(item))
);
public readonly isUnknownAddon$ = this.listItem$.pipe(
map((item) => {
return (
!item.isLoadOnDemand &&
!this.hasIgnoreReason(item) &&
!this.hasWarning(item) &&
item.addon.providerName === ADDON_PROVIDER_UNKNOWN
);
})
);
public readonly installedVersion$ = this.listItem$.pipe(map((item) => item.addon.installedVersion));
public readonly latestVersion$ = this.listItem$.pipe(map((item) => item.addon.latestVersion));
public readonly thumbnailUrl$ = this.listItem$.pipe(map((item) => item.addon.thumbnailUrl));
public readonly showUpdateVersion$ = combineLatest([
this.listItem$,
this.sessionService.myAddonsCompactVersion$,
]).pipe(
map(([item, compactVersion]) => {
return compactVersion && item.needsUpdate();
})
);
public set listItem(item: AddonViewModel) {
this._listItemSrc.next(item);
}
public get channelTranslationKey(): string {
const channelType = this.listItem.addon?.channelType ?? AddonChannelType.Stable;
return channelType === AddonChannelType.Alpha
? "COMMON.ENUM.ADDON_CHANNEL_TYPE.ALPHA"
: "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA";
}
public constructor(
private _translateService: TranslateService,
private _dialogFactory: DialogFactory,
public sessionService: SessionService
) {}
public constructor(private _translateService: TranslateService, private _dialogFactory: DialogFactory) {}
public agInit(params: MyAddonsAddonCellComponentParams): void {
this.listItem = params.data;
this.showUpdateToVersion = this.listItem.showUpdate;
this.warningType = this.listItem.addon?.warningType;
this.warningText = this.getWarningText();
this.hasMultipleProviders =
this.listItem.addon === undefined ? false : AddonUtils.hasMultipleProviders(this.listItem.addon);
public agInit(params: ICellRendererParams): void {
this._listItemSrc.next(params.data);
}
public refresh(): boolean {
@@ -64,27 +136,19 @@ export class MyAddonsAddonCellComponent implements AgRendererComponent {
public afterGuiAttached?(): void {}
public viewDetails(): void {
this._dialogFactory.getAddonDetailsDialog(this.listItem);
this._dialogFactory.getAddonDetailsDialog(this._listItemSrc.value);
}
public getThumbnailUrl(): string {
return this.listItem?.addon?.thumbnailUrl ?? "";
public getRequireDependencyCount(item: AddonViewModel): number {
return item.getDependencies(AddonDependencyType.Required).length;
}
public getRequireDependencyCount(): number {
return this.listItem.getDependencies(AddonDependencyType.Required).length;
public hasIgnoreReason(item: AddonViewModel): boolean {
return !!item?.addon?.ignoreReason;
}
public hasRequiredDependencies(): boolean {
return this.getRequireDependencyCount() > 0;
}
public hasIgnoreReason(): boolean {
return !!this.listItem?.addon?.ignoreReason;
}
public getIgnoreTooltipKey(): string {
switch (this.listItem.addon?.ignoreReason) {
public getIgnoreTooltipKey(item: AddonViewModel): string {
switch (item.addon?.ignoreReason) {
case "git_repo":
return "PAGES.MY_ADDONS.ADDON_IS_CODE_REPOSITORY";
case "missing_dependency":
@@ -94,8 +158,8 @@ export class MyAddonsAddonCellComponent implements AgRendererComponent {
}
}
public getIgnoreIcon(): string {
switch (this.listItem.addon?.ignoreReason) {
public getIgnoreIcon(item: AddonViewModel): string {
switch (item.addon?.ignoreReason) {
case "git_repo":
return "fas:code";
case "missing_dependency":
@@ -105,20 +169,20 @@ export class MyAddonsAddonCellComponent implements AgRendererComponent {
}
}
public hasWarning(): boolean {
return this.warningType !== undefined;
public hasWarning(item: AddonViewModel): boolean {
return item?.addon?.warningType !== undefined;
}
public getWarningText(): string {
if (!this.warningType) {
public getWarningText(item: AddonViewModel): string {
if (!this.hasWarning(item)) {
return "";
}
const toolTipParams = {
providerName: this.listItem.providerName,
providerName: item.providerName,
};
switch (this.warningType) {
switch (item.addon.warningType) {
case AddonWarningType.MissingOnProvider:
return this._translateService.instant("COMMON.ADDON_WARNING.MISSING_ON_PROVIDER_TOOLTIP", toolTipParams);
case AddonWarningType.NoProviderFiles:

View File

@@ -1,5 +1,5 @@
import { AgGridModule } from "ag-grid-angular";
import { LIGHTBOX_CONFIG, LightboxModule } from "ng-gallery/lightbox";
import { LightboxModule } from "ng-gallery/lightbox";
import { CommonModule, DatePipe } from "@angular/common";
import { NgModule } from "@angular/core";
@@ -40,18 +40,19 @@ import { MatModule } from "../../mat-module";
import { DownloadCountPipe } from "../../pipes/download-count.pipe";
import { GetAddonListItemFilePropPipe } from "../../pipes/get-addon-list-item-file-prop.pipe";
import { InterfaceFormatPipe } from "../../pipes/interface-format.pipe";
import { InvertBoolPipe } from "../../pipes/inverse-bool.pipe";
import { NgxDatePipe } from "../../pipes/ngx-date.pipe";
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
import { SizeDisplayPipe } from "../../pipes/size-display.pipe";
import { TrustHtmlPipe } from "../../pipes/trust-html.pipe";
import { SharedModule } from "../../shared.module";
import { AboutComponent } from "../about/about.component";
import { AccountPageComponent } from "../account-page/account-page.component";
import { GetAddonsComponent } from "../get-addons/get-addons.component";
import { MyAddonsComponent } from "../my-addons/my-addons.component";
import { OptionsComponent } from "../options/options.component";
import { HomeRoutingModule } from "./home-routing.module";
import { HomeComponent } from "./home.component";
import { AccountPageComponent } from "../account-page/account-page.component";
@NgModule({
declarations: [
@@ -66,6 +67,7 @@ import { AccountPageComponent } from "../account-page/account-page.component";
PotentialAddonTableColumnComponent,
DownloadCountPipe,
InterfaceFormatPipe,
InvertBoolPipe,
NgxDatePipe,
GetAddonListItemFilePropPipe,
RelativeDurationPipe,

View File

@@ -7,8 +7,9 @@
<div class="select-container">
<mat-form-field>
<mat-label>{{ "PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate }}</mat-label>
<mat-select class="select pointer" [(value)]="selectedInstallationId" (selectionChange)="onClientChange()"
[disabled]="enableControls === false" [disableOptionCentering]="true">
<mat-select class="select pointer" [value]="selectedWowInstallationId$ | async"
(selectionChange)="onClientChange($event)" [disabled]="enableControls$ | async | invertBool"
[disableOptionCentering]="true">
<mat-option [value]="installation.id" *ngFor="let installation of wowInstallations$ | async">
{{ installation.label }}
</mat-option>
@@ -16,12 +17,12 @@
</mat-form-field>
</div>
<div class="right-container">
<div class="filter-container" *ngIf="selectedInstallation !== undefined">
<div class="filter-container" *ngIf="hasSelectedWowInstallationId$ | async">
<mat-form-field>
<mat-label>{{ "PAGES.MY_ADDONS.FILTER_LABEL" | translate }}</mat-label>
<input matInput (input)="filterInput$.next($event.target.value)" [(ngModel)]="filter" />
<button mat-button color="accent" *ngIf="filter" matSuffix mat-icon-button aria-label="Clear"
(click)="onClearFilter()">
<input #addonFilter matInput />
<button mat-button color="accent" *ngIf="filterInput$ | async" matSuffix mat-icon-button aria-label="Clear"
(click)="onClickClearFilter()">
<mat-icon svgIcon="fas:times"></mat-icon>
</button>
</mat-form-field>
@@ -31,13 +32,12 @@
<div class="row">
<button mat-flat-button color="primary" class="menu-button"
[matTooltip]="'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false || enableUpdateAll === false" (click)="onUpdateAll()">
[disabled]="enableUpdateAll$ | async | invertBool" (click)="onUpdateAll()">
{{ "PAGES.MY_ADDONS.UPDATE_ALL_BUTTON" | translate }}
</button>
<div class="menu-button-divider"></div>
<button mat-flat-button color="primary" class="chip col justify-content-center"
[matMenuTriggerFor]="updateAllMenu"
[disabled]="enableControls === false || (addonService.anyUpdatesAvailable$ | async) === false">
[matMenuTriggerFor]="updateAllMenu" [disabled]="enableUpdateExtra$ | async | invertBool">
<mat-icon svgIcon="fas:caret-down"></mat-icon>
</button>
</div>
@@ -52,35 +52,34 @@
<!-- CHECK UPDATES -->
<button mat-flat-button color="primary"
[matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false" (click)="onRefresh()">
[disabled]="enableControls$ | async | invertBool" (click)="onRefresh()">
{{ "PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON" | translate }}
</button>
<!-- RESCAN -->
<button mat-flat-button color="primary"
[matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false" (click)="onReScan()">
[disabled]="enableControls$ | async | invertBool" (click)="onReScan()">
{{ "PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON" | translate }}
</button>
</div>
</div>
</div>
<div class="spinner-container flex-grow-1" *ngIf="(isBusy$ | async) === true">
<app-progress-spinner [message]="spinnerMessage"> </app-progress-spinner>
<div class="spinner-container flex-grow-1" *ngIf="isBusy$ | async">
<app-progress-spinner [message]="spinnerMessage$ | async"> </app-progress-spinner>
</div>
<div *ngIf="(isBusy$ | async) === false && hasData === false" class="no-addons-container text-1 flex-grow-1">
<div *ngIf="showNoAddons$ | async" class="no-addons-container text-1 flex-grow-1">
<h1>{{ "COMMON.SEARCH.NO_ADDONS" | translate }}</h1>
</div>
<ag-grid-angular class="wu-ag-table ag-theme-material" [hidden]="(isBusy$ | async) === true || hasData === false"
[rowData]="rowData" [columnDefs]="columnDefs" [rowSelection]="'multiple'" [getRowNodeId]="getRowNodeId"
[frameworkComponents]="frameworkComponents" [rowHeight]="63" [immutableData]="true" [rowClassRules]="rowClassRules"
<ag-grid-angular class="wu-ag-table ag-theme-material" [hidden]="hideGrid$ | async" [rowData]="rowDataG" [columnDefs]="columnDefs"
[rowSelection]="'multiple'" [getRowNodeId]="getRowNodeId" [frameworkComponents]="frameworkComponents"
[rowHeight]="63" [immutableData]="true" [rowClassRules]="rowClassRules"
[overlayNoRowsTemplate]="overlayNoRowsTemplate" (gridReady)="onGridReady($event)"
(rowDoubleClicked)="onRowDoubleClicked($event)" (rowClicked)="onRowClicked($event)"
(rowDataUpdated)="onRowDataChanged()" (sortChanged)="onSortChanged($event)"
(cellContextMenu)="onCellContext($event)" (keydown)="handleKeyboardEvent($event)"
(firstDataRendered)="onFirstDataRendered()">
(sortChanged)="onSortChanged($event)" (cellContextMenu)="onCellContext($event)"
(keydown)="handleKeyboardEvent($event)" (firstDataRendered)="onFirstDataRendered()">
</ag-grid-angular>
</div>

View File

@@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from "rxjs";
import { OverlayModule } from "@angular/cdk/overlay";
import { HttpClient, HttpClientModule } from "@angular/common/http";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { CUSTOM_ELEMENTS_SCHEMA, ElementRef } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { MatDialog } from "@angular/material/dialog";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
@@ -25,6 +25,17 @@ import { overrideIconModule } from "../../tests/mock-mat-icon";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
import { PushService } from "../../services/push/push.service";
import { InvertBoolPipe } from "../../pipes/inverse-bool.pipe";
export class MockElementRef extends ElementRef {
public constructor() {
super(null);
}
}
export function mockElementFactory(): ElementRef {
return new ElementRef({ nativeElement: jasmine.createSpyObj("nativeElement", ["value"]) });
}
describe("MyAddonsComponent", () => {
let component: MyAddonsComponent;
@@ -72,6 +83,7 @@ describe("MyAddonsComponent", () => {
selectedClientType$: new BehaviorSubject(WowClientType.Retail).asObservable(),
targetFileInstallComplete$: new Subject<boolean>(),
addonsChanged$: new BehaviorSubject([]),
selectedWowInstallation$: new BehaviorSubject(undefined),
});
warcraftServiceSpy = jasmine.createSpyObj("WarcraftService", [""], {
installedClientTypesSelectItems$: new BehaviorSubject<WowClientType[] | undefined>(undefined).asObservable(),
@@ -87,7 +99,7 @@ describe("MyAddonsComponent", () => {
});
let testBed = TestBed.configureTestingModule({
declarations: [MyAddonsComponent],
declarations: [MyAddonsComponent, InvertBoolPipe],
imports: [
MatModule,
OverlayModule,
@@ -129,6 +141,11 @@ describe("MyAddonsComponent", () => {
fixture = TestBed.createComponent(MyAddonsComponent);
component = fixture.componentInstance;
component.addonFilter = {
nativeElement: jasmine.createSpyObj("nativeElement", ["value"]),
};
console.debug("addonFilter", component.addonFilter);
fixture.detectChanges();
});
@@ -138,6 +155,7 @@ describe("MyAddonsComponent", () => {
});
it("should create", () => {
console.debug("addonFilter", component.addonFilter);
expect(component).toBeTruthy();
});
});

View File

@@ -12,11 +12,20 @@ import {
} from "ag-grid-community";
import * as _ from "lodash";
import { join } from "path";
import { BehaviorSubject, from, Observable, of, Subject, Subscription, zip } from "rxjs";
import { catchError, debounceTime, delay, filter, first, map, switchMap, tap } from "rxjs/operators";
import { BehaviorSubject, combineLatest, from, fromEvent, Observable, of, Subject, Subscription, zip } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, filter, 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";
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewChild,
} from "@angular/core";
import { MatCheckboxChange } from "@angular/material/checkbox";
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
import { MatMenuTrigger } from "@angular/material/menu";
@@ -57,45 +66,95 @@ import { PushService } from "../../services/push/push.service";
styleUrls: ["./my-addons.component.scss"],
})
export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
@Input("tabIndex") public tabIndex!: number;
@Input("tabIndex") public set tabIndex(value: number) {
this._tabIndexSrc.next(value);
}
@ViewChild("addonContextMenuTrigger", { static: false }) public contextMenu!: MatMenuTrigger;
@ViewChild("addonMultiContextMenuTrigger", { static: false }) public multiContextMenu!: MatMenuTrigger;
@ViewChild("columnContextMenuTrigger", { static: false }) public columnContextMenu!: MatMenuTrigger;
@ViewChild("addonFilter") public addonFilter: ElementRef;
// @HostListener("window:keydown", ["$event"])
private readonly _operationErrorSrc = new Subject<Error>();
private readonly _isBusySrc = new BehaviorSubject<boolean>(true);
private readonly _enableControlsSrc = new BehaviorSubject<boolean>(false);
private readonly _tabIndexSrc = new BehaviorSubject<number | undefined>(undefined);
private readonly _baseRowDataSrc = new BehaviorSubject<AddonViewModel[]>([]);
private readonly _filterInputSrc = new BehaviorSubject<string>("");
private readonly _spinnerMessageSrc = new BehaviorSubject<string>("");
public readonly enableControls$ = this._enableControlsSrc.asObservable();
public readonly spinnerMessage$ = this._spinnerMessageSrc.asObservable();
public readonly selectedWowInstallationId$ = this._sessionService.selectedWowInstallation$.pipe(
map((wowInstall) => wowInstall?.id)
);
public readonly hasSelectedWowInstallationId$ = this._sessionService.selectedWowInstallation$.pipe(
map((wowInstall) => wowInstall !== undefined)
);
public readonly isBusy$ = this._isBusySrc.asObservable();
public readonly filterInput$ = this._filterInputSrc.asObservable();
private _subscriptions: Subscription[] = [];
private isSelectedTab = false;
private _lazyLoaded = false;
private _isRefreshing = false;
private _baseRowData: AddonViewModel[] = [];
private _lastSelectionState: RowNode[] = [];
public readonly rowData$ = combineLatest([this._baseRowDataSrc, this._filterInputSrc]).pipe(
map(([rowData, filterVal]) => {
return this.filterAddons(rowData, filterVal);
})
);
public readonly hasData$ = this.rowData$.pipe(map((data) => data.length > 0));
public readonly enableUpdateAll$ = combineLatest([this.enableControls$, this.rowData$]).pipe(
map(([enableControls, rowData]) => {
return enableControls && rowData.some((row) => AddonUtils.needsUpdate(row.addon));
})
);
public readonly enableUpdateExtra$ = combineLatest([
this.enableControls$,
this.addonService.anyUpdatesAvailable$,
]).pipe(
map(([enableControls, updatesAvailable]) => {
return enableControls && updatesAvailable;
})
);
public readonly hideGrid$ = combineLatest([this.isBusy$, this.hasData$]).pipe(
map(([isBusy, hasData]) => {
return isBusy || !hasData;
})
);
public readonly showNoAddons$ = combineLatest([this.isBusy$, this.hasData$]).pipe(
map(([isBusy, hasData]) => {
return !isBusy && !hasData;
})
);
public readonly isSelectedTab$ = combineLatest([this._sessionService.selectedHomeTab$, this._tabIndexSrc]).pipe(
map(([selectedTab, ownTab]) => {
return selectedTab !== undefined && ownTab !== undefined && selectedTab === ownTab;
})
);
public readonly operationError$ = this._operationErrorSrc.asObservable();
private _subscriptions: Subscription[] = [];
private _lazyLoaded = false;
private _isRefreshing = false;
private _lastSelectionState: RowNode[] = [];
public updateAllContextMenu!: MatMenuTrigger;
public spinnerMessage = "";
public contextMenuPosition = { x: "0px", y: "0px" };
public filter = "";
public overlayNoRowsTemplate = "";
public addonUtils = AddonUtils;
public selectedClient = WowClientType.None;
public selectedInstallation: WowInstallation | undefined = undefined;
public wowClientType = WowClientType;
public overlayRef: OverlayRef | null = null;
public enableControls = true;
public wowInstallations$: Observable<WowInstallation[]>;
public selectedInstallationId!: string;
public rowData: AddonViewModel[] = [];
public filterInput$ = new Subject<string>();
public rowDataChange$ = new Subject<boolean>();
// Grid
public rowDataG: any[] = [];
public columnDefs: ColDef[] = [];
public frameworkComponents = {};
public gridApi!: GridApi;
@@ -165,14 +224,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return this.columns.filter((col) => col.visible).map((col) => col.name);
}
public get enableUpdateAll(): boolean {
return _.some(this._baseRowData, (row) => AddonUtils.needsUpdate(row.addon));
}
public get hasData(): boolean {
return this._baseRowData.length > 0;
}
public constructor(
private _sessionService: SessionService,
private _dialog: MatDialog,
@@ -196,11 +247,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.wowInstallations$ = warcraftInstallationService.wowInstallations$;
// When the search input changes debounce it a little before searching
const filterInputSub = this.filterInput$.pipe(debounceTime(200)).subscribe(() => {
this.filterAddons();
});
const addonInstalledSub = this.addonService.addonInstalled$
.pipe(
map((evt) => this.onAddonInstalledEvent(evt)),
@@ -228,13 +274,17 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
.subscribe();
this._subscriptions.push(
this._sessionService.selectedHomeTab$.subscribe(this.onSelectedTabChange),
this.isSelectedTab$
.pipe(
filter((isSelected) => isSelected === true),
switchMap(this.onSelectedTabChange)
)
.subscribe(),
this._sessionService.addonsChanged$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
this._sessionService.targetFileInstallComplete$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
pushUpdateSub,
addonInstalledSub,
addonRemovedSub,
filterInputSub
addonRemovedSub
);
this.frameworkComponents = {
@@ -273,14 +323,17 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
columnDef.hide = !col.visible;
}
});
this.onSelectedTabChange(this._sessionService.getSelectedHomeTab());
}
public ngOnDestroy(): void {
this._subscriptions.forEach((sub) => sub.unsubscribe());
}
public onClickClearFilter(): void {
this.addonFilter.nativeElement.value = "";
this._filterInputSrc.next("");
}
public handleKeyboardEvent(event: KeyboardEvent): void {
if (this.selectAllRows(event)) {
return;
@@ -299,10 +352,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.wowUpService.setMyAddonsSortOrder(minimalState);
}
public onRowDataChanged(): void {
this.rowDataChange$.next(true);
}
public onFirstDataRendered(): void {
this.autoSizeColumns();
}
@@ -324,43 +373,60 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.loadSortOrder();
this.rowDataChange$.pipe(debounceTime(500)).subscribe(() => {
this.redrawRows();
});
this.rowData$
.pipe(
tap((data) => {
this.gridApi.setRowData(data);
this.setPageContextText();
})
)
.subscribe();
}
public ngAfterViewInit(): void {
this._sessionService.myAddonsCompactVersion = !this.getLatestVersionColumnVisible();
if (this.addonFilter?.nativeElement !== undefined) {
const addonFilterSub = fromEvent(this.addonFilter.nativeElement, "keyup")
.pipe(
filter(Boolean),
debounceTime(200),
distinctUntilChanged(),
tap(() => {
console.debug(this.addonFilter.nativeElement.value);
this._filterInputSrc.next(this.addonFilter.nativeElement.value);
})
)
.subscribe();
this._subscriptions.push(addonFilterSub);
}
this._sessionService.autoUpdateComplete$
.pipe(
tap(() => console.log("Checking for addon updates...")),
switchMap(() => from(this.loadAddons(this.selectedInstallation)))
switchMap(() => from(this.loadAddons()))
)
.subscribe(() => {
this._cdRef.markForCheck();
});
}
public onSelectedTabChange = (tabIndex: number): void => {
this.isSelectedTab = tabIndex === this.tabIndex;
if (!this.isSelectedTab) {
return;
}
public onSelectedTabChange = (): Observable<void> => {
this.setPageContextText();
from(this.lazyLoad())
.pipe(
first(),
// delay(400),
// map(() => {
// this.redrawRows();
// }),
catchError((e) => {
console.error(e);
return of(undefined);
})
)
.subscribe();
return from(this.lazyLoad()).pipe(
first(),
tap(() => console.debug("TAP IT")),
// delay(400),
// map(() => {
// this.redrawRows();
// }),
catchError((e) => {
console.error(e);
return of(undefined);
})
);
};
// Get the translated value of the provider name (unknown)
@@ -382,24 +448,26 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this._isRefreshing = true;
this._isBusySrc.next(true);
this.enableControls = false;
this._enableControlsSrc.next(false);
try {
console.debug("onRefresh");
await this.addonService.syncAllClients();
if (this.selectedInstallation) {
await this._wowUpAddonService.updateForInstallation(this.selectedInstallation);
const selectedWowInstall = this._sessionService.getSelectedWowInstallation();
if (selectedWowInstall !== undefined) {
await this._wowUpAddonService.updateForInstallation(selectedWowInstall);
}
await this.loadAddons(this.selectedInstallation);
await this.loadAddons();
await this.updateBadgeCount();
} catch (e) {
console.error(`Failed to refresh addons`, e);
} finally {
this._isBusySrc.next(false);
this.enableControls = true;
this._enableControlsSrc.next(true);
this._isRefreshing = false;
}
};
@@ -433,37 +501,25 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return listItem.addon.warningType === undefined && this.addonService.canReinstall(listItem.addon);
}
/** Handle when the user enters new text into the filter box */
public filterAddons(): void {
if (this.filter.length === 0) {
this.rowData = this._baseRowData;
this._cdRef.detectChanges();
return;
public filterAddons(rowData: AddonViewModel[], filterVal: string): AddonViewModel[] {
if (filterVal.length === 0) {
return rowData;
}
const filter = this.filter.trim().toLowerCase();
const filtered = _.filter(this._baseRowData, (row) => this.filterListItem(row, filter));
this.rowData = filtered;
this._cdRef.detectChanges();
}
// Handle when the user clicks the clear button on the filter input box
public onClearFilter(): void {
this.filter = "";
this.filterInput$.next(this.filter);
const filter = filterVal.trim().toLowerCase();
return rowData.filter((row) => this.filterListItem(row, filter));
}
// Handle when the user clicks the update all button
public async onUpdateAll(): Promise<void> {
if (!this.selectedInstallation) {
const selectedWowInstall = this._sessionService.getSelectedWowInstallation();
if (selectedWowInstall === undefined) {
return;
}
this.enableControls = false;
this._enableControlsSrc.next(false);
const addons = await this.addonService.getAddons(this.selectedInstallation, false);
const addons = await this.addonService.getAddons(selectedWowInstall, false);
try {
const filteredAddons = _.filter(addons, (addon) => AddonUtils.needsUpdate(addon));
@@ -480,7 +536,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
console.error(err);
}
this.enableControls = this.calculateControlState();
this._enableControlsSrc.next(this.calculateControlState());
}
// Handle when the user clicks the update all retail/classic button
@@ -542,6 +598,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
public async onReInstallAddons(listItems: AddonViewModel[]): Promise<void> {
try {
console.debug("onReInstallAddons", listItems);
const tasks = _.map(listItems, (listItem) => this.addonService.installAddon(listItem.addon?.id ?? ""));
await Promise.all(tasks);
} catch (e) {
@@ -573,9 +630,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.gridColumnApi.setColumnVisible(column.name, event.checked);
if (column.name === "latestVersion") {
const updates = [...this._baseRowData];
updates.forEach((update) => (update.showUpdate = !event.checked));
this.rowData = updates;
this._sessionService.myAddonsCompactVersion = !event.checked;
}
}
@@ -598,14 +653,14 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
console.log("Performing re-scan");
return from(this.loadAddons(this.selectedInstallation, true)).pipe(switchMap(() => from(this.onRefresh())));
return from(this.loadAddons(true)).pipe(switchMap(() => from(this.onRefresh())));
})
)
.subscribe();
}
public onClientChange(): void {
this._sessionService.setSelectedWowInstallation(this.selectedInstallationId);
public onClientChange(evt: any): void {
this._sessionService.setSelectedWowInstallation(evt.value);
}
public onRemoveAddon(addon: Addon): void {
@@ -640,7 +695,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
},
});
}),
switchMap(() => from(this.loadAddons(this.selectedInstallation)))
switchMap(() => from(this.loadAddons()))
);
}
})
@@ -722,7 +777,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
public onClickIgnoreAddons(listItems: AddonViewModel[]): void {
const isIgnored = _.every(listItems, (listItem) => listItem.addon?.isIgnored === false);
const rows = [...this._baseRowData];
const rows = _.cloneDeep(this._baseRowDataSrc.value);
try {
for (const listItem of listItems) {
const row = _.find(rows, (r) => r.addon?.id === listItem.addon?.id);
@@ -739,7 +794,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.addonService.saveAddon(row.addon);
}
this.rowData = rows;
this._baseRowDataSrc.next(rows);
} catch (e) {
console.error(`Failed to ignore addon(s)`, e);
} finally {
@@ -771,12 +826,16 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
evt.node.setSelected(true);
}
private getModelById(rows: AddonViewModel[], model: AddonViewModel): AddonViewModel | undefined {
return rows.find((row) => row.addon?.id == model.addon?.id);
}
public onClickAutoUpdateAddons(listItems: AddonViewModel[]): void {
const isAutoUpdate = _.every(listItems, (listItem) => listItem.addon?.autoUpdateEnabled === false);
const rows = [...this._baseRowData];
const rows = _.cloneDeep(this._baseRowDataSrc.value);
try {
for (const listItem of listItems) {
const row = _.find(rows, (r) => r.addon?.id === listItem.addon?.id);
const row = this.getModelById(rows, listItem);
if (!row || !row.addon) {
console.warn("Invalid row data");
continue;
@@ -790,7 +849,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.addonService.saveAddon(row.addon);
}
this.rowData = rows;
this._baseRowDataSrc.next(rows);
} catch (e) {
console.error(e);
this._operationErrorSrc.next(e);
@@ -822,18 +881,14 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return of(undefined);
}
const selectedWowInstall = this._sessionService.getSelectedWowInstallation();
const externalId = _.find(listItem.addon?.externalIds, (extId) => extId.providerName === evt.value);
if (!externalId || !this.selectedInstallation) {
if (!externalId || !selectedWowInstall) {
throw new Error("External id not found");
}
return from(
this.addonService.setProvider(
listItem.addon,
externalId.id,
externalId.providerName,
this.selectedInstallation
)
this.addonService.setProvider(listItem.addon, externalId.id, externalId.providerName, selectedWowInstall)
);
}),
catchError((e) => {
@@ -909,7 +964,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this._lazyLoaded = true;
this._isBusySrc.next(true);
this.enableControls = false;
this._enableControlsSrc.next(false);
// TODO this shouldn't be here
await this.addonService.backfillAddons();
@@ -923,9 +978,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return of(undefined);
}
this.selectedInstallation = installation;
this.selectedInstallationId = installation.id;
return from(this.loadAddons(this.selectedInstallation));
return from(this.loadAddons());
}),
catchError((e) => {
console.error(`selectedInstallationSub failed`, e);
@@ -940,8 +993,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
private async updateAllWithSpinner(...installations: WowInstallation[]): Promise<void> {
this._isBusySrc.next(true);
this.spinnerMessage = this._translateService.instant("PAGES.MY_ADDONS.SPINNER.GATHERING_ADDONS");
this.enableControls = false;
this._spinnerMessageSrc.next(this._translateService.instant("PAGES.MY_ADDONS.SPINNER.GATHERING_ADDONS"));
this._enableControlsSrc.next(false);
let addons: Addon[] = [];
let updatedCt = 0;
@@ -957,14 +1010,16 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
);
if (addons.length === 0) {
await this.loadAddons(this.selectedInstallation);
await this.loadAddons();
return;
}
this.spinnerMessage = this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING", {
updateCount: updatedCt,
addonCount: addons.length,
});
this._spinnerMessageSrc.next(
this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING", {
updateCount: updatedCt,
addonCount: addons.length,
})
);
for (const addon of addons) {
if (!addon.id) {
@@ -980,25 +1035,27 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
continue;
}
this.spinnerMessage = this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING_WITH_ADDON_NAME", {
updateCount: updatedCt,
addonCount: addons.length,
clientType: installation.label,
addonName: addon.name,
});
this._spinnerMessageSrc.next(
this._translateService.instant("PAGES.MY_ADDONS.SPINNER.UPDATING_WITH_ADDON_NAME", {
updateCount: updatedCt,
addonCount: addons.length,
clientType: installation.label,
addonName: addon.name,
})
);
await this.addonService.updateAddon(addon.id);
}
await this.loadAddons(this.selectedInstallation);
await this.loadAddons();
} catch (err) {
console.error("Failed to update classic/retail", err);
this._isBusySrc.next(false);
this._cdRef.detectChanges();
} finally {
this.spinnerMessage = "";
this.enableControls = this.calculateControlState();
this._spinnerMessageSrc.next("");
this._enableControlsSrc.next(this.calculateControlState());
}
}
@@ -1007,21 +1064,21 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.contextMenuPosition.y = `${event.clientY as number}px`;
}
private loadAddons = async (installation: WowInstallation | undefined, reScan = false): Promise<void> => {
private loadAddons = async (reScan = false): Promise<void> => {
const installation = this._sessionService.getSelectedWowInstallation();
if (!installation) {
return;
}
this._isBusySrc.next(true);
this.enableControls = false;
this._enableControlsSrc.next(false);
if (!installation) {
console.warn("Skipping addon load installation unknown");
return;
}
this.rowData = this._baseRowData = [];
this._cdRef.detectChanges();
try {
@@ -1032,25 +1089,26 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
const rowData = this.formatAddons(addons);
this.enableControls = this.calculateControlState();
this._baseRowData = rowData;
this.rowData = this._baseRowData;
this._baseRowDataSrc.next(rowData);
this.setPageContextText();
this._cdRef.detectChanges();
} catch (e) {
console.error(e);
this.enableControls = this.calculateControlState();
} finally {
this._isBusySrc.next(false);
this._cdRef.detectChanges();
this._enableControlsSrc.next(this.calculateControlState());
}
};
private getLatestVersionColumnVisible(): boolean {
return this.columns.find((col) => col.name === "latestVersion")?.visible ?? true;
}
private formatAddons(addons: Addon[]): AddonViewModel[] {
const showUpdate = !(this.columns.find((col) => col.name === "latestVersion")?.visible ?? true);
const viewModels = addons.map((addon) => {
const listItem = new AddonViewModel(addon);
@@ -1058,7 +1116,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
listItem.addon.installedVersion = "";
}
listItem.showUpdate = showUpdate;
return listItem;
});
@@ -1077,54 +1134,58 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
};
private setPageContextText() {
const itemsLength = this.rowData.length;
if (itemsLength === 0) {
return;
}
this.rowData$
.pipe(
first(),
map((data) => {
if (data.length === 0) {
return;
}
this._sessionService.setContextText(
this.tabIndex,
this._translateService.instant("PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.ADDONS_INSTALLED", {
count: itemsLength,
})
);
this._sessionService.setContextText(
this._tabIndexSrc.value,
this._translateService.instant("PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.ADDONS_INSTALLED", {
count: data.length,
})
);
})
)
.subscribe();
}
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;
}
const idx = this._baseRowData.findIndex((r) => r.addon?.id === evt.addon.id);
// If we have a new addon, just put it at the end
if (idx === -1) {
this._baseRowData.push(new AddonViewModel(evt.addon));
this._baseRowData = _.orderBy(this._baseRowData, (row) => row.addon?.name);
} else {
this._baseRowData.splice(idx, 1, new AddonViewModel(evt.addon));
}
this.rowData = [...this._baseRowData];
this.enableControls = this.calculateControlState();
} finally {
this._cdRef.detectChanges();
if (evt.addon.installationId !== this._sessionService.getSelectedWowInstallation()?.id) {
return;
}
if ([AddonInstallState.Complete, AddonInstallState.Error].includes(evt.installState) === false) {
this._enableControlsSrc.next(false);
return;
}
let rowData = _.cloneDeep(this._baseRowDataSrc.value);
const idx = rowData.findIndex((r) => r.addon?.id === evt.addon.id);
// If we have a new addon, just put it at the end
if (idx === -1) {
console.debug("Adding new addon to list");
rowData.push(new AddonViewModel(evt.addon));
rowData = _.orderBy(rowData, (row) => row.canonicalName);
} else {
rowData.splice(idx, 1, new AddonViewModel(evt.addon));
}
this._baseRowDataSrc.next(rowData);
this._enableControlsSrc.next(this.calculateControlState());
};
private onAddonRemoved = (addonId: string) => {
const listItemIdx = this._baseRowData.findIndex((li) => li.addon?.id === addonId);
this._baseRowData.splice(listItemIdx, 1);
const rowData = _.cloneDeep(this._baseRowDataSrc.value);
this.rowData = [...this._baseRowData];
this._cdRef.detectChanges();
const listItemIdx = rowData.findIndex((li) => li.addon?.id === addonId);
rowData.splice(listItemIdx, 1);
this._baseRowDataSrc.next(rowData);
};
private showErrorMessage(title: string, message: string) {
@@ -1170,7 +1231,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
private redrawRows() {
// this.gridApi?.redrawRows();
this.gridApi?.redrawRows();
// this.gridApi?.resetRowHeights();
// this.autoSizeColumns();
this._cdRef.detectChanges();
@@ -1204,26 +1265,24 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return [
{
field: "name",
cellRenderer: "myAddonRenderer",
field: "hash",
flex: 2,
minWidth: 300,
headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER"),
sortable: true,
// autoHeight: true,
cellRenderer: "myAddonRenderer",
colId: "name",
valueGetter: (params) => {
return params.data.canonicalName;
},
comparator: (va, vb, na, nb) => this.compareElement(na, nb, "canonicalName"),
...baseColumn,
},
{
field: "sortOrder",
width: 150,
sortable: true,
headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER"),
cellRenderer: "myAddonStatus",
comparator: (va, vb, na, nb) => this.compareElement(na, nb, "sortOrder"),
field: "sortOrder",
headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER"),
sortable: true,
width: 150,
...baseColumn,
},
{

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from "@angular/core";
import { getGameVersion } from "../utils/addon.utils";
@Pipe({
name: "invertBool",
})
export class InvertBoolPipe implements PipeTransform {
public transform(value: boolean): boolean {
return !value;
}
}

View File

@@ -24,6 +24,7 @@ export class SessionService {
private readonly _addonsChangedSrc = new Subject<boolean>();
private readonly _myAddonsColumnsSrc = new BehaviorSubject<ColumnState[]>([]);
private readonly _targetFileInstallCompleteSrc = new Subject<boolean>();
private readonly _myAddonsCompactVersionSrc = new BehaviorSubject<boolean>(false);
private readonly _getAddonsColumnsSrc = new Subject<ColumnState>();
@@ -42,9 +43,14 @@ export class SessionService {
public readonly wowUpAuthToken$ = this._wowUpAccountService.wowUpAuthTokenSrc.asObservable();
public readonly wowUpAccount$ = this._wowUpAccountService.wowUpAccountSrc.asObservable();
public readonly wowUpAccountPushEnabled$ = this._wowUpAccountService.accountPushSrc.asObservable();
public readonly myAddonsCompactVersion$ = this._myAddonsCompactVersionSrc.asObservable();
public readonly wowUpAuthenticated$ = this.wowUpAccount$.pipe(map((account) => account !== undefined));
public set myAddonsCompactVersion(val: boolean) {
this._myAddonsCompactVersionSrc.next(val);
}
public constructor(
private _warcraftInstallationService: WarcraftInstallationService,
private _preferenceStorageService: PreferenceStorageService,

View File

@@ -41,9 +41,11 @@ const CHANGELOGS: ChangeLog[] = [
<li>Spanish locale updates (SkollVargr)</li>
<li>Chinese locale updates (CyanoHao)</li>
<li>Tweak the tabs some more</li>
<li>Performance improvements for My Addons page</li>
<li>Images details tab is now Previews</li>
<li>Use a different lightbox library for previews</li>
<li>Expanded system proxy support</li>`,
<li>Expanded system proxy support</li>
</ul>`,
},
{
Version: "2.4.4",

View File

@@ -175,7 +175,7 @@
}
},
"ERRORS": {
"ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "Failed to toggle instant updates for your account. Please try again later, or reach out on Discord.",
"ACCOUNT_PUSH_TOGGLE_FAILED_ERROR": "No se pudieron activar las actualizaciones instantáneas en su cuenta. Por favor, inténtelo de nuevo más tarde o contacte con nosotros en Discord.",
"ADDON_INSTALL_ERROR": "Falló la instalación del addon {addonName}. Por favor, inténtelo de nuevo más tarde.",
"ADDON_SCAN_ERROR": "Ocurrió un error al comparar sus carpetas de addons con {providerName}. Por favor, inténtelo de nuevo más tarde.",
"ADDON_SYNC_ERROR": "Ocurrió un error al comprobar actualizaciones desde: {providerName}",
@@ -277,12 +277,12 @@
},
"ACCOUNT": {
"BETA": "Beta",
"LOGIN_BUTTON": "Login Now!",
"LOGOUT_BUTTON": "Logout",
"LOGOUT_CONFIRMATION_MESSAGE": "Are you sure you want to log out? All of your local account data will be removed, until you login again.",
"LOGOUT_CONFIRMATION_TITLE": "Logout?",
"MANAGE_ACCOUNT_BUTTON": "Manage Account",
"TITLE": "Account"
"LOGIN_BUTTON": "¡Iniciar sesión ahora!",
"LOGOUT_BUTTON": "Cerrar sesión",
"LOGOUT_CONFIRMATION_MESSAGE": "¿Quiere cerrar la sesión? Toda la información local sobre su cuenta será eliminada hasta que inicie sesión de nuevo.",
"LOGOUT_CONFIRMATION_TITLE": "¿Cerrar sesión?",
"MANAGE_ACCOUNT_BUTTON": "Administrar cuenta",
"TITLE": "Cuenta"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categorías",
@@ -307,7 +307,7 @@
},
"HOME": {
"ABOUT_TAB_TITLE": "Acerca de",
"ACCOUNT_TAB_TITLE": "Account",
"ACCOUNT_TAB_TITLE": "Cuenta",
"GET_ADDONS_TAB_TITLE": "Obtener addons",
"GUIDE_TAB_TITLE": "Guía",
"MIGRATING_ADDONS": "Migrando lista de addons...",
@@ -463,7 +463,7 @@
"TITLE": "Depuración"
},
"TABS": {
"ABOUT": "About",
"ABOUT": "Acerca de",
"ADDONS": "Addons",
"APPLICATION": "Aplicación",
"CLIENTS": "Clientes",

View File

@@ -88,6 +88,7 @@ declare global {
handlebars: any;
autoLaunch: any;
};
baseBgColor: string;
platform: string;
userDataPath: string;
logPath: string;

View File

@@ -230,6 +230,7 @@ body {
--background-secondary-4: #333333;
--background-secondary-5: #222222;
--control-color: #536dfe;
--epic-color: #a335ee;
--rare-color: #0070dd;
--scrollbar-track-color: #333333;
--text-1: #ffffff;

View File

@@ -28,12 +28,19 @@
</div>
</app-root>
<script>
document.body.classList.add(window.platform)
if (window.platform === 'darwin') {
document.getElementById('preload-logo').style.borderRadius = '23px';
}
try {
window.global = window;
document.body.classList.add(window.platform)
if (window.platform === 'darwin') {
document.getElementById('preload-logo').style.borderRadius = '23px';
}
document.body.style.backgroundColor = window.baseBgColor;
} catch (e) {
console.error(e)
}
</script>
</body>
</html>
</html>

View File

@@ -2,7 +2,6 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"module": "commonjs",
"types": [
"jasmine",
"node"