Merge branch 'develop' into pr/317

This commit is contained in:
jliddev
2020-11-16 22:35:47 -06:00
127 changed files with 2304 additions and 1919 deletions

View File

@@ -10,6 +10,7 @@
main.js
ipc-events.js
app-updater.js
window-state.js
src/**/*.js
!src/karma.conf.js
*.js.map

View File

@@ -10,9 +10,11 @@
main.js
ipc-events.js
app-updater.js
window-state.js
src/**/*.js
!src/karma.conf.js
*.js.map
*.hbs
# dependencies
/node_modules

View File

@@ -1,3 +1,3 @@
{
"printWidth": 120
"printWidth": 120
}

View File

@@ -12,10 +12,7 @@ module.exports = (config, options) => {
}
let fileReplacementParts = fileReplacement["with"].split(".");
if (
fileReplacementParts.length > 1 &&
["web"].indexOf(fileReplacementParts[1]) >= 0
) {
if (fileReplacementParts.length > 1 && ["web"].indexOf(fileReplacementParts[1]) >= 0) {
config.target = "web";
}
break;

View File

@@ -13,9 +13,7 @@ import {
APP_UPDATE_START_DOWNLOAD,
} from "./src/common/constants";
export const checkForUpdates = async function checkForUpdates(
win: BrowserWindow
) {
export const checkForUpdates = async function checkForUpdates(win: BrowserWindow) {
let result = null;
try {
@@ -63,14 +61,14 @@ export function initializeAppUpdateIpcHandlers(win: BrowserWindow) {
return await autoUpdater.downloadUpdate();
});
// Used this solution for Mac support
// Used this solution for Mac support
// https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-372091881
ipcMain.handle(APP_UPDATE_INSTALL, async () => {
log.info(APP_UPDATE_INSTALL);
app.removeAllListeners('window-all-closed');
app.removeAllListeners("window-all-closed");
var browserWindows = BrowserWindow.getAllWindows();
browserWindows.forEach(function(browserWindow) {
browserWindow.removeAllListeners('close');
browserWindows.forEach(function (browserWindow) {
browserWindow.removeAllListeners("close");
});
autoUpdater.quitAndInstall();
});

View File

@@ -5,22 +5,26 @@ import {
Menu,
MenuItem,
MenuItemConstructorOptions,
screen,
powerMonitor,
} from "electron";
import * as log from "electron-log";
import * as Store from "electron-store";
import { arch, release } from "os";
import * as os from "os";
import * as path from "path";
import { Subject } from "rxjs";
import { debounceTime } from "rxjs/operators";
import * as url from "url";
import * as platform from "./platform";
import { initializeAppUpdateIpcHandlers, initializeAppUpdater } from "./app-updater";
import "./ipc-events";
import { initializeIpcHanders } from "./ipc-events";
import { COLLAPSE_TO_TRAY_PREFERENCE_KEY, USE_HARDWARE_ACCELERATION_PREFERENCE_KEY } from "./src/common/constants";
import { WindowState } from "./src/common/models/window-state";
import {
COLLAPSE_TO_TRAY_PREFERENCE_KEY,
CURRENT_THEME_KEY,
DEFAULT_BG_COLOR,
DEFAULT_LIGHT_BG_COLOR,
USE_HARDWARE_ACCELERATION_PREFERENCE_KEY,
} from "./src/common/constants";
import { AppOptions } from "./src/common/wowup/app-options";
import { windowStateManager } from "./window-state";
const startedAt = Date.now();
const preferenceStore = new Store({ name: "preferences" });
@@ -61,9 +65,8 @@ if (preferenceStore.get(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY) === "false") {
app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
const USER_AGENT = `WowUp-Client/${app.getVersion()} (${release()}; ${arch()};${
isPortable ? " portable;" : ""
} +https://wowup.io)`;
const portableStr = isPortable ? " portable;" : "";
const USER_AGENT = `WowUp-Client/${app.getVersion()} (${os.type()}; ${os.release()}; ${os.arch()}; ${portableStr} +https://wowup.io)`;
log.info("USER_AGENT", USER_AGENT);
const argv = require("minimist")(process.argv.slice(1), {
@@ -74,71 +77,10 @@ function canStartHidden() {
return argv.hidden || app.getLoginItemSettings().wasOpenedAsHidden;
}
function windowStateManager(windowName: string, { width, height }: { width: number; height: number }) {
let window: BrowserWindow;
let windowState: WindowState;
const saveState$ = new Subject<void>();
function setState() {
let setDefaults = false;
windowState = preferenceStore.get(`${windowName}-window-state`) as WindowState;
if (!windowState) {
setDefaults = true;
} else {
log.info("found window state:", windowState);
const valid = screen.getAllDisplays().some((display) => {
return (
windowState.x >= display.bounds.x &&
windowState.y >= display.bounds.y &&
windowState.x + windowState.width <= display.bounds.x + display.bounds.width &&
windowState.y + windowState.height <= display.bounds.y + display.bounds.height
);
});
if (!valid) {
log.info("reset window state, bounds are outside displays");
setDefaults = true;
}
}
if (setDefaults) {
log.info("setting window defaults");
windowState = <WindowState>{ width, height };
}
}
function saveState() {
log.info("saving window state");
if (!window.isMaximized() && !window.isFullScreen()) {
windowState = { ...windowState, ...window.getBounds() };
}
windowState.isMaximized = window.isMaximized();
windowState.isFullScreen = window.isFullScreen();
preferenceStore.set(`${windowName}-window-state`, windowState);
}
function monitorState(win: BrowserWindow) {
window = win;
win.on("close", saveState);
win.on("resize", () => saveState$.next());
win.on("move", () => saveState$.next());
win.on("closed", () => saveState$.unsubscribe());
}
saveState$.pipe(debounceTime(500)).subscribe(() => saveState());
setState();
return {
...windowState,
monitorState,
};
}
function createWindow(): BrowserWindow {
const savedTheme = preferenceStore.get(CURRENT_THEME_KEY) as string;
const backgroundColor = savedTheme && savedTheme.indexOf("light") !== -1 ? DEFAULT_LIGHT_BG_COLOR : DEFAULT_BG_COLOR;
// Main object for managing window state
// Initialize with a window name and default size
const mainWindowManager = windowStateManager("main", {
@@ -151,7 +93,7 @@ function createWindow(): BrowserWindow {
height: mainWindowManager.height,
x: mainWindowManager.x,
y: mainWindowManager.y,
backgroundColor: "#444444",
backgroundColor,
title: "WowUp",
titleBarStyle: "hidden",
webPreferences: {
@@ -161,7 +103,7 @@ function createWindow(): BrowserWindow {
webSecurity: false,
enableRemoteModule: true,
},
minWidth: 900,
minWidth: 940,
minHeight: 550,
show: false,
};
@@ -170,6 +112,11 @@ function createWindow(): BrowserWindow {
windowOptions.frame = false;
}
// Attempt to fix the missing icon issue on Ubuntu
if (platform.isLinux) {
windowOptions.icon = path.join(__dirname, "assets", "wowup_logo_512np.png");
}
// Create the browser window.
win = new BrowserWindow(windowOptions);
initializeIpcHanders(win);
@@ -181,6 +128,23 @@ function createWindow(): BrowserWindow {
win.webContents.userAgent = USER_AGENT;
// See https://www.electronjs.org/docs/api/web-contents#event-render-process-gone
win.webContents.on("render-process-gone", (evt, details) => {
log.error("webContents render-process-gone");
log.error(evt);
log.error(details);
});
// See https://www.electronjs.org/docs/api/web-contents#event-unresponsive
win.webContents.on("unresponsive", () => {
log.error("webContents unresponsive");
});
// See https://www.electronjs.org/docs/api/web-contents#event-responsive
win.webContents.on("responsive", () => {
log.error("webContents responsive");
});
win.once("ready-to-show", () => {
if (canStartHidden()) {
return;
@@ -198,16 +162,13 @@ function createWindow(): BrowserWindow {
if (platform.isMac) {
win.on("close", (e) => {
if (appIsQuitting) {
if (appIsQuitting || preferenceStore.get(COLLAPSE_TO_TRAY_PREFERENCE_KEY) !== "true") {
return;
}
e.preventDefault();
win.hide();
if (preferenceStore.get(COLLAPSE_TO_TRAY_PREFERENCE_KEY) === "true") {
app.dock.hide();
}
app.dock.hide();
});
}
@@ -231,23 +192,6 @@ function createWindow(): BrowserWindow {
);
}
// Emitted when the window is closed.
// win.on('closed', () => {
// // Dereference the window object, usually you would store window
// // in an array if your app supports multi windows, this is the time
// // when you should delete the corresponding element.
// win = null;
// });
// win.on('minimize', function (event) {
// event.preventDefault();
// win.hide();
// });
// win.on('restore', function (event) {
// win.show();
// });
return win;
}
@@ -270,7 +214,7 @@ try {
});
}
app.allowRendererProcessReuse = true;
app.allowRendererProcessReuse = false;
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
@@ -291,9 +235,9 @@ try {
app.on("window-all-closed", () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== "darwin") {
app.quit();
}
// if (process.platform !== "darwin") {
app.quit();
// }
});
app.on("activate", () => {
@@ -308,6 +252,22 @@ try {
createWindow();
}
});
powerMonitor.on("resume", () => {
log.info("powerMonitor resume");
});
powerMonitor.on("suspend", () => {
log.info("powerMonitor suspend");
});
powerMonitor.on("lock-screen", () => {
log.info("powerMonitor lock-screen");
});
powerMonitor.on("unlock-screen", () => {
log.info("powerMonitor unlock-screen");
});
} catch (e) {
// Catch Error
// throw e;

View File

@@ -1,7 +1,7 @@
{
"name": "wowup",
"productName": "WowUp",
"version": "2.0.0-beta.11",
"version": "2.0.0-beta.15",
"description": "Word of Warcraft addon updater",
"homepage": "https://wowup.io",
"author": {
@@ -46,21 +46,21 @@
},
"devDependencies": {
"@angular-builders/custom-webpack": "10.0.1",
"@angular-devkit/build-angular": "0.1002.0",
"@angular-devkit/build-angular": "0.1100.1",
"@angular-eslint/builder": "0.6.0-beta.0",
"@angular-eslint/eslint-plugin": "0.6.0-beta.0",
"@angular-eslint/eslint-plugin-template": "0.6.0-beta.0",
"@angular-eslint/template-parser": "0.6.0-beta.0",
"@angular/cli": "10.2.0",
"@angular/common": "10.2.2",
"@angular/compiler": "10.2.2",
"@angular/compiler-cli": "10.2.2",
"@angular/core": "10.2.2",
"@angular/forms": "10.2.2",
"@angular/language-service": "10.2.2",
"@angular/platform-browser": "10.2.2",
"@angular/platform-browser-dynamic": "10.2.2",
"@angular/router": "10.2.2",
"@angular/cli": "11.0.1",
"@angular/common": "11.0.0",
"@angular/compiler": "11.0.0",
"@angular/compiler-cli": "11.0.0",
"@angular/core": "11.0.0",
"@angular/forms": "11.0.0",
"@angular/language-service": "11.0.0",
"@angular/platform-browser": "11.0.0",
"@angular/platform-browser-dynamic": "11.0.0",
"@angular/router": "11.0.0",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/adm-zip": "0.4.33",
@@ -73,9 +73,9 @@
"@types/opossum": "4.1.1",
"@types/slug": "0.9.1",
"@types/uuid": "8.3.0",
"@typescript-eslint/eslint-plugin": "4.6.1",
"@typescript-eslint/eslint-plugin-tslint": "4.6.1",
"@typescript-eslint/parser": "4.6.1",
"@typescript-eslint/eslint-plugin": "4.7.0",
"@typescript-eslint/eslint-plugin-tslint": "4.7.0",
"@typescript-eslint/parser": "4.7.0",
"chai": "4.2.0",
"conventional-changelog-cli": "2.1.1",
"core-js": "3.7.0",
@@ -88,7 +88,7 @@
"i18next-json-sync": "2.3.1",
"jasmine-core": "3.6.0",
"jasmine-spec-reporter": "6.0.0",
"karma": "5.2.3",
"karma": "5.1.1",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-electron": "6.3.1",
"karma-jasmine": "4.0.1",
@@ -105,19 +105,20 @@
"typescript": "~4.0.5",
"wait-on": "5.2.0",
"webdriver-manager": "12.1.7",
"zone.js": "~0.11.3"
"zone.js": "0.10.3"
},
"engines": {
"node": ">=10.13.0"
},
"dependencies": {
"@angular/animations": "~10.2.2",
"@angular/cdk": "10.2.7",
"@angular/material": "10.2.7",
"@angular/animations": "~11.0.0",
"@angular/cdk": "11.0.0",
"@angular/material": "11.0.0",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@microsoft/applicationinsights-web": "2.5.9",
"@types/lodash": "4.14.165",
"adm-zip": "0.4.16",
"async": "3.2.0",

View File

@@ -8,26 +8,13 @@ import { AddonSearchResult } from "../models/wowup/addon-search-result";
export interface AddonProvider {
name: AddonProviderType;
getAll(
clientType: WowClientType,
addonIds: string[]
): Promise<AddonSearchResult[]>;
getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]>;
getFeaturedAddons(
clientType: WowClientType,
channelType?: AddonChannelType
): Promise<AddonSearchResult[]>;
getFeaturedAddons(clientType: WowClientType, channelType?: AddonChannelType): Promise<AddonSearchResult[]>;
searchByQuery(
query: string,
clientType: WowClientType,
channelType?: AddonChannelType
): Promise<AddonSearchResult[]>;
searchByQuery(query: string, clientType: WowClientType, channelType?: AddonChannelType): Promise<AddonSearchResult[]>;
searchByUrl(
addonUri: URL,
clientType: WowClientType
): Promise<AddonSearchResult>;
searchByUrl(addonUri: URL, clientType: WowClientType): Promise<AddonSearchResult>;
searchByName(
addonName: string,
@@ -36,25 +23,13 @@ export interface AddonProvider {
nameOverride?: string
): Promise<AddonSearchResult[]>;
getById(
addonId: string,
clientType: WowClientType
): Observable<AddonSearchResult>;
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult>;
isValidAddonUri(addonUri: URL): boolean;
onPostInstall(addon: Addon): void;
scan(
clientType: WowClientType,
addonChannelType: AddonChannelType,
addonFolders: AddonFolder[]
): Promise<void>;
scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise<void>;
}
export type AddonProviderType =
| "Curse"
| "GitHub"
| "TukUI"
| "WowInterface"
| "WowUp";
export type AddonProviderType = "Curse" | "GitHub" | "TukUI" | "WowInterface" | "WowUp";

View File

@@ -58,10 +58,12 @@ export class GitHubAddonProvider implements AddonProvider {
const results = await this.getReleases(repoPath).toPromise();
const latestRelease = this.getLatestRelease(results);
if (!latestRelease) {
console.log("latestRelease results", results);
throw new Error(`No release found in ${addonUri}`);
}
const asset = this.getValidAsset(latestRelease, clientType);
console.log("latestRelease", latestRelease);
if (asset == null) {
throw new Error(`No release assets found in ${addonUri}`);
}

View File

@@ -20,10 +20,7 @@ const API_URL = "https://www.tukui.org/api.php";
const CLIENT_API_URL = "https://www.tukui.org/client-api.php";
export class TukUiAddonProvider implements AddonProvider {
private readonly _circuitBreaker: CircuitBreaker<
[clientType: WowClientType],
TukUiAddon[]
>;
private readonly _circuitBreaker: CircuitBreaker<[clientType: WowClientType], TukUiAddon[]>;
public readonly name = "TukUI";
@@ -45,10 +42,7 @@ export class TukUiAddonProvider implements AddonProvider {
});
}
async getAll(
clientType: WowClientType,
addonIds: string[]
): Promise<AddonSearchResult[]> {
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
let results: AddonSearchResult[] = [];
try {
@@ -63,32 +57,21 @@ export class TukUiAddonProvider implements AddonProvider {
return results;
}
public async getFeaturedAddons(
clientType: WowClientType
): Promise<AddonSearchResult[]> {
public async getFeaturedAddons(clientType: WowClientType): Promise<AddonSearchResult[]> {
const tukUiAddons = await this.getAllAddons(clientType);
return tukUiAddons.map((addon) => this.toSearchResult(addon));
}
async searchByQuery(
query: string,
clientType: WowClientType
): Promise<AddonSearchResult[]> {
async searchByQuery(query: string, clientType: WowClientType): Promise<AddonSearchResult[]> {
const addons = await this.getAllAddons(clientType);
const canonQuery = query.toLowerCase();
let similarAddons = _.filter(
addons,
(addon) => addon.name.toLowerCase().indexOf(canonQuery) !== -1
);
let similarAddons = _.filter(addons, (addon) => addon.name.toLowerCase().indexOf(canonQuery) !== -1);
similarAddons = _.orderBy(similarAddons, ["downloads"]);
return _.map(similarAddons, (addon) => this.toSearchResult(addon));
}
searchByUrl(
addonUri: URL,
clientType: WowClientType
): Promise<AddonSearchResult> {
searchByUrl(addonUri: URL, clientType: WowClientType): Promise<AddonSearchResult> {
throw new Error("Method not implemented.");
}
@@ -112,10 +95,7 @@ export class TukUiAddonProvider implements AddonProvider {
return results;
}
getById(
addonId: string,
clientType: WowClientType
): Observable<AddonSearchResult | undefined> {
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult | undefined> {
return from(this.getAllAddons(clientType)).pipe(
map((addons) => {
const match = _.find(addons, (addon) => addon.id === addonId);
@@ -139,15 +119,9 @@ export class TukUiAddonProvider implements AddonProvider {
for (let addonFolder of addonFolders) {
let tukUiAddon: TukUiAddon;
if (addonFolder.toc?.tukUiProjectId) {
tukUiAddon = _.find(
allAddons,
(addon) => addon.id.toString() === addonFolder.toc.tukUiProjectId
);
tukUiAddon = _.find(allAddons, (addon) => addon.id.toString() === addonFolder.toc.tukUiProjectId);
} else {
const results = await this.searchAddons(
addonFolder.toc.title,
clientType
);
const results = await this.searchAddons(addonFolder.toc.title, clientType);
tukUiAddon = _.first(results);
}
@@ -182,15 +156,10 @@ export class TukUiAddonProvider implements AddonProvider {
private async searchAddons(addonName: string, clientType: WowClientType) {
var addons = await this.getAllAddons(clientType);
return addons.filter(
(addon) => addon.name.toLowerCase() === addonName.toLowerCase()
);
return addons.filter((addon) => addon.name.toLowerCase() === addonName.toLowerCase());
}
private toSearchResult(
addon: TukUiAddon,
folderName?: string
): AddonSearchResult | undefined {
private toSearchResult(addon: TukUiAddon, folderName?: string): AddonSearchResult | undefined {
if (!addon) {
return undefined;
}
@@ -217,9 +186,7 @@ export class TukUiAddonProvider implements AddonProvider {
};
}
private getAllAddons = async (
clientType: WowClientType
): Promise<TukUiAddon[]> => {
private getAllAddons = async (clientType: WowClientType): Promise<TukUiAddon[]> => {
if (clientType === WowClientType.None) {
return [];
}
@@ -246,9 +213,7 @@ export class TukUiAddonProvider implements AddonProvider {
const url = new URL(API_URL);
url.searchParams.append(query, "all");
const addons = await this._httpClient
.get<TukUiAddon[]>(url.toString())
.toPromise();
const addons = await this._httpClient.get<TukUiAddon[]>(url.toString()).toPromise();
if (this.isRetail(clientType)) {
addons.push(await this.getTukUiRetailAddon().toPromise());
addons.push(await this.getElvUiRetailAddon().toPromise());

View File

@@ -20,10 +20,7 @@ const API_URL = "https://api.mmoui.com/v4/game/WOW";
const ADDON_URL = "https://www.wowinterface.com/downloads/info";
export class WowInterfaceAddonProvider implements AddonProvider {
private readonly _circuitBreaker: CircuitBreaker<
[addonId: string],
AddonDetailsResponse
>;
private readonly _circuitBreaker: CircuitBreaker<[addonId: string], AddonDetailsResponse>;
public readonly name = "WowInterface";
@@ -45,10 +42,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
});
}
async getAll(
clientType: WowClientType,
addonIds: string[]
): Promise<AddonSearchResult[]> {
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
var searchResults: AddonSearchResult[] = [];
for (let addonId of addonIds) {
@@ -63,23 +57,15 @@ export class WowInterfaceAddonProvider implements AddonProvider {
return searchResults;
}
public async getFeaturedAddons(
clientType: WowClientType
): Promise<AddonSearchResult[]> {
public async getFeaturedAddons(clientType: WowClientType): Promise<AddonSearchResult[]> {
return [];
}
async searchByQuery(
query: string,
clientType: WowClientType
): Promise<AddonSearchResult[]> {
async searchByQuery(query: string, clientType: WowClientType): Promise<AddonSearchResult[]> {
return [];
}
async searchByUrl(
addonUri: URL,
clientType: WowClientType
): Promise<AddonSearchResult> {
async searchByUrl(addonUri: URL, clientType: WowClientType): Promise<AddonSearchResult> {
const addonId = this.getAddonId(addonUri);
if (!addonId) {
throw new Error(`Addon ID not found ${addonUri}`);
@@ -102,14 +88,9 @@ export class WowInterfaceAddonProvider implements AddonProvider {
throw new Error("Method not implemented.");
}
getById(
addonId: string,
clientType: WowClientType
): Observable<AddonSearchResult> {
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
return from(this._circuitBreaker.fire(addonId)).pipe(
map((result) =>
result ? this.toAddonSearchResult(result, "") : undefined
)
map((result) => (result ? this.toAddonSearchResult(result, "") : undefined))
);
}
@@ -131,16 +112,9 @@ export class WowInterfaceAddonProvider implements AddonProvider {
continue;
}
const details = await this._circuitBreaker.fire(
addonFolder.toc.wowInterfaceId
);
const details = await this._circuitBreaker.fire(addonFolder.toc.wowInterfaceId);
addonFolder.matchingAddon = this.toAddon(
details,
clientType,
addonChannelType,
addonFolder
);
addonFolder.matchingAddon = this.toAddon(details, clientType, addonChannelType, addonFolder);
}
}
@@ -161,9 +135,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
throw new Error(`Unhandled URL: ${addonUri}`);
}
private getAddonDetails = (
addonId: string
): Promise<AddonDetailsResponse> => {
private getAddonDetails = (addonId: string): Promise<AddonDetailsResponse> => {
console.debug("getAddonDetails");
const url = new URL(`${API_URL}/filedetails/${addonId}.json`);
@@ -212,10 +184,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
};
}
private toAddonSearchResult(
response: AddonDetailsResponse,
folderName?: string
): AddonSearchResult {
private toAddonSearchResult(response: AddonDetailsResponse, folderName?: string): AddonSearchResult {
try {
var searchResultFile: AddonSearchResultFile = {
channelType: AddonChannelType.Stable,

View File

@@ -22,43 +22,27 @@ const API_URL = AppConfig.wowUpHubUrl;
export class WowUpAddonProvider implements AddonProvider {
public readonly name = "WowUp";
constructor(
private _httpClient: HttpClient,
private _electronService: ElectronService
) {}
constructor(private _httpClient: HttpClient, private _electronService: ElectronService) {}
async getAll(
clientType: WowClientType,
addonIds: string[]
): Promise<AddonSearchResult[]> {
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
const url = `${API_URL}/addons`;
const addons = await this._httpClient
.get<WowUpAddonRepresentation[]>(url.toString())
.toPromise();
const addons = await this._httpClient.get<WowUpAddonRepresentation[]>(url.toString()).toPromise();
// TODO
return [];
}
public async getFeaturedAddons(
clientType: WowClientType
): Promise<AddonSearchResult[]> {
public async getFeaturedAddons(clientType: WowClientType): Promise<AddonSearchResult[]> {
// TODO
return [];
}
async searchByQuery(
query: string,
clientType: WowClientType
): Promise<AddonSearchResult[]> {
async searchByQuery(query: string, clientType: WowClientType): Promise<AddonSearchResult[]> {
// TODO
return [];
}
async searchByUrl(
addonUri: URL,
clientType: WowClientType
): Promise<AddonSearchResult> {
async searchByUrl(addonUri: URL, clientType: WowClientType): Promise<AddonSearchResult> {
// TODO
return undefined;
}
@@ -73,10 +57,7 @@ export class WowUpAddonProvider implements AddonProvider {
return [];
}
getById(
addonId: string,
clientType: WowClientType
): Observable<AddonSearchResult> {
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
// TODO
return of(undefined);
}
@@ -90,11 +71,7 @@ export class WowUpAddonProvider implements AddonProvider {
throw new Error("Method not implemented.");
}
async scan(
clientType: WowClientType,
addonChannelType: any,
addonFolders: AddonFolder[]
): Promise<void> {
async scan(clientType: WowClientType, addonChannelType: any, addonFolders: AddonFolder[]): Promise<void> {
// const url = `${API_URL}/addons`;
// const addons = await this._httpClient
// .get<WuAddon[]>(url.toString())
@@ -130,9 +107,7 @@ export class WowUpAddonProvider implements AddonProvider {
}
const matchedScanResults = scanResults.filter((sr) => !!sr.exactMatch);
const matchedScanResultIds = matchedScanResults.map(
(sr) => sr.exactMatch.id
);
const matchedScanResultIds = matchedScanResults.map((sr) => sr.exactMatch.id);
for (let addonFolder of addonFolders) {
var scanResult = scanResults.find((sr) => sr.path === addonFolder.path);
@@ -142,11 +117,7 @@ export class WowUpAddonProvider implements AddonProvider {
}
try {
const newAddon = this.getAddon(
clientType,
addonChannelType,
scanResult
);
const newAddon = this.getAddon(clientType, addonChannelType, scanResult);
addonFolder.matchingAddon = newAddon;
} catch (err) {
@@ -158,19 +129,11 @@ export class WowUpAddonProvider implements AddonProvider {
}
}
private hasMatchingFingerprint(
scanResult: WowUpScanResult,
release: WowUpAddonReleaseRepresentation
) {
return release.addonFolders.some(
(addonFolder) => addonFolder.fingerprint == scanResult.fingerprint
);
private hasMatchingFingerprint(scanResult: WowUpScanResult, release: WowUpAddonReleaseRepresentation) {
return release.addonFolders.some((addonFolder) => addonFolder.fingerprint == scanResult.fingerprint);
}
private isGameType(
release: WowUpAddonReleaseRepresentation,
clientType: WowClientType
) {
private isGameType(release: WowUpAddonReleaseRepresentation, clientType: WowClientType) {
return release.game_type === this.getWowGameType(clientType);
}
@@ -187,9 +150,7 @@ export class WowUpAddonProvider implements AddonProvider {
}
}
private getAddonsByFingerprints(
fingerprints: string[]
): Observable<GetAddonsByFingerprintResponse> {
private getAddonsByFingerprints(fingerprints: string[]): Observable<GetAddonsByFingerprintResponse> {
const url = `${API_URL}/addons/fingerprint`;
return this._httpClient.post<any>(url, {
@@ -197,9 +158,7 @@ export class WowUpAddonProvider implements AddonProvider {
});
}
private getScanResults = async (
addonFolders: AddonFolder[]
): Promise<AppWowUpScanResult[]> => {
private getScanResults = async (addonFolders: AddonFolder[]): Promise<AppWowUpScanResult[]> => {
const t1 = Date.now();
const filePaths = addonFolders.map((addonFolder) => addonFolder.path);
@@ -224,9 +183,7 @@ export class WowUpAddonProvider implements AddonProvider {
(af) => af.load_on_demand === false
);
const authors = scanResult.exactMatch.owner_name;
const folderList = scanResult.exactMatch.matched_release.addonFolders
.map((af) => af.folder_name)
.join(", ");
const folderList = scanResult.exactMatch.matched_release.addonFolders.map((af) => af.folder_name).join(", ");
let channelType = addonChannelType;
let latestVersion = primaryAddonFolder.version;

View File

@@ -16,7 +16,7 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes), HomeRoutingModule],
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: "legacy" }), HomeRoutingModule],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -1,19 +1,23 @@
<div *ngIf="quitEnabled === false" class="app">
<app-titlebar></app-titlebar>
<div *ngIf="quitEnabled === false" class="app" [ngClass]="[wowUpService.currentTheme]">
<app-titlebar class="bg-primary"></app-titlebar>
<div class="content">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>
</div>
<div *ngIf="quitEnabled === true" class="pre-load-container" style="
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
color: white;
justify-content: center;
align-items: center;
">
<div
*ngIf="quitEnabled === true"
class="pre-load-container"
style="
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
color: white;
justify-content: center;
align-items: center;
"
>
<img class="logo" src="assets/wowup_logo_512np.png" style="width: 200px" />
</div>
</div>

View File

@@ -1,5 +1,3 @@
:host {
}
.app {
height: 100vh;
overflow: hidden;

View File

@@ -1,21 +1,26 @@
import { async, TestBed } from "@angular/core/testing";
import { TestBed, waitForAsync } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { TranslateModule } from "@ngx-translate/core";
import { AppComponent } from "./app.component";
import { ElectronService } from "./services";
describe("AppComponent", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
providers: [ElectronService],
imports: [RouterTestingModule, TranslateModule.forRoot()],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AppComponent],
providers: [ElectronService],
imports: [RouterTestingModule, TranslateModule.forRoot()],
}).compileComponents();
})
);
it("should create the app", async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(
"should create the app",
waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
})
);
});

View File

@@ -1,7 +1,17 @@
import { AfterViewInit, ChangeDetectionStrategy, Component } from "@angular/core";
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { TranslateService } from "@ngx-translate/core";
import { CREATE_TRAY_MENU_CHANNEL } from "../common/constants";
import { OverlayContainer } from "@angular/cdk/overlay";
import {
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
CREATE_TRAY_MENU_CHANNEL,
CURRENT_THEME_KEY,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
HORDE_THEME,
} from "../common/constants";
import { SystemTrayConfig } from "../common/wowup/system-tray-config";
import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetry-dialog.component";
import { ElectronService } from "./services";
@@ -10,6 +20,7 @@ import { AnalyticsService } from "./services/analytics/analytics.service";
import { FileService } from "./services/files/file.service";
import { WowUpService } from "./services/wowup/wowup.service";
import { IconService } from "./services/icons/icon.service";
import { SessionService } from "./services/session/session.service";
import { filter } from "rxjs/operators";
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
@@ -20,7 +31,7 @@ const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
styleUrls: ["./app.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {
export class AppComponent implements OnInit, AfterViewInit {
private _autoUpdateInterval?: number;
public get quitEnabled() {
@@ -32,12 +43,32 @@ export class AppComponent implements AfterViewInit {
private _electronService: ElectronService,
private _fileService: FileService,
private translate: TranslateService,
private _wowUpService: WowUpService,
private _dialog: MatDialog,
private _addonService: AddonService,
private _iconService: IconService
private _iconService: IconService,
private _sessionService: SessionService,
public overlayContainer: OverlayContainer,
public wowUpService: WowUpService
) {}
ngOnInit(): void {
this.overlayContainer.getContainerElement().classList.add(this.wowUpService.currentTheme);
this.wowUpService.preferenceChange$.pipe(filter((pref) => pref.key === CURRENT_THEME_KEY)).subscribe((pref) => {
this.overlayContainer
.getContainerElement()
.classList.remove(
HORDE_THEME,
HORDE_LIGHT_THEME,
ALLIANCE_THEME,
ALLIANCE_LIGHT_THEME,
DEFAULT_THEME,
DEFAULT_LIGHT_THEME
);
this.overlayContainer.getContainerElement().classList.add(pref.value);
});
}
ngAfterViewInit(): void {
this.createSystemTray();
@@ -48,7 +79,10 @@ export class AppComponent implements AfterViewInit {
}
this.onAutoUpdateInterval();
this._autoUpdateInterval = window.setInterval(this.onAutoUpdateInterval, AUTO_UPDATE_PERIOD_MS);
this._autoUpdateInterval = window.setInterval(() => {
this.onAutoUpdateInterval();
this._sessionService.autoUpdateComplete();
}, AUTO_UPDATE_PERIOD_MS);
}
openDialog(): void {
@@ -73,7 +107,7 @@ export class AppComponent implements AfterViewInit {
return;
}
if (this._wowUpService.enableSystemNotifications) {
if (this.wowUpService.enableSystemNotifications) {
const iconPath = await this._fileService.getAssetFilePath("wowup_logo_512np.png");
const translated = await this.translate
.get(["APP.AUTO_UPDATE_NOTIFICATION_TITLE", "APP.AUTO_UPDATE_NOTIFICATION_BODY"], {

View File

@@ -30,5 +30,4 @@ export class GetAddonListItem {
constructor(searchResult: AddonSearchResult) {
this.searchResult = searchResult;
}
}

View File

@@ -116,5 +116,4 @@ export class AddonViewModel {
? this.addon.dependencies
: _.filter(this.addon.dependencies, (dep) => dep.type === dependencyType);
}
}

View File

@@ -1,11 +1,16 @@
<div class="addon-detail-view">
<div class="row align-items-start">
<h3 mat-dialog-title class="flex-grow-1">{{ title }}</h3>
<mat-icon class="close-icon" color="accent" [mat-dialog-close]="true" [ngStyle]="{ cursor: 'pointer' }"
svgIcon="fas:times">
<mat-icon
class="close-icon"
color="accent"
[mat-dialog-close]="true"
[ngStyle]="{ cursor: 'pointer' }"
svgIcon="fas:times"
>
</mat-icon>
</div>
<div *ngIf="defaultImageUrl" class="row justify-content-center image-row">
<div *ngIf="defaultImageUrl" class="row justify-content-center image-row bg-secondary-4">
<img class="image" [src]="defaultImageUrl" alt="Addon Picture" />
</div>
@@ -19,12 +24,15 @@
<div>
<div class="row align-items-center">
<div class="mr-2">
<div> {{ 'DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX' | translate }} {{ provider }}</div>
<div>{{ "DIALOGS.ADDON_DETAILS.VIEW_ON_PROVIDER_PREFIX" | translate }} {{ provider }}</div>
<div class="sub-text text-right selectable">{{ getExternalId() }}</div>
</div>
<a class="icon-link" appExternalLink [href]="externalUrl" [matTooltip]="
'DIALOGS.ADDON_DETAILS.VIEW_IN_BROWSER_BUTTON' | translate
">
<a
class="icon-link"
appExternalLink
[href]="externalUrl"
[matTooltip]="'DIALOGS.ADDON_DETAILS.VIEW_IN_BROWSER_BUTTON' | translate"
>
<mat-icon class="open-in-browser-icon" color="accent" svgIcon="fas:external-link-alt"></mat-icon>
</a>
</div>
@@ -33,7 +41,7 @@
<!-- DEPENDENCIES -->
<div *ngIf="hasRequiredDependencies()" class="addon-dependencies row align-items-center">
<mat-icon svgIcon="fas:link" class="mr-1"></mat-icon>
<span translate [translateParams]="{ 'dependencyCount': getRequiredDependencyCount() }">
<span translate [translateParams]="{ dependencyCount: getRequiredDependencyCount() }">
DIALOGS.ADDON_DETAILS.DEPENDENCY_TEXT
</span>
</div>
@@ -41,9 +49,12 @@
<div class="mat-caption addon-detail-summary">{{ summary }}</div>
</div>
<div mat-dialog-actions class="row align-items-center justify-content-end">
<app-addon-install-button *ngIf="showInstallButton" [addonSearchResult]="model.searchResult"
(onViewUpdated)="onInstallUpdated()">
<app-addon-install-button
*ngIf="showInstallButton"
[addonSearchResult]="model.searchResult"
(onViewUpdated)="onInstallUpdated()"
>
</app-addon-install-button>
<app-addon-update-button *ngIf="showUpdateButton" [listItem]="model.listItem"></app-addon-update-button>
</div>
</div>
</div>

View File

@@ -1,8 +1,9 @@
@import "../../../variables.scss";
.image-row {
background-color: $dark-4;
margin-bottom: 16px;
border-radius: 4px;
overflow: hidden;
.image {
max-width: 100%;

View File

@@ -1,11 +1,4 @@
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from "@angular/core";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
@@ -43,9 +36,7 @@ export class AddonInstallButtonComponent implements OnInit, OnDestroy {
this._sessionService.selectedClientType
);
this.disableButton = isInstalled;
this.buttonText = this.getButtonText(
isInstalled ? AddonInstallState.Complete : AddonInstallState.Unknown
);
this.buttonText = this.getButtonText(isInstalled ? AddonInstallState.Complete : AddonInstallState.Unknown);
const addonInstalledSub = this._addonService.addonInstalled$
.pipe(
@@ -72,10 +63,7 @@ export class AddonInstallButtonComponent implements OnInit, OnDestroy {
}
public getIsButtonActive(installState: AddonInstallState) {
return (
installState !== AddonInstallState.Unknown &&
installState !== AddonInstallState.Complete
);
return installState !== AddonInstallState.Unknown && installState !== AddonInstallState.Complete;
}
public getIsButtonDisabled(installState: AddonInstallState) {
@@ -108,9 +96,6 @@ export class AddonInstallButtonComponent implements OnInit, OnDestroy {
}
public async onInstallUpdateClick() {
await this._addonService.installPotentialAddon(
this.addonSearchResult,
this._sessionService.selectedClientType
);
await this._addonService.installPotentialAddon(this.addonSearchResult, this._sessionService.selectedClientType);
}
}

View File

@@ -1,9 +1,4 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from "@angular/core";
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
import { AddonProviderType } from "../../addon-providers/addon-provider";
@Component({

View File

@@ -8,16 +8,8 @@ describe("AddonUpdateButtonComponent", () => {
it("should create", () => {
inject(
[AddonService, AnalyticsService, TranslateService],
(
addonService: AddonService,
analyticsService: AnalyticsService,
translateService: TranslateService
) => {
const instance = new AddonUpdateButtonComponent(
addonService,
analyticsService,
translateService
);
(addonService: AddonService, analyticsService: AnalyticsService, translateService: TranslateService) => {
const instance = new AddonUpdateButtonComponent(addonService, analyticsService, translateService);
expect(instance).toBeTruthy();
}
);

View File

@@ -1,11 +1,4 @@
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from "@angular/core";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
@@ -52,9 +45,9 @@ export class AddonUpdateButtonComponent implements OnInit, OnDestroy {
}
public getActionLabel() {
return `${getEnumName(WowClientType, this.listItem?.addon?.clientType)}|${
this.listItem?.addon.providerName
}|${this.listItem?.addon.externalId}|${this.listItem?.addon.name}`;
return `${getEnumName(WowClientType, this.listItem?.addon?.clientType)}|${this.listItem?.addon.providerName}|${
this.listItem?.addon.externalId
}|${this.listItem?.addon.name}`;
}
public getInstallProgress() {
@@ -69,10 +62,7 @@ export class AddonUpdateButtonComponent implements OnInit, OnDestroy {
}
public getIsButtonDisabled() {
return (
this.listItem?.isUpToDate ||
this.listItem?.installState < AddonInstallState.Unknown
);
return this.listItem?.isUpToDate || this.listItem?.installState < AddonInstallState.Unknown;
}
public getButtonText() {
@@ -84,35 +74,23 @@ export class AddonUpdateButtonComponent implements OnInit, OnDestroy {
}
public onInstallUpdateClick() {
this._analyticsService.trackUserAction(
"addons",
"update_addon",
this.getActionLabel()
);
this._addonService.installAddon(this.listItem.addon.id);
}
public getStatusText() {
if (this.listItem?.needsInstall) {
return this._translateService.instant(
"PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON"
);
return this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON");
}
if (this.listItem?.needsUpdate) {
return this._translateService.instant(
"PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON"
);
return this._translateService.instant("PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON");
}
if (!this.listItem) {
return "";
}
return this._translateService.instant(
this.listItem.stateTextTranslationKey
);
return this._translateService.instant(this.listItem.stateTextTranslationKey);
}
private getInstallStateText(installState: AddonInstallState) {
@@ -122,9 +100,7 @@ export class AddonUpdateButtonComponent implements OnInit, OnDestroy {
case AddonInstallState.Complete:
return this._translateService.instant("COMMON.ADDON_STATE.UPTODATE");
case AddonInstallState.Downloading:
return this._translateService.instant(
"COMMON.ADDON_STATUS.DOWNLOADING"
);
return this._translateService.instant("COMMON.ADDON_STATUS.DOWNLOADING");
case AddonInstallState.Installing:
return this._translateService.instant("COMMON.ADDON_STATUS.INSTALLING");
case AddonInstallState.Pending:

View File

@@ -7,12 +7,7 @@
<button mat-button [mat-dialog-close]="false" color="primary">
{{ "DIALOGS.CONFIRM.NEGATIVE_BUTTON" | translate }}
</button>
<button
mat-raised-button
[mat-dialog-close]="true"
cdkFocusInitial
color="primary"
>
<button mat-raised-button [mat-dialog-close]="true" cdkFocusInitial color="primary">
{{ "DIALOGS.CONFIRM.POSITIVE_BUTTON" | translate }}
</button>
</div>

View File

@@ -1,38 +1,59 @@
<footer class="bg-dark-4 text-light-2">
<a appExternalLink class="patreon-link mr-2" href="https://www.patreon.com/jliddev" matTooltip="{{
'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.PATREON_SUPPORT' | translate
}}">
<footer class="bg-secondary-4 text-light-2">
<a
appExternalLink
class="patreon-link hover-bg-secondary-2 mr-2"
href="https://www.patreon.com/jliddev"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.PATREON_SUPPORT' | translate }}"
>
<img class="patron-img" src="assets/images/patreon_logo_small.png" />
</a>
<a appExternalLink class="discord-link mr-2" href="https://discord.gg/rk4F5aD" matTooltip="{{
'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate
}}">
<img class="discord-img" src="assets/images/discord_logo_small.png" />
<a
appExternalLink
class="link discord-link text-1 hover-bg-secondary-2 mr-2"
href="https://discord.gg/rk4F5aD"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate }}"
>
<mat-icon svgIcon="fab:discord"></mat-icon>
</a>
<a appExternalLink class="github-link mr-2" href="https://github.com/WowUp/WowUp" matTooltip="{{
'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GITHUB' | translate
}}">
<img class="github-img" src="assets/images/github_logo.png" />
<a
appExternalLink
class="link github-link text-1 hover-bg-secondary-2 mr-2"
href="https://github.com/WowUp/WowUp"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GITHUB' | translate }}"
>
<mat-icon svgIcon="fab:github"></mat-icon>
</a>
<a appExternalLink class="guide-link mr-2" href="https://wowup.io/guide/section/my-addons/overview" matTooltip="{{
'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GUIDE' | translate
}}">
<a
appExternalLink
class="link guide-link text-1 hover-bg-secondary-2 mr-2"
href="https://wowup.io/guide/section/my-addons/overview"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GUIDE' | translate }}"
>
<mat-icon svgIcon="far:question-circle"></mat-icon>
</a>
<p class="flex-grow-1">{{ sessionService.statusText$ | async }}</p>
<div class="row align-items-center">
<p class="flex-grow-1 text-1">{{ sessionService.statusText$ | async }}</p>
<div class="row align-items-center text-1">
<p class="mr-3">{{ sessionService.pageContextText$ | async }}</p>
<p>v{{ wowUpService.applicationVersion }}</p>
<button *ngIf="isWowUpUpdateAvailable && !isWowUpdateDownloading" class="footer-button update-button ml-3"
matTooltip="{{ updateIconTooltip | translate }}" [disabled]="isUpdatingWowUp" (click)="onClickUpdateWowup()">
<button
*ngIf="isWowUpUpdateAvailable && !isWowUpdateDownloading"
class="footer-button update-button ml-3"
matTooltip="{{ updateIconTooltip | translate }}"
[disabled]="isUpdatingWowUp"
(click)="onClickUpdateWowup()"
>
<mat-icon svgIcon="fas:arrow-up"></mat-icon>
</button>
<button *ngIf="isWowUpdateDownloading" class="footer-button downloading-button ml-3 animate">
<button *ngIf="isWowUpdateDownloading" class="footer-button downloading-button text-2 ml-3 animate">
<mat-icon svgIcon="fas:angle-double-down"></mat-icon>
</button>
<button *ngIf="!isWowUpUpdateAvailable && !isWowUpdateDownloading" class="footer-button check-update-button ml-3"
[ngClass]="{'animate': isCheckingForUpdates}" (click)="onClickCheckForUpdates()"
matTooltip="{{'APP.SYSTEM_TRAY.CHECK_UPDATE' | translate}}">
<button
*ngIf="!isWowUpUpdateAvailable && !isWowUpdateDownloading"
class="footer-button text-1 check-update-button ml-3"
[ngClass]="{ animate: isCheckingForUpdates }"
(click)="onClickCheckForUpdates()"
matTooltip="{{ 'APP.SYSTEM_TRAY.CHECK_UPDATE' | translate }}"
>
<mat-icon svgIcon="fas:sync-alt"></mat-icon>
</button>
</div>

View File

@@ -21,51 +21,33 @@ footer {
font-size: 0.8em;
}
.link {
.mat-icon {
height: 20px;
width: 20px;
}
}
.patreon-link {
padding: 0 0.25em;
&:hover {
background-color: $dark-3;
}
.patron-img {
height: 25px;
}
}
.discord-link {
padding: 0 0.25em;
&:hover {
background-color: $dark-3;
}
.discord-img {
height: 25px;
}
height: 20px;
padding: 2.5px 0.25em;
}
.github-link {
height: 20px;
padding: 2.5px 0.25em;
&:hover {
background-color: $dark-3;
}
.github-img {
height: 20px;
}
}
.guide-link {
height: 20px;
padding: 2.5px 0.25em;
color: $white-1;
&:hover {
background-color: $dark-3;
}
.mat-icon {
height: 20px;
width: 20px;
}
}
.footer-button {
@@ -78,7 +60,6 @@ footer {
&.update-button {
background-color: green;
color: $white-1;
&:hover {
background-color: darkgreen;
@@ -97,8 +78,6 @@ footer {
}
&.check-update-button {
color: $white-2;
&:hover {
cursor: pointer;
}
@@ -109,8 +88,6 @@ footer {
}
&.downloading-button {
color: $white-2;
&.animate {
animation: fadeInDown 1s infinite linear;
}

View File

@@ -5,14 +5,10 @@
</p>
<p>{{ "DIALOGS.INSTALL_FROM_URL.SUPPORTED_SOURCES" | translate }}</p>
<mat-form-field class="url-input-container">
<mat-label>{{
"DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_LABEL" | translate
}}</mat-label>
<mat-label>{{ "DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_LABEL" | translate }}</mat-label>
<input
matInput
[placeholder]="
'DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_PLACEHOLDER' | translate
"
[placeholder]="'DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_PLACEHOLDER' | translate"
[(ngModel)]="query"
(keyup.enter)="onImportUrl()"
[disabled]="isBusy === true"
@@ -34,37 +30,30 @@
<mat-spinner diameter="50"></mat-spinner>
</div>
<div *ngIf="isBusy === false && addon !== undefined" class="addon-container">
<div
class="addon-thumb"
[style.backgroundImage]="'url(' + addon.thumbnailUrl + ')'"
></div>
<div class="addon-thumb" [style.backgroundImage]="'url(' + addon.thumbnailUrl + ')'"></div>
<div class="addon-info">
<h4>{{ addon.name }}</h4>
<p>{{ addon.author }}</p>
<p class="addon-download-count">
{{ addon.downloadCount | downloadCount }} downloads on
{{ addon.providerName }}
{{
"DIALOGS.INSTALL_FROM_URL.DOWNLOAD_COUNT"
| translate
: {
count: addon.downloadCount,
textCount: (addon.downloadCount | downloadCount),
provider: addon.providerName
}
}}
</p>
</div>
<div class="install-container">
<mat-spinner
*ngIf="showInstallSpinner === true"
diameter="40"
></mat-spinner>
<button
*ngIf="showInstallButton === true"
mat-flat-button
color="primary"
(click)="onInstall()"
>
<mat-spinner *ngIf="showInstallSpinner === true" diameter="40"></mat-spinner>
<button *ngIf="showInstallButton === true" mat-flat-button color="primary" (click)="onInstall()">
{{ "DIALOGS.INSTALL_FROM_URL.INSTALL_BUTTON" | translate }}
</button>
<div [hidden]="showInstallSuccess === false">
<div class="success-icon">
<img
src="assets/images/checkbox-marked-circle-green.svg"
class="icon-larger"
/>
<img src="assets/images/checkbox-marked-circle-green.svg" class="icon-larger" />
</div>
<div>
{{ "DIALOGS.INSTALL_FROM_URL.INSTALL_SUCCESS_LABEL" | translate }}

View File

@@ -44,8 +44,7 @@
.success-icon {
text-align: center;
filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg)
brightness(96%) contrast(106%);
filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%);
}
.icon-larger {

View File

@@ -51,10 +51,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
this.showInstallSpinner = true;
this._installSubscription = from(
this._addonService.installPotentialAddon(
this.addon,
this._sessionService.selectedClientType
)
this._addonService.installPotentialAddon(this.addon, this._sessionService.selectedClientType)
).subscribe({
next: () => {
this.showInstallSpinner = false;
@@ -64,11 +61,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
console.error(err);
this.showInstallSpinner = false;
this.showInstallButton = true;
this.showErrorMessage(
this._translateService.instant(
"DIALOGS.INSTALL_FROM_URL.ERROR.INSTALL_FAILED"
)
);
this.showErrorMessage(this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.INSTALL_FAILED"));
},
});
}
@@ -88,10 +81,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
}
try {
const importedAddon = await this._addonService.getAddonByUrl(
url,
this._sessionService.selectedClientType
);
const importedAddon = await this._addonService.getAddonByUrl(url, this._sessionService.selectedClientType);
console.debug(importedAddon);
if (!importedAddon) {
@@ -112,14 +102,10 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
let message = err.message;
if (err instanceof HttpErrorResponse) {
message = this._translateService.instant(
"DIALOGS.INSTALL_FROM_URL.ERROR.NO_ADDON_FOUND"
);
message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.NO_ADDON_FOUND");
} else if (err.code && err.code === "EOPENBREAKER") {
// Provider circuit breaker is open
message = this._translateService.instant(
"DIALOGS.INSTALL_FROM_URL.ERROR.FAILED_TO_CONNECT"
);
message = this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.FAILED_TO_CONNECT");
}
this.showErrorMessage(message);
@@ -127,10 +113,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
}
private addonExists(externalId: string) {
return this._addonService.isInstalled(
externalId,
this._sessionService.selectedClientType
);
return this._addonService.isInstalled(externalId, this._sessionService.selectedClientType);
}
private getUrlFromQuery(): URL | undefined {
@@ -138,11 +121,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
return new URL(this.query);
} catch (err) {
console.error(`Invalid url: ${this.query}`);
this.showErrorMessage(
this._translateService.instant(
"DIALOGS.INSTALL_FROM_URL.ERROR.INVALID_URL"
)
);
this.showErrorMessage(this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.INVALID_URL"));
return undefined;
}
}
@@ -151,9 +130,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
const dialogRef = this._dialog.open(AlertDialogComponent, {
minWidth: 250,
data: {
title: this._translateService.instant(
"DIALOGS.INSTALL_FROM_URL.ERROR.TITLE"
),
title: this._translateService.instant("DIALOGS.INSTALL_FROM_URL.ERROR.TITLE"),
message: errorMessage,
},
});

View File

@@ -1,12 +1,4 @@
import {
Component,
EventEmitter,
Input,
NgZone,
OnDestroy,
OnInit,
Output,
} from "@angular/core";
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { AddonViewModel } from "../../business-objects/my-addon-list-item";
@@ -24,10 +16,7 @@ export class MyAddonStatusColumnComponent implements OnInit, OnDestroy {
return this.listItem?.isUpToDate || this.listItem?.isIgnored;
}
constructor(
private _translateService: TranslateService,
private _ngzone: NgZone
) {}
constructor(private _translateService: TranslateService, private _ngzone: NgZone) {}
ngOnInit(): void {}

View File

@@ -1,52 +1,80 @@
<div class="addon-column row align-items-center">
<div class="thumbnail-container">
<div *ngIf="listItem.hasThumbnail === true" class="addon-logo-container"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"></div>
<div
*ngIf="listItem.hasThumbnail === true"
class="addon-logo-container bg-secondary-3"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"
></div>
<div *ngIf="listItem.hasThumbnail === false" class="addon-logo-container">
<div class="addon-logo-letter">
<div class="addon-logo-letter text-3">
{{ listItem.thumbnailLetter }}
</div>
</div>
<div *ngIf="listItem.isBetaChannel || listItem.isAlphaChannel" class="channel" [ngClass]="{
<div
*ngIf="listItem.isBetaChannel || listItem.isAlphaChannel"
class="channel bg-secondary-3"
[ngClass]="{
beta: listItem.isBetaChannel,
alpha: listItem.isAlphaChannel
}">
}"
>
{{ listItem.isAlphaChannel ? "Alpha" : "Beta" }}
</div>
</div>
<div>
<a class="addon-title mat-subheading-2" (click)="viewDetails()"
[ngClass]="{ ignored: listItem.isIgnored }">{{ listItem.addon.name }}</a>
<a class="addon-title hover-text-2 mat-subheading-2" (click)="viewDetails()" [ngClass]="{ 'text-3': listItem.isIgnored }">{{
listItem.addon.name
}}</a>
<div class="addon-funding">
<a *ngIf="listItem.addon.patreonFundingLink" appExternalLink [href]="listItem.addon.patreonFundingLink"
matTooltip="Support the author on Patreon">
<a
*ngIf="listItem.addon.patreonFundingLink"
appExternalLink
[href]="listItem.addon.patreonFundingLink"
matTooltip="Support the author on Patreon"
>
<img class="funding-icon" src="assets/images/patreon_logo_small.png" />
</a>
<a *ngIf="listItem.addon.githubFundingLink" appExternalLink [href]="listItem.addon.githubFundingLink"
matTooltip="Support the author on GitHub">
<a
*ngIf="listItem.addon.githubFundingLink"
appExternalLink
[href]="listItem.addon.githubFundingLink"
matTooltip="Support the author on GitHub"
>
<img class="funding-icon" src="assets/images/github_logo_small.png" />
</a>
<a *ngIf="listItem.addon.customFundingLink" appExternalLink [href]="listItem.addon.customFundingLink"
matTooltip="Support this author">
<a
*ngIf="listItem.addon.customFundingLink"
appExternalLink
[href]="listItem.addon.customFundingLink"
matTooltip="Support this author"
>
<img class="funding-icon" src="assets/images/custom_funding_logo_small.png" />
</a>
</div>
<div class="addon-version row align-items-center" [ngClass]="{ ignored: listItem.isIgnored }">
<div class="addon-version text-2 row align-items-center" [ngClass]="{ ignored: listItem.isIgnored }">
<div *ngIf="this.listItem.isAutoUpdate === true" class="mr-2">
<mat-icon class="auto-update-icon" [matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate"
svgIcon="far:clock">
<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()"
class="mr-2"
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate: dependencyTooltip"
>
<mat-icon class="auto-update-icon" svgIcon="fas:link"></mat-icon>
</div>
{{ listItem.addon.installedVersion }}
<div class="update-available row"
*ngIf="showUpdateToVersion && listItem.addon.latestVersion !== listItem.addon.installedVersion">
<div
class="update-available row"
*ngIf="showUpdateToVersion && listItem.addon.latestVersion !== listItem.addon.installedVersion"
>
<mat-icon class="upgrade-icon" svgIcon="fas:play"></mat-icon>
<div>{{ listItem.addon.latestVersion }}</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -8,7 +8,6 @@
margin-right: 11px;
.addon-logo-container {
background-color: $dark-2;
width: 40px;
height: 40px;
display: flex;
@@ -25,14 +24,12 @@
}
.addon-logo-letter {
color: $white-4;
font-size: 2em;
font-weight: 400;
}
}
.channel {
background: $dark-4;
text-align: center;
font-weight: 400;
font-size: 0.8em;
@@ -51,21 +48,15 @@
// word-break: break-all;
white-space: normal;
text-decoration: none;
color: $white-1;
&:hover {
cursor: pointer;
text-decoration: underline;
color: $white-2;
}
&.ignored {
color: $white-4;
}
}
.addon-version {
color: $white-2;
.update-available {
color: $artifact;
@@ -78,14 +69,11 @@
.auto-update-icon {
height: 11px;
width: 11px;
//vertical-align: text-bottom;
color: $white-1;
}
.addon-funding {
a {
margin-right: 1em;
color: $white-1;
}
.funding-icon {
width: 15px;

View File

@@ -9,9 +9,9 @@
<div>
{{ "PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_LABEL" | translate }}
</div>
<small class="hint">{{ "PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_DESCRIPTION" | translate }}</small>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.SET_LANGUAGE_DESCRIPTION" | translate }}</small>
</div>
<mat-form-field class="light-select">
<mat-form-field class="">
<mat-label>{{ "PAGES.OPTIONS.APPLICATION.CURRENT_LANGUAGE_LABEL" | translate }}</mat-label>
<mat-select [value]="currentLanguage" (selectionChange)="onCurrentLanguageChange($event)">
<mat-option *ngFor="let language of languages" [value]="language.localeId">
@@ -21,6 +21,25 @@
</mat-form-field>
</div>
</div>
<!-- THEME -->
<div class="toggle">
<div class="row align-items-center">
<div class="flex-grow-1">
<div>
{{ "PAGES.OPTIONS.APPLICATION.THEME_LABEL" | translate }}
</div>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.THEME_DESCRIPTION" | translate }}</small>
</div>
<mat-form-field class="">
<mat-label>{{ "PAGES.OPTIONS.APPLICATION.THEME_LABEL" | translate }}</mat-label>
<mat-select [(value)]="wowupService.currentTheme">
<mat-option *ngFor="let theme of themes" [value]="theme.class">
{{ theme.display | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- TELEMETRY -->
<div class="toggle">
<div class="row align-items-center">
@@ -31,7 +50,7 @@
</div>
<mat-slide-toggle [checked]="telemetryEnabled" (change)="onTelemetryChange($event)"></mat-slide-toggle>
</div>
<small class="hint">
<small class="text-2">
{{ "PAGES.OPTIONS.APPLICATION.TELEMETRY_DESCRIPTION" | translate }}
</small>
</div>
@@ -43,11 +62,9 @@
{{ "PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_LABEL" | translate }}
</div>
</div>
<mat-slide-toggle [(checked)]="collapseToTray" (change)="onCollapseChange($event)" appUserActionTracker
category="Options" action="CollapseToTray" [label]="collapseToTray">
</mat-slide-toggle>
<mat-slide-toggle [(checked)]="collapseToTray" (change)="onCollapseChange($event)"> </mat-slide-toggle>
</div>
<small class="hint">{{ minimizeOnCloseDescription }}</small>
<small class="text-2">{{ minimizeOnCloseDescription }}</small>
</div>
<!-- SYSTEM NOTIFICATIONS -->
<div class="toggle">
@@ -57,12 +74,13 @@
{{ "PAGES.OPTIONS.APPLICATION.ENABLE_SYSTEM_NOTIFICATIONS_LABEL" | translate }}
</div>
</div>
<mat-slide-toggle [checked]="wowupService.enableSystemNotifications"
(change)="onEnableSystemNotifications($event)" appUserActionTracker category="Options"
action="EnableSystemNotifications" [label]="wowupService.enableSystemNotifications">
<mat-slide-toggle
[checked]="wowupService.enableSystemNotifications"
(change)="onEnableSystemNotifications($event)"
>
</mat-slide-toggle>
</div>
<small class="hint">
<small class="text-2">
{{ "PAGES.OPTIONS.APPLICATION.ENABLE_SYSTEM_NOTIFICATIONS_DESCRIPTION" | translate }}
</small>
</div>
@@ -74,11 +92,10 @@
{{ "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_LABEL" | translate }}
</div>
</div>
<mat-slide-toggle [(checked)]="useHardwareAcceleration" (change)="onUseHardwareAccelerationChange($event)"
appUserActionTracker category="Options" action="UseHardwareAcceleration" [label]="useHardwareAcceleration">
<mat-slide-toggle [(checked)]="useHardwareAcceleration" (change)="onUseHardwareAccelerationChange($event)">
</mat-slide-toggle>
</div>
<small class="hint">{{ "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_DESCRIPTION" | translate }}</small>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.USE_HARDWARE_ACCELERATION_DESCRIPTION" | translate }}</small>
</div>
<!-- START WITH SYSTEM -->
<div class="toggle">
@@ -87,11 +104,13 @@
<div>
{{ "PAGES.OPTIONS.APPLICATION.START_WITH_SYSTEM_LABEL" | translate }}
</div>
<small class="hint">{{ "PAGES.OPTIONS.APPLICATION.START_WITH_SYSTEM_DESCRIPTION" | translate }}</small>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.START_WITH_SYSTEM_DESCRIPTION" | translate }}</small>
</div>
<mat-slide-toggle [(ngModel)]="startWithSystem" [(checked)]="startWithSystem"
(change)="onStartWithSystemChange($event)" appUserActionTracker category="Options" action="StartWithSystem"
[label]="startWithSystem">
<mat-slide-toggle
[(ngModel)]="startWithSystem"
[(checked)]="startWithSystem"
(change)="onStartWithSystemChange($event)"
>
</mat-slide-toggle>
</div>
</div>
@@ -102,12 +121,15 @@
<div>
{{ "PAGES.OPTIONS.APPLICATION.START_MINIMIZED_LABEL" | translate }}
</div>
<small class="hint">{{ "PAGES.OPTIONS.APPLICATION.START_MINIMIZED_DESCRIPTION" | translate }}</small>
<small class="text-2">{{ "PAGES.OPTIONS.APPLICATION.START_MINIMIZED_DESCRIPTION" | translate }}</small>
</div>
<mat-slide-toggle [(ngModel)]="startMinimized" [(disabled)]="!startWithSystem" [(checked)]="startMinimized"
(change)="onStartMinimizedChange($event)" appUserActionTracker category="Options" action="StartMinimized"
[label]="startMinimized">
<mat-slide-toggle
[(ngModel)]="startMinimized"
[(disabled)]="!startWithSystem"
[(checked)]="startMinimized"
(change)="onStartMinimizedChange($event)"
>
</mat-slide-toggle>
</div>
</div>
</div>
</div>

View File

@@ -3,10 +3,20 @@ import { MatDialog } from "@angular/material/dialog";
import { MatSelectChange } from "@angular/material/select";
import { MatSlideToggleChange } from "@angular/material/slide-toggle";
import { TranslateService } from "@ngx-translate/core";
import { ElectronService } from "app/services";
import { AnalyticsService } from "app/services/analytics/analytics.service";
import { WowUpService } from "app/services/wowup/wowup.service";
import { ElectronService } from "../../services";
import { AnalyticsService } from "../../services/analytics/analytics.service";
import { SessionService } from "../../services/session/session.service";
import { WowUpService } from "../../services/wowup/wowup.service";
import { Theme } from "../../models/wowup/theme";
import { ConfirmDialogComponent } from "../confirm-dialog/confirm-dialog.component";
import {
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
HORDE_THEME,
} from "../../../common/constants";
interface LocaleListItem {
localeId: string;
@@ -39,11 +49,21 @@ export class OptionsAppSectionComponent implements OnInit {
{ localeId: "zh", label: "简体中文" },
];
public themes: Theme[] = [
{ display: "APP.THEME.DEFAULT", class: DEFAULT_THEME },
{ display: "APP.THEME.DEFAULT_LIGHT", class: DEFAULT_LIGHT_THEME },
{ display: "APP.THEME.ALLIANCE", class: ALLIANCE_THEME },
{ display: "APP.THEME.ALLIANCE_LIGHT", class: ALLIANCE_LIGHT_THEME },
{ display: "APP.THEME.HORDE", class: HORDE_THEME },
{ display: "APP.THEME.HORDE_LIGHT", class: HORDE_LIGHT_THEME },
];
constructor(
private _analyticsService: AnalyticsService,
private _dialog: MatDialog,
private _electronService: ElectronService,
private _translateService: TranslateService,
public sessionService: SessionService,
public wowupService: WowUpService
) {}

View File

@@ -7,9 +7,7 @@
<div class="row align-items-center">
<div class="flex-grow-1">
<div>{{ "PAGES.OPTIONS.DEBUG.LOG_FILES_LABEL" | translate }}</div>
<small class="hint">{{
"PAGES.OPTIONS.DEBUG.LOG_FILES_DESCRIPTION" | translate
}}</small>
<small class="text-2">{{ "PAGES.OPTIONS.DEBUG.LOG_FILES_DESCRIPTION" | translate }}</small>
</div>
<div>
<button
@@ -30,9 +28,7 @@
<div class="row align-items-center">
<div class="flex-grow-1">
<div>{{ "PAGES.OPTIONS.DEBUG.DEBUG_DATA_LABEL" | translate }}</div>
<small class="hint">{{
"PAGES.OPTIONS.DEBUG.DEBUG_DATA_DESCRIPTION" | translate
}}</small>
<small class="text-2">{{ "PAGES.OPTIONS.DEBUG.DEBUG_DATA_DESCRIPTION" | translate }}</small>
</div>
<div>
<button

View File

@@ -3,10 +3,6 @@
.container {
padding: 1em;
.hint {
color: $white-3;
}
.section {
margin-top: 1em;
}

View File

@@ -8,10 +8,7 @@ import { WowUpService } from "app/services/wowup/wowup.service";
styleUrls: ["./options-debug-section.component.scss"],
})
export class OptionsDebugSectionComponent implements OnInit {
constructor(
private _addonService: AddonService,
private _wowupService: WowUpService
) {}
constructor(private _addonService: AddonService, private _wowupService: WowUpService) {}
ngOnInit(): void {}

View File

@@ -18,11 +18,7 @@
{{ "PAGES.OPTIONS.WOW.RESCAN_CLIENTS_BUTTON" | translate }}
</button>
</div>
<app-wow-client-options
class="client-section"
*ngFor="let clientType of wowClientTypes"
[clientType]="clientType"
>
<app-wow-client-options class="client-section" *ngFor="let clientType of wowClientTypes" [clientType]="clientType">
</app-wow-client-options>
</div>
</div>

View File

@@ -21,17 +21,12 @@ export class OptionsWowSectionComponent implements OnInit {
public wowUpReleaseChannels: {
type: WowUpReleaseChannelType;
name: string;
}[] = getEnumList(WowUpReleaseChannelType).map(
(type: WowUpReleaseChannelType) => ({
type,
name: getEnumName(WowUpReleaseChannelType, type),
})
);
}[] = getEnumList(WowUpReleaseChannelType).map((type: WowUpReleaseChannelType) => ({
type,
name: getEnumName(WowUpReleaseChannelType, type),
}));
constructor(
private _warcraftService: WarcraftService,
private _wowupService: WowUpService
) {}
constructor(private _warcraftService: WarcraftService, private _wowupService: WowUpService) {}
ngOnInit(): void {
this.wowUpReleaseChannel = this._wowupService.wowUpReleaseChannel;

View File

@@ -1,9 +1,9 @@
<div class="addon-column row align-items-center">
<div class="thumbnail-container">
<div *ngIf="hasThumbnail === true" class="addon-logo-container"
<div class="thumbnail-container bg-secondary-3">
<div *ngIf="hasThumbnail === true" class="addon-logo-container "
[style.backgroundImage]="'url(' + addon.thumbnailUrl + ')'"></div>
<div *ngIf="hasThumbnail === false" class="addon-logo-container">
<div class="addon-logo-letter">
<div class="addon-logo-letter text-3">
{{ thumbnailLetter }}
</div>
</div>
@@ -13,16 +13,13 @@
</div>
</div>
<div>
<a class="addon-title mat-subheading-2" (click)="viewDetails()">{{
addon.name
}}</a>
<div class="addon-version">
<a class="addon-title hover-text-2 mat-subheading-2" (click)="viewDetails()">{{ addon.name }}</a>
<div class="addon-version text-2">
<span *ngIf="hasRequiredDependencies()" class="dependency-icon mr-1"
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate:dependencyTooltip">
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate: dependencyTooltip">
<mat-icon svgIcon="fas:link"></mat-icon>
</span>
{{ addon | getAddonListItemFileProp: "version":channel }}
</div>
</div>
</div>

View File

@@ -8,7 +8,6 @@
margin-right: 11px;
.addon-logo-container {
background-color: $dark-2;
width: 40px;
height: 40px;
display: flex;
@@ -41,7 +40,6 @@
}
.addon-logo-letter {
color: $white-4;
font-size: 2em;
font-weight: 400;
}
@@ -50,7 +48,6 @@
.addon-title {
white-space: normal;
text-decoration: none;
color: $white-1;
&:hover {
cursor: pointer;
@@ -67,7 +64,6 @@
.mat-icon {
width: 11px;
height: 11px;
color: $white-1;
}
}
}

View File

@@ -4,12 +4,9 @@ import { PotentialAddonTableColumnComponent } from "./potential-addon-table-colu
describe("PotentialAddonTableColumnComponent", () => {
it("should create", () => {
inject(
[GetAddonListItemFilePropPipe],
(propPipe: GetAddonListItemFilePropPipe) => {
const pipe = new PotentialAddonTableColumnComponent(propPipe);
expect(pipe).toBeTruthy();
}
);
inject([GetAddonListItemFilePropPipe], (propPipe: GetAddonListItemFilePropPipe) => {
const pipe = new PotentialAddonTableColumnComponent(propPipe);
expect(pipe).toBeTruthy();
});
});
});

View File

@@ -1,15 +1,4 @@
<button
mat-flat-button
color="primary"
class="progress-button"
(click)="onClickButton($event)"
[disabled]="disable"
>
<mat-progress-bar
class="progress-bar"
color="secondary"
[value]="value"
*ngIf="showProgress"
></mat-progress-bar>
<button mat-flat-button color="primary" class="progress-button" (click)="onClickButton($event)" [disabled]="disable">
<mat-progress-bar class="progress-bar" color="secondary" [value]="value" *ngIf="showProgress"></mat-progress-bar>
<ng-content></ng-content>
</button>

View File

@@ -1,12 +1,4 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
} from "@angular/core";
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from "@angular/core";
@Component({
selector: "app-progress-button",

View File

@@ -1,4 +1,4 @@
<div class="busy-container">
<div class="busy-container text-1">
<mat-spinner [diameter]="50"></mat-spinner>
<pre>{{ message || defaultMessage }}</pre>
</div>

View File

@@ -6,7 +6,6 @@
flex-direction: column;
align-items: center;
margin-top: 2em;
color: $white-1;
}
pre {

View File

@@ -14,10 +14,8 @@ export class ProgressSpinnerComponent implements OnInit {
constructor(private _translateService: TranslateService) {}
ngOnInit(): void {
this._translateService
.get("COMMON.PROGRESS_SPINNER.LOADING")
.subscribe((translatedStr) => {
this.defaultMessage = translatedStr;
});
this._translateService.get("COMMON.PROGRESS_SPINNER.LOADING").subscribe((translatedStr) => {
this.defaultMessage = translatedStr;
});
}
}

View File

@@ -1,35 +1,49 @@
<div class="titlebar bg-purple-1 text-light" [ngClass]="{
<div
class="titlebar bg-primary text-light"
[ngClass]="{
mac: electronService.isMac,
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div class="titlebar-drag-region" (dblclick)="onDblClick()"></div>
<div class="window-logo-container" *ngIf="electronService.isWin">
<img src="assets/wowup_logo_512np.png" />
}"
>
<div class="theme-logo">
<img [src]="getLogoPath()" />
</div>
<div class="titlebar-drag-region" (dblclick)="onDblClick()"></div>
<!-- <div class="window-logo-container" *ngIf="electronService.isWin">
<img src="assets/wowup_logo_512np.png" />
</div> -->
<div class="title-container">
<div>WowUp.io</div>
</div>
<div *ngIf="isMac" class="window-control-container pointer">
<div class="window-control" (click)="onClickDebug()">
<div class="window-control hover-primary-2" (click)="onClickDebug()">
<mat-icon class="debug-button pointer" svgIcon="fas:bug"></mat-icon>
</div>
</div>
<div *ngIf="electronService.isWin || electronService.isLinux" class="window-control-container row align-items-center">
<div class="window-control" (click)="onClickDebug()">
<div class="window-control hover-primary-2" (click)="onClickDebug()">
<mat-icon class="debug-button" svgIcon="fas:bug"></mat-icon>
</div>
<div class="window-control" (click)="electronService.minimizeWindow()">
<div class="window-control hover-primary-2" (click)="electronService.minimizeWindow()">
<img src="assets/chrome-minimize.svg" />
</div>
<div class="window-control" *ngIf="isMaximized === false" (click)="electronService.maximizeWindow()">
<div
class="window-control hover-primary-2"
*ngIf="isMaximized === false"
(click)="electronService.maximizeWindow()"
>
<img src="assets/chrome-maximize.svg" />
</div>
<div class="window-control" *ngIf="isMaximized === true" (click)="electronService.unmaximizeWindow()">
<div
class="window-control hover-primary-2"
*ngIf="isMaximized === true"
(click)="electronService.unmaximizeWindow()"
>
<img src="assets/chrome-restore.svg" />
</div>
<div class="window-control" (click)="onClickClose()">
<div class="window-control hover-primary-2" (click)="onClickClose()">
<img src="assets/chrome-close.svg" />
</div>
</div>
</div>
</div>

View File

@@ -5,14 +5,39 @@
flex-direction: row;
align-items: center;
position: relative;
margin-top: 4px;
.debug-button {
height: 15px;
width: 15px;
}
.theme-logo {
z-index: 1;
position: fixed;
top: 0;
left: 4px;
height: 82px;
opacity: 0.3;
overflow: hidden;
img {
position: relative;
left: 0;
height: 150%;
width: auto;
}
}
}
.mac {
height: 22px;
.theme-logo {
height: 70px;
right: 4px;
left: initial;
}
}
.windows,
.linux {
@@ -64,7 +89,6 @@
z-index: 2500;
&:hover {
background-color: $purple-2;
cursor: pointer;
}

View File

@@ -1,4 +1,12 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import {
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
HORDE_THEME,
} from "common/constants";
import { platform } from "os";
import { Subscription } from "rxjs";
import { AppConfig } from "../../../environments/environment";
@@ -21,16 +29,10 @@ export class TitlebarComponent implements OnInit, OnDestroy {
private _subscriptions: Subscription[] = [];
constructor(
public electronService: ElectronService,
private _wowUpService: WowUpService,
private _ngZone: NgZone
) {
const windowMaximizedSubscription = this.electronService.windowMaximized$.subscribe(
(maximized) => {
this._ngZone.run(() => (this.isMaximized = maximized));
}
);
constructor(public electronService: ElectronService, private _wowUpService: WowUpService, private _ngZone: NgZone) {
const windowMaximizedSubscription = this.electronService.windowMaximized$.subscribe((maximized) => {
this._ngZone.run(() => (this.isMaximized = maximized));
});
this._subscriptions = [windowMaximizedSubscription];
}
@@ -41,6 +43,21 @@ export class TitlebarComponent implements OnInit, OnDestroy {
this._subscriptions.forEach((subscription) => subscription.unsubscribe());
}
getLogoPath() {
switch (this._wowUpService.currentTheme) {
case HORDE_THEME:
case HORDE_LIGHT_THEME:
return "assets/images/horde-1.png";
case ALLIANCE_THEME:
case ALLIANCE_LIGHT_THEME:
return "assets/images/alliance-1.png";
case DEFAULT_THEME:
case DEFAULT_LIGHT_THEME:
default:
return "assets/images/wowup-white-1.png";
}
}
onClickClose() {
if (this._wowUpService.collapseToTray) {
this.electronService.hideWindow();
@@ -57,10 +74,7 @@ export class TitlebarComponent implements OnInit, OnDestroy {
const win = this.electronService.remote.getCurrentWindow();
if (this.isMac) {
const action = this.electronService.remote.systemPreferences.getUserDefault(
"AppleActionOnDoubleClick",
"string"
);
const action = this.electronService.remote.systemPreferences.getUserDefault("AppleActionOnDoubleClick", "string");
if (action === "Maximize") {
if (win.isMaximized()) {
win.unmaximize();

View File

@@ -1,13 +1,12 @@
<mat-divider></mat-divider>
<div class="title">{{ clientTypeName }}</div>
<div class="title">{{ clientTypeName | translate }}</div>
<div class="row">
<mat-form-field class="grow folder-input">
<mat-label>{{
"PAGES.OPTIONS.WOW.CLIENT_TYPE_PATH_LABEL"
| translate: { clientTypeName: clientTypeName }
"PAGES.OPTIONS.WOW.CLIENT_TYPE_PATH_LABEL" | translate: { clientTypeName: (clientTypeName | translate) }
}}</mat-label>
<input matInput disabled [value]="clientLocation" />
<mat-hint>
<mat-hint class="text-2">
{{
"PAGES.OPTIONS.WOW.CLIENT_TYPE_INPUT_HINT"
| translate
@@ -34,10 +33,8 @@
<div class="grow">
<p>{{ "PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_LABEL" | translate }}</p>
</div>
<mat-form-field class="light-select">
<mat-label>{{
"PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_SELECT_LABEL" | translate
}}</mat-label>
<mat-form-field class="">
<mat-label>{{ "PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_SELECT_LABEL" | translate }}</mat-label>
<mat-select
[(value)]="selectedAddonChannelType"
(selectionChange)="onDefaultAddonChannelChange($event)"
@@ -46,20 +43,16 @@
[action]="clientTypeName + 'DefaultChannel'"
[label]="selectedAddonChannelType"
>
<mat-option
*ngFor="let channel of addonChannelInfos"
[value]="channel.type"
>{{ channel.name | translate }}</mat-option
>
<mat-option *ngFor="let channel of addonChannelInfos" [value]="channel.type">{{
channel.name | translate
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="row">
<div class="grow">
<p>{{ "PAGES.OPTIONS.WOW.AUTO_UPDATE_LABEL" | translate }}</p>
<small class="hint">{{
"PAGES.OPTIONS.WOW.AUTO_UPDATE_DESCRIPTION" | translate
}}</small>
<small class="text-2">{{ "PAGES.OPTIONS.WOW.AUTO_UPDATE_DESCRIPTION" | translate }}</small>
</div>
<mat-slide-toggle
[(checked)]="clientAutoUpdate"

View File

@@ -24,13 +24,7 @@ describe("WowClientOptionsComponent", () => {
it("should create", () => {
inject(
[
MatDialog,
ElectronService,
WarcraftService,
WowUpService,
ChangeDetectorRef,
],
[MatDialog, ElectronService, WarcraftService, WowUpService, ChangeDetectorRef],
(
matDialog: MatDialog,
electronService: ElectronService,

View File

@@ -1,10 +1,4 @@
import {
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
} from "@angular/core";
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { MatSelectChange } from "@angular/material/select";
import { MatSlideToggleChange } from "@angular/material/slide-toggle";
@@ -50,33 +44,23 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
) {
this.addonChannelInfos = this.getAddonChannelInfos();
const warcraftProductSubscription = this._warcraftService.products$.subscribe(
(products) => {
const product = products.find((p) => p.clientType === this.clientType);
if (product) {
this.clientLocation = product.location;
this._cdRef.detectChanges();
}
const warcraftProductSubscription = this._warcraftService.products$.subscribe((products) => {
const product = products.find((p) => p.clientType === this.clientType);
if (product) {
this.clientLocation = product.location;
this._cdRef.detectChanges();
}
);
});
this.subscriptions.push(warcraftProductSubscription);
}
ngOnInit(): void {
this.selectedAddonChannelType = this._wowupService.getDefaultAddonChannel(
this.clientType
);
this.clientAutoUpdate = this._wowupService.getDefaultAutoUpdate(
this.clientType
);
this.clientTypeName = getEnumName(WowClientType, this.clientType);
this.clientFolderName = this._warcraftService.getClientFolderName(
this.clientType
);
this.clientLocation = this._warcraftService.getClientLocation(
this.clientType
);
this.selectedAddonChannelType = this._wowupService.getDefaultAddonChannel(this.clientType);
this.clientAutoUpdate = this._wowupService.getDefaultAutoUpdate(this.clientType);
this.clientTypeName = `COMMON.CLIENT_TYPES.${getEnumName(WowClientType, this.clientType).toUpperCase()}`;
this.clientFolderName = this._warcraftService.getClientFolderName(this.clientType);
this.clientLocation = this._warcraftService.getClientLocation(this.clientType);
}
ngOnDestroy(): void {
@@ -108,14 +92,10 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
});
}
private async selectWowClientPath(
clientType: WowClientType
): Promise<string> {
const dialogResult = await this._electronService.remote.dialog.showOpenDialog(
{
properties: ["openDirectory"],
}
);
private async selectWowClientPath(clientType: WowClientType): Promise<string> {
const dialogResult = await this._electronService.remote.dialog.showOpenDialog({
properties: ["openDirectory"],
});
if (dialogResult.canceled) {
return "";
@@ -129,28 +109,15 @@ export class WowClientOptionsComponent implements OnInit, OnDestroy {
console.log("dialogResult", selectedPath);
const clientRelativePath = this._warcraftService.getClientRelativePath(
clientType,
selectedPath
);
const clientRelativePath = this._warcraftService.getClientRelativePath(clientType, selectedPath);
if (
this._warcraftService.setWowFolderPath(clientType, clientRelativePath)
) {
if (this._warcraftService.setWowFolderPath(clientType, clientRelativePath)) {
return clientRelativePath;
}
const clientFolderName = this._warcraftService.getClientFolderName(
clientType
);
const clientExecutableName = this._warcraftService.getExecutableName(
clientType
);
const clientExecutablePath = path.join(
clientRelativePath,
clientFolderName,
clientExecutableName
);
const clientFolderName = this._warcraftService.getClientFolderName(clientType);
const clientExecutableName = this._warcraftService.getExecutableName(clientType);
const clientExecutablePath = path.join(clientRelativePath, clientFolderName, clientExecutableName);
const dialogRef = this._dialog.open(AlertDialogComponent, {
data: {
title: `Alert`,

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ExternalLinkDirective } from "./external-link.directive";
describe("ExternalLinkDirective", () => {
let directive: ExternalLinkDirective;
let fixture: ComponentFixture<ExternalLinkDirective>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ExternalLinkDirective],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ExternalLinkDirective],
}).compileComponents();
})
);
it("should create an instance", () => {
fixture = TestBed.createComponent(ExternalLinkDirective);

View File

@@ -1,8 +1,4 @@
import {
HttpHandler,
HttpInterceptor,
HttpRequest,
} from "@angular/common/http";
import { HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
@Injectable()

View File

@@ -0,0 +1,5 @@
// Attempt to make a simple container for localizing select items
export interface SelectItem<T> {
display: string;
value: T;
}

View File

@@ -0,0 +1,4 @@
export interface Theme {
display: string;
class: string;
}

View File

@@ -3,11 +3,14 @@
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div class="theme-logo">
<img [src]="getLogoPath()" />
</div>
<div class="about">
<div class="header">
<div class="header text-1">
<img class="logo" src="assets/wowup_logo_512np.png" />
<h2>{{ "PAGES.ABOUT.TITLE" | translate }}</h2>
<div class="version">v{{ version }}</div>
<div class="version text-2">v{{ version }}</div>
<div class="link-container">
<a appExternalLink mat-stroked-button href="https://wowup.io" appUserActionTracker category="About"
action="ViewWebsite">
@@ -15,14 +18,14 @@
</a>
</div>
</div>
<div class="changelog-container">
<div class="changelog-container text-1">
<h2>
{{ "PAGES.ABOUT.CHANGE_LOG_SECTION_LABEL" | translate }}
</h2>
<ul class="change-log-list">
<li class="changelog" *ngFor="let cl of changeLogs">
<div class="version mat-subheading-2">{{ cl.Version }}</div>
<pre *ngIf="cl.Description" class="description selectable">{{ cl.Description }}</pre>
<li class="changelog bg-secondary-4 border-primary" *ngFor="let cl of changeLogs">
<div class="version mat-subheading-2">{{ cl.Version }}</div>
<pre *ngIf="cl.Description" class="description selectable">{{ cl.Description }}</pre>
<pre *ngIf="cl.changes" class="description selectable">{{ formatChanges(cl) }}</pre>
</li>
</ul>

View File

@@ -6,6 +6,22 @@
}
}
.theme-logo {
opacity: 0.02;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
img {
height: 100vh;
}
}
.about-container {
display: flex;
justify-content: center;
@@ -15,7 +31,6 @@
.header {
text-align: center;
color: $white-1;
.logo {
width: 150px;
@@ -25,10 +40,6 @@
margin-bottom: 0;
}
.version {
color: $white-3;
}
.link-container {
margin-top: 1em;
}
@@ -40,7 +51,6 @@
.changelog-container {
padding: 1em;
color: $white-1;
.change-log-list {
padding-left: 0;
@@ -49,14 +59,9 @@
.changelog {
list-style-type: none;
margin: 1em 0;
border-left: 3px solid $purple-1;
border-left-width: 3px;
border-left-style: solid;
padding: 0.5em 1em 1em 1em;
background-color: $dark-4;
.version {
// font-size: 1em;
// font-weight: bold;
}
.description {
margin: 0;

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { AboutComponent } from "./about.component";
describe("AboutComponent", () => {
let component: AboutComponent;
let fixture: ComponentFixture<AboutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AboutComponent],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AboutComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(AboutComponent);

View File

@@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
import {
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
HORDE_THEME,
} from "common/constants";
import { remote } from "electron";
import * as ChangeLogJson from "../../../assets/changelog.json";
import { ChangeLog } from "../../models/wowup/change-log";
@@ -17,7 +25,7 @@ export class AboutComponent implements OnInit {
public version = "";
public changeLogs: ChangeLog[] = ChangeLogJson.ChangeLogs;
constructor(private wowup: WowUpService, public electronService: ElectronService) {}
constructor(private wowUpService: WowUpService, public electronService: ElectronService) {}
ngOnInit(): void {
this.version = remote.app.getVersion();
@@ -26,4 +34,22 @@ export class AboutComponent implements OnInit {
formatChanges(changeLog: ChangeLog): string {
return changeLog.changes.join("\n");
}
getLogoPath() {
switch (this.wowUpService.currentTheme) {
case HORDE_THEME:
return "assets/images/horde-1.png";
case HORDE_LIGHT_THEME:
return "assets/images/horde-dark-1.png";
case ALLIANCE_THEME:
return "assets/images/alliance-1.png";
case ALLIANCE_LIGHT_THEME:
return "assets/images/alliance-dark-1.png";
case DEFAULT_LIGHT_THEME:
return "assets/images/wowup-dark-1.png";
case DEFAULT_THEME:
default:
return "assets/images/wowup-white-1.png";
}
}
}

View File

@@ -6,15 +6,11 @@
<div class="control-container">
<div class="select-container">
<mat-form-field>
<mat-label>{{
"PAGES.GET_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate
}}</mat-label>
<mat-label>{{ "PAGES.GET_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate }}</mat-label>
<mat-select class="select" [(value)]="selectedClient" (selectionChange)="onClientChange()"
[disabled]="isBusy === true">
<mat-option [value]="clientType" *ngFor="
let clientType of warcraftService.installedClientTypes$ | async
">
{{ warcraftService.getClientDisplayName(clientType) }}
<mat-option [value]="clientType.value" *ngFor="let clientType of warcraftService.installedClientTypesSelectItems$ | async">
{{ clientType.display | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@@ -22,9 +18,7 @@
<div class="right-container">
<div class="search-container">
<mat-form-field>
<mat-label>{{
"PAGES.GET_ADDONS.SEARCH_LABEL" | translate
}}</mat-label>
<mat-label>{{ "PAGES.GET_ADDONS.SEARCH_LABEL" | translate }}</mat-label>
<input matInput type="text" [(ngModel)]="query" (keyup.enter)="onSearch()" />
<button mat-button color="accent" *ngIf="query" matSuffix mat-icon-button aria-label="Clear"
(click)="onClearSearch()">
@@ -49,7 +43,7 @@
<app-progress-spinner> </app-progress-spinner>
</div>
<div *ngIf="isBusy === false && dataSource.data.length === 0" class="no-addons-container flex-grow-1">
<div *ngIf="isBusy === false && dataSource.data.length === 0" class="no-addons-container flex-grow-1 text-1">
<h1>{{ "COMMON.SEARCH.NO_ADDONS" | translate }}</h1>
</div>
@@ -69,9 +63,7 @@
<ng-container matColumnDef="downloadCount">
<th mat-header-cell mat-sort-header *matHeaderCellDef>
{{
"PAGES.GET_ADDONS.TABLE.DOWNLOAD_COUNT_COLUMN_HEADER" | translate
}}
{{ "PAGES.GET_ADDONS.TABLE.DOWNLOAD_COUNT_COLUMN_HEADER" | translate }}
</th>
<td mat-cell *matCellDef="let element">
{{ element.downloadCount | downloadCount }}
@@ -84,13 +76,10 @@
</th>
<td mat-cell *matCellDef="let element">
<ng-template let-releasedAt [ngTemplateOutletContext]="{
$implicit:
element
| getAddonListItemFileProp: 'releaseDate':defaultAddonChannel
$implicit: element | getAddonListItemFileProp: 'releaseDate':defaultAddonChannel
}" [ngTemplateOutlet]="t" #t>
<div [matTooltip]="
'COMMON.DATES.DATETIME_SHORT' | translate: { d: releasedAt }
" matTooltipPosition="above" matTooltipShowDelay="500">
<div [matTooltip]="'COMMON.DATES.DATETIME_SHORT' | translate: { d: releasedAt }" matTooltipPosition="above"
matTooltipShowDelay="500">
{{ releasedAt | relativeDuration }}
</div>
</ng-template>
@@ -129,10 +118,10 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="hover-bg-secondary-2"
(contextmenu)="onHeaderContext($event); $event.preventDefault()"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns; let i = index" (dblclick)="onDoubleClickRow(row)">
</tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns; let i = index" class="hover-bg-secondary-2"
(dblclick)="onDoubleClickRow(row)"></tr>
</table>
</div>
</div>

View File

@@ -44,7 +44,6 @@
.no-addons-container {
display: flex;
color: $white-1;
justify-content: center;
flex-direction: column;
align-items: center;
@@ -64,34 +63,6 @@
width: 100%;
}
tr {
&:hover {
background-color: $dark-1;
}
}
.addon-logo-container {
background-color: $dark-2;
width: 40px;
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 0.5em;
flex-shrink: 0;
.addon-logo {
width: 100%;
}
}
.addon-title {
font-weight: 400;
font-size: 1.1em;
word-break: break-all;
white-space: pre-wrap;
}
.cell-padding {
padding-right: 1em;
}

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { GetAddonsComponent } from "./get-addons.component";
describe("GetAddonsComponent", () => {
let component: GetAddonsComponent;
let fixture: ComponentFixture<GetAddonsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GetAddonsComponent],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GetAddonsComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(GetAddonsComponent);

View File

@@ -138,7 +138,7 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
if (this._automaticSort) {
return;
}
if (this.table) {
this.table.nativeElement.scrollIntoView({ behavior: "smooth" });
}

View File

@@ -1,4 +1,4 @@
<div class="page-container" [ngClass]="{ linux: electronService.isLinux }">
<div class="page-container bg-secondary-3" [ngClass]="{ linux: electronService.isLinux }">
<div class="tabs">
<mat-tab-group
mat-align-tabs="center"
@@ -8,22 +8,13 @@
[(selectedIndex)]="selectedIndex"
(selectedIndexChange)="onSelectedIndexChange($event)"
>
<mat-tab
[disabled]="hasWowClient !== true"
[label]="'PAGES.HOME.MY_ADDONS_TAB_TITLE' | translate"
>
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.MY_ADDONS_TAB_TITLE' | translate">
<app-my-addons [tabIndex]="0"></app-my-addons>
</mat-tab>
<mat-tab
[disabled]="hasWowClient !== true"
[label]="'PAGES.HOME.GET_ADDONS_TAB_TITLE' | translate"
>
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.GET_ADDONS_TAB_TITLE' | translate">
<app-get-addons [tabIndex]="1"></app-get-addons>
</mat-tab>
<mat-tab
[disabled]="hasWowClient !== true"
[label]="'PAGES.HOME.ABOUT_TAB_TITLE' | translate"
>
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.ABOUT_TAB_TITLE' | translate">
<app-about [tabIndex]="2"></app-about>
</mat-tab>
<mat-tab [label]="'PAGES.HOME.OPTIONS_TAB_TITLE' | translate">

View File

@@ -1,7 +1,5 @@
@import "../../../custom-theme.scss";
:host {
}
.app-logo {
position: absolute;
width: 40px;

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { TranslateModule } from "@ngx-translate/core";
import { HomeComponent } from "./home.component";
@@ -7,12 +7,14 @@ describe("HomeComponent", () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HomeComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HomeComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
@@ -24,10 +26,11 @@ describe("HomeComponent", () => {
expect(component).toBeTruthy();
});
it("should render title in a h1 tag", async(() => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector("h1").textContent).toContain(
"PAGES.HOME.TITLE"
);
}));
it(
"should render title in a h1 tag",
waitForAsync(() => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector("h1").textContent).toContain("PAGES.HOME.TITLE");
})
);
});

View File

@@ -64,13 +64,7 @@ import { HomeComponent } from "./home.component";
OptionsDebugSectionComponent,
OptionsAddonSectionComponent,
],
imports: [
CommonModule,
SharedModule,
HomeRoutingModule,
MatModule,
DirectiveModule,
],
imports: [CommonModule, SharedModule, HomeRoutingModule, MatModule, DirectiveModule],
providers: [DatePipe, GetAddonListItemFilePropPipe],
})
export class HomeModule {}

View File

@@ -1,83 +0,0 @@
.control-container {
display: flex;
flex-direction: row;
padding: 0 1em 0 1em;
}
.control-container .select-container {
padding-top: 1em;
flex-grow: 1;
}
.control-container .button-container {
display: flex;
flex-direction: row;
align-items: center;
}
.control-container .button-container button:not(:last-child) {
margin-right: 0.5em;
}
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
.table-container {
overflow: auto;
}
.table-container table {
width: 100%;
}
.table-container tr:hover {
background-color: #666666;
}
.table-container .addon-logo-container {
background-color: #555555;
width: 40px;
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 0.5em;
flex-shrink: 0;
}
.table-container .addon-logo-container .addon-logo {
width: 100%;
}
.table-container .addon-title {
font-weight: bold;
font-size: 1.1em;
word-break: break-all;
white-space: pre-wrap;
}
.table-container .status-column {
display: flex;
justify-content: center;
width: 130px;
align-items: center;
}
.table-container .status-column .auto-update-icon {
margin-right: 0.5em;
}
.table-container .status-column .progress-text {
text-align: center;
}
.table-container .status-column .addon-progress {
width: 110px;
}
.author-column {
width: 100px;
}
.provider-column {
min-width: 60px;
}
.column-menu {
padding: 0.5em;
color: #ffffff;
}
.column-menu p {
margin-bottom: 0.5em;
}

View File

@@ -3,14 +3,15 @@
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div class="control-container">
<div class="control-container bg-secondary-3">
<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)]="selectedClient" (selectionChange)="onClientChange()"
[disabled]="enableControls === false">
<mat-option [value]="clientType" *ngFor="let clientType of warcraftService.installedClientTypes$ | async">
{{ warcraftService.getClientDisplayName(clientType) }}
<mat-option [value]="clientType.value"
*ngFor="let clientType of warcraftService.installedClientTypesSelectItems$ | async">
{{ clientType.display | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@@ -52,11 +53,11 @@
<app-progress-spinner [message]="spinnerMessage"> </app-progress-spinner>
</div>
<div *ngIf="isBusy === false && sortedListItems.length === 0" class="no-addons-container flex-grow-1">
<div *ngIf="isBusy === false && sortedListItems.length === 0" class="no-addons-container text-1 flex-grow-1">
<h1>{{ "COMMON.SEARCH.NO_ADDONS" | translate }}</h1>
</div>
<div class="table-container flex-grow-1" [hidden]="isBusy === true || sortedListItems.length === 0">
<div class="table-container bg-secondary-2 flex-grow-1" [hidden]="isBusy === true || sortedListItems.length === 0">
<table #table mat-table matSort (matSortChange)="onSortChange()" [dataSource]="dataSource"
[matSortActive]="activeSort" [matSortDirection]="activeSortDirection" class="mat-elevation-z8">
<ng-container matColumnDef="addon.name">
@@ -160,13 +161,14 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="hover-bg-secondary-2"
(contextmenu)="onHeaderContext($event); $event.preventDefault()"></tr>
<tr mat-row *matRowDef="let row; let i = index; columns: displayedColumns" tabindex="0"
[ngClass]="{ 'selected-row': row.selected }" (click)="onRowClicked($event, row, i)"
(dblclick)="openDetailDialog(row)" (contextmenu)="onCellContext($event, row)"
(keydown.control.a)="selectAllRows($event)" (keydown.meta.a)="selectAllRows($event)"></tr>
class="hover-bg-secondary-2" [ngClass]="{ 'bg-secondary-4': row.selected }"
(click)="onRowClicked($event, row, i)" (dblclick)="openDetailDialog(row)"
(contextmenu)="onCellContext($event, row)" (keydown.control.a)="selectAllRows($event)"
(keydown.meta.a)="selectAllRows($event)"></tr>
</table>
</div>
</div>

View File

@@ -53,22 +53,13 @@
}
.table-container {
// height: calc(100% - 75px);
background-color: $dark-2;
overflow: auto;
table {
width: 100%;
}
tr {
&:hover {
background-color: $dark-1;
}
}
.addon-logo-container {
background-color: $dark-2;
width: 40px;
height: 40px;
display: flex;
@@ -120,10 +111,6 @@
}
}
.selected-row {
background: $dark-4;
}
.addon-provider {
display: flex;
align-items: center;

View File

@@ -1,5 +1,6 @@
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@@ -19,8 +20,8 @@ import { MatTableDataSource } from "@angular/material/table";
import { TranslateService } from "@ngx-translate/core";
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
import * as _ from "lodash";
import { BehaviorSubject, from, Subscription } from "rxjs";
import { map } from "rxjs/operators";
import { BehaviorSubject, from, Observable, Subscription } from "rxjs";
import { filter, map } from "rxjs/operators";
import { AddonViewModel } from "../../business-objects/my-addon-list-item";
import { AddonDetailComponent, AddonDetailModel } from "../../components/addon-detail/addon-detail.component";
import { ConfirmDialogComponent } from "../../components/confirm-dialog/confirm-dialog.component";
@@ -28,6 +29,7 @@ import { Addon } from "../../entities/addon";
import { WowClientType } from "../../models/warcraft/wow-client-type";
import { AddonInstallState } from "../../models/wowup/addon-install-state";
import { ColumnState } from "../../models/wowup/column-state";
import { SelectItem } from "../../models/wowup/select-item";
import { ElectronService } from "../../services";
import { AddonService } from "../../services/addons/addon.service";
import { SessionService } from "../../services/session/session.service";
@@ -36,7 +38,6 @@ import { WowUpService } from "../../services/wowup/wowup.service";
import { getEnumName } from "../../utils/enum.utils";
import { stringIncludes } from "../../utils/string.utils";
import { WowUpAddonService } from "../../services/wowup/wowup-addon.service";
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
@Component({
selector: "app-my-addons",
@@ -44,16 +45,16 @@ import { AddonChannelType } from "app/models/wowup/addon-channel-type";
styleUrls: ["./my-addons.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyAddonsComponent implements OnInit, OnDestroy {
export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
@Input("tabIndex") tabIndex: number;
@ViewChild("addonContextMenuTrigger") contextMenu: MatMenuTrigger;
@ViewChild("addonMultiContextMenuTrigger") multiContextMenu: MatMenuTrigger;
@ViewChild("columnContextMenuTrigger") columnContextMenu: MatMenuTrigger;
@ViewChild("updateAllContextMenuTrigger")
@ViewChild("addonContextMenuTrigger", { static: false }) contextMenu: MatMenuTrigger;
@ViewChild("addonMultiContextMenuTrigger", { static: false }) multiContextMenu: MatMenuTrigger;
@ViewChild("columnContextMenuTrigger", { static: false }) columnContextMenu: MatMenuTrigger;
@ViewChild("updateAllContextMenuTrigger", { static: false })
updateAllContextMenu: MatMenuTrigger;
@ViewChild(MatSort, { static: false }) sort: MatSort;
@ViewChild("table", { read: ElementRef }) table: ElementRef;
@ViewChild("table", { static: false, read: ElementRef }) table: ElementRef;
private readonly _displayAddonsSrc = new BehaviorSubject<AddonViewModel[]>([]);
@@ -193,6 +194,13 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
this.subscriptions.forEach((sub) => sub.unsubscribe());
}
public ngAfterViewInit(): void {
this._sessionService.autoUpdateComplete$.subscribe(() => {
this._cdRef.markForCheck();
this.onRefresh();
});
}
public isLatestUpdateColumnVisible(): boolean {
return this.columns.find((column) => column.name === "addon.latestVersion").visible;
}
@@ -637,7 +645,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
private loadAddons(clientType: WowClientType, rescan = false) {
this.isBusy = true;
this.enableControls = false;
this._cdRef.detectChanges();
console.log("Load-addons", clientType);

View File

@@ -7,7 +7,10 @@
}"
>
<div class="nav-container">
<div class="nav-item-container">
<div class="theme-logo">
<img [src]="getLogoPath()" />
</div>
<div class="nav-item-container" [ngClass]="[this.wowUpService.currentTheme]">
<div class="nav-item-list-wrap">
<div class="nav-item-list">
<mat-action-list>
@@ -27,14 +30,10 @@
</div>
</div>
</div>
<div class="nav-content-container">
<div class="nav-content-container bg-secondary-2 text-1">
<div class="nav-content-wrap">
<div class="nav-content">
<mat-tab-group
class="no-tabs content-tab"
animationDuration="0ms"
[(selectedIndex)]="optionTabIndex"
>
<mat-tab-group class="no-tabs content-tab" animationDuration="0ms" [(selectedIndex)]="optionTabIndex">
<mat-tab label="WorldOfWarcraft" class="tab">
<app-options-wow-section></app-options-wow-section>
</mat-tab>

View File

@@ -10,13 +10,29 @@
}
}
.theme-logo {
content: "";
opacity: 0.02;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
img {
height: 100vh;
}
}
.nav-container {
height: 100%;
display: flex;
width: 100%;
.nav-item-container {
background: $dark-3;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
@@ -27,6 +43,7 @@
z-index: 1;
-webkit-box-sizing: border-box;
box-sizing: border-box;
// overflow: hidden;
.nav-item-list-wrap {
flex: 1 0 auto;
@@ -49,8 +66,6 @@
}
}
.nav-content-container {
background: $dark-2;
color: $white-1;
position: relative;
display: flex;
-webkit-box-flex: 1;
@@ -83,6 +98,7 @@
flex: 1 1 auto;
min-height: 0;
-webkit-box-flex: 1;
z-index: 2;
.content-tab {
position: relative;
@@ -97,84 +113,3 @@
}
}
.options {
max-width: 900px;
width: 100%;
height: 100%;
.option-card {
margin-top: 1em;
background-color: $dark-2;
&:last-child {
margin-bottom: 1em;
}
}
.title {
font-size: 1.5em;
flex-grow: 1;
}
.setting-section {
background-color: $dark-4;
}
.folder-input {
margin-right: 1em;
}
}
.row {
display: flex;
flex-direction: row;
align-items: center;
// margin: 1em 0;
padding: 0.5em;
&.np {
padding: 0;
}
&.sub {
padding-left: 2em;
}
}
.col {
padding: 0.5em;
}
hr {
border: 1px solid $white-4;
}
p {
margin: 0;
padding: 0;
}
.hint {
color: $white-3;
}
.hover {
&:hover {
cursor: pointer;
}
}
.grow {
flex-grow: 1;
}
.no-shrink {
flex-shrink: 0;
}
table {
width: 100%;
}
section {
color: $white-1;
padding: 1em;
.select-button {
flex-shrink: 0;
}
small {
color: $white-2;
}
}

View File

@@ -1,9 +1,12 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
import {
ChangeDetectionStrategy,
Component,
Input,
OnInit,
} from "@angular/core";
ALLIANCE_LIGHT_THEME,
ALLIANCE_THEME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEME,
HORDE_LIGHT_THEME,
HORDE_THEME,
} from "common/constants";
import { ElectronService } from "../../services";
import { WowUpService } from "../../services/wowup/wowup.service";
@@ -18,10 +21,25 @@ export class OptionsComponent implements OnInit {
public optionTabIndex = 0;
constructor(
public wowupService: WowUpService,
public electronService: ElectronService
) {}
constructor(public wowUpService: WowUpService, public electronService: ElectronService) {}
ngOnInit(): void {}
getLogoPath() {
switch (this.wowUpService.currentTheme) {
case HORDE_THEME:
return "assets/images/horde-1.png";
case HORDE_LIGHT_THEME:
return "assets/images/horde-dark-1.png";
case ALLIANCE_THEME:
return "assets/images/alliance-1.png";
case ALLIANCE_LIGHT_THEME:
return "assets/images/alliance-dark-1.png";
case DEFAULT_LIGHT_THEME:
return "assets/images/wowup-dark-1.png";
case DEFAULT_THEME:
default:
return "assets/images/wowup-white-1.png";
}
}
}

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { DownloadCountPipe } from "./download-count.pipe";
describe("DownloadCountPipe", () => {
let directive: DownloadCountPipe;
let fixture: ComponentFixture<DownloadCountPipe>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DownloadCountPipe],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DownloadCountPipe],
}).compileComponents();
})
);
it("should create an instance", () => {
fixture = TestBed.createComponent(DownloadCountPipe);

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { GetAddonListItemFilePropPipe } from "./get-addon-list-item-file-prop.pipe";
describe("GetAddonListItemFilePropPipe", () => {
let directive: GetAddonListItemFilePropPipe;
let fixture: ComponentFixture<GetAddonListItemFilePropPipe>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GetAddonListItemFilePropPipe],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GetAddonListItemFilePropPipe],
}).compileComponents();
})
);
it("should create an instance", () => {
fixture = TestBed.createComponent(GetAddonListItemFilePropPipe);

View File

@@ -8,11 +8,7 @@ import * as SearchResults from "../utils/search-result.utils";
name: "getAddonListItemFileProp",
})
export class GetAddonListItemFilePropPipe implements PipeTransform {
transform(
item: GetAddonListItem,
prop: string,
channel: AddonChannelType
): any {
transform(item: GetAddonListItem, prop: string, channel: AddonChannelType): any {
let file = SearchResults.getLatestFile(item.searchResult, channel);
if (!file) {
file = _.first(_.orderBy(item.searchResult.files, "releaseDate", "desc"));

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { InterfaceFormatPipe } from "./interface-format.pipe";
describe("InterfaceFormatPipe", () => {
let directive: InterfaceFormatPipe;
let fixture: ComponentFixture<InterfaceFormatPipe>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [InterfaceFormatPipe],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [InterfaceFormatPipe],
}).compileComponents();
})
);
it("should create an instance", () => {
fixture = TestBed.createComponent(InterfaceFormatPipe);

View File

@@ -5,12 +5,9 @@ import { RelativeDurationPipe } from "./relative-duration-pipe";
describe("RelativeDurationPipe", () => {
it("create an instance", () => {
inject(
[DatePipe, TranslateService],
(datePipe: DatePipe, translateService: TranslateService) => {
const pipe = new RelativeDurationPipe(translateService);
expect(pipe).toBeTruthy();
}
);
inject([DatePipe, TranslateService], (datePipe: DatePipe, translateService: TranslateService) => {
const pipe = new RelativeDurationPipe(translateService);
expect(pipe).toBeTruthy();
});
});
});

View File

@@ -72,7 +72,10 @@ export class AddonService {
var searchTasks = this.getEnabledAddonProviders().map((p) => p.searchByQuery(query, clientType));
var searchResults = await Promise.all(searchTasks);
await this._analyticsService.trackUserAction("addons", "search", `${clientType}|${query}`);
await this._analyticsService.trackAction("addon-search", {
clientType: getEnumName(WowClientType, clientType),
query,
});
const flatResults = searchResults.flat(1);
@@ -270,7 +273,11 @@ export class AddonService {
const actionLabel = `${getEnumName(WowClientType, addon.clientType)}|${addon.providerName}|${addon.externalId}|${
addon.name
}`;
this._analyticsService.trackUserAction("addons", "install_by_id", actionLabel);
this._analyticsService.trackAction("install-addon", {
clientType: getEnumName(WowClientType, addon.clientType),
provider: addon.providerName,
addon: actionLabel,
});
await this.installDependencies(addon, onUpdate);

View File

@@ -1,5 +1,6 @@
import { HttpClient, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { ApplicationInsights } from "@microsoft/applicationinsights-web";
import { BehaviorSubject } from "rxjs";
import { v4 as uuidv4 } from "uuid";
import { AppConfig } from "../../../environments/environment";
@@ -16,6 +17,8 @@ export class AnalyticsService {
private readonly _appVersion: string;
private readonly _telemetryEnabledSrc = new BehaviorSubject(false);
private _insights?: ApplicationInsights;
public readonly telemetryPromptUsedKey = "telemetry_prompt_sent";
public readonly telemetryEnabledKey = "telemetry_enabled";
public readonly telemetryEnabled$ = this._telemetryEnabledSrc.asObservable();
@@ -25,19 +28,24 @@ export class AnalyticsService {
}
public get shouldPromptTelemetry() {
return (
this._preferenceStorageService.get(this.telemetryEnabledKey) === undefined
);
return this._preferenceStorageService.get(this.telemetryEnabledKey) === undefined;
}
public get telemetryEnabled() {
const preference = this._preferenceStorageService.findByKey(
this.telemetryEnabledKey
);
return preference === true.toString();
const preference = this._preferenceStorageService.findByKey(this.telemetryEnabledKey);
const value = preference === true.toString();
this.configureAppInsights(value);
return value;
}
public set telemetryEnabled(value: boolean) {
if (this._insights) {
this._insights.appInsights.config.disableTelemetry = value;
console.debug("disableTelemetry", this._insights.appInsights.config.disableTelemetry);
}
this._preferenceStorageService.set(this.telemetryEnabledKey, value);
this._telemetryEnabledSrc.next(value);
}
@@ -54,23 +62,41 @@ export class AnalyticsService {
}
public async trackStartup() {
await this.track((params) => {
params.set("t", "pageview");
params.set("dp", "app/startup");
});
//Record an event
await this.track2("app-startup");
// await this.track((params) => {
// params.set("t", "pageview");
// params.set("dp", "app/startup");
// });
}
public async trackUserAction(
category: string,
action: string,
label: string = null
) {
await this.track((params) => {
params.set("t", "event");
params.set("ec", category);
params.set("ea", action);
params.set("el", label);
public async trackAction(name: string, properties: object = undefined) {
await this.track2(name, properties);
}
public async trackUserAction(category: string, action: string, label: string = null) {
await this.track2(category, {
action,
label,
});
// await this.track((params) => {
// params.set("t", "event");
// params.set("ec", category);
// params.set("ea", action);
// params.set("el", label);
// });
}
private async track2(name: string, properties: object = undefined) {
if (!this.telemetryEnabled) {
return;
}
this._insights?.trackEvent({ name, properties });
console.debug("Track", name);
}
private async track(action: (params: HttpParams) => void = undefined) {
@@ -109,9 +135,7 @@ export class AnalyticsService {
}
private loadInstallId() {
let installId = this._preferenceStorageService.findByKey(
this.installIdPreferenceKey
);
let installId = this._preferenceStorageService.findByKey(this.installIdPreferenceKey);
if (installId) {
return installId;
}
@@ -121,4 +145,25 @@ export class AnalyticsService {
return installId;
}
private configureAppInsights(enable: boolean) {
if (!enable || this._insights) {
return;
}
this._insights = new ApplicationInsights({
config: {
instrumentationKey: AppConfig.azure.applicationInsightsKey,
},
});
this._insights.loadAppInsights();
this._insights.trackPageView();
// If telemetry is off, dont let it track anything
this._insights.addTelemetryInitializer((envelop) => {
if (!this.telemetryEnabled) {
return false;
}
});
}
}

View File

@@ -28,10 +28,7 @@ export class DownloadSevice {
const eventHandler = (_evt: any, arg: DownloadStatus) => {
if (arg.type !== DownloadStatusType.Progress) {
this._electronService.ipcRenderer.off(
request.responseKey,
eventHandler
);
this._electronService.ipcRenderer.off(request.responseKey, eventHandler);
}
switch (arg.type) {

View File

@@ -195,6 +195,18 @@ export class ElectronService {
return new Notification(title, options);
}
public isHandlingProtocol(protocol: string): boolean {
return this.remote.app.isDefaultProtocolClient(protocol);
}
public setHandleProtocol(protocol: string, enable: boolean) {
if (enable) {
return this.remote.app.setAsDefaultProtocolClient(protocol);
} else {
return this.remote.app.removeAsDefaultProtocolClient(protocol);
}
}
public async sendIpcValueMessage<TIN, TOUT>(channel: string, value: TIN): Promise<TOUT> {
const request: ValueRequest<TIN> = {
value,

View File

@@ -25,24 +25,15 @@ export class FileService {
constructor(private _electronService: ElectronService) {}
public async getAssetFilePath(fileName: string) {
return await this._electronService.ipcRenderer.invoke(
GET_ASSET_FILE_PATH,
fileName
);
return await this._electronService.ipcRenderer.invoke(GET_ASSET_FILE_PATH, fileName);
}
public async createDirectory(directoryPath: string) {
return await this._electronService.ipcRenderer.invoke(
CREATE_DIRECTORY_CHANNEL,
directoryPath
);
return await this._electronService.ipcRenderer.invoke(CREATE_DIRECTORY_CHANNEL, directoryPath);
}
public async showDirectory(sourceDir: string) {
return await this._electronService.ipcRenderer.invoke(
SHOW_DIRECTORY,
sourceDir
);
return await this._electronService.ipcRenderer.invoke(SHOW_DIRECTORY, sourceDir);
}
public async pathExists(sourcePath: string): Promise<boolean> {
@@ -50,10 +41,7 @@ export class FileService {
return Promise.resolve(false);
}
return await this._electronService.ipcRenderer.invoke(
PATH_EXISTS_CHANNEL,
sourcePath
);
return await this._electronService.ipcRenderer.invoke(PATH_EXISTS_CHANNEL, sourcePath);
}
/**
@@ -64,19 +52,13 @@ export class FileService {
throw new Error("remove sourcePath required");
}
return await this._electronService.ipcRenderer.invoke(
DELETE_DIRECTORY_CHANNEL,
sourcePath
);
return await this._electronService.ipcRenderer.invoke(DELETE_DIRECTORY_CHANNEL, sourcePath);
}
/**
* Copy a file or folder
*/
public async copy(
sourceFilePath: string,
destinationFilePath: string
): Promise<string> {
public async copy(sourceFilePath: string, destinationFilePath: string): Promise<string> {
const request: CopyFileRequest = {
destinationFilePath,
sourceFilePath,
@@ -96,33 +78,21 @@ export class FileService {
}
public async readFile(sourcePath: string): Promise<string> {
return await this._electronService.ipcRenderer.invoke(
READ_FILE_CHANNEL,
sourcePath
);
return await this._electronService.ipcRenderer.invoke(READ_FILE_CHANNEL, sourcePath);
}
public async writeFile(sourcePath: string, contents: string): Promise<string> {
return await this._electronService.ipcRenderer.invoke(
WRITE_FILE_CHANNEL,
sourcePath,
contents,
);
return await this._electronService.ipcRenderer.invoke(WRITE_FILE_CHANNEL, sourcePath, contents);
}
public async listDirectories(sourcePath: string): Promise<string[]> {
return await this._electronService.ipcRenderer.invoke(
LIST_DIRECTORIES_CHANNEL,
sourcePath
);
return await this._electronService.ipcRenderer.invoke(LIST_DIRECTORIES_CHANNEL, sourcePath);
}
public listEntries(sourcePath: string, filter: string) {
const globFilter = globrex(filter);
return fs
.readdirSync(sourcePath, { withFileTypes: true })
.filter((entry) => !!globFilter.regex.test(entry.name));
return fs.readdirSync(sourcePath, { withFileTypes: true }).filter((entry) => !!globFilter.regex.test(entry.name));
}
public listFiles(sourcePath: string, filter: string) {
@@ -134,10 +104,7 @@ export class FileService {
.map((entry) => entry.name);
}
public async unzipFile(
zipFilePath: string,
outputFolder: string
): Promise<string> {
public async unzipFile(zipFilePath: string, outputFolder: string): Promise<string> {
console.log("unzipFile", zipFilePath);
const request: UnzipRequest = {
@@ -146,9 +113,6 @@ export class FileService {
responseKey: uuidv4(),
};
return await this._electronService.ipcRenderer.invoke(
UNZIP_FILE_CHANNEL,
request
);
return await this._electronService.ipcRenderer.invoke(UNZIP_FILE_CHANNEL, request);
}
}

View File

@@ -14,6 +14,7 @@ import {
faLink,
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faClock } from "@fortawesome/free-regular-svg-icons";
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
@Injectable({
providedIn: "root",
@@ -30,6 +31,8 @@ export class IconService {
this.addSvg(faClock);
this.addSvg(faBug);
this.addSvg(faLink);
this.addSvg(faDiscord);
this.addSvg(faGithub);
}
async addSvg(icon: IconDefinition) {

View File

@@ -9,25 +9,26 @@ import { WowUpService } from "../wowup/wowup.service";
providedIn: "root",
})
export class SessionService {
private readonly _selectedClientTypeSrc = new BehaviorSubject(
WowClientType.None
);
private readonly _selectedClientTypeSrc = new BehaviorSubject(WowClientType.None);
private readonly _pageContextTextSrc = new BehaviorSubject(""); // right side bar text, context to the screen
private readonly _statusTextSrc = new BehaviorSubject(""); // left side bar text, context to the app
private readonly _selectedHomeTabSrc = new BehaviorSubject(0);
private readonly _autoUpdateCompleteSrc = new BehaviorSubject(0);
public readonly selectedClientType$ = this._selectedClientTypeSrc.asObservable();
public readonly statusText$ = this._statusTextSrc.asObservable();
public readonly selectedHomeTab$ = this._selectedHomeTabSrc.asObservable();
public readonly pageContextText$ = this._pageContextTextSrc.asObservable();
public readonly autoUpdateComplete$ = this._autoUpdateCompleteSrc.asObservable();
constructor(
private _warcraftService: WarcraftService,
private _wowUpService: WowUpService
) {
constructor(private _warcraftService: WarcraftService, private _wowUpService: WowUpService) {
this.loadInitialClientType().pipe(first()).subscribe();
}
public autoUpdateComplete() {
this._autoUpdateCompleteSrc.next(Date.now());
}
public setContextText(tabIndex: number, text: string) {
if (tabIndex !== this._selectedHomeTabSrc.value) {
return;
@@ -62,15 +63,10 @@ export class SessionService {
console.log("installedClientTypes", installedClientTypes);
const lastSelectedType = this._wowUpService.lastSelectedClientType;
console.log("lastSelectedType", lastSelectedType);
let initialClientType = installedClientTypes.length
? installedClientTypes[0]
: WowClientType.None;
let initialClientType = installedClientTypes.length ? installedClientTypes[0] : WowClientType.None;
// If the user has no stored type, or the type is no longer found just set it.
if (
lastSelectedType == WowClientType.None ||
!installedClientTypes.some((ct) => ct == lastSelectedType)
) {
if (lastSelectedType == WowClientType.None || !installedClientTypes.some((ct) => ct == lastSelectedType)) {
this._wowUpService.lastSelectedClientType = initialClientType;
} else {
initialClientType = lastSelectedType;

View File

@@ -70,19 +70,13 @@ export class AddonStorageService {
return addons[0];
}
public getAllForClientType(
clientType: WowClientType,
validator?: (addon: Addon) => boolean
) {
public getAllForClientType(clientType: WowClientType, validator?: (addon: Addon) => boolean) {
const addons: Addon[] = [];
this.query((store) => {
for (const result of store) {
const addon = result[1] as Addon;
if (
addon.clientType === clientType &&
(!validator || validator(addon))
) {
if (addon.clientType === clientType && (!validator || validator(addon))) {
addons.push(addon);
}
}

View File

@@ -4,6 +4,7 @@ import * as path from "path";
import { BehaviorSubject, from } from "rxjs";
import { filter, map, switchMap } from "rxjs/operators";
import { ElectronService } from "..";
import { SelectItem } from "../../models/wowup/select-item";
import { InstalledProduct } from "../../models/warcraft/installed-product";
import { ProductDb } from "../../models/warcraft/product-db";
import { WowClientType } from "../../models/warcraft/wow-client-type";
@@ -40,19 +41,31 @@ const BETA_LOCATION_KEY = "wow_beta_location";
export class WarcraftService {
private readonly _impl: WarcraftServiceImpl;
private readonly _productsSrc = new BehaviorSubject<InstalledProduct[]>([]);
private readonly _installedClientTypesSrc = new BehaviorSubject<
WowClientType[] | undefined
>(undefined);
private readonly _installedClientTypesSrc = new BehaviorSubject<WowClientType[] | undefined>(undefined);
private _productDbPath = "";
public products$ = this._productsSrc.asObservable();
public productsReady$ = this.products$.pipe(
filter((products) => Array.isArray(products))
);
public productsReady$ = this.products$.pipe(filter((products) => Array.isArray(products)));
public installedClientTypes$ = this._installedClientTypesSrc.asObservable();
// Map the client types so that we can localize them
public installedClientTypesSelectItems$ = this._installedClientTypesSrc.pipe(
filter((clientTypes) => !!clientTypes),
map((clientTypes) => {
return clientTypes.map(
(ct): SelectItem<WowClientType> => {
const clientTypeName = getEnumName(WowClientType, ct).toUpperCase();
return {
display: `COMMON.CLIENT_TYPES.${clientTypeName}`,
value: ct,
};
}
);
})
);
constructor(
private _electronService: ElectronService,
private _fileService: FileService,
@@ -66,9 +79,7 @@ export class WarcraftService {
map((productDbPath) => (this._productDbPath = productDbPath)),
map(() => this.scanProducts()),
switchMap(() => from(this.getWowClientTypes())),
map((installedClientTypes) =>
this._installedClientTypesSrc.next(installedClientTypes)
)
map((installedClientTypes) => this._installedClientTypesSrc.next(installedClientTypes))
)
.subscribe();
}
@@ -123,9 +134,7 @@ export class WarcraftService {
public getProductLocation(clientType: WowClientType) {
const clientFolderName = this.getClientFolderName(clientType);
const clientLocation = this._productsSrc.value.find(
(product) => product.name === clientFolderName
);
const clientLocation = this._productsSrc.value.find((product) => product.name === clientFolderName);
return clientLocation?.location ?? "";
}
@@ -147,9 +156,7 @@ export class WarcraftService {
continue;
}
console.log(
`clientLocation ${clientLocation}, productLocation ${productLocation}`
);
console.log(`clientLocation ${clientLocation}, productLocation ${productLocation}`);
if (this.arePathsEqual(clientLocation, productLocation)) {
continue;
}
@@ -178,9 +185,7 @@ export class WarcraftService {
return addonFolders;
}
const directories = await this._fileService.listDirectories(
addonFolderPath
);
const directories = await this._fileService.listDirectories(addonFolderPath);
// const directories = files.filter(dirent => dirent.isDirectory()).map(dirent => dirent.name);
for (let i = 0; i < directories.length; i += 1) {
const dir = directories[i];
@@ -193,10 +198,7 @@ export class WarcraftService {
return addonFolders;
}
private async getAddonFolder(
addonFolderPath: string,
dir: string
): Promise<AddonFolder> {
private async getAddonFolder(addonFolderPath: string, dir: string): Promise<AddonFolder> {
try {
const dirPath = path.join(addonFolderPath, dir);
const dirFiles = fs.readdirSync(dirPath);
@@ -233,10 +235,7 @@ export class WarcraftService {
return this._preferenceStorageService.set(clientLocationKey, clientPath);
}
public setWowFolderPath(
clientType: WowClientType,
folderPath: string
): boolean {
public setWowFolderPath(clientType: WowClientType, folderPath: string): boolean {
const relativePath = this.getClientRelativePath(clientType, folderPath);
if (!this.isClientFolder(clientType, relativePath)) {
@@ -246,11 +245,7 @@ export class WarcraftService {
this.setClientLocation(clientType, relativePath);
from(this.getWowClientTypes())
.pipe(
map((wowClientTypes) =>
this._installedClientTypesSrc.next(wowClientTypes)
)
)
.pipe(map((wowClientTypes) => this._installedClientTypesSrc.next(wowClientTypes)))
.subscribe();
return true;
@@ -259,10 +254,7 @@ export class WarcraftService {
public getClientRelativePath(clientType: WowClientType, folderPath: string) {
const clientFolderName = this.getClientFolderName(clientType);
const clientFolderIdx = folderPath.indexOf(clientFolderName);
const relativePath =
clientFolderIdx === -1
? folderPath
: folderPath.substring(0, clientFolderIdx);
const relativePath = clientFolderIdx === -1 ? folderPath : folderPath.substring(0, clientFolderIdx);
return path.normalize(relativePath);
}
@@ -272,11 +264,7 @@ export class WarcraftService {
const relativePath = this.getClientRelativePath(clientType, folderPath);
const executableName = this.getExecutableName(clientType);
const executablePath = path.join(
relativePath,
clientFolderName,
executableName
);
const executablePath = path.join(relativePath, clientFolderName, executableName);
return fs.existsSync(executablePath);
}
@@ -345,12 +333,7 @@ export class WarcraftService {
case WowClientType.Beta:
return BETA_LOCATION_KEY;
default:
throw new Error(
`Failed to get client location key: ${clientType}, ${getEnumName(
WowClientType,
clientType
)}`
);
throw new Error(`Failed to get client location key: ${clientType}, ${getEnumName(WowClientType, clientType)}`);
}
}

View File

@@ -14,11 +14,7 @@ export class WarcraftServiceWin implements WarcraftServiceImpl {
const driveNames = diskInfo.map((i) => i.mounted);
for (const name of driveNames) {
const agentPath = path.join(
name,
WINDOWS_BLIZZARD_AGENT_PATH,
BLIZZARD_PRODUCT_DB_NAME
);
const agentPath = path.join(name, WINDOWS_BLIZZARD_AGENT_PATH, BLIZZARD_PRODUCT_DB_NAME);
const exists = await FileUtils.exists(agentPath);
if (exists) {

View File

@@ -16,8 +16,6 @@ export class WowUpApiService {
public getLatestVersion(): Observable<LatestVersionResponse> {
const url = new URL(`${API_URL}/wowup/latest`);
return this._httpClient
.get<LatestVersionResponse>(url.toString())
.pipe(tap((res) => console.log(res)));
return this._httpClient.get<LatestVersionResponse>(url.toString()).pipe(tap((res) => console.log(res)));
}
}

View File

@@ -28,6 +28,8 @@ import {
GET_ADDONS_HIDDEN_COLUMNS_KEY,
GET_ADDONS_SORT_ORDER,
ENABLED_ADDON_PROVIDERS_KEY,
CURRENT_THEME_KEY,
DEFAULT_THEME,
} from "../../../common/constants";
import { WowClientType } from "../../models/warcraft/wow-client-type";
import { AddonChannelType } from "../../models/wowup/addon-channel-type";
@@ -154,6 +156,16 @@ export class WowUpService {
this._preferenceChangeSrc.next({ key, value: value.toString() });
}
public get currentTheme() {
return this._preferenceStorageService.get(CURRENT_THEME_KEY);
}
public set currentTheme(value: string) {
const key = CURRENT_THEME_KEY;
this._preferenceStorageService.set(key, value);
this._preferenceChangeSrc.next({ key, value: value });
}
public get useHardwareAcceleration() {
const preference = this._preferenceStorageService.findByKey(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY);
return preference === "true";
@@ -348,6 +360,7 @@ export class WowUpService {
this.setDefaultPreference(ENABLE_SYSTEM_NOTIFICATIONS_PREFERENCE_KEY, true);
this.setDefaultPreference(COLLAPSE_TO_TRAY_PREFERENCE_KEY, true);
this.setDefaultPreference(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY, true);
this.setDefaultPreference(CURRENT_THEME_KEY, DEFAULT_THEME);
this.setDefaultPreference(WOWUP_RELEASE_CHANNEL_PREFERENCE_KEY, this.getDefaultReleaseChannel());
this.setDefaultPreference(ENABLED_ADDON_PROVIDERS_KEY, this._addonProviderFactory
.getAll()

View File

@@ -1,15 +1,17 @@
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { PageNotFoundComponent } from "./page-not-found.component";
describe("PageNotFoundComponent", () => {
let component: PageNotFoundComponent;
let fixture: ComponentFixture<PageNotFoundComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PageNotFoundComponent],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PageNotFoundComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(PageNotFoundComponent);

View File

@@ -1,6 +1,17 @@
import { createHash } from "crypto";
export function stringIncludes(value: string, search: string) {
if (value == null) {
return false;
}
return value.trim().toLowerCase().indexOf(search.trim().toLowerCase()) >= 0;
}
/**
* Get the sha1 hash of a string
*/
export function getSha1Hash(str: string): string {
const shasum = createHash("sha1");
shasum.update(str);
return shasum.digest("hex");
}

View File

@@ -1,5 +1,57 @@
{
"ChangeLogs": [
{
"Version": "2.0.0-beta.15",
"changes": [
"Fix an issue where light/dark themes could overlap",
"Fix an issue with a nested translate call"
]
},
{
"Version": "2.0.0-beta.14",
"changes": [
"Russian locale updates (Medok)",
"Spanish locale updates (SkollVargr)",
"German locale updates (Glow)",
"Portuguese locale updates (Dogo)",
"Korean locale updates (soaakim)",
"French locale updates (Zazou)",
"Theme updates (NoobTaco)",
"Add light themes",
"App can now be resized via the top of the frame",
"Fix an issue with the scrollbar in options not being clickable anymore"
]
},
{
"Version": "2.0.0-beta.13",
"changes": [
"German locale updates (chops)",
"French locale updates (Zazou)",
"Italian locale updates (Bito)",
"Theme updates (NoobTaco)",
"Fix Korean locale",
"Fix Chinese locale",
"Fix NB locale"
]
},
{
"Version": "2.0.0-beta.12",
"changes": [
"Russian locale updates (Medok)",
"German locale updates (Glow)",
"French locale updates (Zazou)",
"Chinese locale updates (CyanoHao)",
"Korean locale updates (soaakim)",
"Spanish locale updates (SkollVargr)",
"Add the ability to localize the 'Install by URL' download count (chops)",
"Add a new theming feature for Alliance and Horde",
"Auto update timer will now refresh the 'My Addons' list",
"Update to Angular 11",
"Fix issue with window snap fullscreen not saving screen",
"Fix an issue with the app icon not showing up on ubuntu",
"The app will now quit if you close the window on Mac and 'Minimize to Tray' is off"
]
},
{
"Version": "2.0.0-beta.11",
"changes": ["Italian locale updates (Bito)", "Fix a bug with the Spanish locale file"]

View File

@@ -7,6 +7,14 @@
"QUIT_ACTION": "Beenden",
"SHOW_ACTION": "Öffnen"
},
"THEME": {
"ALLIANCE": "Allianz",
"ALLIANCE_LIGHT": "Alliance Light",
"DEFAULT": "Standard",
"DEFAULT_LIGHT": "Default Light",
"HORDE": "Horde",
"HORDE_LIGHT": "Horde Light"
},
"WOWUP_UPDATE": {
"DOWNLOADED_TOOLTIP": "WowUp Update installieren",
"INSTALL_MESSAGE": "Willst du WowUp neu Starten um das Update zu installieren?",
@@ -38,6 +46,13 @@
"UNINSTALLING": "Deinstalliere",
"UPDATING": "Aktualisiere..."
},
"CLIENT_TYPES": {
"BETA": "Beta",
"CLASSIC": "Classic",
"CLASSICPTR": "Classic PTR",
"RETAIL": "Retail",
"RETAILPTR": "Retail PTR"
},
"DATES": {
"DATETIME_SHORT": "{d, date, short} {d, time, short}",
"DAYS_AGO": "Vor {count} {count, plural, one{Tag} other{Tagen}}",
@@ -48,7 +63,7 @@
"YESTERDAY": "Gestern"
},
"DEPENDENCY": {
"TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{Abhängigkeit} other{Abhängigkeiten} benötigt}"
"TOOLTIP": "{dependencyCount} {dependencyCount, plural, one{Abhängigkeit} other{Abhängigkeiten}} benötigt"
},
"DOWNLOAD_COUNT": {
"BILLION": "{count} Milliarden",
@@ -88,6 +103,7 @@
"ADDON_URL_INPUT_PLACEHOLDER": "z.B. GitHub oder WowInterface URL",
"CLOSE_BUTTON": "Schließen",
"DESCRIPTION": "Wenn Sie ein Addon direkt von einer URL installieren möchten, fügen Sie es unten in der Zeile ein.",
"DOWNLOAD_COUNT": "{textCount} {count, plural, one{Download} other{Downloads}} auf {provider}",
"ERROR": {
"FAILED_TO_CONNECT": "Keine Verbindung mit der API möglich, warte ein wenig und versuche es erneut.",
"INSTALL_FAILED": "Beim Versuch das Addon zu installieren ist etwas schief gelaufen, bitte versuche er erneut.\n\nWenn diese Nachricht weiterhin erscheint, kannst du auf unsrem Discord im Kamlen #wow-support nach Hilfe fragen.",
@@ -227,6 +243,8 @@
"START_WITH_SYSTEM_LABEL": "Starte WowUp mit dem System",
"TELEMETRY_DESCRIPTION": "Helfen Sie WowUp zu verbessern, indem Sie anonyme Installationsdaten und/oder Fehler senden.",
"TELEMETRY_LABEL": "Telemetrie",
"THEME_DESCRIPTION": "Ändere das Farbschma nach deinen Wünschen",
"THEME_LABEL": "Farbschema",
"TITLE": "Applikation",
"USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Neustart der App?",
"USE_HARDWARE_ACCELERATION_DESCRIPTION": "Das Deaktivieren der Hardwarebeschleunigung kann FPS Probleme und Probleme beim Rendern beheben. Eine Änderung dieser Einstellung benötigt einen Neustart der App.",

View File

@@ -7,6 +7,14 @@
"QUIT_ACTION": "Quit",
"SHOW_ACTION": "Show"
},
"THEME": {
"ALLIANCE": "Alliance",
"ALLIANCE_LIGHT": "Alliance Light",
"DEFAULT": "Default",
"DEFAULT_LIGHT": "Default Light",
"HORDE": "Horde",
"HORDE_LIGHT": "Horde Light"
},
"WOWUP_UPDATE": {
"DOWNLOADED_TOOLTIP": "Install WowUp update",
"INSTALL_MESSAGE": "Do you want to restart WowUp and install the update?",
@@ -38,17 +46,24 @@
"UNINSTALLING": "Uninstalling",
"UPDATING": "Updating..."
},
"CLIENT_TYPES": {
"BETA": "Beta",
"CLASSIC": "Classic",
"CLASSICPTR": "Classic PTR",
"RETAIL": "Retail",
"RETAILPTR": "Retail PTR"
},
"DATES": {
"DATETIME_SHORT": "{d, date, short} {d, time, short}",
"DAYS_AGO": "{count} {count, plural, one{day} other{days}} ago",
"HOURS_AGO": "{count} {count, plural, one{hour} other{hours}} ago",
"DAYS_AGO": "{count} {count, plural, =1{day} other{days}} ago",
"HOURS_AGO": "{count} {count, plural, =1{hour} other{hours}} ago",
"JUST_NOW": "Just now",
"MONTHS_AGO": "{count} {count, plural, one{month} other{months}} ago",
"YEARS_AGO": "{count} {count, plural, one{year} other{years}} ago",
"MONTHS_AGO": "{count} {count, plural, =1{month} other{months}} ago",
"YEARS_AGO": "{count} {count, plural, =1{year} other{years}} ago",
"YESTERDAY": "Yesterday"
},
"DEPENDENCY": {
"TOOLTIP": "{dependencyCount} required {dependencyCount, plural, one{dependency} other{dependencies}}"
"TOOLTIP": "{dependencyCount} required {dependencyCount, plural, =1{dependency} other{dependencies}}"
},
"DOWNLOAD_COUNT": {
"BILLION": "{count} billion",
@@ -72,7 +87,7 @@
"DIALOGS": {
"ADDON_DETAILS": {
"BY_AUTHOR": "By {authorName}",
"DEPENDENCY_TEXT": "This addon has {dependencyCount} required {dependencyCount, plural, one{dependency} other{dependencies}}",
"DEPENDENCY_TEXT": "This addon has {dependencyCount} required {dependencyCount, plural, =1{dependency} other{dependencies}}",
"VIEW_IN_BROWSER_BUTTON": "View in browser",
"VIEW_ON_PROVIDER_PREFIX": "View on"
},
@@ -88,6 +103,7 @@
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub or WowInterface URL",
"CLOSE_BUTTON": "Close",
"DESCRIPTION": "If you want to install an addon directly from a URL paste it below to get started.",
"DOWNLOAD_COUNT": "{textCount} {count, plural, =1{download} other{downloads}} on {provider}",
"ERROR": {
"FAILED_TO_CONNECT": "Cannot connect to API, please wait a bit and try again.",
"INSTALL_FAILED": "Something went wrong when trying to install the addon, please try again.\n\nIf this message keeps showing up, you can get help on Discord in the #wow-support channel.",
@@ -190,7 +206,7 @@
"CONFIRMATION_LESS_THAN_THREE": "Are you sure you want to remove the following {count} addons?",
"CONFIRMATION_MORE_THAN_THREE": "Are you sure you want to remove the selected {count} addons?",
"CONFIRMATION_ONE": "Are you sure you want to remove {addonName}?",
"DEPENDENCY_MESSAGE": "{addonName} has {dependencyCount} {dependencyCount, plural, one{dependency} other{dependencies}}. Do you want to remove them also?",
"DEPENDENCY_MESSAGE": "{addonName} has {dependencyCount} {dependencyCount, plural, =1{dependency} other{dependencies}}. Do you want to remove them also?",
"DEPENDENCY_TITLE": "Remove Addon Dependencies?",
"TITLE": "Uninstall {count, plural, =1{Addon} other{Addons}}?"
},
@@ -227,6 +243,8 @@
"START_WITH_SYSTEM_LABEL": "Launch WowUp with system",
"TELEMETRY_DESCRIPTION": "Help improve WowUp by sending anonymous install data and/or errors.",
"TELEMETRY_LABEL": "Telemetry",
"THEME_DESCRIPTION": "Change the color theme to whatever you like",
"THEME_LABEL": "Color Theme",
"TITLE": "Application",
"USE_HARDWARE_ACCELERATION_CONFIRMATION_LABEL": "Do you want to restart?",
"USE_HARDWARE_ACCELERATION_DESCRIPTION": "Disabling hardware acceleration might solve FPS issues and fix other rendering issues in this app. Changing this setting requires a restart.",

Some files were not shown because too many files have changed in this diff Show More