Merge branch 'feature/toggle-addon-specific-auto-update-notifications' of https://github.com/tellier-dev/WowUp into feature/toggle-addon-specific-auto-update-notifications

This commit is contained in:
Povel Galfvensjö
2021-09-06 09:34:46 +02:00
61 changed files with 3812 additions and 233 deletions

View File

@@ -70,6 +70,10 @@ import {
IPC_WOWUP_GET_SCAN_RESULTS,
IPC_WRITE_FILE_CHANNEL,
DEFAULT_FILE_MODE,
IPC_PUSH_INIT,
IPC_PUSH_REGISTER,
IPC_PUSH_UNREGISTER,
IPC_PUSH_SUBSCRIBE,
} from "../src/common/constants";
import { CurseFolderScanResult } from "../src/common/curse/curse-folder-scan-result";
import { Addon } from "../src/common/entities/addon";
@@ -89,6 +93,7 @@ import { chmodDir, copyDir, getDirTree, getLastModifiedFileDate, readDirRecursiv
import { addonStore } from "./stores";
import { createTray, restoreWindow } from "./system-tray";
import { WowUpFolderScanner } from "./wowup-folder-scanner";
import * as push from "./push";
let USER_AGENT = "";
let PENDING_OPEN_URLS: string[] = [];
@@ -478,6 +483,22 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
return await dialog.showOpenDialog(options);
});
handle(IPC_PUSH_INIT, (evt) => {
return push.startPushService();
});
handle(IPC_PUSH_REGISTER, async (evt, appId: string) => {
return await push.registerForPush(appId);
});
handle(IPC_PUSH_UNREGISTER, async (evt) => {
return await push.unregisterPush();
});
handle(IPC_PUSH_SUBSCRIBE, async (evt, channel) => {
return await push.subscribeToChannel(channel);
});
ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => {
handleDownloadFile(arg).catch((e) => console.error(e));
});

View File

@@ -23,6 +23,7 @@ import {
IPC_POWER_MONITOR_RESUME,
IPC_POWER_MONITOR_SUSPEND,
IPC_POWER_MONITOR_UNLOCK,
IPC_PUSH_NOTIFICATION,
IPC_WINDOW_ENTER_FULLSCREEN,
IPC_WINDOW_LEAVE_FULLSCREEN,
IPC_WINDOW_MAXIMIZED,
@@ -40,6 +41,7 @@ import { MainChannels } from "../src/common/wowup";
import { AppOptions } from "../src/common/wowup/models";
import { initializeStoreIpcHandlers, preferenceStore } from "./stores";
import { windowStateManager } from "./window-state";
import { pushEvents, PUSH_NOTIFICATION_EVENT } from "./push";
// LOGGING SETUP
// Override the default log path so they aren't a pain to find on Mac
@@ -282,6 +284,10 @@ function createWindow(): BrowserWindow {
initializeIpcHandlers(win, USER_AGENT);
initializeStoreIpcHandlers();
pushEvents.on(PUSH_NOTIFICATION_EVENT, (data) => {
win.webContents.send(IPC_PUSH_NOTIFICATION, data);
});
// Keep track of window state
mainWindowManager.monitorState(win);
@@ -351,6 +357,7 @@ function createWindow(): BrowserWindow {
win.on("close", (e) => {
if (appIsQuitting || preferenceStore.get(COLLAPSE_TO_TRAY_PREFERENCE_KEY) !== "true") {
pushEvents.removeAllListeners(PUSH_NOTIFICATION_EVENT);
return;
}
e.preventDefault();

View File

@@ -0,0 +1,64 @@
import * as log from "electron-log";
import { EventEmitter } from "events";
export const PUSH_NOTIFICATION_EVENT = "push-notification";
export const pushEvents = new EventEmitter();
const channelSubscriptions = new Map<string, boolean>();
let Pushy: any;
export function startPushService(): boolean {
if (!Pushy) {
Pushy = require("pushy-electron");
}
// Listen for push notifications
Pushy.setNotificationListener((data) => {
pushEvents.emit(PUSH_NOTIFICATION_EVENT, data);
});
Pushy.listen();
return true;
}
export async function registerForPush(appId: string): Promise<string> {
if (!Pushy) {
throw new Error("Push not started");
}
if (!appId) {
throw new Error("Invalid push app id");
}
return await Pushy.register({ appId });
}
export async function unregisterPush(): Promise<void> {
for (const [key] of channelSubscriptions) {
try {
await Pushy.unsubscribe(key);
} catch (e) {
console.error(e);
}
}
channelSubscriptions.clear();
Pushy.disconnect();
}
export async function subscribeToChannel(channel: string): Promise<void> {
// Make sure the user is registered
if (!Pushy.isRegistered()) {
throw new Error("Push services not registered");
}
if (channelSubscriptions.has(channel)) {
log.warn(`Already listening: ${channel}`);
return;
}
// Subscribe the user to a topic
await Pushy.subscribe(channel);
channelSubscriptions.set(channel, true);
log.debug(`Subscribed: ${channel}`);
}

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.11",
"version": "2.5.0-beta.13",
"description": "World of Warcraft addon updater",
"homepage": "https://wowup.io",
"author": {
@@ -153,6 +153,7 @@
"opossum": "6.2.0",
"p-limit": "3.1.0",
"protobufjs": "6.11.2",
"pushy-electron": "1.0.8",
"rxjs": "6.6.7",
"slug": "5.1.0",
"string-similarity": "4.0.4",

View File

@@ -343,6 +343,7 @@ export class CurseAddonProvider extends AddonProvider {
{
fingerprints,
},
undefined,
AppConfig.wowUpHubHttpTimeoutMs
);

View File

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

View File

@@ -7,13 +7,13 @@
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate }}">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fab:discord"></mat-icon>
</a>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === false">
<div class="bnet-btn bnet-btn-sm ml-2" (click)="wowUpService.login()">Login</div>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="btn">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fas:check-circle"
matTooltip="{{ 'You\'re logged in!' | translate }}"></mat-icon>
<!-- <div *ngIf="(sessionService.wowUpAuthenticated$ | async) === false">
<div class="bnet-btn bnet-btn-sm ml-2" (click)="onClickAccount()">Login</div>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="btn" (click)="onClickAccount()"
matTooltip="{{ 'You\'re logged in!' | translate }}">
{{accountDisplayName$ | async}}
</div> -->
<p class="text-1">{{ sessionService.statusText$ | async }}</p>
<div class="flex-grow-1"></div>
<p class="mr-3">{{ sessionService.pageContextText$ | async }}</p>

View File

@@ -15,12 +15,16 @@ import { MatIcon } from "@angular/material/icon";
import { MatIconTestingModule } from "@angular/material/icon/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { mockPreload } from "../../tests/test-helpers";
import { ElectronService } from "../../services";
import { LinkService } from "../../services/links/link.service";
/** Fix icon warning? https://stackoverflow.com/a/62277810 */
describe("FooterComponent", () => {
let fixture: ComponentFixture<FooterComponent>;
let wowUpServiceSpy: WowUpService;
let sessionServiceSpy: SessionService;
let electronService: ElectronService;
let linkService: LinkService;
beforeEach(async () => {
mockPreload();
@@ -35,6 +39,13 @@ describe("FooterComponent", () => {
sessionServiceSpy = jasmine.createSpyObj("SessionService", [""], {
statusText$: new BehaviorSubject(""),
pageContextText$: new BehaviorSubject(""),
wowUpAccount$: new Subject(),
});
linkService = jasmine.createSpyObj("LinkService", [""], {});
electronService = jasmine.createSpyObj("ElectronService", [""], {
appUpdate$: new Subject(),
});
await TestBed.configureTestingModule({
@@ -61,8 +72,10 @@ describe("FooterComponent", () => {
.overrideComponent(FooterComponent, {
set: {
providers: [
{ provide: ElectronService, useValue: electronService },
{ provide: WowUpService, useValue: wowUpServiceSpy },
{ provide: SessionService, useValue: sessionServiceSpy },
{ provide: LinkService, useValue: linkService },
],
},
})

View File

@@ -9,6 +9,8 @@ import { ConfirmDialogComponent } from "../confirm-dialog/confirm-dialog.compone
import { combineLatest, from, Observable, of } from "rxjs";
import { catchError, map, switchMap } from "rxjs/operators";
import { AppUpdateState } from "../../../common/wowup/models";
import { TAB_INDEX_ABOUT } from "../../../common/constants";
import { LinkService } from "../../services/links/link.service";
@Component({
selector: "app-footer",
@@ -26,6 +28,9 @@ export class FooterComponent implements OnInit {
public appUpdateState = AppUpdateState;
public appUpdateState$: Observable<AppUpdateState> = this.electronService.appUpdate$.pipe(map((evt) => evt.state));
public accountDisplayName$: Observable<string> = this.sessionService.wowUpAccount$.pipe(
map((account) => account?.displayName ?? "")
);
public appUpdateProgress$: Observable<number> = combineLatest([
of(0),
@@ -39,7 +44,8 @@ export class FooterComponent implements OnInit {
public wowUpService: WowUpService,
public sessionService: SessionService,
private electronService: ElectronService,
private _wowupService: WowUpService
private _wowupService: WowUpService,
private _linkService: LinkService
) {}
public ngOnInit(): void {
@@ -57,6 +63,10 @@ export class FooterComponent implements OnInit {
this.wowUpService.checkForAppUpdate();
}
public onClickAccount(): void {
this.sessionService.selectedHomeTab = TAB_INDEX_ABOUT;
}
private portableUpdate() {
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
data: {
@@ -74,7 +84,7 @@ export class FooterComponent implements OnInit {
}
return from(
this._wowupService.openExternalLink(
this._linkService.openExternalLink(
`${AppConfig.wowupRepositoryUrl}/releases/tag/v${this.wowUpService.availableVersion}`
)
);

View File

@@ -6,6 +6,7 @@
</a>
<a *ngIf="size === 'small'" class="funding-button small text-1 hover-primary-2 text-1-hover" appExternalLink
[matTooltip]="tooltipKey | translate:{platform:fundingName}" [href]="funding.url">
<mat-icon *ngIf="isFontIcon === true" [svgIcon]="iconSrc"></mat-icon>
<mat-icon *ngIf="isFontIcon === true" [class]="getClassName()" [svgIcon]="iconSrc">
</mat-icon>
<img *ngIf="isFontIcon === false" class="funding-icon" [src]="iconSrc" />
</a>

View File

@@ -41,3 +41,9 @@
}
}
}
.patreon-icon {
color: var(--patreon-color);
}
.custom-icon {
color: var(--wow-gold-color);
}

View File

@@ -26,6 +26,16 @@ export class FundingButtonComponent implements OnInit {
return `PAGES.MY_ADDONS.FUNDING_TOOLTIP.${this.funding.platform.toUpperCase()}`;
}
public getClassName(): string {
switch (this.funding.platform) {
case "PATREON":
return "patreon-icon";
case "GITHUB":
return "github-icon";
default:
return "custom-icon";
}
}
private getIsFontIcon(): boolean {
switch (this.funding.platform) {
case "PATREON":

View File

@@ -15,7 +15,7 @@
</mat-icon> -->
</div>
<div class="spacer"></div>
<a appExternalLink class="text-1 tab hover-bg-secondary-2" href="https://wowup.io/guide/section/my-addons/overview"
<a appExternalLink class="text-1 tab hover-bg-secondary-2" [href]="wowUpWebsiteUrl + '/guide'"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GUIDE' | translate }}">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="far:question-circle"></mat-icon>
<div class="tab-title">{{ 'PAGES.HOME.GUIDE_TAB_TITLE' | translate }}</div>
@@ -28,6 +28,20 @@
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate }}">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fab:discord"></mat-icon>
</a> -->
<div class="tab hover-bg-secondary-2" [ngClass]="{ selected: isAccountSelected$ | async }"
(click)="onClickTab(TAB_INDEX_ACCOUNT)">
<!-- <div class="tab-icon"> -->
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fas:user-circle"
[ngClass]="{ 'tab-icon-active': isAccountSelected$ | async }">
</mat-icon>
<!-- </div> -->
<div>
<div class="tab-title">{{ 'PAGES.HOME.ACCOUNT_TAB_TITLE' | translate }}
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="tab-subtitle">Logged in</div>
</div>
</div>
<div *ngFor="let tab of tabsBottom" class="tab hover-bg-secondary-2"
[ngClass]="{ selected: tab.isSelected$ | async, disabled: tab.isDisabled$ | async }" (click)="tab.onClick(tab)">
<!-- <div class="tab-icon"> -->

View File

@@ -40,6 +40,9 @@
.tab-title {
color: var(--text-4);
margin: 0;
padding: 0;
line-height: 1em;
max-width: 200px;
overflow: hidden;
transition: all 0.3s ease 0.1s;
@@ -47,6 +50,13 @@
white-space: nowrap;
}
.tab-subtitle {
font-size: 0.8em;
margin: 0;
padding: 0;
line-height: 1em;
}
.tab-icon {
margin-right: 0.5em;
transition: margin-right 0.3s ease 0.1s;

View File

@@ -1,15 +1,17 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, Subject } from "rxjs";
import { ElectronService } from "../../services";
import { SessionService } from "../../services/session/session.service";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { getStandardImports, mockPreload } from "../../tests/test-helpers";
import { HorizontalTabsComponent } from "./horizontal-tabs.component";
describe("VerticalTabsComponent", () => {
describe("HorizontalTabsComponent", () => {
let component: HorizontalTabsComponent;
let fixture: ComponentFixture<HorizontalTabsComponent>;
let sessionService: SessionService;
let electronService: any;
let warcraftInstallationService: WarcraftInstallationService;
beforeEach(async () => {
@@ -18,8 +20,11 @@ describe("VerticalTabsComponent", () => {
sessionService = jasmine.createSpyObj("SessionService", ["getSelectedClientType", "getSelectedDetailsTab"], {
getSelectedWowInstallation: () => "description",
selectedHomeTab$: new BehaviorSubject(1),
wowUpAccount$: new Subject(),
});
electronService = jasmine.createSpyObj("ElectronService", [""], {});
warcraftInstallationService = jasmine.createSpyObj("WarcraftInstallationService", [""], {
wowInstallations$: new BehaviorSubject([]),
});
@@ -32,6 +37,7 @@ describe("VerticalTabsComponent", () => {
testBed = testBed.overrideComponent(HorizontalTabsComponent, {
set: {
providers: [
{ provide: ElectronService, useValue: electronService },
{ provide: SessionService, useValue: sessionService },
{ provide: WarcraftInstallationService, useValue: warcraftInstallationService },
],

View File

@@ -13,6 +13,7 @@ import {
import { Observable, of } from "rxjs";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { ElectronService } from "../../services";
import { AppConfig } from "../../../environments/environment";
interface Tab {
titleKey?: string;
@@ -30,6 +31,11 @@ interface Tab {
styleUrls: ["./horizontal-tabs.component.scss"],
})
export class HorizontalTabsComponent implements OnInit {
public wowUpWebsiteUrl = AppConfig.wowUpWebsiteUrl;
public TAB_INDEX_ACCOUNT = TAB_INDEX_ABOUT;
public isAccountSelected$ = this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT));
private myAddonsTab: Tab = {
titleKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE",
tooltipKey: "PAGES.HOME.MY_ADDONS_TAB_TITLE",
@@ -92,7 +98,7 @@ export class HorizontalTabsComponent implements OnInit {
public tabsTop: Tab[] = [this.myAddonsTab, this.getAddonsTab, this.newsTab];
public tabsBottom: Tab[] = [this.aboutTab, this.settingsTab];
public tabsBottom: Tab[] = [this.settingsTab];
public constructor(
public electronService: ElectronService,
@@ -101,4 +107,8 @@ export class HorizontalTabsComponent implements OnInit {
) {}
public ngOnInit(): void {}
public onClickTab(tabIndex: number): void {
this.sessionService.selectedHomeTab = tabIndex;
}
}

View File

@@ -10,11 +10,12 @@
listItem.addon?.name
}}</a>
</div>
<div *ngIf="listItem.addon?.fundingLinks" class="addon-funding">
<app-funding-button *ngFor="let link of listItem.addon?.fundingLinks ?? []" [funding]="link" size="small">
</app-funding-button>
</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">
</app-funding-button>
</div>
<div *ngIf="listItem.isBetaChannel() || listItem.isAlphaChannel()" class="channel bg-secondary-3 mr-2"
[ngClass]="{
beta: listItem.isBetaChannel(),

View File

@@ -1,8 +1,12 @@
@import "../../../variables.scss";
.addon-column {
padding-top: 0.6em;
padding-bottom: 0.6em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
padding-top: 0.5em;
padding-bottom: 0.5em;
.thumbnail-container {
margin-right: 11px;
@@ -57,7 +61,7 @@
}
.title-container {
padding-bottom: 0.5em;
// padding-bottom: 0.5em;
}
.addon-title {

View File

@@ -28,3 +28,17 @@ export interface WowUpSearchAddonsResponse {
addons: WowUpAddonRepresentation[];
count: number;
}
export interface WowUpGetAccountResponse {
displayName: string;
patreonTier: string;
config: WowUpAccountConfig;
}
export interface WowUpAccountConfig {
pushAppId: string;
pushChannels: {
addonUpdates: string;
alerts: string;
};
}

View File

@@ -1,5 +1,6 @@
export interface Toc {
fileName: string;
filePath: string;
interface: string;
title?: string;
author?: string;

View File

@@ -1,11 +1,11 @@
<div class="tab-container about-container" [ngClass]="{
<div class="about-container" [ngClass]="{
mac: electronService.isMac,
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div class="theme-logo">
<!-- <div class="theme-logo">
<div class="logo-img"></div>
</div>
</div> -->
<div class="about">
<div class="header text-1">
<img class="logo mt-3 mat-elevation-z2" loading="lazy" src="assets/wowup_logo_purple.png" />

View File

@@ -3,33 +3,73 @@
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div class="control-container text-1">
<div class="theme-logo">
<div class="logo-img"></div>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === false" class="control-container text-1">
<div class="account-container p-3 bg-secondary-4 mat-elevation-z8 rounded">
<h1>Account <span class="text-control"><i>Beta</i></span></h1>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === false">
<p>Login now to get the most out of your WowUp application.</p>
<button mat-flat-button color="primary" (click)="testLogin()">
{{ "Login Now!" | translate }}
<h1>{{'PAGES.ACCOUNT.TITLE' | translate}} <span class="text-control"><i><sup>{{'PAGES.ACCOUNT.BETA' |
translate}}</sup></i></span></h1>
<p>Login now to get the most out of your WowUp application.</p>
<p>We're working on bringing in some optional cloud based services that we think can really enhance your addon
management experience.</p>
<p>By clicking the login button you will be taken to the WowUp website to start the process.</p>
<div class="mt-3 row justify-content-center">
<button mat-flat-button color="primary" (click)="onClickLogin()">
{{ "PAGES.ACCOUNT.LOGIN_BUTTON" | translate }}
</button>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true">
<h4>You're logged in!</h4>
<p>You're now able to access our various cloud services to help you maximize your addon experience.</p>
<div class="row align-items-center">
<div class="flex-grow-1">
<div>
{{ "Instant Updates" | translate }}
</div>
<small class="text-2">{{ "You should get addon updates as soon as we know about them" | translate }}</small>
</div>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="control-container text-1">
<div class="authorized-container p-3 bg-secondary-4 mat-elevation-z8 rounded">
<h1>{{'PAGES.ACCOUNT.TITLE' | translate}} <span class="text-control"><i><sup>{{'PAGES.ACCOUNT.BETA' |
translate}}</sup></i></span></h1>
<h3>You're logged in!</h3>
<p>You're now able to access our various cloud services to help you maximize your addon experience.</p>
<div class="toggle-row row align-items-center">
<div class="flex-grow-1">
<div>
{{ "Instant Updates" | translate }}
</div>
<mat-slide-toggle>
</mat-slide-toggle>
<small class="text-2">{{ "No more waiting for timers, get updates as soon as we see them" | translate
}}</small>
</div>
<div class="row justify-content-end mt-3">
<button mat-flat-button color="warn" (click)="logout()">
{{ "Logout" | translate }}
</button>
<mat-slide-toggle [checked]="sessionService.wowUpAccountPushEnabled$ | async" (change)="onToggleAccountPush($event)">
</mat-slide-toggle>
</div>
<div class="toggle-row row align-items-center">
<div class="flex-grow-1">
<div>
{{ "Cloud Addon Sync" | translate }}
</div>
<small class="text-2">{{ "The simplest way to manage your addon backups for all your machines" | translate
}}</small>
</div>
<mat-slide-toggle disabled>
</mat-slide-toggle>
</div>
<div class="toggle-row row align-items-center">
<div class="flex-grow-1">
<div>
{{ "Cloud Settings Sync" | translate }}
</div>
<small class="text-2">{{ "Easy to use, hopefully, way to manage your settings between characters and machines"
|
translate
}}</small>
</div>
<mat-slide-toggle disabled>
</mat-slide-toggle>
</div>
<div class="mt-3 row">
<button mat-button color="warn" class="mr-3" (click)="onClickLogout()">
{{ "PAGES.ACCOUNT.LOGOUT_BUTTON" | translate }}
</button>
<div class="flex-grow-1"></div>
<button mat-button color="primary" (click)="onClickManageAccount()">
{{ "PAGES.ACCOUNT.MANAGE_ACCOUNT_BUTTON" | translate }}
</button>
</div>
</div>
</div>

View File

@@ -8,6 +8,40 @@
height: 100%;
}
.authorized-container {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
max-width: 100%;
box-sizing: border-box;
width: 500px;
.toggle-row {
margin-top: 1em;
}
}
.account-container {
width: 400px;
}
.theme-logo {
content: "";
opacity: 0.02;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: hidden;
pointer-events: none;
.logo-img {
height: 100vh;
background-image: var(--theme-logo);
background-repeat: no-repeat;
background-position: 0 0;
background-size: contain;
}
}

View File

@@ -1,6 +1,15 @@
import { Component } from "@angular/core";
import { MatSlideToggleChange } from "@angular/material/slide-toggle";
import { TranslateService } from "@ngx-translate/core";
import { of } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { AppConfig } from "../../../environments/environment";
import { ElectronService } from "../../services";
import { DialogFactory } from "../../services/dialog/dialog.factory";
import { LinkService } from "../../services/links/link.service";
import { SessionService } from "../../services/session/session.service";
import { SnackbarService } from "../../services/snackbar/snackbar.service";
import { WowUpService } from "../../services/wowup/wowup.service";
@Component({
selector: "app-account-page",
@@ -8,15 +17,53 @@ import { SessionService } from "../../services/session/session.service";
styleUrls: ["./account-page.component.scss"],
})
export class AccountPageComponent {
public constructor(public electronService: ElectronService, public sessionService: SessionService) {}
public constructor(
public electronService: ElectronService,
public sessionService: SessionService,
public wowUpService: WowUpService,
private _dialogFactory: DialogFactory,
private _translateService: TranslateService,
private _snackbarService: SnackbarService,
private _linkService: LinkService
) {}
public logout(): void {
this.sessionService.clearWowUpAuthToken();
public onClickLogin(): void {
this.sessionService.login();
}
public testLogin(): void {
this.electronService._customProtocolSrc.next(
"wowup://login/desktop/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
);
public onToggleAccountPush = async (evt: MatSlideToggleChange): Promise<void> => {
try {
await this.sessionService.toggleAccountPush(evt.checked);
} catch (e) {
evt.source.checked = !evt.source.checked;
console.error("Failed to toggle account push", e);
this._snackbarService.showErrorSnackbar("COMMON.ERRORS.ACCOUNT_PUSH_TOGGLE_FAILED_ERROR");
}
};
public onClickLogout(): void {
const title = this._translateService.instant("PAGES.ACCOUNT.LOGOUT_CONFIRMATION_TITLE");
const message = this._translateService.instant("PAGES.ACCOUNT.LOGOUT_CONFIRMATION_MESSAGE");
const dialogRef = this._dialogFactory.getConfirmDialog(title, message);
dialogRef
.afterClosed()
.pipe(
map((result) => {
if (result) {
this.sessionService.logout();
}
}),
catchError((error) => {
console.error(error);
return of(undefined);
})
)
.subscribe();
}
public onClickManageAccount(): void {
this._linkService.openExternalLink(`${AppConfig.wowUpWebsiteUrl}/account`).catch((e) => console.error(e));
}
}

View File

@@ -207,7 +207,7 @@ export class HomeComponent implements AfterViewInit, OnDestroy {
};
public onSelectedIndexChange(index: number): void {
this.sessionService.selectedHomeTab = index;
// this.sessionService.selectedHomeTab = index;
}
private onAddonScanError = (error: AddonScanError) => {

View File

@@ -65,17 +65,17 @@
</div>
</div>
<div class="spinner-container flex-grow-1" *ngIf="isBusy === true">
<div class="spinner-container flex-grow-1" *ngIf="(isBusy$ | async) === true">
<app-progress-spinner [message]="spinnerMessage"> </app-progress-spinner>
</div>
<div *ngIf="isBusy === false && hasData === false" class="no-addons-container text-1 flex-grow-1">
<div *ngIf="(isBusy$ | async) === false && hasData === false" 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 === true || hasData === false"
<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]="65" [immutableData]="true" [rowClassRules]="rowClassRules"
[frameworkComponents]="frameworkComponents" [rowHeight]="63" [immutableData]="true" [rowClassRules]="rowClassRules"
[overlayNoRowsTemplate]="overlayNoRowsTemplate" (gridReady)="onGridReady($event)"
(rowDoubleClicked)="onRowDoubleClicked($event)" (rowClicked)="onRowClicked($event)"
(rowDataUpdated)="onRowDataChanged()" (sortChanged)="onSortChanged($event)"

View File

@@ -1,7 +1,7 @@
@import "../../../variables.scss";
.wu-ag-table {
width: calc(100% - 6px);
width: 100%;
height: calc(100% - 72px);
}
@@ -81,78 +81,80 @@
}
.table-container {
height: calc(100% - 72px);
overflow: auto;
overflow-x: hidden;
table {
width: 100%;
}
.addon-logo-container {
width: 40px;
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 0.5em;
flex-shrink: 0;
// .addon-logo-container {
// width: 40px;
// height: 40px;
// display: flex;
// flex-direction: column;
// justify-content: center;
// margin-right: 0.5em;
// flex-shrink: 0;
.addon-logo {
width: 100%;
}
}
// .addon-logo {
// width: 100%;
// }
// }
.addon-title {
font-weight: 400;
font-size: 1.1em;
word-break: break-all;
white-space: pre-wrap;
}
// .addon-title {
// font-weight: 400;
// font-size: 1.1em;
// word-break: break-all;
// white-space: pre-wrap;
// }
.game-version-cell {
min-width: 110px;
}
// .game-version-cell {
// min-width: 110px;
// }
.installed-at-cell,
.released-at-cell {
min-width: 100px;
}
// .installed-at-cell,
// .released-at-cell {
// min-width: 100px;
// }
.status-column {
// display: flex;
width: 130px;
// align-items: center;
// .status-column {
// // display: flex;
// width: 130px;
// // align-items: center;
.auto-update-icon {
margin-left: 0.5em;
}
// .auto-update-icon {
// margin-left: 0.5em;
// }
.progress-text {
text-align: center;
}
// .progress-text {
// text-align: center;
// }
.addon-progress {
width: 110px;
}
}
// .addon-progress {
// width: 110px;
// }
// }
.status-icon {
height: 11px;
width: 11px;
}
// .status-icon {
// height: 11px;
// width: 11px;
// }
.addon-provider {
display: flex;
align-items: center;
// .addon-provider {
// display: flex;
// align-items: center;
.addon-provider-name {
margin-right: 0.25em;
}
// .addon-provider-name {
// margin-right: 0.25em;
// }
.provider-logo {
width: 15px;
height: 15px;
}
}
// .provider-logo {
// width: 15px;
// height: 15px;
// }
// }
.cell-padding {
padding-right: 1em;

View File

@@ -24,6 +24,7 @@ import { MyAddonsComponent } from "./my-addons.component";
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";
describe("MyAddonsComponent", () => {
let component: MyAddonsComponent;
@@ -35,6 +36,7 @@ describe("MyAddonsComponent", () => {
let addonServiceSpy: any;
let warcraftServiceSpy: any;
let warcraftInstallationService: WarcraftInstallationService;
let pushService: PushService;
beforeEach(async () => {
wowUpAddonServiceSpy = jasmine.createSpyObj("WowUpAddonService", ["updateForClientType"], {
@@ -52,6 +54,10 @@ describe("MyAddonsComponent", () => {
}
);
pushService = jasmine.createSpyObj("PushService", [""], {
addonUpdate$: new Subject<any>(),
});
const testSortOrder: SortOrder = {
colId: "name",
sort: "asc",
@@ -113,6 +119,7 @@ describe("MyAddonsComponent", () => {
{ provide: SessionService, useValue: sessionServiceSpy },
{ provide: WarcraftService, useValue: warcraftServiceSpy },
{ provide: WarcraftInstallationService, useValue: warcraftInstallationService },
{ provide: PushService, useValue: pushService },
],
},
});

View File

@@ -12,8 +12,8 @@ import {
} from "ag-grid-community";
import * as _ from "lodash";
import { join } from "path";
import { from, Observable, of, Subject, Subscription, zip } from "rxjs";
import { catchError, debounceTime, delay, first, map, switchMap, tap } from "rxjs/operators";
import { BehaviorSubject, from, Observable, of, Subject, Subscription, zip } from "rxjs";
import { catchError, debounceTime, delay, 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";
@@ -49,6 +49,7 @@ import { WowUpService } from "../../services/wowup/wowup.service";
import * as AddonUtils from "../../utils/addon.utils";
import { stringIncludes } from "../../utils/string.utils";
import { SortOrder } from "../../models/wowup/sort-order";
import { PushService } from "../../services/push/push.service";
@Component({
selector: "app-my-addons",
@@ -64,6 +65,9 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
// @HostListener("window:keydown", ["$event"])
private readonly _operationErrorSrc = new Subject<Error>();
private readonly _isBusySrc = new BehaviorSubject<boolean>(true);
public readonly isBusy$ = this._isBusySrc.asObservable();
private _subscriptions: Subscription[] = [];
private isSelectedTab = false;
@@ -84,7 +88,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
public selectedInstallation: WowInstallation | undefined = undefined;
public wowClientType = WowClientType;
public overlayRef: OverlayRef | null = null;
public isBusy = true;
public enableControls = true;
public wowInstallations$: Observable<WowInstallation[]>;
public selectedInstallationId!: string;
@@ -178,6 +181,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
private _wowUpAddonService: WowUpAddonService,
private _translateService: TranslateService,
private _snackbarService: SnackbarService,
private _pushService: PushService,
public addonService: AddonService,
public electronService: ElectronService,
public overlay: Overlay,
@@ -211,10 +215,23 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
)
.subscribe();
// If an update push comes in, check if we have any addons installed with any of the ids, if so refresh
const pushUpdateSub = this._pushService.addonUpdate$
.pipe(
debounceTime(5000),
filter((addons) => {
const addonIds = addons.map((addon) => addon.addonId);
return this.addonService.hasAnyWithExternalAddonIds(addonIds);
}),
switchMap(() => from(this.onRefresh()))
)
.subscribe();
this._subscriptions.push(
this._sessionService.selectedHomeTab$.subscribe(this.onSelectedTabChange),
this._sessionService.addonsChanged$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
this._sessionService.targetFileInstallComplete$.pipe(switchMap(() => from(this.onRefresh()))).subscribe(),
pushUpdateSub,
addonInstalledSub,
addonRemovedSub,
filterInputSub
@@ -307,7 +324,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this.loadSortOrder();
this.rowDataChange$.pipe(debounceTime(50)).subscribe(() => {
this.rowDataChange$.pipe(debounceTime(500)).subscribe(() => {
this.redrawRows();
});
}
@@ -334,10 +351,10 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
from(this.lazyLoad())
.pipe(
first(),
delay(400),
map(() => {
this.redrawRows();
}),
// delay(400),
// map(() => {
// this.redrawRows();
// }),
catchError((e) => {
console.error(e);
return of(undefined);
@@ -364,7 +381,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
this._isRefreshing = true;
this.isBusy = true;
this._isBusySrc.next(true);
this.enableControls = false;
try {
@@ -380,7 +397,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
} catch (e) {
console.error(`Failed to refresh addons`, e);
} finally {
this.isBusy = false;
this._isBusySrc.next(false);
this.enableControls = true;
this._isRefreshing = false;
}
@@ -930,7 +948,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
this._lazyLoaded = true;
this.isBusy = true;
this._isBusySrc.next(true);
this.enableControls = false;
// TODO this shouldn't be here
@@ -960,7 +979,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
private async updateAllWithSpinner(...installations: WowInstallation[]): Promise<void> {
this.isBusy = true;
this._isBusySrc.next(true);
this.spinnerMessage = this._translateService.instant("PAGES.MY_ADDONS.SPINNER.GATHERING_ADDONS");
this.enableControls = false;
@@ -1014,7 +1034,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
await this.loadAddons(this.selectedInstallation);
} catch (err) {
console.error("Failed to update classic/retail", err);
this.isBusy = false;
this._isBusySrc.next(false);
this._cdRef.detectChanges();
} finally {
this.spinnerMessage = "";
@@ -1032,7 +1053,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return;
}
this.isBusy = true;
this._isBusySrc.next(true);
this.enableControls = false;
if (!installation) {
@@ -1056,15 +1078,14 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
this._baseRowData = rowData;
this.rowData = this._baseRowData;
this.isBusy = false;
this.setPageContextText();
this._cdRef.detectChanges();
} catch (e) {
console.error(e);
this.isBusy = false;
this.enableControls = this.calculateControlState();
} finally {
this._isBusySrc.next(false);
this._cdRef.detectChanges();
}
};
@@ -1190,9 +1211,9 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
}
private redrawRows() {
this.gridApi?.redrawRows();
this.gridApi?.resetRowHeights();
this.autoSizeColumns();
// this.gridApi?.redrawRows();
// this.gridApi?.resetRowHeights();
// this.autoSizeColumns();
this._cdRef.detectChanges();
}
@@ -1229,7 +1250,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
minWidth: 300,
headerName: this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER"),
sortable: true,
autoHeight: true,
// autoHeight: true,
cellRenderer: "myAddonRenderer",
colId: "name",
valueGetter: (params) => {

View File

@@ -515,6 +515,16 @@ export class AddonService {
});
}
public getAllByExternalAddonId(externalAddonIds: string[]): Addon[] {
return this._addonStorage.queryAll((addon) => {
return externalAddonIds.includes(addon.externalId);
});
}
public hasAnyWithExternalAddonIds(externalAddonIds: string[]): boolean {
return this.getAllByExternalAddonId(externalAddonIds).length > 0;
}
public updateAddon(
addonId: string,
onUpdate: (installState: AddonInstallState, progress: number) => void = () => {},
@@ -1786,7 +1796,7 @@ export class AddonService {
}
try {
const tocPaths = this.getTocPaths(addon);
const tocPaths = await this.getTocPaths(addon);
const tocFiles = await Promise.all(_.map(tocPaths, (tocPath) => this._tocService.parse(tocPath)));
const orderedTocFiles = _.orderBy(tocFiles, ["wowInterfaceId", "loadOnDemand"], ["desc", "asc"]);
const primaryToc = _.first(orderedTocFiles);
@@ -1808,7 +1818,7 @@ export class AddonService {
return result;
}
public getTocPaths(addon: Addon): string[] {
public async getTocPaths(addon: Addon): Promise<string[]> {
if (!addon.installationId) {
return [];
}
@@ -1820,9 +1830,14 @@ export class AddonService {
const addonFolderPath = this._warcraftService.getAddonFolderPath(installation);
return _.map(addon.installedFolderList, (installedFolder) =>
path.join(addonFolderPath, installedFolder, `${installedFolder}.toc`)
const addonTocs = await this._tocService.getAllTocs(
addonFolderPath,
addon.installedFolderList,
installation.clientType
);
const tocPaths = addonTocs.map((toc) => toc.filePath);
return tocPaths;
}
private getAddonProvider(addonUri: URL): AddonProvider | undefined {

View File

@@ -34,7 +34,6 @@ import {
IPC_SET_ZOOM_LIMITS,
IPC_SHOW_OPEN_DIALOG,
IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT,
IPC_UPDATE_APP_BADGE,
IPC_WINDOW_LEAVE_FULLSCREEN,
IPC_WINDOW_MAXIMIZED,
IPC_WINDOW_MINIMIZED,

View File

@@ -10,6 +10,9 @@ import {
ExternalUrlConfirmationDialogComponent,
} from "../../components/external-url-confirmation-dialog/external-url-confirmation-dialog.component";
import { WowUpService } from "../wowup/wowup.service";
import { AnalyticsService } from "../analytics/analytics.service";
import { USER_ACTION_OPEN_LINK } from "../../../common/constants";
import { ElectronService } from "../electron/electron.service";
@Injectable({
providedIn: "root",
@@ -17,16 +20,25 @@ import { WowUpService } from "../wowup/wowup.service";
export class LinkService {
public constructor(
private _dialog: MatDialog,
private _analyticsService: AnalyticsService,
private _electronService: ElectronService,
private _wowUpService: WowUpService,
private _translateService: TranslateService
) {}
public async openExternalLink(url: string): Promise<void> {
this._analyticsService.trackAction(USER_ACTION_OPEN_LINK, {
link: url,
});
await this._electronService.openExternal(url);
}
public confirmLinkNavigation(href: string): Observable<any> {
return from(this._wowUpService.isTrustedDomain(href)).pipe(
first(),
switchMap((isTrusted) => {
if (isTrusted) {
return from(this._wowUpService.openExternalLink(href));
return from(this.openExternalLink(href));
} else {
return this.showLinkNavigationDialog(href);
}
@@ -56,10 +68,10 @@ export class LinkService {
if (result.trustDomain !== "") {
return from(this._wowUpService.trustDomain(result.trustDomain)).pipe(
switchMap(() => from(this._wowUpService.openExternalLink(href)))
switchMap(() => from(this.openExternalLink(href)))
);
} else {
return from(this._wowUpService.openExternalLink(href));
return from(this.openExternalLink(href));
}
}),
catchError((e) => {

View File

@@ -32,7 +32,7 @@ export class CircuitBreakerWrapper {
resetTimeout: resetTimeoutMs,
errorFilter: (err) => {
// Don't trip the breaker on a 404
return err.status === 404;
return err.status >= 400 && err.status < 500;
},
});
this._cb.on("open", () => {
@@ -47,10 +47,16 @@ export class CircuitBreakerWrapper {
return (await this._cb.fire(action)) as TOUT;
}
public getJson<T>(url: URL | string, timeoutMs?: number): Promise<T> {
public getJson<T>(
url: URL | string,
headers: {
[header: string]: string | string[];
} = {},
timeoutMs?: number
): Promise<T> {
return this.fire(() =>
this._httpClient
.get<T>(url.toString(), { headers: { ...CACHE_CONTROL_HEADERS } })
.get<T>(url.toString(), { headers: { ...CACHE_CONTROL_HEADERS, ...headers } })
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
.toPromise()
);
@@ -65,10 +71,34 @@ export class CircuitBreakerWrapper {
);
}
public postJson<T>(url: URL | string, body: unknown, timeoutMs?: number): Promise<T> {
public postJson<T>(
url: URL | string,
body: unknown,
headers: {
[header: string]: string | string[];
} = {},
timeoutMs?: number
): Promise<T> {
const cheaders = headers || {};
return this.fire<T>(() =>
this._httpClient
.post<T>(url.toString(), body)
.post<T>(url.toString(), body, { headers: { ...cheaders } })
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
.toPromise()
);
}
public deleteJson<T>(
url: URL | string,
headers: {
[header: string]: string | string[];
} = {},
timeoutMs?: number
): Promise<T> {
const cheaders = headers || {};
return this.fire<T>(() =>
this._httpClient
.delete<T>(url.toString(), { headers: { ...cheaders } })
.pipe(first(), timeout(timeoutMs ?? this._defaultTimeoutMs))
.toPromise()
);

View File

@@ -0,0 +1,43 @@
import { Injectable } from "@angular/core";
import { Subject } from "rxjs";
import { ElectronService } from "..";
import { IPC_PUSH_NOTIFICATION } from "../../../common/constants";
import { AddonUpdatePushNotification, PushNotification } from "../../../common/wowup/models";
@Injectable({
providedIn: "root",
})
export class PushService {
private readonly _addonUpdateSrc = new Subject<AddonUpdatePushNotification[]>();
public readonly addonUpdate$ = this._addonUpdateSrc.asObservable();
public constructor(private _electronService: ElectronService) {
this._electronService.onRendererEvent(IPC_PUSH_NOTIFICATION, (evt, data) => {
try {
this.parsePushNotification(data);
} catch (e) {
console.error("Failed to handle push notification", e);
}
});
}
private parsePushNotification(data: PushNotification<any>) {
switch (data.action) {
case "addon-update":
this.parseAddonUpdateNotification(data);
break;
default:
console.warn("Unhandled push notification", data.action);
}
}
private parseAddonUpdateNotification(note: PushNotification<AddonUpdatePushNotification[]>) {
if (typeof note.message === "string") {
note.message = JSON.parse(note.message) as AddonUpdatePushNotification[];
}
console.debug("parseAddonUpdateNotification", note);
this._addonUpdateSrc.next(note.message);
}
}

View File

@@ -1,21 +1,16 @@
import * as _ from "lodash";
import { BehaviorSubject, from, Subject } from "rxjs";
import { BehaviorSubject, Subject } from "rxjs";
import { Injectable } from "@angular/core";
import {
APP_PROTOCOL_NAME,
SELECTED_DETAILS_TAB_KEY,
STORAGE_WOWUP_AUTH_TOKEN,
TAB_INDEX_SETTINGS,
} from "../../../common/constants";
import { SELECTED_DETAILS_TAB_KEY, TAB_INDEX_SETTINGS } from "../../../common/constants";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service";
import { ColumnState } from "../../models/wowup/column-state";
import { ElectronService } from "../electron/electron.service";
import { filter, map, switchMap } from "rxjs/operators";
import { getProtocol } from "../../utils/string.utils";
import { map } from "rxjs/operators";
import { WowUpApiService } from "../wowup-api/wowup-api.service";
import { WowUpAccountService } from "../wowup/wowup-account.service";
@Injectable({
providedIn: "root",
@@ -29,7 +24,6 @@ export class SessionService {
private readonly _addonsChangedSrc = new Subject<boolean>();
private readonly _myAddonsColumnsSrc = new BehaviorSubject<ColumnState[]>([]);
private readonly _targetFileInstallCompleteSrc = new Subject<boolean>();
private readonly _wowUpAuthTokenSrc = new BehaviorSubject<string>("");
private readonly _getAddonsColumnsSrc = new Subject<ColumnState>();
@@ -45,14 +39,16 @@ export class SessionService {
public readonly getAddonsHiddenColumns$ = this._getAddonsColumnsSrc.asObservable();
public readonly targetFileInstallComplete$ = this._targetFileInstallCompleteSrc.asObservable();
public readonly editingWowInstallationId$ = new BehaviorSubject<string>("");
public readonly wowUpAuthToken$ = this._wowUpAuthTokenSrc.asObservable();
public readonly wowUpAuthToken$ = this._wowUpAccountService.wowUpAuthTokenSrc.asObservable();
public readonly wowUpAccount$ = this._wowUpAccountService.wowUpAccountSrc.asObservable();
public readonly wowUpAccountPushEnabled$ = this._wowUpAccountService.accountPushSrc.asObservable();
public readonly wowUpAuthenticated$ = this.wowUpAuthToken$.pipe(map((token) => !!token && token.length > 10));
public readonly wowUpAuthenticated$ = this.wowUpAccount$.pipe(map((account) => account !== undefined));
public constructor(
private _electronService: ElectronService,
private _warcraftInstallationService: WarcraftInstallationService,
private _preferenceStorageService: PreferenceStorageService
private _preferenceStorageService: PreferenceStorageService,
private _wowUpAccountService: WowUpAccountService
) {
this._selectedDetailTabType =
this._preferenceStorageService.getObject<DetailsTabType>(SELECTED_DETAILS_TAB_KEY) || "description";
@@ -60,53 +56,22 @@ export class SessionService {
this._warcraftInstallationService.wowInstallations$.subscribe((installations) =>
this.onWowInstallationsChange(installations)
);
this._electronService.customProtocol$
.pipe(
filter((protocol) => !!protocol),
map((protocol) => this.handleLoginProtocol(protocol))
)
.subscribe();
this.loadAuthToken();
}
/**
* Handle the post login protocol message
* wowup://login/desktop/#{token}
*/
private handleLoginProtocol = (protocol: string): void => {
const protocolName = getProtocol(protocol);
if (protocolName !== APP_PROTOCOL_NAME) {
return;
}
const parts = protocol.split("/");
if (parts[2] !== "login" || parts[3] !== "desktop") {
return;
}
const token = parts[4];
if (typeof token !== "string" || token.length < 10) {
console.warn("Invalid auth token", protocol);
return;
}
console.debug("GOT WOWUP PROTOCOL", protocol);
window.localStorage.setItem(STORAGE_WOWUP_AUTH_TOKEN, token);
this._wowUpAuthTokenSrc.next(token);
};
private loadAuthToken(): void {
const storedToken = window.localStorage.getItem(STORAGE_WOWUP_AUTH_TOKEN);
if (storedToken) {
this._wowUpAuthTokenSrc.next(storedToken);
}
public get wowUpAuthToken(): string {
return this._wowUpAccountService.wowUpAuthTokenSrc.value;
}
public clearWowUpAuthToken(): void {
window.localStorage.removeItem(STORAGE_WOWUP_AUTH_TOKEN);
this._wowUpAuthTokenSrc.next("");
public login(): void {
this._wowUpAccountService.login();
}
public logout(): void {
this._wowUpAccountService.logout();
}
public async toggleAccountPush(enabled: boolean): Promise<void> {
return await this._wowUpAccountService.toggleAccountPush(enabled);
}
public isAuthenticated(): boolean {

View File

@@ -43,6 +43,7 @@ export class TocService {
return {
fileName,
filePath: tocPath,
author: this.getValue(TOC_AUTHOR, tocText),
curseProjectId: this.getValue(TOC_X_CURSE_PROJECT_ID, tocText),
interface: this.getValue(TOC_INTERFACE, tocText),

View File

@@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
import { from, Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { AppConfig } from "../../../environments/environment";
import { WowUpGetAccountResponse } from "../../models/wowup-api/api-responses";
import { BlockListRepresentation } from "../../models/wowup-api/block-list";
import { CachingService } from "../caching/caching-service";
import { CircuitBreakerWrapper, NetworkService } from "../network/network.service";
@@ -21,6 +22,28 @@ export class WowUpApiService {
this.getBlockList().subscribe();
}
public async getAccount(authToken: string): Promise<WowUpGetAccountResponse> {
const url = new URL(`${API_URL}/account`);
return await this._circuitBreaker.getJson<WowUpGetAccountResponse>(
url,
this.getAuthorizationHeader(authToken),
5000
);
}
public async registerPushToken(authToken: string, pushToken: string, deviceType: string): Promise<any> {
const url = new URL(`${API_URL}/account/push`);
url.searchParams.set("push_token", pushToken);
url.searchParams.set("os", deviceType);
return this._circuitBreaker.postJson<any>(url, {}, this.getAuthorizationHeader(authToken));
}
public async removePushToken(authToken: string, pushToken: string): Promise<any> {
const url = new URL(`${API_URL}/account/push/${pushToken}`);
return this._circuitBreaker.deleteJson<any>(url, this.getAuthorizationHeader(authToken));
}
public getBlockList(): Observable<BlockListRepresentation> {
const cached = this._cacheService.get<BlockListRepresentation>(BLOCKLIST_CACHE_KEY);
if (cached) {
@@ -35,4 +58,10 @@ export class WowUpApiService {
})
);
}
private getAuthorizationHeader(authToken: string): { Authorization: string } {
return {
Authorization: `Bearer ${authToken}`,
};
}
}

View File

@@ -21,6 +21,10 @@ const CHANGELOGS: ChangeLog[] = [
<div>
<h4 style="margin-top: 1em;">New Features</h4>
<ul>
<li>
<div>The first stage of a set of cloud based features you've been asking for</div>
<img style="width: 80%;" loading="lazy" src="https://cdn.wowup.io/patch-notes/2-5/account-2.5-preview.png">
</li>
<li>Added WowUpHub category support</li>
<li>Added WowUpHub preview support</li>
<li>Added Mac M1 builds</li>

View File

@@ -0,0 +1,195 @@
import { BehaviorSubject, from } from "rxjs";
import { filter, map, switchMap } from "rxjs/operators";
import { Injectable } from "@angular/core";
import {
ACCT_FEATURE_KEYS,
ACCT_PUSH_ENABLED_KEY,
APP_PROTOCOL_NAME,
IPC_PUSH_INIT,
IPC_PUSH_REGISTER,
IPC_PUSH_SUBSCRIBE,
IPC_PUSH_UNREGISTER,
STORAGE_WOWUP_AUTH_TOKEN,
} from "../../../common/constants";
import { AppConfig } from "../../../environments/environment";
import { getProtocol } from "../../utils/string.utils";
import { ElectronService } from "../electron/electron.service";
import { LinkService } from "../links/link.service";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { WowUpApiService } from "../wowup-api/wowup-api.service";
import { WowUpGetAccountResponse } from "../../models/wowup-api/api-responses";
import { HttpErrorResponse } from "@angular/common/http";
@Injectable({
providedIn: "root",
})
export class WowUpAccountService {
public readonly wowUpAuthTokenSrc = new BehaviorSubject<string>("");
public readonly wowUpAccountSrc = new BehaviorSubject<WowUpGetAccountResponse | undefined>(undefined);
public readonly accountPushSrc = new BehaviorSubject<boolean>(false);
public get accountPushEnabled(): boolean {
return this._preferenceStorageService.findByKey(ACCT_PUSH_ENABLED_KEY) === true.toString();
}
public set accountPushEnabled(enabled: boolean) {
this._preferenceStorageService.set(ACCT_PUSH_ENABLED_KEY, enabled);
}
public get authToken(): string {
return this.wowUpAuthTokenSrc.value;
}
public get account(): WowUpGetAccountResponse | undefined {
return this.wowUpAccountSrc.value;
}
public constructor(
private _electronService: ElectronService,
private _linkService: LinkService,
private _preferenceStorageService: PreferenceStorageService,
private _wowUpApiService: WowUpApiService
) {
this.wowUpAuthTokenSrc
.pipe(
filter((token) => !!token && token.length > 10),
switchMap((token) => from(this.onAuthTokenChanged(token)))
)
.subscribe();
this._electronService.customProtocol$
.pipe(
filter((protocol) => !!protocol),
map((protocol) => this.handleLoginProtocol(protocol))
)
.subscribe();
this.loadAuthToken();
console.debug("accountPushEnabled", this.accountPushEnabled);
if (this.accountPushEnabled) {
this.initializePush().catch((e) => console.error(e));
this.accountPushSrc.next(true);
}
}
public login(): void {
this._linkService
.openExternalLink(`${AppConfig.wowUpWebsiteUrl}/login?client=desktop`)
.catch((e) => console.error(e));
}
public logout(): void {
this.clearAuthToken();
this.resetAccountPreferences();
}
private onAuthTokenChanged = async (token: string) => {
try {
const account = await this._wowUpApiService.getAccount(token);
console.debug("Account", account);
this.wowUpAccountSrc.next(account);
if (this.accountPushEnabled) {
await this.toggleAccountPush(true);
}
} catch (e) {
console.error(e);
// Check if user is no longer authorized
if (e instanceof HttpErrorResponse && [403, 401].includes(e.status)) {
this.logout();
}
}
};
private clearAuthToken(): void {
window.localStorage.removeItem(STORAGE_WOWUP_AUTH_TOKEN);
this.wowUpAuthTokenSrc.next("");
this.wowUpAccountSrc.next(undefined);
}
private loadAuthToken(): void {
const storedToken = window.localStorage.getItem(STORAGE_WOWUP_AUTH_TOKEN);
if (storedToken) {
console.debug("loaded auth token", storedToken);
this.wowUpAuthTokenSrc.next(storedToken);
}
}
/**
* Handle the post login protocol message
* wowup://login/desktop/#{token}
*/
private handleLoginProtocol = (protocol: string): void => {
const protocolName = getProtocol(protocol);
if (protocolName !== APP_PROTOCOL_NAME) {
return;
}
const parts = protocol.split("/");
if (parts[2] !== "login" || parts[3] !== "desktop") {
return;
}
const token = parts[4];
if (typeof token !== "string" || token.length < 10) {
console.warn("Invalid auth token", protocol);
return;
}
console.debug("GOT WOWUP PROTOCOL", protocol);
window.localStorage.setItem(STORAGE_WOWUP_AUTH_TOKEN, token);
this.wowUpAuthTokenSrc.next(token);
};
// ACCOUNT FEATURES
public async toggleAccountPush(enabled: boolean): Promise<void> {
try {
if (enabled) {
await this.initializePush();
await this.registerForPush(this.authToken, this.account.config.pushAppId);
if (this.account.config?.pushChannels?.addonUpdates) {
await this.subscribe(this.account.config.pushChannels.addonUpdates);
}
} else {
await this.unregisterForPush(this.authToken, this.account.config.pushAppId);
}
this.accountPushEnabled = enabled;
} catch (e) {
console.error("Failed to toggle account push", e);
throw e;
}
}
// LOCAL PREFERENCES
private resetAccountPreferences(): void {
for (const key of ACCT_FEATURE_KEYS) {
this._preferenceStorageService.set(key, false);
}
}
// PUSH
public async initializePush(): Promise<boolean> {
return await this._electronService.invoke(IPC_PUSH_INIT);
}
public async registerForPush(authToken: string, pushAppId: string): Promise<string> {
const pushToken = await this._electronService.invoke(IPC_PUSH_REGISTER, pushAppId);
await this._wowUpApiService.registerPushToken(authToken, pushToken, this._electronService.platform);
return pushToken;
}
public async unregisterForPush(authToken: string, pushAppId: string): Promise<void> {
const pushToken = await this._electronService.invoke(IPC_PUSH_REGISTER, pushAppId);
await this._electronService.invoke(IPC_PUSH_UNREGISTER);
await this._wowUpApiService.removePushToken(authToken, pushToken);
}
private async subscribe(channel: string): Promise<void> {
await this._electronService.invoke(IPC_PUSH_SUBSCRIBE, channel);
}
}

View File

@@ -7,6 +7,7 @@ import { Injectable } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import {
ACCT_PUSH_ENABLED_KEY,
ADDON_MIGRATION_VERSION_KEY,
ADDON_PROVIDERS_KEY,
COLLAPSE_TO_TRAY_PREFERENCE_KEY,
@@ -14,6 +15,7 @@ import {
DEFAULT_AUTO_UPDATE_PREFERENCE_KEY_SUFFIX,
DEFAULT_CHANNEL_PREFERENCE_KEY_SUFFIX,
DEFAULT_THEME,
DEFAULT_TRUSTED_DOMAINS,
ENABLE_APP_BADGE_KEY,
ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY,
GET_ADDONS_HIDDEN_COLUMNS_KEY,
@@ -30,7 +32,6 @@ import {
START_WITH_SYSTEM_PREFERENCE_KEY,
TRUSTED_DOMAINS_KEY,
UPDATE_NOTES_POPUP_VERSION_KEY,
USER_ACTION_OPEN_LINK,
USE_HARDWARE_ACCELERATION_PREFERENCE_KEY,
USE_SYMLINK_MODE_PREFERENCE_KEY,
WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY,
@@ -43,7 +44,6 @@ import { PreferenceChange } from "../../models/wowup/preference-change";
import { SortOrder } from "../../models/wowup/sort-order";
import { WowUpReleaseChannelType } from "../../models/wowup/wowup-release-channel-type";
import { getEnumList, getEnumName } from "../../utils/enum.utils";
import { AnalyticsService } from "../analytics/analytics.service";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";
import { PreferenceStorageService } from "../storage/preference-storage.service";
@@ -68,8 +68,7 @@ export class WowUpService {
private _preferenceStorageService: PreferenceStorageService,
private _electronService: ElectronService,
private _fileService: FileService,
private _translateService: TranslateService,
private _analyticsService: AnalyticsService
private _translateService: TranslateService
) {
this.setDefaultPreferences()
// .then(() => console.debug("Set default preferences"))
@@ -85,10 +84,6 @@ export class WowUpService {
.catch((e) => console.error(e));
}
public login(): void {
this.openExternalLink("http://localhost:3000/dev/login?client=desktop").catch((e) => console.error(e));
}
public async getApplicationVersion(): Promise<string> {
const appVersion = await this._electronService.invoke<string>(IPC_GET_APP_VERSION);
return `${appVersion}${this._electronService.isPortable ? " (portable)" : ""}`;
@@ -99,13 +94,6 @@ export class WowUpService {
return appVersion.toLowerCase().indexOf("beta") != -1;
}
public async openExternalLink(url: string): Promise<void> {
this._analyticsService.trackAction(USER_ACTION_OPEN_LINK, {
link: url,
});
await this._electronService.openExternal(url);
}
/**
* This is called before the app component is initialized in order to catch issues
* with unsupported languages
@@ -365,6 +353,10 @@ export class WowUpService {
public async isTrustedDomain(href: string | URL): Promise<boolean> {
const url = href instanceof URL ? href : new URL(href);
if (DEFAULT_TRUSTED_DOMAINS.includes(url.hostname)) {
return true;
}
const trustedDomains = await this.getTrustedDomains();
return trustedDomains.includes(url.hostname);
}
@@ -400,7 +392,8 @@ export class WowUpService {
this.setDefaultPreference(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, await this.getDefaultReleaseChannel());
this.setDefaultPreference(USE_SYMLINK_MODE_PREFERENCE_KEY, false);
this.setDefaultPreference(ENABLE_APP_BADGE_KEY, true);
this.setDefaultPreference(TRUSTED_DOMAINS_KEY, ["wowup.io", "discord.gg", "www.patreon.com", "github.com"]);
this.setDefaultPreference(TRUSTED_DOMAINS_KEY, DEFAULT_TRUSTED_DOMAINS);
this.setDefaultPreference(ACCT_PUSH_ENABLED_KEY, false);
this.setDefaultClientPreferences();
}

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "Vyskytla se chyba při párování vaší složky addonů s poskytovatelem {providerName}, zkuste to prosím později.",
"ADDON_SYNC_ERROR": "Vyskytla se chyba při zjišťování aktualizací od poskytovatele {providerName}, zkuste to prosím později.",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Navštivte naše webové stránky!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Addon '{addonName}' konnte nicht installiert werden. Versuche es später erneut.",
"ADDON_SCAN_ERROR": "Beim Abgleichen deiner Addon-Ordner mit {providerName} ist ein Fehler aufgetreten. Versuche es später erneut.",
"ADDON_SYNC_ERROR": "Beim Überprüfen auf Aktualisierungen von '{providerName}' ist ein Fehler aufgetreten.",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Schau dir die Website an!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Kategorien",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Kategorien",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.",
"ADDON_SYNC_ERROR": "An error occurred checking for updates from {providerName}, please try again later.",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Check out the website!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"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}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "¡Visite la página web!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categorías",
"ADDON_CATEGORIES_MENU_TITLE": "Categorías de addons",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "Une erreur s'est produite lors de la correspondance de vos dossiers d'addons avec {providerName}, merci de réessayer plus tard.",
"ADDON_SYNC_ERROR": "Une erreur s'est produite lors de la vérification des mises à jour depuis : {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Découvrez le site web !"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Fallito nell'installare l'addon {addonName}. Per favore riprovare più tardi.",
"ADDON_SCAN_ERROR": "Errore nel sincronizzare le cartelle dell'addon con {providerName}, per favore riprovare più tardi.",
"ADDON_SYNC_ERROR": "Errore nel controllare gli aggioranementi da: {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Dai un'occhiata al sito!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categorie",
"ADDON_CATEGORIES_MENU_TITLE": "Categorie Addons",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.",
"ADDON_SYNC_ERROR": "An error occurred checking for updates from: {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "웹사이트로 이동"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.",
"ADDON_SYNC_ERROR": "An error occurred checking for updates from: {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Besøk nettsiden!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Failed to install addon, {addonName}. Please try again later.",
"ADDON_SCAN_ERROR": "An error occurred matching your addon folders with {providerName}, please try again later.",
"ADDON_SYNC_ERROR": "An error occurred checking for updates from: {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Conheça o nosso site!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Categories",
"ADDON_CATEGORIES_MENU_TITLE": "Addon Categories",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "Не удалось установить модификацию {addonName}. Попробуйте позже.",
"ADDON_SCAN_ERROR": "Возникла ошибка при сравнении ваших папок с {providerName}, попробуйте позже.",
"ADDON_SYNC_ERROR": "Возникла ошибка при проверке обновлений с: {providerName}",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "Посетите наш сайт!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "Категории",
"ADDON_CATEGORIES_MENU_TITLE": "Категории модификаций",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "安裝插件 {addonName} 失敗。請稍後再試。",
"ADDON_SCAN_ERROR": "在與 {providerName} 匹配插件資料夾時出現錯誤,請稍後再試。",
"ADDON_SYNC_ERROR": "從 {providerName} 上檢查更新時出現錯誤。",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "訪問網站!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "類別",
"ADDON_CATEGORIES_MENU_TITLE": "插件類別",

View File

@@ -175,6 +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.",
"ADDON_INSTALL_ERROR": "安装插件 {addonName} 失败。请稍后再试。",
"ADDON_SCAN_ERROR": "在与 {providerName} 匹配插件文件夹时出现错误,请稍后再试。",
"ADDON_SYNC_ERROR": "从 {providerName} 上检查更新时出现错误。",
@@ -274,6 +275,15 @@
"TITLE": "WowUp.io",
"WEBSITE_LINK_LABEL": "访问网站!"
},
"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"
},
"GET_ADDONS": {
"ADDON_CATEGORIES_BUTTON": "类别",
"ADDON_CATEGORIES_MENU_TITLE": "插件类别",

View File

@@ -87,6 +87,11 @@ export const IPC_APP_INSTALL_UPDATE = "app-install-update";
export const IPC_APP_CHECK_UPDATE = "app-check-update";
export const IPC_UPDATE_APP_BADGE = "update-app-badge";
export const IPC_WINDOW_RESUME = "window-resume";
export const IPC_PUSH_INIT = "push-init";
export const IPC_PUSH_REGISTER = "push-register";
export const IPC_PUSH_UNREGISTER = "push-unregister";
export const IPC_PUSH_SUBSCRIBE = "push-subscribe";
export const IPC_PUSH_NOTIFICATION = "push-notification";
// IPC STORAGE
export const IPC_STORE_GET_OBJECT = "store-get-object";
@@ -129,6 +134,9 @@ export const RETAIL_PTR_LOCATION_KEY = "wow_retail_ptr_location"; // TODO remove
export const CLASSIC_LOCATION_KEY = "wow_classic_location"; // TODO remove, deprecated
export const CLASSIC_PTR_LOCATION_KEY = "wow_classic_ptr_location"; // TODO remove, deprecated
export const BETA_LOCATION_KEY = "wow_beta_location"; // TODO remove, deprecated
export const ACCT_PUSH_ENABLED_KEY = "acct_push_enabled";
export const ACCT_FEATURE_KEYS = [ACCT_PUSH_ENABLED_KEY];
// THEMES
export const DEFAULT_THEME = "default-theme";
@@ -154,7 +162,7 @@ export const MIN_VISIBLE_ON_SCREEN = 32;
export const WOWUP_LOGO_FILENAME = "wowup_logo_purple.png";
export const WOWUP_LOGO_MAC_SYSTEM_TRAY = "wowupBlackLgNopadTemplate.png";
export const DEFAULT_FILE_MODE = 0o655;
export const DEFAULT_TRUSTED_DOMAINS = ["wowup.io", "dev.wowup.io", "discord.gg", "www.patreon.com", "github.com"];
export const WOW_CLASSIC_FOLDER = "_classic_";
export const WOW_CLASSIC_ERA_FOLDER = "_classic_era_";
export const WOW_RETAIL_FOLDER = "_retail_";

View File

@@ -19,7 +19,8 @@ declare type MainChannels =
| "request-install-from-url"
| "custom-protocol-received"
| "app-update-state"
| "window-resume";
| "window-resume"
| "push-notification";
// Events that can be sent from renderer to main
declare type RendererChannels =
@@ -74,7 +75,11 @@ declare type RendererChannels =
| "app-install-update"
| "update-app-badge"
| "list-dir-recursive"
| "get-directory-tree";
| "get-directory-tree"
| "push-init"
| "push-register"
| "push-unregister"
| "push-subscribe";
declare global {
interface Window {

View File

@@ -114,3 +114,20 @@ export interface WowUpScanResult {
folderName: string;
path: string;
}
export type PushAction = "addon-update";
export interface PushNotification<T extends PushNotificationData | string> {
action: PushAction;
sender: string;
message: string | T;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PushNotificationData {}
export interface AddonUpdatePushNotification extends PushNotificationData {
provider: string;
addonName: string;
addonId: string;
}

View File

@@ -214,6 +214,12 @@ $alliance-theme-light: mat.define-light-theme(
}
// VARS
body {
--patreon-color: #e64049;
--github-color: #9032ad;
--wow-gold-color: #ffcc00;
}
.default-theme {
--background-primary: #6b69d6;
--background-primary-2: #504fa1;

View File

@@ -6,6 +6,7 @@
export const AppConfig = {
production: false,
environment: "DEV",
wowUpWebsiteUrl: "https://dev.wowup.io",
wowUpApiUrl: "https://api.dev.wowup.io",
wowUpHubUrl: "https://hub.dev.wowup.io",
googleAnalyticsId: "UA-92563227-4",

View File

@@ -1,11 +1,13 @@
export const AppConfig = {
production: true,
environment: "PROD",
wowUpApiUrl: "https://api.wowup.io",
wowUpWebsiteUrl: "https://dev.wowup.io",
wowUpApiUrl: "https://api.dev.wowup.io",
wowUpHubUrl: "https://hub.dev.wowup.io",
googleAnalyticsId: "UA-92563227-4",
wowupRepositoryUrl: "https://github.com/WowUp/WowUp",
warcraftTavernNewsFeedUrl: "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
warcraftTavernNewsFeedUrl:
"https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
azure: {
applicationInsightsKey: "4a53e8d9-796c-4f80-b1a6-9a058374dd6d",
},

View File

@@ -1,11 +1,13 @@
export const AppConfig = {
production: false,
environment: "LOCAL",
wowUpWebsiteUrl: "https://dev.wowup.io",
wowUpApiUrl: "https://api.dev.wowup.io",
wowUpHubUrl: "https://hub.dev.wowup.io",
googleAnalyticsId: "UA-92563227-4",
wowupRepositoryUrl: "https://github.com/WowUp/WowUp",
warcraftTavernNewsFeedUrl: "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
warcraftTavernNewsFeedUrl:
"https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
azure: {
applicationInsightsKey: "4a53e8d9-796c-4f80-b1a6-9a058374dd6d",
},

View File

@@ -6,10 +6,12 @@
export const AppConfig = {
production: false,
environment: "DEV",
wowUpWebsiteUrl: "https://dev.wowup.io",
wowUpApiUrl: "https://api.dev.wowup.io",
wowUpHubUrl: "https://hub.dev.wowup.io",
googleAnalyticsId: "UA-92563227-4",
warcraftTavernNewsFeedUrl: "https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
warcraftTavernNewsFeedUrl:
"https://www.warcrafttavern.com/?call_custom_simple_rss=1&csrp_post_type=wow-classic-news,tbc-classic-news,retail-news&csrp_thumbnail_size=medium",
newsRefreshIntervalMs: 3600000, // 1 hour
featuredAddonsCacheTimeSec: 30, // 30 sec
};