mirror of
https://github.com/WowUp/WowUp.git
synced 2026-05-24 22:46:45 -04:00
merge in electron
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { ipcMain, shell } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as async from "async";
|
||||
import * as path from "path";
|
||||
import * as admZip from "adm-zip";
|
||||
import { ncp } from "ncp";
|
||||
import * as rimraf from "rimraf";
|
||||
|
||||
import { readDirRecursive } from "./file.utils";
|
||||
import { readDirRecursive, readFile } from "./file.utils";
|
||||
import {
|
||||
CURSE_HASH_FILE_CHANNEL,
|
||||
LIST_DIRECTORIES_CHANNEL,
|
||||
@@ -11,6 +15,13 @@ import {
|
||||
PATH_EXISTS_CHANNEL,
|
||||
CURSE_GET_SCAN_RESULTS,
|
||||
WOWUP_GET_SCAN_RESULTS,
|
||||
UNZIP_FILE_CHANNEL,
|
||||
COPY_FILE_CHANNEL,
|
||||
COPY_DIRECTORY_CHANNEL,
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
RENAME_DIRECTORY_CHANNEL,
|
||||
READ_FILE_CHANNEL,
|
||||
GET_ASSET_FILE_PATH,
|
||||
} from "./src/common/constants";
|
||||
import { CurseGetScanResultsRequest } from "./src/common/curse/curse-get-scan-results-request";
|
||||
import { CurseGetScanResultsResponse } from "./src/common/curse/curse-get-scan-results-response";
|
||||
@@ -28,6 +39,14 @@ import { WowUpGetScanResultsRequest } from "./src/common/wowup/wowup-get-scan-re
|
||||
import { WowUpGetScanResultsResponse } from "./src/common/wowup/wowup-get-scan-results-response";
|
||||
import { WowUpFolderScanner } from "./src/common/wowup/wowup-folder-scanner";
|
||||
import { WowUpScanResult } from "./src/common/wowup/wowup-scan-result";
|
||||
import { UnzipRequest } from "./src/common/models/unzip-request";
|
||||
import { UnzipStatus } from "./src/common/models/unzip-status";
|
||||
import { UnzipStatusType } from "./src/common/models/unzip-status-type";
|
||||
import { CopyFileRequest } from "./src/common/models/copy-file-request";
|
||||
import { CopyDirectoryRequest } from "./src/common/models/copy-directory-request";
|
||||
import { DeleteDirectoryRequest } from "./src/common/models/delete-directory-request";
|
||||
import { ReadFileRequest } from "./src/common/models/read-file-request";
|
||||
import { ReadFileResponse } from "./src/common/models/read-file-response";
|
||||
|
||||
const nativeAddon = require("./build/Release/addon.node");
|
||||
|
||||
@@ -36,6 +55,13 @@ ipcMain.on(SHOW_DIRECTORY, async (evt, arg: ShowDirectoryRequest) => {
|
||||
evt.reply(arg.responseKey, true);
|
||||
});
|
||||
|
||||
ipcMain.on(GET_ASSET_FILE_PATH, async (evt, arg: ValueRequest<string>) => {
|
||||
const response: ValueResponse<string> = {
|
||||
value: path.join(__dirname, "assets", arg.value),
|
||||
};
|
||||
evt.reply(arg.responseKey, response);
|
||||
});
|
||||
|
||||
ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => {
|
||||
// console.log(CURSE_HASH_FILE_CHANNEL, arg);
|
||||
|
||||
@@ -84,7 +110,7 @@ ipcMain.on(LIST_FILES_CHANNEL, async (evt, arg: ListFilesRequest) => {
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
evt.reply(arg.sourcePath, response);
|
||||
evt.reply(arg.responseKey, response);
|
||||
});
|
||||
|
||||
ipcMain.on(LIST_DIRECTORIES_CHANNEL, (evt, arg: ValueRequest<string>) => {
|
||||
@@ -175,3 +201,62 @@ ipcMain.on(
|
||||
evt.reply(arg.responseKey, response);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(UNZIP_FILE_CHANNEL, (evt, arg: UnzipRequest) => {
|
||||
const zipFilePath = arg.zipFilePath;
|
||||
const outputFolder = arg.outputFolder;
|
||||
|
||||
const zip = new admZip(zipFilePath);
|
||||
zip.extractAllToAsync(outputFolder, true, (err) => {
|
||||
const status: UnzipStatus = {
|
||||
type: UnzipStatusType.Complete,
|
||||
outputFolder,
|
||||
};
|
||||
|
||||
if (err) {
|
||||
status.type = UnzipStatusType.Error;
|
||||
status.error = err;
|
||||
}
|
||||
|
||||
evt.reply(arg.responseKey, status);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_FILE_CHANNEL, (evt, arg: CopyFileRequest) => {
|
||||
console.log("Copy File", arg);
|
||||
fs.copyFile(arg.sourceFilePath, arg.destinationFilePath, (err) => {
|
||||
evt.reply(arg.responseKey, { error: err });
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_DIRECTORY_CHANNEL, (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log("Copy Dir", arg);
|
||||
ncp(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
evt.reply(arg.responseKey, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(DELETE_DIRECTORY_CHANNEL, (evt, arg: DeleteDirectoryRequest) => {
|
||||
console.log("Delete Dir", arg);
|
||||
rimraf(arg.sourcePath, (err) => {
|
||||
evt.reply(arg.responseKey, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(RENAME_DIRECTORY_CHANNEL, (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log("Rename Dir", arg);
|
||||
fs.rename(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
evt.reply(arg.responseKey, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(READ_FILE_CHANNEL, async (evt, arg: ReadFileRequest) => {
|
||||
// console.log('Read File', arg);
|
||||
const response: ReadFileResponse = { data: "" };
|
||||
try {
|
||||
response.data = await readFile(arg.sourcePath);
|
||||
} catch (err) {
|
||||
response.error = err;
|
||||
}
|
||||
evt.reply(arg.responseKey, response);
|
||||
});
|
||||
|
||||
@@ -12,41 +12,19 @@ import {
|
||||
} from "electron";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
import * as fs from "fs";
|
||||
import { release, arch } from "os";
|
||||
import * as electronDl from "electron-dl";
|
||||
import * as admZip from "adm-zip";
|
||||
import { DownloadRequest } from "./src/common/models/download-request";
|
||||
import { DownloadStatus } from "./src/common/models/download-status";
|
||||
import { DownloadStatusType } from "./src/common/models/download-status-type";
|
||||
import { UnzipStatus } from "./src/common/models/unzip-status";
|
||||
import {
|
||||
DOWNLOAD_FILE_CHANNEL,
|
||||
UNZIP_FILE_CHANNEL,
|
||||
COPY_FILE_CHANNEL,
|
||||
COPY_DIRECTORY_CHANNEL,
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
RENAME_DIRECTORY_CHANNEL,
|
||||
READ_FILE_CHANNEL,
|
||||
} from "./src/common/constants";
|
||||
import { UnzipStatusType } from "./src/common/models/unzip-status-type";
|
||||
import { UnzipRequest } from "./src/common/models/unzip-request";
|
||||
import { CopyFileRequest } from "./src/common/models/copy-file-request";
|
||||
import { CopyDirectoryRequest } from "./src/common/models/copy-directory-request";
|
||||
import { DeleteDirectoryRequest } from "./src/common/models/delete-directory-request";
|
||||
import { ReadFileRequest } from "./src/common/models/read-file-request";
|
||||
import { ReadFileResponse } from "./src/common/models/read-file-response";
|
||||
import { DOWNLOAD_FILE_CHANNEL } from "./src/common/constants";
|
||||
import "./ipc-events";
|
||||
import { ncp } from "ncp";
|
||||
import * as rimraf from "rimraf";
|
||||
import * as log from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import * as Store from "electron-store";
|
||||
import { readFile } from "./file.utils";
|
||||
import { WindowState } from './src/common/models/window-state';
|
||||
import { isBetween } from './src/common/utils/number.utils';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { WindowState } from "./src/common/models/window-state";
|
||||
import { Subject } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
const isMac = process.platform === "darwin";
|
||||
const isWin = process.platform === "win32";
|
||||
@@ -68,42 +46,44 @@ autoUpdater.on("update-downloaded", () => {
|
||||
|
||||
const appMenuTemplate: Array<MenuItemConstructorOptions | MenuItem> = isMac
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [{ role: "quit" }],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "selectAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [{ role: "quit" }],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "selectAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const appMenu = Menu.buildFromTemplate(appMenuTemplate);
|
||||
Menu.setApplicationMenu(appMenu);
|
||||
|
||||
app.disableHardwareAcceleration(); // Try to improve font blur?
|
||||
|
||||
const LOG_PATH = path.join(app.getPath("userData"), "logs");
|
||||
app.setAppLogsPath(LOG_PATH);
|
||||
log.transports.file.resolvePath = (
|
||||
@@ -159,43 +139,50 @@ function createTray() {
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
function windowStateManager(windowName: string, { width, height }: { width: number, height: number }) {
|
||||
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;
|
||||
windowState = preferenceStore.get(
|
||||
`${windowName}-window-state`
|
||||
) as WindowState;
|
||||
|
||||
if (!windowState) {
|
||||
setDefaults = true;
|
||||
} else {
|
||||
log.info('found window state:', windowState);
|
||||
log.info("found window state:", windowState);
|
||||
|
||||
const valid = screen.getAllDisplays().some(display => {
|
||||
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
|
||||
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');
|
||||
log.info("reset window state, bounds are outside displays");
|
||||
setDefaults = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
log.info('setting window defaults');
|
||||
log.info("setting window defaults");
|
||||
windowState = <WindowState>{ width, height };
|
||||
}
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
log.info('saving window state');
|
||||
log.info("saving window state");
|
||||
if (!window.isMaximized() && !window.isFullScreen()) {
|
||||
windowState = { ...windowState, ...window.getBounds() };
|
||||
}
|
||||
@@ -207,37 +194,38 @@ function windowStateManager(windowName: string, { width, height }: { width: numb
|
||||
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());
|
||||
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());
|
||||
saveState$.pipe(debounceTime(500)).subscribe(() => saveState());
|
||||
|
||||
setState();
|
||||
|
||||
return ({
|
||||
return {
|
||||
...windowState,
|
||||
monitorState,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createWindow(): BrowserWindow {
|
||||
// Main object for managing window state
|
||||
// Initialize with a window name and default size
|
||||
const mainWindowManager = windowStateManager('main', { width: 900, height: 600 });
|
||||
const mainWindowManager = windowStateManager("main", {
|
||||
width: 900,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
const windowOptions: BrowserWindowConstructorOptions = {
|
||||
width: mainWindowManager.width,
|
||||
height: mainWindowManager.height,
|
||||
x: mainWindowManager.x,
|
||||
y: mainWindowManager.y,
|
||||
backgroundColor: '#444444',
|
||||
title: 'WowUp',
|
||||
titleBarStyle: 'hidden',
|
||||
backgroundColor: "#444444",
|
||||
title: "WowUp",
|
||||
titleBarStyle: "hidden",
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegration: true,
|
||||
@@ -262,21 +250,20 @@ function createWindow(): BrowserWindow {
|
||||
|
||||
win.webContents.userAgent = USER_AGENT;
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
win.once("ready-to-show", () => {
|
||||
win.show();
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
.then((result) => {
|
||||
console.log('UPDATE', result)
|
||||
})
|
||||
autoUpdater.checkForUpdatesAndNotify().then((result) => {
|
||||
console.log("UPDATE", result);
|
||||
});
|
||||
});
|
||||
|
||||
win.once('show', () => {
|
||||
win.once("show", () => {
|
||||
if (mainWindowManager.isFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
} else if (mainWindowManager.isMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (isMac) {
|
||||
win.on("close", (e) => {
|
||||
@@ -381,84 +368,26 @@ ipcMain.on(DOWNLOAD_FILE_CHANNEL, async (evt, arg: DownloadRequest) => {
|
||||
const download = await electronDl.download(win, arg.url, {
|
||||
directory: arg.outputFolder,
|
||||
onProgress: (progress) => {
|
||||
win.webContents.send(arg.url, {
|
||||
const progressStatus: DownloadStatus = {
|
||||
type: DownloadStatusType.Progress,
|
||||
progress: parseFloat((progress.percent * 100.0).toFixed(2)),
|
||||
} as DownloadStatus);
|
||||
};
|
||||
|
||||
win.webContents.send(arg.responseKey, progressStatus);
|
||||
},
|
||||
});
|
||||
|
||||
win.webContents.send(arg.url, {
|
||||
const status: DownloadStatus = {
|
||||
type: DownloadStatusType.Complete,
|
||||
savePath: download.getSavePath(),
|
||||
} as DownloadStatus);
|
||||
};
|
||||
win.webContents.send(arg.responseKey, status);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
win.webContents.send(arg.url, {
|
||||
const status: DownloadStatus = {
|
||||
type: DownloadStatusType.Error,
|
||||
error: err,
|
||||
} as DownloadStatus);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => {
|
||||
const zipFilePath = arg.zipFilePath;
|
||||
const outputFolder = arg.outputFolder;
|
||||
|
||||
const zip = new admZip(zipFilePath);
|
||||
zip.extractAllToAsync(outputFolder, true, (err) => {
|
||||
const status: UnzipStatus = {
|
||||
type: UnzipStatusType.Complete,
|
||||
outputFolder,
|
||||
};
|
||||
|
||||
if (err) {
|
||||
status.type = UnzipStatusType.Error;
|
||||
status.error = err;
|
||||
}
|
||||
|
||||
win.webContents.send(zipFilePath, status);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest) => {
|
||||
console.log("Copy File", arg);
|
||||
fs.copyFile(arg.sourceFilePath, arg.destinationFilePath, (err) => {
|
||||
win.webContents.send(arg.destinationFilePath, { error: err });
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_DIRECTORY_CHANNEL, async (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log("Copy Dir", arg);
|
||||
ncp(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
win.webContents.send(arg.destinationPath, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
async (evt, arg: DeleteDirectoryRequest) => {
|
||||
console.log("Delete Dir", arg);
|
||||
rimraf(arg.sourcePath, (err) => {
|
||||
win.webContents.send(arg.sourcePath, err);
|
||||
});
|
||||
win.webContents.send(arg.responseKey, status);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(RENAME_DIRECTORY_CHANNEL, async (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log("Rename Dir", arg);
|
||||
fs.rename(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
win.webContents.send(arg.destinationPath, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(READ_FILE_CHANNEL, async (evt, arg: ReadFileRequest) => {
|
||||
// console.log('Read File', arg);
|
||||
const response: ReadFileResponse = { data: "" };
|
||||
try {
|
||||
response.data = await readFile(arg.sourcePath);
|
||||
} catch (err) {
|
||||
response.error = err;
|
||||
}
|
||||
win.webContents.send(arg.sourcePath, response);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wowup",
|
||||
"productName": "WowUp",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"version": "2.0.0-alpha.11",
|
||||
"description": "Word of Warcraft addon updater",
|
||||
"homepage": "https://github.com/maximegris/angular-electron",
|
||||
"author": {
|
||||
@@ -69,6 +69,7 @@
|
||||
"@types/node": "12.12.62",
|
||||
"@types/opossum": "4.1.1",
|
||||
"@types/rimraf": "3.0.0",
|
||||
"@types/uuid": "8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.3.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "4.3.0",
|
||||
"@typescript-eslint/parser": "4.3.0",
|
||||
|
||||
@@ -1,32 +1,60 @@
|
||||
import { WowClientType } from "../models/warcraft/wow-client-type";
|
||||
import { Addon } from "../entities/addon";
|
||||
import { PotentialAddon } from "../models/wowup/potential-addon";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { Observable } from "rxjs";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
|
||||
export interface AddonProvider {
|
||||
|
||||
name: AddonProviderType;
|
||||
|
||||
getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]>;
|
||||
getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
): Promise<AddonSearchResult[]>;
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]>;
|
||||
getFeaturedAddons(
|
||||
clientType: WowClientType,
|
||||
channelType?: AddonChannelType
|
||||
): Promise<AddonSearchResult[]>;
|
||||
|
||||
searchByQuery(query: string, clientType: WowClientType): Promise<PotentialAddon[]>;
|
||||
searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType,
|
||||
channelType?: AddonChannelType
|
||||
): Promise<AddonSearchResult[]>;
|
||||
|
||||
searchByUrl(addonUri: URL, clientType: WowClientType): Promise<PotentialAddon>;
|
||||
searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult>;
|
||||
|
||||
searchByName(addonName: string, folderName: string, clientType: WowClientType, nameOverride?: string): Promise<AddonSearchResult[]>;
|
||||
searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
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";
|
||||
|
||||
@@ -5,10 +5,9 @@ import { HttpClient } from "@angular/common/http";
|
||||
import { map } from "rxjs/operators";
|
||||
import * as _ from "lodash";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
import { AddonChannelType } from "../models/wowup/addon-channel-type";
|
||||
import { PotentialAddon } from "../models/wowup/potential-addon";
|
||||
import { CachingService } from "app/services/caching/caching-service";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { ElectronService } from "app/services";
|
||||
@@ -23,19 +22,47 @@ import { CurseSearchResult } from "../../common/curse/curse-search-result";
|
||||
import { CurseFile } from "common/curse/curse-file";
|
||||
import { CurseReleaseType } from "common/curse/curse-release-type";
|
||||
import { CurseGetFeaturedResponse } from "app/models/curse/curse-get-featured-response";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
|
||||
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
|
||||
const HUB_API_URL = "https://hub.dev.wowup.io";
|
||||
|
||||
export class CurseAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreaker<
|
||||
[clientType: () => Promise<any>],
|
||||
any
|
||||
>;
|
||||
|
||||
private getCircuitBreaker<T>() {
|
||||
return this._circuitBreaker as CircuitBreaker<
|
||||
[clientType: () => Promise<T>],
|
||||
T
|
||||
>;
|
||||
}
|
||||
|
||||
public readonly name = "Curse";
|
||||
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService
|
||||
) {}
|
||||
) {
|
||||
this._circuitBreaker = new CircuitBreaker(
|
||||
(action) => this.sendRequest(action),
|
||||
{
|
||||
resetTimeout: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
async scan(
|
||||
this._circuitBreaker.on("open", () => {
|
||||
console.log(`${this.name} circuit breaker open`);
|
||||
});
|
||||
this._circuitBreaker.on("close", () => {
|
||||
console.log(`${this.name} circuit breaker close`);
|
||||
});
|
||||
}
|
||||
|
||||
public async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
@@ -58,7 +85,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
);
|
||||
const addonIds = _.uniq(matchedScanResultIds);
|
||||
|
||||
var addonResults = await this.getAllIds(addonIds).toPromise();
|
||||
var addonResults = await this.getAllIds(addonIds);
|
||||
|
||||
for (let addonFolder of addonFolders) {
|
||||
var scanResult = scanResults.find(
|
||||
@@ -78,93 +105,17 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
const newAddon = this.getAddon(
|
||||
clientType,
|
||||
addonChannelType,
|
||||
scanResult
|
||||
);
|
||||
const newAddon = this.getAddon(clientType, scanResult);
|
||||
|
||||
addonFolder.matchingAddon = newAddon;
|
||||
} catch (err) {
|
||||
console.error(scanResult);
|
||||
console.error(err);
|
||||
// TODO
|
||||
// _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async mapAddonFolders(
|
||||
scanResults: AppCurseScanResult[],
|
||||
clientType: WowClientType
|
||||
) {
|
||||
if (clientType === WowClientType.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(
|
||||
scanResults.map((result) => result.fingerprint)
|
||||
).toPromise();
|
||||
|
||||
console.log(fingerprintResponse);
|
||||
|
||||
for (let scanResult of scanResults) {
|
||||
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
|
||||
scanResult.exactMatch = fingerprintResponse.exactMatches.find(
|
||||
(exactMatch) =>
|
||||
this.isGameVersionFlavor(
|
||||
exactMatch.file.gameVersionFlavor,
|
||||
clientType
|
||||
) && this.hasMatchingFingerprint(scanResult, exactMatch)
|
||||
);
|
||||
|
||||
// If the addon does not have an exact match, check the partial matches.
|
||||
if (!scanResult.exactMatch) {
|
||||
scanResult.exactMatch = fingerprintResponse.partialMatches.find(
|
||||
(partialMatch) =>
|
||||
partialMatch.file?.modules?.some(
|
||||
(module) => module.fingerprint === scanResult.fingerprint
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasMatchingFingerprint(
|
||||
scanResult: AppCurseScanResult,
|
||||
exactMatch: CurseMatch
|
||||
) {
|
||||
return exactMatch.file.modules.some(
|
||||
(m) => m.fingerprint === scanResult.fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
private isGameVersionFlavor(
|
||||
gameVersionFlavor: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
return gameVersionFlavor === this.getGameVersionFlavor(clientType);
|
||||
}
|
||||
|
||||
private getAddonsByFingerprints(
|
||||
fingerprints: number[]
|
||||
): Observable<CurseFingerprintsResponse> {
|
||||
const url = `${API_URL}/fingerprint`;
|
||||
|
||||
return this._httpClient.post<CurseFingerprintsResponse>(url, fingerprints);
|
||||
}
|
||||
|
||||
private getAllIds(addonIds: number[]): Observable<CurseSearchResult[]> {
|
||||
if (!addonIds?.length) {
|
||||
return of([]);
|
||||
}
|
||||
|
||||
const url = `${API_URL}/addon`;
|
||||
|
||||
return this._httpClient.post<CurseSearchResult[]>(url, addonIds);
|
||||
}
|
||||
|
||||
private getScanResults = async (
|
||||
public getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<AppCurseScanResult[]> => {
|
||||
const t1 = Date.now();
|
||||
@@ -199,6 +150,115 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
});
|
||||
};
|
||||
|
||||
private async mapAddonFolders(
|
||||
scanResults: AppCurseScanResult[],
|
||||
clientType: WowClientType
|
||||
) {
|
||||
if (clientType === WowClientType.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
scanResults.forEach((result) => {
|
||||
console.debug(result.folderName, result.fingerprint);
|
||||
});
|
||||
|
||||
const fingerprintResponse = await this.getAddonsByFingerprintsW(
|
||||
scanResults.map((result) => result.fingerprint)
|
||||
);
|
||||
|
||||
console.log("fingerprintResponse", fingerprintResponse);
|
||||
|
||||
for (let scanResult of scanResults) {
|
||||
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
|
||||
scanResult.exactMatch = fingerprintResponse.exactMatches.find(
|
||||
(exactMatch) =>
|
||||
this.isGameVersionFlavor(
|
||||
exactMatch.file.gameVersionFlavor,
|
||||
clientType
|
||||
) && this.hasMatchingFingerprint(scanResult, exactMatch)
|
||||
);
|
||||
|
||||
// If the addon does not have an exact match, check the partial matches.
|
||||
if (!scanResult.exactMatch && fingerprintResponse.partialMatches) {
|
||||
scanResult.exactMatch = fingerprintResponse.partialMatches.find(
|
||||
(partialMatch) =>
|
||||
partialMatch.file?.modules?.some(
|
||||
(module) => module.fingerprint === scanResult.fingerprint
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasMatchingFingerprint(
|
||||
scanResult: AppCurseScanResult,
|
||||
exactMatch: CurseMatch
|
||||
) {
|
||||
return exactMatch.file.modules.some(
|
||||
(m) => m.fingerprint === scanResult.fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
private isGameVersionFlavor(
|
||||
gameVersionFlavor: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
return gameVersionFlavor === this.getGameVersionFlavor(clientType);
|
||||
}
|
||||
|
||||
private async getAddonsByFingerprintsW(fingerprints: number[]) {
|
||||
const url = `${HUB_API_URL}/curseforge/addons/fingerprint`;
|
||||
|
||||
console.log(`Wowup Fetching fingerprints`, JSON.stringify(fingerprints));
|
||||
|
||||
return await this._httpClient
|
||||
.post<CurseFingerprintsResponse>(url, {
|
||||
fingerprints,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return await this.getCircuitBreaker<CurseFingerprintsResponse>().fire(
|
||||
async () =>
|
||||
await this._httpClient
|
||||
.post<CurseFingerprintsResponse>(url, fingerprints)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
private async getAddonsByFingerprints(
|
||||
fingerprints: number[]
|
||||
): Promise<CurseFingerprintsResponse> {
|
||||
const url = `${API_URL}/fingerprint`;
|
||||
|
||||
console.log(`Curse Fetching fingerprints`, JSON.stringify(fingerprints));
|
||||
|
||||
return await this.getCircuitBreaker<CurseFingerprintsResponse>().fire(
|
||||
async () =>
|
||||
await this._httpClient
|
||||
.post<CurseFingerprintsResponse>(url, fingerprints)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
private async getAllIds(addonIds: number[]): Promise<CurseSearchResult[]> {
|
||||
if (!addonIds?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const url = `${API_URL}/addon`;
|
||||
|
||||
return await this.getCircuitBreaker<CurseSearchResult[]>().fire(
|
||||
async () =>
|
||||
await this._httpClient
|
||||
.post<CurseSearchResult[]>(url, addonIds)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
private sendRequest<T>(action: () => Promise<T>): Promise<T> {
|
||||
return action.call(this);
|
||||
}
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
@@ -210,7 +270,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
const addonResults: AddonSearchResult[] = [];
|
||||
const searchResults = await this.getAllIds(
|
||||
addonIds.map((id) => parseInt(id, 10))
|
||||
).toPromise();
|
||||
);
|
||||
|
||||
for (let result of searchResults) {
|
||||
const latestFiles = this.getLatestFiles(result, clientType);
|
||||
@@ -227,15 +287,15 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return addonResults;
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return this.getFeaturedAddonList().pipe(
|
||||
map((addons) => {
|
||||
return this.filterFeaturedAddons(addons, clientType);
|
||||
}),
|
||||
map((filteredAddons) => {
|
||||
return filteredAddons.map((addon) => this.getPotentialAddon(addon));
|
||||
})
|
||||
);
|
||||
public async getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
const addons = await this.getFeaturedAddonList();
|
||||
const filteredAddons = this.filterFeaturedAddons(addons, clientType);
|
||||
return filteredAddons.map((addon) => {
|
||||
const latestFiles = this.getLatestFiles(addon, clientType);
|
||||
return this.getAddonSearchResult(addon, latestFiles);
|
||||
});
|
||||
}
|
||||
|
||||
private filterFeaturedAddons(
|
||||
@@ -259,18 +319,20 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
async searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
var searchResults: PotentialAddon[] = [];
|
||||
clientType: WowClientType,
|
||||
channelType?: AddonChannelType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
channelType = channelType || AddonChannelType.Stable;
|
||||
var searchResults: AddonSearchResult[] = [];
|
||||
|
||||
var response = await this.getSearchResults(query).toPromise();
|
||||
var response = await this.getSearchResults(query);
|
||||
for (let result of response) {
|
||||
var latestFiles = this.getLatestFiles(result, clientType);
|
||||
if (!latestFiles.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchResults.push(this.getPotentialAddon(result));
|
||||
searchResults.push(this.getAddonSearchResult(result));
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
@@ -279,7 +341,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon> {
|
||||
): Promise<AddonSearchResult> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
@@ -292,12 +354,17 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
private getSearchResults(query: string): Observable<CurseSearchResult[]> {
|
||||
private async getSearchResults(query: string): Promise<CurseSearchResult[]> {
|
||||
const url = new URL(`${API_URL}/addon/search`);
|
||||
url.searchParams.set("gameId", "1");
|
||||
url.searchParams.set("searchFilter", query);
|
||||
|
||||
return this._httpClient.get<CurseSearchResult[]>(url.toString());
|
||||
return await this.getCircuitBreaker<CurseSearchResult[]>().fire(
|
||||
async () =>
|
||||
await this._httpClient
|
||||
.get<CurseSearchResult[]>(url.toString())
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
getById(
|
||||
@@ -306,7 +373,12 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
): Observable<AddonSearchResult> {
|
||||
const url = `${API_URL}/addon/${addonId}`;
|
||||
|
||||
return this._httpClient.get<CurseSearchResult>(url).pipe(
|
||||
return from(
|
||||
this.getCircuitBreaker<CurseSearchResult>().fire(
|
||||
async () =>
|
||||
await this._httpClient.get<CurseSearchResult>(url).toPromise()
|
||||
)
|
||||
).pipe(
|
||||
map((result) => {
|
||||
if (!result) {
|
||||
return null;
|
||||
@@ -334,23 +406,9 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
private getPotentialAddon(result: CurseSearchResult): PotentialAddon {
|
||||
return {
|
||||
author: this.getAuthor(result),
|
||||
downloadCount: result.downloadCount,
|
||||
externalId: result.id.toString(),
|
||||
externalUrl: result.websiteUrl,
|
||||
name: result.name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: this.getThumbnailUrl(result),
|
||||
summary: result.summary,
|
||||
screenshotUrls: this.getScreenshotUrls(result),
|
||||
};
|
||||
}
|
||||
|
||||
private getAddonSearchResult(
|
||||
result: CurseSearchResult,
|
||||
latestFiles: CurseFile[]
|
||||
latestFiles: CurseFile[] = []
|
||||
): AddonSearchResult {
|
||||
try {
|
||||
const thumbnailUrl = this.getThumbnailUrl(result);
|
||||
@@ -378,7 +436,8 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
thumbnailUrl,
|
||||
externalUrl: result.websiteUrl,
|
||||
providerName: this.name,
|
||||
files: searchResultFiles,
|
||||
files: _.orderBy(searchResultFiles, f => f.channelType).reverse(),
|
||||
downloadCount: result.downloadCount,
|
||||
};
|
||||
|
||||
return searchResult;
|
||||
@@ -388,13 +447,13 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private getFeaturedAddonList(): Observable<CurseSearchResult[]> {
|
||||
private async getFeaturedAddonList(): Promise<CurseSearchResult[]> {
|
||||
const url = `${API_URL}/addon/featured`;
|
||||
const cachedResponse = this._cachingService.get<CurseGetFeaturedResponse>(
|
||||
url
|
||||
);
|
||||
if (cachedResponse) {
|
||||
return of(cachedResponse.Popular);
|
||||
return cachedResponse.Popular;
|
||||
}
|
||||
|
||||
const body = {
|
||||
@@ -404,17 +463,22 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
updatedCount: 0,
|
||||
};
|
||||
|
||||
return this._httpClient.post<CurseGetFeaturedResponse>(url, body).pipe(
|
||||
map((result) => {
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this._cachingService.set(url, result);
|
||||
|
||||
return result.Popular;
|
||||
})
|
||||
const result = await this.getCircuitBreaker<
|
||||
CurseGetFeaturedResponse
|
||||
>().fire(
|
||||
async () =>
|
||||
await this._httpClient
|
||||
.post<CurseGetFeaturedResponse>(url, body)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this._cachingService.set(url, result);
|
||||
|
||||
return result.Popular;
|
||||
}
|
||||
|
||||
private getChannelType(releaseType: CurseReleaseType): AddonChannelType {
|
||||
@@ -491,30 +555,35 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
private getAddon(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
scanResult: AppCurseScanResult
|
||||
): Addon {
|
||||
const currentVersion = scanResult.exactMatch.file;
|
||||
|
||||
const authors = scanResult.searchResult.authors
|
||||
.map((author) => author.name)
|
||||
.join(", ");
|
||||
|
||||
const folderList = scanResult.exactMatch.file.modules
|
||||
.map((module) => module.foldername)
|
||||
.join(",");
|
||||
|
||||
const latestFiles = this.getLatestFiles(
|
||||
scanResult.searchResult,
|
||||
clientType
|
||||
);
|
||||
|
||||
let channelType = addonChannelType;
|
||||
let channelType = this.getChannelType(
|
||||
scanResult.exactMatch.file.releaseType
|
||||
);
|
||||
let latestVersion = latestFiles.find(
|
||||
(lf) => this.getChannelType(lf.releaseType) <= addonChannelType
|
||||
(lf) => this.getChannelType(lf.releaseType) <= channelType
|
||||
);
|
||||
|
||||
// If there were no releases that met the channel type restrictions
|
||||
if (!latestVersion) {
|
||||
latestVersion = _.first(latestFiles);
|
||||
channelType = this.getWowUpChannel(latestVersion.releaseType);
|
||||
console.warn("falling back to default channel");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -536,6 +605,9 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
latestVersion: latestVersion.displayName,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: this.getThumbnailUrl(scanResult.searchResult),
|
||||
screenshotUrls: this.getScreenshotUrls(scanResult.searchResult),
|
||||
downloadCount: scanResult.searchResult.downloadCount,
|
||||
summary: scanResult.searchResult.summary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,211 +5,231 @@ import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { forkJoin, Observable, of } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import * as _ from 'lodash';
|
||||
import { extname } from 'path';
|
||||
import * as _ from "lodash";
|
||||
import { extname } from "path";
|
||||
import { GitHubAsset } from "app/models/github/github-asset";
|
||||
import { GitHubRepository } from "app/models/github/github-repository";
|
||||
import { AddonSearchResultFile } from "app/models/wowup/addon-search-result-file";
|
||||
|
||||
const API_URL = "https://api.github.com/repos";
|
||||
const RELEASE_CONTENT_TYPES = [
|
||||
"application/x-zip-compressed",
|
||||
"application/zip"
|
||||
"application/x-zip-compressed",
|
||||
"application/zip",
|
||||
];
|
||||
|
||||
export class GitHubAddonProvider implements AddonProvider {
|
||||
public readonly name = "GitHub";
|
||||
public readonly name = "GitHub";
|
||||
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
) { }
|
||||
constructor(private _httpClient: HttpClient) {}
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
var searchResults: AddonSearchResult[] = []
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
): Promise<AddonSearchResult[]> {
|
||||
var searchResults: AddonSearchResult[] = [];
|
||||
|
||||
for (let addonId in addonIds) {
|
||||
var result = await this.getById(addonId, clientType).toPromise();
|
||||
if (result == null) {
|
||||
continue;
|
||||
}
|
||||
for (let addonId of addonIds) {
|
||||
var result = await this.getById(addonId, clientType).toPromise();
|
||||
if (result == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchResults.push(result);
|
||||
searchResults.push(result);
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
async getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult> {
|
||||
const repoPath = addonUri.pathname;
|
||||
const repoExtension = extname(repoPath); // if the repo has the git extension it wont work?
|
||||
if (!repoPath || repoExtension) {
|
||||
throw new Error(`Invlaid URL: ${addonUri}`);
|
||||
}
|
||||
|
||||
const results = await this.getReleases(repoPath).toPromise();
|
||||
const latestRelease = this.getLatestRelease(results);
|
||||
const asset = this.getValidAsset(latestRelease, clientType);
|
||||
|
||||
if (asset == null) {
|
||||
throw new Error(`No release found: ${addonUri}`);
|
||||
}
|
||||
|
||||
var repository = await this.getRepository(repoPath).toPromise();
|
||||
var author = repository.owner.login;
|
||||
var authorImageUrl = repository.owner.avatar_url;
|
||||
|
||||
var potentialAddon: AddonSearchResult = {
|
||||
author: author,
|
||||
downloadCount: asset.download_count,
|
||||
externalId: repoPath,
|
||||
externalUrl: repository.html_url,
|
||||
name: repository.name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: authorImageUrl,
|
||||
};
|
||||
|
||||
return potentialAddon;
|
||||
}
|
||||
|
||||
async searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
nameOverride?: string
|
||||
): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getById(
|
||||
addonId: string,
|
||||
clientType: WowClientType
|
||||
): Observable<AddonSearchResult> {
|
||||
return forkJoin([
|
||||
this.getReleases(addonId),
|
||||
this.getRepository(addonId),
|
||||
]).pipe(
|
||||
map(([releases, repository]) => {
|
||||
if (!releases?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return of([]);
|
||||
}
|
||||
|
||||
async searchByQuery(query: string, clientType: WowClientType): Promise<PotentialAddon[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByUrl(addonUri: URL, clientType: WowClientType): Promise<PotentialAddon> {
|
||||
const repoPath = addonUri.pathname;
|
||||
const repoExtension = extname(repoPath); // if the repo has the git extension it wont work?
|
||||
if (!repoPath || repoExtension) {
|
||||
throw new Error(`Invlaid URL: ${addonUri}`);
|
||||
const latestRelease = this.getLatestRelease(releases);
|
||||
if (!latestRelease) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const results = await this.getReleases(repoPath).toPromise();
|
||||
const latestRelease = this.getLatestRelease(results);
|
||||
const asset = this.getValidAsset(latestRelease, clientType);
|
||||
|
||||
if (asset == null) {
|
||||
throw new Error(`No release found: ${addonUri}`);
|
||||
if (!asset) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var repository = await this.getRepository(repoPath).toPromise();
|
||||
var author = repository.owner.login;
|
||||
var authorImageUrl = repository.owner.avatar_url;
|
||||
const author = repository.owner.login;
|
||||
const authorImageUrl = repository.owner.avatar_url;
|
||||
const addonName = this.getAddonName(addonId);
|
||||
|
||||
var potentialAddon: PotentialAddon = {
|
||||
author: author,
|
||||
downloadCount: asset.download_count,
|
||||
externalId: repoPath,
|
||||
externalUrl: latestRelease.url,
|
||||
name: repository.name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: authorImageUrl
|
||||
var searchResultFile: AddonSearchResultFile = {
|
||||
channelType: AddonChannelType.Stable,
|
||||
downloadUrl: asset.browser_download_url,
|
||||
folders: [addonName],
|
||||
gameVersion: "",
|
||||
version: asset.name,
|
||||
releaseDate: new Date(asset.created_at),
|
||||
};
|
||||
|
||||
return potentialAddon;
|
||||
var searchResult: AddonSearchResult = {
|
||||
author: author,
|
||||
externalId: addonId,
|
||||
externalUrl: repository.html_url,
|
||||
files: [searchResultFile],
|
||||
name: addonName,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: authorImageUrl,
|
||||
};
|
||||
|
||||
return searchResult;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
return addonUri.host && addonUri.host.endsWith("github.com");
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {}
|
||||
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<void> {}
|
||||
|
||||
private getLatestRelease(releases: GitHubRelease[]): GitHubRelease {
|
||||
let sortedReleases = _.filter(releases, (r) => !r.draft);
|
||||
sortedReleases = _.sortBy(
|
||||
sortedReleases,
|
||||
(release) => new Date(release.published_at)
|
||||
).reverse();
|
||||
|
||||
return _.first(sortedReleases);
|
||||
}
|
||||
|
||||
private getValidAsset(
|
||||
release: GitHubRelease,
|
||||
clientType: WowClientType
|
||||
): GitHubAsset {
|
||||
const sortedAssets = _.filter(
|
||||
release.assets,
|
||||
(asset) =>
|
||||
this.isNotNoLib(asset) &&
|
||||
this.isValidContentType(asset) &&
|
||||
this.isValidClientType(clientType, asset)
|
||||
);
|
||||
|
||||
return _.first(sortedAssets);
|
||||
}
|
||||
|
||||
private isNotNoLib(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().indexOf("-nolib") === -1;
|
||||
}
|
||||
|
||||
private isValidContentType(asset: GitHubAsset): boolean {
|
||||
return RELEASE_CONTENT_TYPES.some((ct) => ct == asset.content_type);
|
||||
}
|
||||
|
||||
private isValidClientType(
|
||||
clientType: WowClientType,
|
||||
asset: GitHubAsset
|
||||
): boolean {
|
||||
const isClassic = this.isClassicAsset(asset);
|
||||
|
||||
switch (clientType) {
|
||||
case WowClientType.Retail:
|
||||
case WowClientType.RetailPtr:
|
||||
case WowClientType.Beta:
|
||||
return !isClassic;
|
||||
case WowClientType.Classic:
|
||||
case WowClientType.ClassicPtr:
|
||||
return isClassic;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async searchByName(addonName: string, folderName: string, clientType: WowClientType, nameOverride?: string): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
private isClassicAsset(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().endsWith("-classic.zip");
|
||||
}
|
||||
|
||||
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
|
||||
return forkJoin([
|
||||
this.getReleases(addonId),
|
||||
this.getRepository(addonId)
|
||||
])
|
||||
.pipe(
|
||||
map(([releases, repository]) => {
|
||||
if (!releases?.length) {
|
||||
return undefined;
|
||||
}
|
||||
private getAddonName(addonId: string): string {
|
||||
return addonId.split("/").filter((str) => !!str)[1];
|
||||
}
|
||||
|
||||
const latestRelease = this.getLatestRelease(releases);
|
||||
if (!latestRelease) {
|
||||
return undefined;
|
||||
}
|
||||
private getReleases(repositoryPath: string): Observable<GitHubRelease[]> {
|
||||
const url = `${API_URL}${repositoryPath}/releases`;
|
||||
return this._httpClient.get<GitHubRelease[]>(url.toString());
|
||||
}
|
||||
|
||||
const asset = this.getValidAsset(latestRelease, clientType);
|
||||
if (!asset) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const author = repository.owner.login;
|
||||
const authorImageUrl = repository.owner.avatar_url;
|
||||
const addonName = this.getAddonName(addonId);
|
||||
|
||||
var searchResultFile: AddonSearchResultFile = {
|
||||
channelType: AddonChannelType.Stable,
|
||||
downloadUrl: asset.browser_download_url,
|
||||
folders: [addonName],
|
||||
gameVersion: '',
|
||||
version: asset.name,
|
||||
releaseDate: new Date(asset.created_at)
|
||||
};
|
||||
|
||||
var searchResult: AddonSearchResult = {
|
||||
author: author,
|
||||
externalId: addonId,
|
||||
externalUrl: asset.url,
|
||||
files: [searchResultFile],
|
||||
name: addonName,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: authorImageUrl
|
||||
};
|
||||
|
||||
return searchResult;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
return addonUri.host &&
|
||||
addonUri.host.endsWith("github.com");
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {
|
||||
}
|
||||
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<void> {
|
||||
}
|
||||
|
||||
private getLatestRelease(releases: GitHubRelease[]): GitHubRelease {
|
||||
let sortedReleases = _.filter(releases, r => !r.draft);
|
||||
sortedReleases = _.sortBy(sortedReleases, release => new Date(release.published_at))
|
||||
.reverse();
|
||||
|
||||
return _.first(sortedReleases);
|
||||
}
|
||||
|
||||
private getValidAsset(release: GitHubRelease, clientType: WowClientType): GitHubAsset {
|
||||
const sortedAssets = _.filter(
|
||||
release.assets,
|
||||
asset => this.isNotNoLib(asset) &&
|
||||
this.isValidContentType(asset) &&
|
||||
this.isValidClientType(clientType, asset));
|
||||
|
||||
return _.first(sortedAssets);
|
||||
}
|
||||
|
||||
private isNotNoLib(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().indexOf("-nolib") === -1;
|
||||
}
|
||||
|
||||
private isValidContentType(asset: GitHubAsset): boolean {
|
||||
return RELEASE_CONTENT_TYPES.some(ct => ct == asset.content_type);
|
||||
}
|
||||
|
||||
private isValidClientType(clientType: WowClientType, asset: GitHubAsset): boolean {
|
||||
const isClassic = this.isClassicAsset(asset);
|
||||
|
||||
switch (clientType) {
|
||||
case WowClientType.Retail:
|
||||
case WowClientType.RetailPtr:
|
||||
case WowClientType.Beta:
|
||||
return !isClassic;
|
||||
case WowClientType.Classic:
|
||||
case WowClientType.ClassicPtr:
|
||||
return isClassic;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isClassicAsset(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().endsWith("-classic.zip");
|
||||
}
|
||||
|
||||
private getAddonName(addonId: string): string {
|
||||
return addonId.split("/").filter(str => !!str)[1];
|
||||
}
|
||||
|
||||
private getReleases(repositoryPath: string): Observable<GitHubRelease[]> {
|
||||
const url = `${API_URL}${repositoryPath}/releases`;
|
||||
|
||||
return this._httpClient.get<GitHubRelease[]>(url.toString());
|
||||
}
|
||||
|
||||
private getRepository(repositoryPath: string): Observable<GitHubRepository> {
|
||||
const url = `${API_URL}${repositoryPath}`;
|
||||
return this._httpClient.get<GitHubRepository>(url.toString());
|
||||
}
|
||||
|
||||
}
|
||||
private getRepository(repositoryPath: string): Observable<GitHubRepository> {
|
||||
const url = `${API_URL}${repositoryPath}`;
|
||||
return this._httpClient.get<GitHubRepository>(url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,51 +5,58 @@ import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { CachingService } from "app/services/caching/caching-service";
|
||||
import { ElectronService } from "app/services/electron/electron.service";
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import * as _ from 'lodash';
|
||||
import * as _ from "lodash";
|
||||
import { AddonSearchResultFile } from "app/models/wowup/addon-search-result-file";
|
||||
import { map } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as CircuitBreaker from 'opossum';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
|
||||
const API_URL = "https://www.tukui.org/api.php";
|
||||
const CLIENT_API_URL = "https://www.tukui.org/client-api.php";
|
||||
const CACHE_TIME = 10 * 60 * 1000;
|
||||
|
||||
export class TukUiAddonProvider implements AddonProvider {
|
||||
private readonly _circuitBreaker: CircuitBreaker<
|
||||
[clientType: WowClientType],
|
||||
TukUiAddon[]
|
||||
>;
|
||||
|
||||
public readonly name = "TukUI";
|
||||
|
||||
private readonly _circuitBreaker: CircuitBreaker<[clientType: WowClientType], TukUiAddon[]>;
|
||||
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) {
|
||||
this._circuitBreaker = new CircuitBreaker(
|
||||
this.fetchApiResults,
|
||||
{
|
||||
resetTimeout: 60000
|
||||
});
|
||||
this._circuitBreaker = new CircuitBreaker(this.fetchApiResults, {
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
|
||||
this._circuitBreaker.on('open', () => { console.log(`${this.name} circuit breaker open`); });
|
||||
this._circuitBreaker.on('close', () => { console.log(`${this.name} circuit breaker close`); });
|
||||
this._circuitBreaker.on("open", () => {
|
||||
console.log(`${this.name} circuit breaker open`);
|
||||
});
|
||||
this._circuitBreaker.on("close", () => {
|
||||
console.log(`${this.name} circuit breaker close`);
|
||||
});
|
||||
}
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
): Promise<AddonSearchResult[]> {
|
||||
let results: AddonSearchResult[] = [];
|
||||
|
||||
try {
|
||||
const addons = await this.getAllAddons(clientType);
|
||||
results = addons.filter(addon => _.some(addonIds, aid => aid === addon.id))
|
||||
.map(addon => this.toSearchResult(addon, ''));
|
||||
results = addons
|
||||
.filter((addon) => _.some(addonIds, (aid) => aid === addon.id))
|
||||
.map((addon) => this.toSearchResult(addon, ""));
|
||||
} catch (err) {
|
||||
// _analyticsService.Track(ex, "Failed to search TukUi");
|
||||
}
|
||||
@@ -57,29 +64,41 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
return results;
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return from(this.getAllAddons(clientType))
|
||||
.pipe(
|
||||
map(tukUiAddons => {
|
||||
return tukUiAddons.map(addon => this.toPotentialAddon(addon));
|
||||
})
|
||||
);
|
||||
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<PotentialAddon[]> {
|
||||
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);
|
||||
similarAddons = _.orderBy(similarAddons, ['downloads']);
|
||||
let similarAddons = _.filter(
|
||||
addons,
|
||||
(addon) => addon.name.toLowerCase().indexOf(canonQuery) !== -1
|
||||
);
|
||||
similarAddons = _.orderBy(similarAddons, ["downloads"]);
|
||||
|
||||
return _.map(similarAddons, addon => this.toPotentialAddon(addon));
|
||||
return _.map(similarAddons, (addon) => this.toSearchResult(addon));
|
||||
}
|
||||
|
||||
searchByUrl(addonUri: URL, clientType: WowClientType): Promise<PotentialAddon> {
|
||||
searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async searchByName(addonName: string, folderName: string, clientType: WowClientType, nameOverride?: string): Promise<AddonSearchResult[]> {
|
||||
async searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
nameOverride?: string
|
||||
): Promise<AddonSearchResult[]> {
|
||||
const results: AddonSearchResult[] = [];
|
||||
try {
|
||||
const addons = await this.searchAddons(addonName, clientType);
|
||||
@@ -94,31 +113,42 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
return results;
|
||||
}
|
||||
|
||||
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult | undefined> {
|
||||
return from(this.getAllAddons(clientType))
|
||||
.pipe(
|
||||
map(addons => {
|
||||
const match = _.find(addons, addon => addon.id === addonId);
|
||||
return this.toSearchResult(match, '');
|
||||
})
|
||||
)
|
||||
getById(
|
||||
addonId: string,
|
||||
clientType: WowClientType
|
||||
): Observable<AddonSearchResult | undefined> {
|
||||
return from(this.getAllAddons(clientType)).pipe(
|
||||
map((addons) => {
|
||||
const match = _.find(addons, (addon) => addon.id === addonId);
|
||||
return this.toSearchResult(match, "");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {
|
||||
}
|
||||
onPostInstall(addon: Addon): void {}
|
||||
|
||||
async scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise<void> {
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<void> {
|
||||
const allAddons = await this.getAllAddons(clientType);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -142,43 +172,37 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
latestVersion: tukUiAddon.version,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: tukUiAddon.screenshot_url,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
updatedAt: new Date(),
|
||||
summary: tukUiAddon.small_desc,
|
||||
downloadCount: Number.parseFloat(tukUiAddon.downloads),
|
||||
screenshotUrls: [tukUiAddon.screenshot_url],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 toPotentialAddon(addon: TukUiAddon): PotentialAddon {
|
||||
return {
|
||||
author: addon.author,
|
||||
downloadCount: parseInt(addon.downloads, 10),
|
||||
externalId: addon.id,
|
||||
externalUrl: addon.web_url,
|
||||
name: addon.name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: addon.screenshot_url,
|
||||
summary: addon.small_desc
|
||||
};
|
||||
}
|
||||
|
||||
private toSearchResult(addon: TukUiAddon, folderName: string): AddonSearchResult | undefined {
|
||||
private toSearchResult(
|
||||
addon: TukUiAddon,
|
||||
folderName?: string
|
||||
): AddonSearchResult | undefined {
|
||||
if (!addon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var latestFile: AddonSearchResultFile = {
|
||||
channelType: AddonChannelType.Stable,
|
||||
folders: [folderName],
|
||||
folders: folderName ? [folderName] : [],
|
||||
downloadUrl: addon.url,
|
||||
gameVersion: addon.patch,
|
||||
version: addon.version,
|
||||
releaseDate: new Date(addon.lastUpdate)
|
||||
releaseDate: new Date(addon.lastUpdate),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -188,11 +212,14 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
thumbnailUrl: addon.screenshot_url,
|
||||
externalUrl: addon.web_url,
|
||||
providerName: this.name,
|
||||
files: [latestFile]
|
||||
downloadCount: parseInt(addon.downloads, 10),
|
||||
files: [latestFile],
|
||||
};
|
||||
}
|
||||
|
||||
private getAllAddons = async (clientType: WowClientType): Promise<TukUiAddon[]> => {
|
||||
private getAllAddons = async (
|
||||
clientType: WowClientType
|
||||
): Promise<TukUiAddon[]> => {
|
||||
if (clientType === WowClientType.None) {
|
||||
return [];
|
||||
}
|
||||
@@ -206,40 +233,42 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
try {
|
||||
const addons = await this._circuitBreaker.fire(clientType);
|
||||
|
||||
console.log('CACHED')
|
||||
console.log("CACHED");
|
||||
this._cachingService.set(cacheKey, addons, CACHE_TIME);
|
||||
return addons;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private fetchApiResults = async (clientType: WowClientType) => {
|
||||
const query = this.getAddonsSuffix(clientType);
|
||||
const url = new URL(API_URL);
|
||||
url.searchParams.append(query, 'all');
|
||||
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());
|
||||
}
|
||||
|
||||
return addons;
|
||||
}
|
||||
};
|
||||
|
||||
private getTukUiRetailAddon() {
|
||||
return this.getClientApiAddon('tukui');
|
||||
return this.getClientApiAddon("tukui");
|
||||
}
|
||||
|
||||
private getElvUiRetailAddon() {
|
||||
return this.getClientApiAddon('elvui');
|
||||
return this.getClientApiAddon("elvui");
|
||||
}
|
||||
|
||||
private getClientApiAddon(addonName: string): Observable<TukUiAddon> {
|
||||
const url = new URL(CLIENT_API_URL);
|
||||
url.searchParams.append('ui', addonName);
|
||||
url.searchParams.append("ui", addonName);
|
||||
|
||||
return this._httpClient.get<TukUiAddon>(url.toString());
|
||||
}
|
||||
@@ -265,7 +294,7 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
case WowClientType.Beta:
|
||||
return "addons";
|
||||
default:
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +308,7 @@ export class TukUiAddonProvider implements AddonProvider {
|
||||
case WowClientType.Beta:
|
||||
return "tukui_addons";
|
||||
default:
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,26 @@ import { AddonDetailsResponse } from "app/models/wow-interface/addon-details-res
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { ElectronService } from "app/services";
|
||||
import { CachingService } from "app/services/caching/caching-service";
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AddonProvider } from "./addon-provider";
|
||||
import * as _ from 'lodash';
|
||||
import * as _ from "lodash";
|
||||
import { AddonSearchResultFile } from "app/models/wowup/addon-search-result-file";
|
||||
import { map } from "rxjs/operators";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as CircuitBreaker from "opossum";
|
||||
|
||||
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
|
||||
>;
|
||||
|
||||
public readonly name = "WowInterface";
|
||||
|
||||
constructor(
|
||||
@@ -27,9 +32,23 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) { }
|
||||
) {
|
||||
this._circuitBreaker = new CircuitBreaker(this.getAddonDetails, {
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
this._circuitBreaker.on("open", () => {
|
||||
console.log(`${this.name} circuit breaker open`);
|
||||
});
|
||||
this._circuitBreaker.on("close", () => {
|
||||
console.log(`${this.name} circuit breaker close`);
|
||||
});
|
||||
}
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
): Promise<AddonSearchResult[]> {
|
||||
var searchResults: AddonSearchResult[] = [];
|
||||
|
||||
for (let addonId of addonIds) {
|
||||
@@ -44,55 +63,84 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return of([]);
|
||||
}
|
||||
|
||||
async searchByQuery(query: string, clientType: WowClientType): Promise<PotentialAddon[]> {
|
||||
public async getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByUrl(addonUri: URL, clientType: WowClientType): Promise<PotentialAddon> {
|
||||
async searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult> {
|
||||
const addonId = this.getAddonId(addonUri);
|
||||
if (!addonId) {
|
||||
throw new Error(`Addon ID not found ${addonUri}`);
|
||||
}
|
||||
|
||||
var addon = await this.getAddonDetails(addonId).toPromise();
|
||||
var addon = await this._circuitBreaker.fire(addonId);
|
||||
if (addon == null) {
|
||||
throw new Error(`Bad addon api response ${addonUri}`);
|
||||
}
|
||||
|
||||
return this.toPotentialAddon(addon);
|
||||
return this.toAddonSearchResult(addon);
|
||||
}
|
||||
|
||||
searchByName(addonName: string, folderName: string, clientType: WowClientType, nameOverride?: string): Promise<AddonSearchResult[]> {
|
||||
searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
nameOverride?: string
|
||||
): Promise<AddonSearchResult[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getById(addonId: string, clientType: WowClientType): Observable<AddonSearchResult> {
|
||||
return from(this.getAddonDetails(addonId))
|
||||
.pipe(
|
||||
map(result => result ? this.toAddonSearchResult(result, '') : undefined)
|
||||
);
|
||||
getById(
|
||||
addonId: string,
|
||||
clientType: WowClientType
|
||||
): Observable<AddonSearchResult> {
|
||||
return from(this._circuitBreaker.fire(addonId)).pipe(
|
||||
map((result) =>
|
||||
result ? this.toAddonSearchResult(result, "") : undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
return addonUri.host && addonUri.host.endsWith('wowinterface.com');
|
||||
return addonUri.host && addonUri.host.endsWith("wowinterface.com");
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise<void> {
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<void> {
|
||||
for (let addonFolder of addonFolders) {
|
||||
if (!addonFolder?.toc?.wowInterfaceId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const details = await this.getAddonDetails(addonFolder.toc.wowInterfaceId).toPromise();
|
||||
addonFolder.matchingAddon = this.toAddon(details, clientType, addonChannelType, addonFolder);
|
||||
const details = await this._circuitBreaker.fire(
|
||||
addonFolder.toc.wowInterfaceId
|
||||
);
|
||||
|
||||
addonFolder.matchingAddon = this.toAddon(
|
||||
details,
|
||||
clientType,
|
||||
addonChannelType,
|
||||
addonFolder
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,14 +151,16 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
private getAddonDetails(addonId: string): Observable<AddonDetailsResponse> {
|
||||
private getAddonDetails = (
|
||||
addonId: string
|
||||
): Promise<AddonDetailsResponse> => {
|
||||
console.debug("getAddonDetails");
|
||||
const url = new URL(`${API_URL}/filedetails/${addonId}.json`);
|
||||
|
||||
return this._httpClient
|
||||
.get<AddonDetailsResponse[]>(url.toString())
|
||||
.pipe(
|
||||
map(responses => _.first(responses))
|
||||
);
|
||||
.pipe(map((responses) => _.first(responses)))
|
||||
.toPromise();
|
||||
};
|
||||
|
||||
private getThumbnailUrl(response: AddonDetailsResponse) {
|
||||
@@ -137,7 +187,7 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
externalId: response.id.toString(),
|
||||
externalUrl: this.getAddonUrl(response),
|
||||
folderName: addonFolder.name,
|
||||
gameVersion: '',
|
||||
gameVersion: "",
|
||||
installedAt: new Date(),
|
||||
installedFolders: addonFolder.name,
|
||||
installedVersion: addonFolder.toc?.version,
|
||||
@@ -145,31 +195,25 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
latestVersion: response.version,
|
||||
name: response.title,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: this.getThumbnailUrl(response)
|
||||
};
|
||||
}
|
||||
|
||||
private toPotentialAddon(response: AddonDetailsResponse): PotentialAddon {
|
||||
return {
|
||||
providerName: this.name,
|
||||
name: response.title,
|
||||
downloadCount: response.downloads,
|
||||
thumbnailUrl: this.getThumbnailUrl(response),
|
||||
externalId: response.id.toString(),
|
||||
externalUrl: this.getAddonUrl(response),
|
||||
author: response.author,
|
||||
summary: response.description,
|
||||
screenshotUrls: response.images?.map((img) => img.imageUrl),
|
||||
downloadCount: response.downloads,
|
||||
};
|
||||
}
|
||||
|
||||
private toAddonSearchResult(response: AddonDetailsResponse, folderName: string): AddonSearchResult {
|
||||
private toAddonSearchResult(
|
||||
response: AddonDetailsResponse,
|
||||
folderName?: string
|
||||
): AddonSearchResult {
|
||||
try {
|
||||
var searchResultFile: AddonSearchResultFile = {
|
||||
channelType: AddonChannelType.Stable,
|
||||
version: response.version,
|
||||
downloadUrl: response.downloadUri,
|
||||
folders: [folderName],
|
||||
gameVersion: '',
|
||||
releaseDate: new Date()
|
||||
folders: folderName ? [folderName] : [],
|
||||
gameVersion: "",
|
||||
releaseDate: new Date(),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -179,10 +223,11 @@ export class WowInterfaceAddonProvider implements AddonProvider {
|
||||
thumbnailUrl: this.getThumbnailUrl(response),
|
||||
externalUrl: this.getAddonUrl(response),
|
||||
providerName: this.name,
|
||||
files: [searchResultFile]
|
||||
downloadCount: response.downloads,
|
||||
files: [searchResultFile],
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to create addon search result', err);
|
||||
console.error("Failed to create addon search result", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { Addon } from "../entities/addon";
|
||||
import { WowClientType } from "../models/warcraft/wow-client-type";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { PotentialAddon } from "../models/wowup/potential-addon";
|
||||
import { AddonProvider, AddonProviderType } from "./addon-provider";
|
||||
import { WowUpAddonRepresentation } from "../models/wowup-api/wowup-addon.representation";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
@@ -39,45 +38,54 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
.get<WowUpAddonRepresentation[]>(url.toString())
|
||||
.toPromise();
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return of([]);
|
||||
public async getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Promise<AddonSearchResult[]> {
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
async searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
): Promise<AddonSearchResult[]> {
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
searchByUrl(
|
||||
async searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon> {
|
||||
throw new Error("Method not implemented.");
|
||||
): Promise<AddonSearchResult> {
|
||||
// TODO
|
||||
return undefined;
|
||||
}
|
||||
|
||||
searchByName(
|
||||
async searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
nameOverride?: string
|
||||
): Promise<AddonSearchResult[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
return [];
|
||||
}
|
||||
|
||||
getById(
|
||||
addonId: string,
|
||||
clientType: WowClientType
|
||||
): Observable<AddonSearchResult> {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {
|
||||
@@ -255,7 +263,7 @@ export class WowUpAddonProvider implements AddonProvider {
|
||||
thumbnailUrl: scanResult.exactMatch.image_url,
|
||||
patreonFundingLink: scanResult.exactMatch.patreon_funding_link,
|
||||
customFundingLink: scanResult.exactMatch.custom_funding_link,
|
||||
githubFundingLink: scanResult.exactMatch.github_funding_link
|
||||
githubFundingLink: scanResult.exactMatch.github_funding_link,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetr
|
||||
import { ElectronService } from "./services";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { FileService } from "./services/files/file.service";
|
||||
import { WarcraftService } from "./services/warcraft/warcraft.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
|
||||
@@ -21,7 +22,8 @@ export class AppComponent implements AfterViewInit {
|
||||
|
||||
constructor(
|
||||
private _analyticsService: AnalyticsService,
|
||||
private electronService: ElectronService,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService,
|
||||
private translate: TranslateService,
|
||||
private warcraft: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
@@ -30,14 +32,14 @@ export class AppComponent implements AfterViewInit {
|
||||
) {
|
||||
this.translate.setDefaultLang("en");
|
||||
|
||||
this.translate.use(this.electronService.locale);
|
||||
this.translate.use(this._electronService.locale);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this._analyticsService.shouldPromptTelemetry) {
|
||||
this.openDialog();
|
||||
} else {
|
||||
// TODO track startup
|
||||
this._analyticsService.trackStartup();
|
||||
}
|
||||
|
||||
this.onAutoUpdateInterval();
|
||||
@@ -53,12 +55,28 @@ export class AppComponent implements AfterViewInit {
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this._wowUpService.telemetryEnabled = result;
|
||||
this._analyticsService.telemetryEnabled = result;
|
||||
if (result) {
|
||||
this._analyticsService.trackStartup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onAutoUpdateInterval = async () => {
|
||||
console.log("Auto update");
|
||||
const updateCount = await this._addonService.processAutoUpdates();
|
||||
|
||||
if (updateCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iconPath = await this._fileService.getAssetFilePath(
|
||||
"wowup_logo_512np.png"
|
||||
);
|
||||
|
||||
this._electronService.showNotification("Auto Updates", {
|
||||
body: `Automatically updated ${updateCount} addons.`,
|
||||
icon: iconPath,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
HTTP_INTERCEPTORS,
|
||||
} from "@angular/common/http";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
|
||||
import { ErrorHandlerIntercepter } from './interceptors/error-handler-intercepter';
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
@@ -28,6 +28,8 @@ import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { DirectiveModule } from "./directive.module";
|
||||
import { MatModule } from "./mat-module";
|
||||
import { MatProgressButtonsModule } from "mat-progress-buttons";
|
||||
import { ElectronService } from "./services";
|
||||
import { PreferenceStorageService } from "./services/storage/preference-storage.service";
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
@@ -61,8 +63,8 @@ export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
useClass: DefaultHeadersInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: AnalyticsService },
|
||||
{ provide: ErrorHandler, useClass: ErrorHandlerIntercepter, deps: [AnalyticsService] },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { AddonSearchResultFile } from "app/models/wowup/addon-search-result-file";
|
||||
import * as _ from "lodash";
|
||||
|
||||
export class GetAddonListItem {
|
||||
public readonly searchResult: AddonSearchResult;
|
||||
|
||||
get downloadCount() {
|
||||
return this.searchResult.downloadCount || 0;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.searchResult.name;
|
||||
}
|
||||
|
||||
get thumbnailUrl() {
|
||||
return this.searchResult.thumbnailUrl;
|
||||
}
|
||||
|
||||
get author() {
|
||||
return this.searchResult.author;
|
||||
}
|
||||
|
||||
get providerName() {
|
||||
return this.searchResult.providerName;
|
||||
}
|
||||
|
||||
constructor(searchResult: AddonSearchResult) {
|
||||
this.searchResult = searchResult;
|
||||
}
|
||||
|
||||
public getLatestFile(channel: AddonChannelType): AddonSearchResultFile {
|
||||
return _.find(this.searchResult.files, (f) => f.channelType <= channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Addon } from "app/entities/addon";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonDisplayState } from "../models/wowup/addon-display-state";
|
||||
|
||||
export class AddonViewModel {
|
||||
public readonly addon: Addon;
|
||||
|
||||
isInstalling: boolean = false;
|
||||
installProgress: number = 0;
|
||||
statusText: string = "";
|
||||
selected: boolean = false;
|
||||
|
||||
get needsInstall() {
|
||||
return (
|
||||
!this.isInstalling && this.displayState === AddonDisplayState.Install
|
||||
);
|
||||
}
|
||||
|
||||
get needsUpdate() {
|
||||
return !this.isInstalling && this.displayState === AddonDisplayState.Update;
|
||||
}
|
||||
|
||||
get isAutoUpdate() {
|
||||
return this.addon.autoUpdateEnabled;
|
||||
}
|
||||
|
||||
get isUpToDate() {
|
||||
return (
|
||||
!this.isInstalling && this.displayState === AddonDisplayState.UpToDate
|
||||
);
|
||||
}
|
||||
|
||||
get isIgnored() {
|
||||
return this.displayState === AddonDisplayState.Ignored;
|
||||
}
|
||||
|
||||
get isStableChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Stable;
|
||||
}
|
||||
|
||||
get isBetaChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Beta;
|
||||
}
|
||||
|
||||
get isAlphaChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Alpha;
|
||||
}
|
||||
|
||||
get displayState(): AddonDisplayState {
|
||||
if (this.addon.isIgnored) {
|
||||
return AddonDisplayState.Ignored;
|
||||
}
|
||||
|
||||
if (!this.addon.installedVersion) {
|
||||
return AddonDisplayState.Install;
|
||||
}
|
||||
|
||||
if (this.addon.installedVersion !== this.addon.latestVersion) {
|
||||
return AddonDisplayState.Update;
|
||||
}
|
||||
|
||||
if (this.addon.installedVersion === this.addon.latestVersion) {
|
||||
return AddonDisplayState.UpToDate;
|
||||
}
|
||||
|
||||
return AddonDisplayState.Unknown;
|
||||
}
|
||||
|
||||
constructor(addon?: Addon) {
|
||||
this.addon = addon;
|
||||
this.statusText = this.getStateText();
|
||||
}
|
||||
|
||||
public onClicked() {
|
||||
this.selected = !this.selected;
|
||||
}
|
||||
|
||||
public getStateText() {
|
||||
switch (this.displayState) {
|
||||
case AddonDisplayState.UpToDate:
|
||||
return "Up to Date";
|
||||
case AddonDisplayState.Ignored:
|
||||
return "Ignored";
|
||||
case AddonDisplayState.Update:
|
||||
case AddonDisplayState.Install:
|
||||
return "Install";
|
||||
case AddonDisplayState.Unknown:
|
||||
default:
|
||||
console.log("Unhandled display state", this.displayState);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Addon } from "app/entities/addon";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonDisplayState } from "../models/wowup/addon-display-state";
|
||||
|
||||
export class MyAddonsListItem {
|
||||
addon: Addon;
|
||||
|
||||
isInstalling: boolean = false;
|
||||
installProgress: number = 0;
|
||||
statusText: string = '';
|
||||
selected: boolean = false;
|
||||
|
||||
get needsInstall() {
|
||||
return !this.isInstalling && this.displayState === AddonDisplayState.Install;
|
||||
}
|
||||
|
||||
get needsUpdate() {
|
||||
return !this.isInstalling && this.displayState === AddonDisplayState.Update;
|
||||
}
|
||||
|
||||
get isUpToDate() {
|
||||
return !this.isInstalling && this.displayState === AddonDisplayState.UpToDate;
|
||||
}
|
||||
|
||||
get isIgnored() {
|
||||
return this.displayState === AddonDisplayState.Ignored;
|
||||
}
|
||||
|
||||
get isStableChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Stable;
|
||||
}
|
||||
|
||||
get isBetaChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Beta;
|
||||
}
|
||||
|
||||
get isAlphaChannel() {
|
||||
return this.addon.channelType === AddonChannelType.Alpha;
|
||||
}
|
||||
|
||||
get displayState(): AddonDisplayState {
|
||||
if (this.addon.isIgnored) {
|
||||
return AddonDisplayState.Ignored;
|
||||
}
|
||||
|
||||
if (!this.addon.installedVersion) {
|
||||
return AddonDisplayState.Install;
|
||||
}
|
||||
|
||||
if (this.addon.installedVersion !== this.addon.latestVersion) {
|
||||
return AddonDisplayState.Update;
|
||||
}
|
||||
|
||||
if (this.addon.installedVersion === this.addon.latestVersion) {
|
||||
return AddonDisplayState.UpToDate;
|
||||
}
|
||||
|
||||
return AddonDisplayState.Unknown;
|
||||
}
|
||||
|
||||
constructor(addon?: Addon) {
|
||||
this.addon = addon;
|
||||
this.statusText = this.getStateText();
|
||||
}
|
||||
|
||||
public onClicked() {
|
||||
this.selected = !this.selected;
|
||||
}
|
||||
|
||||
public getStateText() {
|
||||
switch (this.displayState) {
|
||||
case AddonDisplayState.UpToDate:
|
||||
return "Up to Date";
|
||||
case AddonDisplayState.Ignored:
|
||||
return "Ignored";
|
||||
case AddonDisplayState.Update:
|
||||
case AddonDisplayState.Install:
|
||||
case AddonDisplayState.Unknown:
|
||||
default:
|
||||
console.log('Unhandled display state', this.displayState)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,40 @@
|
||||
<div class="addon-detail-view">
|
||||
<div class="addon-detail-title">
|
||||
<img mat-card-avatar class="addon-header-thumbnail" [src]="addonDetail.thumbnailUrl" />
|
||||
<h2 mat-dialog-title>{{ addonDetail.name }}</h2>
|
||||
<img
|
||||
mat-card-avatar
|
||||
class="addon-header-thumbnail"
|
||||
[src]="addon.thumbnailUrl"
|
||||
/>
|
||||
<h2 mat-dialog-title>{{ addon.name }}</h2>
|
||||
<button mat-icon-button [mat-dialog-close]="true">
|
||||
<mat-icon class="close-icon" color="accent">close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="addon-detail-subtitle">
|
||||
<div class="mat-subheading-1">By {{ addonDetail.author }}</div>
|
||||
<app-addon-provider-badge [providerName]="addonDetail.providerName"></app-addon-provider-badge>
|
||||
<div class="mat-subheading-1">By {{ addon.author }}</div>
|
||||
<app-addon-provider-badge
|
||||
[providerName]="addon.providerName"
|
||||
></app-addon-provider-badge>
|
||||
</div>
|
||||
<mat-dialog-content>
|
||||
<div class="mat-caption addon-detail-summary">
|
||||
{{ addonDetail.summary }}
|
||||
{{ addon.summary }}
|
||||
</div>
|
||||
<div class="screenshot-container">
|
||||
<img [src]="defaultImageUrl" alt="Addon Picture" />
|
||||
</div>
|
||||
<p>
|
||||
{{ addonDetail.description }}
|
||||
</p>
|
||||
<img mat-card-image [src]="defaultImageUrl" alt="Addon Picture" />
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="dialog-buttons">
|
||||
<a
|
||||
appExternalLink
|
||||
mat-stroked-button
|
||||
cdkFocusInitial
|
||||
color="primary"
|
||||
[href]="addon.externalUrl"
|
||||
>{{ "DIALOGS.ADDON_DETAILS.VIEW_IN_BROWSER_BUTTON" | translate }}</a
|
||||
>
|
||||
<div class="btn-wrapper">
|
||||
<a appExternalLink mat-stroked-button color="primary" [href]="addonDetail.externalUrl">
|
||||
{{'DIALOGS.ADDON_DETAILS.VIEW_IN_BROWSER_BUTTON' | translate}}
|
||||
</a>
|
||||
<app-addon-install-button [addon]="addonDetail"> </app-addon-install-button>
|
||||
<app-addon-install-button [addon]="addon"> </app-addon-install-button>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
.addon-detail-view {
|
||||
width: 450px;
|
||||
width: 35vw;
|
||||
max-width: 500px;
|
||||
margin-top: 8px;
|
||||
|
||||
.addon-detail-title,
|
||||
.addon-detail-subtitle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h2 {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-icon-button .mat-button-wrapper,
|
||||
.close-icon {
|
||||
cursor: pointer !important;
|
||||
@@ -16,15 +22,20 @@
|
||||
}
|
||||
.addon-detail-summary {
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
.dialog-buttons {
|
||||
display:flex;
|
||||
display: flex;
|
||||
.btn-wrapper {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshot-container {
|
||||
img {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { MAT_DIALOG_DATA } from "@angular/material/dialog";
|
||||
import { AddonDetailModel } from "app/models/wowup/addon-detail.model";
|
||||
import { Addon } from "app/entities/addon";
|
||||
|
||||
export interface AddonDetailModel {
|
||||
author?: string;
|
||||
externalUrl?: string;
|
||||
name: string;
|
||||
providerName?: string;
|
||||
screenshotUrls?: string[];
|
||||
summary?: string;
|
||||
thumbnailUrl?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-addon-detail",
|
||||
@@ -8,23 +18,19 @@ import { AddonDetailModel } from "app/models/wowup/addon-detail.model";
|
||||
styleUrls: ["./addon-detail.component.scss"],
|
||||
})
|
||||
export class AddonDetailComponent implements OnInit {
|
||||
addonDetail: AddonDetailModel;
|
||||
public addon: AddonDetailModel;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: AddonDetailModel) {
|
||||
this.addonDetail = this.data;
|
||||
this.addon = this.data;
|
||||
}
|
||||
|
||||
get defaultImageUrl(): string {
|
||||
return this.addonDetail?.screenshotUrls && this.addonDetail?.screenshotUrls[0]
|
||||
? this.addonDetail?.screenshotUrls[0]
|
||||
: this.addonDetail?.thumbnailUrl
|
||||
? this.addonDetail?.thumbnailUrl
|
||||
return this.addon?.screenshotUrls && this.addon?.screenshotUrls[0]
|
||||
? this.addon?.screenshotUrls[0]
|
||||
: this.addon?.thumbnailUrl
|
||||
? this.addon?.thumbnailUrl
|
||||
: "";
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
installAddon() {}
|
||||
|
||||
openInBrowser() {}
|
||||
}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<mat-bar-button (btnClick)="onButtonClick()" [options]="buttonOptions" cdkFocusInitial></mat-bar-button>
|
||||
<mat-bar-button (btnClick)="onUninstallClick()" [options]="btnUninstallOptions" id="btn-addon-uninstall"
|
||||
*ngIf="canUninstall">
|
||||
</mat-bar-button>
|
||||
<mat-bar-button (btnClick)="onInstallUpdateClick()" [options]="btnInstallOptions" id="btn-addon-installorupdate">
|
||||
</mat-bar-button>
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { AddonDetailModel } from "app/models/wowup/addon-detail.model";
|
||||
import { AddonInstallState } from "app/models/wowup/addon-install-state";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { AddonViewModel } from "app/business-objects/my-addon-list-item";
|
||||
import { Addon } from "app/entities/addon";
|
||||
import { AddonDisplayState } from "app/models/wowup/addon-display-state";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { getEnumName } from "app/utils/enum.utils";
|
||||
import { MatProgressButtonOptions } from "mat-progress-buttons";
|
||||
import { Subscription } from "rxjs";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { ConfirmDialogComponent } from "../confirm-dialog/confirm-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-addon-install-button",
|
||||
@@ -15,37 +20,46 @@ import { filter, map } from "rxjs/operators";
|
||||
styleUrls: ["./addon-install-button.component.scss"],
|
||||
})
|
||||
export class AddonInstallButtonComponent implements OnInit, OnDestroy {
|
||||
@Input("addon") addon: PotentialAddon | AddonDetailModel;
|
||||
@Input() addon: Addon;
|
||||
@Input() hideUninstall = false;
|
||||
|
||||
isInstalled: boolean;
|
||||
canUninstall: boolean;
|
||||
hasUpdate: boolean;
|
||||
buttonOptions: MatProgressButtonOptions;
|
||||
addonModel: AddonViewModel;
|
||||
|
||||
isInstalled = false;
|
||||
btnUninstallOptions: MatProgressButtonOptions;
|
||||
btnInstallOptions: MatProgressButtonOptions;
|
||||
|
||||
private _subscriptions: Subscription[];
|
||||
|
||||
get canUninstall(): boolean {
|
||||
return this.isInstalled && !this.hideUninstall;
|
||||
}
|
||||
|
||||
get shouldDisableInstallButton(): boolean {
|
||||
return (
|
||||
(!this.addonModel.needsInstall && !this.addonModel.needsUpdate) ||
|
||||
this.addonModel.isInstalling
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _sessionService: SessionService
|
||||
private _sessionService: SessionService,
|
||||
private _dialog: MatDialog,
|
||||
private _translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.addonModel = new AddonViewModel(this.addon);
|
||||
this.isInstalled = this._addonService.isInstalled(
|
||||
this.addon.externalId,
|
||||
this._sessionService.selectedClientType
|
||||
);
|
||||
this.setButtonOptions();
|
||||
|
||||
const addonUpdateSubscription = this._addonService.addonInstalled$
|
||||
.pipe(
|
||||
filter((x) => x.addon.externalId === this.addon.externalId),
|
||||
map((event: AddonUpdateEvent) => {
|
||||
if (event.installState === AddonInstallState.Complete) {
|
||||
this.isInstalled = true;
|
||||
this.setButtonOptions();
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
.pipe(filter((x) => x.addon.externalId === this.addon.externalId))
|
||||
.subscribe((event) => this.onAddonUpdate(event));
|
||||
this._subscriptions = [addonUpdateSubscription];
|
||||
}
|
||||
|
||||
@@ -53,70 +67,130 @@ export class AddonInstallButtonComponent implements OnInit, OnDestroy {
|
||||
this._subscriptions.forEach((x) => x.unsubscribe());
|
||||
}
|
||||
|
||||
setButtonOptions(): void {
|
||||
if (!this.isInstalled) {
|
||||
this.buttonOptions = this.getBaseBtnOptions();
|
||||
} else if (this.isInstalled && !this.canUninstall) {
|
||||
this.buttonOptions = this.getInstalledBtnOptions();
|
||||
} else if (this.isInstalled && this.canUninstall) {
|
||||
this.buttonOptions = this.getUninstallBtnOptions();
|
||||
} else if (this.isInstalled && this.hasUpdate) {
|
||||
this.buttonOptions = this.getUpdateBtnOptions();
|
||||
onAddonUpdate(event: AddonUpdateEvent): void {
|
||||
const addonModel = new AddonViewModel(event.addon);
|
||||
addonModel.installProgress = event.progress;
|
||||
// addonModel.updateInstallState(event.installState);
|
||||
// addonModel.setStatusText(event.installState);
|
||||
this.addonModel = addonModel;
|
||||
|
||||
if (event.installState === 4) {
|
||||
this.setButtonOptions();
|
||||
} else {
|
||||
this.updateButtonOptions();
|
||||
}
|
||||
}
|
||||
|
||||
onButtonClick(): void {
|
||||
this.buttonOptions.active = true;
|
||||
this.buttonOptions.text = "Installing...";
|
||||
setButtonOptions(): void {
|
||||
this.btnInstallOptions = this.getBaseBtnOptions();
|
||||
this.btnUninstallOptions = this.getUninstallBtnOptions();
|
||||
|
||||
if (this.shouldDisableInstallButton) {
|
||||
this.btnInstallOptions.disabled = true;
|
||||
}
|
||||
if (!this.canUninstall) {
|
||||
this.btnUninstallOptions.disabled = true;
|
||||
this.btnUninstallOptions.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonOptions(): void {
|
||||
this.btnInstallOptions.active = this.addonModel.isInstalling;
|
||||
this.btnInstallOptions.value = this.addonModel.installProgress;
|
||||
this.btnInstallOptions.text = this.addonModel.isInstalling
|
||||
? this.getTranslatedStatusText()
|
||||
: this.getTranslatedStateText();
|
||||
}
|
||||
|
||||
onInstallUpdateClick(): void {
|
||||
this.btnInstallOptions.active = true;
|
||||
if (this.addonModel.needsUpdate) {
|
||||
this.updateAddon();
|
||||
} else if (this.addonModel.needsInstall) {
|
||||
this.installAddon();
|
||||
}
|
||||
}
|
||||
|
||||
onUninstallClick(): void {
|
||||
this.confirmRemoveAddon();
|
||||
}
|
||||
|
||||
private installAddon() {
|
||||
this._addonService.installPotentialAddon(
|
||||
this.addon as PotentialAddon,
|
||||
this._sessionService.selectedClientType,
|
||||
(state, progress) => {
|
||||
if (state === AddonInstallState.Complete) {
|
||||
this.isInstalled = true;
|
||||
this.setButtonOptions();
|
||||
}
|
||||
this.buttonOptions.value = progress;
|
||||
this.addonModel.addon as AddonSearchResult,
|
||||
this._sessionService.selectedClientType
|
||||
);
|
||||
}
|
||||
|
||||
private updateAddon() {
|
||||
this._addonService.installAddon(this.addonModel.addon.id);
|
||||
}
|
||||
|
||||
private confirmRemoveAddon() {
|
||||
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: this._translate.instant("DIALOGS.REMOVE_ADDON.TITLE"),
|
||||
message: this._translate.instant("DIALOGS.REMOVE_ADDON.MESSAGE", {
|
||||
addon: this.addon.name,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.btnUninstallOptions.active = true;
|
||||
this.btnUninstallOptions.text = this._translate.instant(
|
||||
"COMMON.ADDON_STATUS.UNINSTALLING"
|
||||
);
|
||||
this._addonService.removeAddon(this.addonModel.addon);
|
||||
// Parent component should listen to addon removed event and make changes.
|
||||
});
|
||||
}
|
||||
|
||||
private getTranslatedStatusText(): string {
|
||||
const status = this.addonModel.statusText;
|
||||
return this._translate.instant(
|
||||
`COMMON.ADDON_STATUS.${status.toUpperCase()}`
|
||||
);
|
||||
}
|
||||
|
||||
private getTranslatedStateText(): string {
|
||||
const state = this.addonModel.displayState;
|
||||
return this._translate.instant(
|
||||
`COMMON.ADDON_STATE.${getEnumName(
|
||||
AddonDisplayState,
|
||||
state
|
||||
).toUpperCase()}`
|
||||
);
|
||||
}
|
||||
|
||||
private getBaseBtnOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
active: false,
|
||||
text: "Install",
|
||||
active: this.addonModel.isInstalling,
|
||||
disabled: this.shouldDisableInstallButton,
|
||||
value: this.addonModel.installProgress,
|
||||
text: this.addonModel.isInstalling
|
||||
? this.getTranslatedStatusText()
|
||||
: this.getTranslatedStateText(),
|
||||
mode: "determinate",
|
||||
buttonColor: "primary",
|
||||
barColor: "accent",
|
||||
customClass: "install-button",
|
||||
raised: true,
|
||||
stroked: false,
|
||||
mode: "determinate",
|
||||
value: 0,
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
};
|
||||
}
|
||||
|
||||
private getInstalledBtnOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
...this.getBaseBtnOptions(),
|
||||
disabled: true,
|
||||
active: false,
|
||||
text: "Installed",
|
||||
};
|
||||
}
|
||||
|
||||
private getUninstallBtnOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
...this.getBaseBtnOptions(),
|
||||
text: "Uninstall",
|
||||
};
|
||||
}
|
||||
|
||||
private getUpdateBtnOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
...this.getBaseBtnOptions(),
|
||||
text: "Update",
|
||||
text: this._translate.instant("COMMON.ADDON_STATE.UNINSTALL"),
|
||||
mode: "indeterminate",
|
||||
buttonColor: "warn",
|
||||
barColor: "primary",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<footer class="bg-dark-4 text-light-2">
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev"
|
||||
matTooltip="Support WowUp on Patreon">
|
||||
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
|
||||
</a>
|
||||
<a appExternalLink class="discord-link" href="https://discord.gg/rk4F5aD" matTooltip="Chat with us on Discord">
|
||||
<img class="discord-img" src="assets/images/discord_logo_small.png" />
|
||||
</a>
|
||||
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
|
||||
<div class="row">
|
||||
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>
|
||||
<p>v{{wowUpService.applicationVersion}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev" matTooltip="Support WowUp on Patreon"
|
||||
appUserActionTracker category="Footer" action="Patreon">
|
||||
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
|
||||
</a>
|
||||
<a appExternalLink class="discord-link" href="https://discord.gg/rk4F5aD" matTooltip="Chat with us on Discord"
|
||||
appUserActionTracker category="Footer" action="Discord">
|
||||
<img class="discord-img" src="assets/images/discord_logo_small.png" />
|
||||
</a>
|
||||
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
|
||||
<div class="row">
|
||||
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>
|
||||
<p>v{{wowUpService.applicationVersion}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -37,6 +37,8 @@ footer {
|
||||
}
|
||||
|
||||
.discord-link {
|
||||
padding: 0 0.25em;
|
||||
|
||||
&:hover {
|
||||
background-color: $dark-3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<mat-bar-button (btnClick)="onInstallUpdateClick()" [options]="buttonOptions$ | async" class="install-button">
|
||||
</mat-bar-button>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GetAddonStatusColumnComponent } from './get-addon-status-column.component';
|
||||
|
||||
describe('GetAddonStatusColumnComponent', () => {
|
||||
let component: GetAddonStatusColumnComponent;
|
||||
let fixture: ComponentFixture<GetAddonStatusColumnComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ GetAddonStatusColumnComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GetAddonStatusColumnComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { GetAddonListItem } from "app/business-objects/get-addon-list-item";
|
||||
import { AddonInstallState } from "app/models/wowup/addon-install-state";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { MatProgressButtonOptions } from "mat-progress-buttons";
|
||||
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: "app-get-addon-status-column",
|
||||
templateUrl: "./get-addon-status-column.component.html",
|
||||
styleUrls: ["./get-addon-status-column.component.scss"],
|
||||
})
|
||||
export class GetAddonStatusColumnComponent implements OnInit, OnDestroy {
|
||||
@Input() listItem: GetAddonListItem;
|
||||
|
||||
private readonly _buttonOptionsSrc: BehaviorSubject<MatProgressButtonOptions>;
|
||||
private installState: AddonInstallState = AddonInstallState.Unknown;
|
||||
private installProgress: number = 0;
|
||||
|
||||
public readonly buttonOptions$: Observable<MatProgressButtonOptions>;
|
||||
|
||||
public get buttonText() {
|
||||
if (this.installState !== AddonInstallState.Unknown) {
|
||||
return this.getInstallStateText(this.installState);
|
||||
}
|
||||
|
||||
return this._translate.instant("COMMON.ADDON_STATE.INSTALL");
|
||||
}
|
||||
|
||||
public getInstallStateText(installState: AddonInstallState) {
|
||||
switch (installState) {
|
||||
case AddonInstallState.BackingUp:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.BACKINGUP");
|
||||
case AddonInstallState.Complete:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.COMPLETE");
|
||||
case AddonInstallState.Downloading:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.DOWNLOADING");
|
||||
case AddonInstallState.Installing:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.INSTALLING");
|
||||
case AddonInstallState.Pending:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.PENDING");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public get isButtonActive() {
|
||||
return (
|
||||
this.installState !== AddonInstallState.Unknown &&
|
||||
this.installState !== AddonInstallState.Complete
|
||||
);
|
||||
}
|
||||
|
||||
public get isButtonDisabled() {
|
||||
return this.installState === AddonInstallState.Complete;
|
||||
}
|
||||
|
||||
public getButtonOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
active: this.isButtonActive,
|
||||
disabled: this.isButtonDisabled,
|
||||
value: this.installProgress,
|
||||
text: this.buttonText,
|
||||
mode: "determinate",
|
||||
buttonColor: "primary",
|
||||
barColor: "accent",
|
||||
customClass: "install-button",
|
||||
raised: false,
|
||||
flat: true,
|
||||
stroked: false,
|
||||
fullWidth: false,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _sessionService: SessionService,
|
||||
private _translate: TranslateService
|
||||
) {
|
||||
this._buttonOptionsSrc = new BehaviorSubject<MatProgressButtonOptions>(
|
||||
this.getButtonOptions()
|
||||
);
|
||||
this.buttonOptions$ = this._buttonOptionsSrc.asObservable();
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._buttonOptionsSrc.complete();
|
||||
}
|
||||
|
||||
onInstallUpdateClick() {
|
||||
this._addonService.installPotentialAddon(
|
||||
this.listItem.searchResult,
|
||||
this._sessionService.selectedClientType,
|
||||
this.onInstallUpdate
|
||||
);
|
||||
}
|
||||
|
||||
private onInstallUpdate = (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => {
|
||||
this.installState = installState;
|
||||
this.installProgress = progress;
|
||||
this._buttonOptionsSrc.next(this.getButtonOptions());
|
||||
};
|
||||
}
|
||||
@@ -40,7 +40,8 @@
|
||||
<button mat-button (click)="onClose()">
|
||||
{{'DIALOGS.INSTALL_FROM_URL.CLOSE_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" cdkFocusInitial (click)="onImportUrl()">
|
||||
<button mat-flat-button color="primary" cdkFocusInitial (click)="onImportUrl()" appUserActionTracker
|
||||
category="InstallFromUrl" action="ImportUrl" [label]="query">
|
||||
{{'DIALOGS.INSTALL_FROM_URL.IMPORT_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,25 +1,24 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { PotentialAddon } from 'app/models/wowup/potential-addon';
|
||||
import { AddonService } from 'app/services/addons/addon.service';
|
||||
import { SessionService } from 'app/services/session/session.service';
|
||||
import { from, Subscription } from 'rxjs';
|
||||
import { AlertDialogComponent } from '../alert-dialog/alert-dialog.component';
|
||||
import { HttpErrorResponse } from "@angular/common/http";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { from, Subscription } from "rxjs";
|
||||
import { AlertDialogComponent } from "../alert-dialog/alert-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-install-from-url-dialog',
|
||||
templateUrl: './install-from-url-dialog.component.html',
|
||||
styleUrls: ['./install-from-url-dialog.component.scss']
|
||||
selector: "app-install-from-url-dialog",
|
||||
templateUrl: "./install-from-url-dialog.component.html",
|
||||
styleUrls: ["./install-from-url-dialog.component.scss"],
|
||||
})
|
||||
export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
public isBusy = false;
|
||||
public showInstallSpinner = false;
|
||||
public showInstallButton = false;
|
||||
public showInstallSuccess = false;
|
||||
public query = '';
|
||||
public addon?: PotentialAddon;
|
||||
public query = "";
|
||||
public addon?: AddonSearchResult;
|
||||
|
||||
private _installSubscription?: Subscription;
|
||||
|
||||
@@ -27,11 +26,10 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
private _addonService: AddonService,
|
||||
private _dialog: MatDialog,
|
||||
private _sessionService: SessionService,
|
||||
public dialogRef: MatDialogRef<InstallFromUrlDialogComponent>,
|
||||
) { }
|
||||
public dialogRef: MatDialogRef<InstallFromUrlDialogComponent>
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._installSubscription?.unsubscribe();
|
||||
@@ -42,7 +40,7 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onClearSearch() {
|
||||
this.query = '';
|
||||
this.query = "";
|
||||
this.onImportUrl();
|
||||
}
|
||||
|
||||
@@ -50,20 +48,23 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
this.showInstallButton = false;
|
||||
this.showInstallSpinner = true;
|
||||
|
||||
this._installSubscription = from(this._addonService
|
||||
.installPotentialAddon(this.addon, this._sessionService.selectedClientType))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.showInstallSpinner = false;
|
||||
this.showInstallSuccess = true;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.showInstallSpinner = false;
|
||||
this.showInstallButton = true;
|
||||
this.showErrorMessage('Failed to install addon.');
|
||||
}
|
||||
});
|
||||
this._installSubscription = from(
|
||||
this._addonService.installPotentialAddon(
|
||||
this.addon,
|
||||
this._sessionService.selectedClientType
|
||||
)
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.showInstallSpinner = false;
|
||||
this.showInstallSuccess = true;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.showInstallSpinner = false;
|
||||
this.showInstallButton = true;
|
||||
this.showErrorMessage("Failed to install addon.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onImportUrl() {
|
||||
@@ -81,12 +82,14 @@ 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.log(importedAddon);
|
||||
if (!importedAddon) {
|
||||
throw new Error('Addon not found');
|
||||
throw new Error("Addon not found");
|
||||
}
|
||||
|
||||
this.addon = importedAddon;
|
||||
@@ -98,13 +101,14 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.showInstallButton = true;
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
let message = err.message;
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
message = `No addon was found.`;
|
||||
} else if (err.code && err.code === "EOPENBREAKER") { // Provider circuit breaker is open
|
||||
message = `Cannot connect to API, please wait a bit and try again.`;
|
||||
}
|
||||
|
||||
this.showErrorMessage(message);
|
||||
@@ -112,16 +116,18 @@ 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 {
|
||||
try {
|
||||
return new URL(this.query);
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.error(`Invalid url: ${this.query}`);
|
||||
this.showErrorMessage('Invalid URL.');
|
||||
this.showErrorMessage("Invalid URL.");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -131,10 +137,9 @@ export class InstallFromUrlDialogComponent implements OnInit, OnDestroy {
|
||||
minWidth: 250,
|
||||
data: {
|
||||
title: `Error`,
|
||||
message: errorMessage
|
||||
}
|
||||
message: errorMessage,
|
||||
},
|
||||
});
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<mat-bar-button *ngIf="showStatusText === false" (btnClick)="onInstallUpdateClick()" [options]="buttonOptions$ | async"
|
||||
class="install-button">
|
||||
</mat-bar-button>
|
||||
|
||||
<div class="row">
|
||||
<div *ngIf="showStatusText === true" class="status-text" [ngClass]="{ 'ignored': listItem.isIgnored }">
|
||||
{{getStatusText()}}
|
||||
</div>
|
||||
<mat-icon *ngIf="this.listItem.isAutoUpdate === true" class="auto-update-icon"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate">
|
||||
update
|
||||
</mat-icon>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.auto-update-icon {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.ignored {
|
||||
color: $white-4;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MyAddonStatusColumnComponent } from './my-addon-status-column.component';
|
||||
|
||||
describe('MyAddonStatusColumnComponent', () => {
|
||||
let component: MyAddonStatusColumnComponent;
|
||||
let fixture: ComponentFixture<MyAddonStatusColumnComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MyAddonStatusColumnComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MyAddonStatusColumnComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { MatProgressButtonOptions } from "mat-progress-buttons";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { AddonInstallState } from "app/models/wowup/addon-install-state";
|
||||
import { AddonViewModel } from "app/business-objects/my-addon-list-item";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-my-addon-status-column",
|
||||
templateUrl: "./my-addon-status-column.component.html",
|
||||
styleUrls: ["./my-addon-status-column.component.scss"],
|
||||
})
|
||||
export class MyAddonStatusColumnComponent implements OnInit, OnDestroy {
|
||||
@Input() listItem: AddonViewModel;
|
||||
|
||||
private readonly _buttonOptionsSrc: BehaviorSubject<MatProgressButtonOptions>;
|
||||
private installState: AddonInstallState = AddonInstallState.Unknown;
|
||||
private installProgress: number = 0;
|
||||
|
||||
public readonly buttonOptions$: Observable<MatProgressButtonOptions>;
|
||||
|
||||
public get showStatusText() {
|
||||
return this.listItem?.isUpToDate || this.listItem?.isIgnored;
|
||||
}
|
||||
|
||||
public get buttonText() {
|
||||
if (this.installState !== AddonInstallState.Unknown) {
|
||||
return this.getInstallStateText(this.installState);
|
||||
}
|
||||
|
||||
return this.getStatusText();
|
||||
}
|
||||
|
||||
public get isButtonActive() {
|
||||
return (
|
||||
this.installState !== AddonInstallState.Unknown &&
|
||||
this.installState !== AddonInstallState.Complete
|
||||
);
|
||||
}
|
||||
|
||||
public get isButtonDisabled() {
|
||||
return (
|
||||
this.listItem?.isUpToDate ||
|
||||
this.installState === AddonInstallState.Complete
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _translate: TranslateService
|
||||
) {
|
||||
this._buttonOptionsSrc = new BehaviorSubject<MatProgressButtonOptions>(
|
||||
this.getButtonOptions()
|
||||
);
|
||||
this.buttonOptions$ = this._buttonOptionsSrc.asObservable();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._buttonOptionsSrc.next(this.getButtonOptions());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._buttonOptionsSrc.complete();
|
||||
}
|
||||
|
||||
public getStatusText() {
|
||||
if (this.listItem?.needsInstall) {
|
||||
return this._translate.instant(
|
||||
"PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.listItem?.needsUpdate) {
|
||||
return this._translate.instant(
|
||||
"PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON"
|
||||
);
|
||||
}
|
||||
|
||||
return this.listItem?.statusText;
|
||||
}
|
||||
|
||||
public onInstallUpdateClick() {
|
||||
this._addonService.installAddon(
|
||||
this.listItem.addon.id,
|
||||
this.onInstallUpdate
|
||||
);
|
||||
}
|
||||
|
||||
private onInstallUpdate = (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => {
|
||||
this.installState = installState;
|
||||
this.installProgress = progress;
|
||||
|
||||
console.log(this.getButtonOptions());
|
||||
this._buttonOptionsSrc.next(this.getButtonOptions());
|
||||
};
|
||||
|
||||
private getInstallStateText(installState: AddonInstallState) {
|
||||
switch (installState) {
|
||||
case AddonInstallState.BackingUp:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.BACKINGUP");
|
||||
case AddonInstallState.Complete:
|
||||
return this._translate.instant("COMMON.ADDON_STATE.UPTODATE");
|
||||
case AddonInstallState.Downloading:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.DOWNLOADING");
|
||||
case AddonInstallState.Installing:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.INSTALLING");
|
||||
case AddonInstallState.Pending:
|
||||
return this._translate.instant("COMMON.ADDON_STATUS.PENDING");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private getButtonOptions(): MatProgressButtonOptions {
|
||||
return {
|
||||
active: this.isButtonActive,
|
||||
disabled: this.isButtonDisabled,
|
||||
value: this.installProgress,
|
||||
text: this.buttonText,
|
||||
mode: "determinate",
|
||||
buttonColor: "primary",
|
||||
barColor: "accent",
|
||||
customClass: "install-button",
|
||||
raised: false,
|
||||
flat: true,
|
||||
stroked: false,
|
||||
fullWidth: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
{{listItem.isAlphaChannel ? 'Alpha': 'Beta'}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<a appExternalLink class="addon-title mat-subheading-2"
|
||||
[href]="listItem.addon.externalUrl">{{listItem.addon.name}}</a>
|
||||
<a class="addon-title mat-subheading-2" (click)="viewDetails()"
|
||||
[ngClass]="{ 'ignored': 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">
|
||||
@@ -23,6 +23,6 @@
|
||||
<img class="funding-icon" src="assets/images/custom_funding_logo_small.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="addon-version">{{listItem.addon.installedVersion}}</div>
|
||||
<div class="addon-version" [ngClass]="{ 'ignored': listItem.isIgnored }">{{listItem.addon.installedVersion}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
.channel {
|
||||
background: $dark-4;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-weight: 400;
|
||||
font-size: 0.8em;
|
||||
|
||||
&.beta {
|
||||
@@ -51,6 +51,10 @@
|
||||
text-decoration: underline;
|
||||
color: $white-2;
|
||||
}
|
||||
|
||||
&.ignored {
|
||||
color: $white-4;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-version {
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Addon } from 'app/entities/addon';
|
||||
import { MyAddonsListItem } from 'app/business-objects/my-addons-list-item';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Addon } from "app/entities/addon";
|
||||
import { AddonViewModel } from "app/business-objects/my-addon-list-item";
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-addons-addon-cell',
|
||||
templateUrl: './my-addons-addon-cell.component.html',
|
||||
styleUrls: ['./my-addons-addon-cell.component.scss']
|
||||
selector: "app-my-addons-addon-cell",
|
||||
templateUrl: "./my-addons-addon-cell.component.html",
|
||||
styleUrls: ["./my-addons-addon-cell.component.scss"],
|
||||
})
|
||||
export class MyAddonsAddonCellComponent implements OnInit {
|
||||
@Input("addon") listItem: AddonViewModel;
|
||||
|
||||
@Input('addon') listItem: MyAddonsListItem;
|
||||
@Output() onViewDetails: EventEmitter<AddonViewModel> = new EventEmitter();
|
||||
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit(): void {}
|
||||
|
||||
viewDetails() {
|
||||
this.onViewDetails.emit(this.listItem);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
</div>
|
||||
<div>
|
||||
<a class="addon-title mat-subheading-2" (click)="viewDetails()">{{addon.name}}</a>
|
||||
<div>{{addon.downloadCount | downloadCount}} downloads</div>
|
||||
<div class="addon-version">{{addonVersion}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,4 +33,8 @@
|
||||
color: $white-2;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-version {
|
||||
color: $white-2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,50 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { GetAddonListItem } from "app/business-objects/get-addon-list-item";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { WowUpService } from "app/services/wowup/wowup.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-potential-addon-table-column",
|
||||
templateUrl: "./potential-addon-table-column.component.html",
|
||||
styleUrls: ["./potential-addon-table-column.component.scss"],
|
||||
})
|
||||
export class PotentialAddonTableColumnComponent implements OnInit {
|
||||
@Input("addon") addon: PotentialAddon;
|
||||
export class PotentialAddonTableColumnComponent implements OnInit, OnChanges {
|
||||
@Input("addon") addon: GetAddonListItem;
|
||||
@Input() channel: AddonChannelType;
|
||||
|
||||
@Output() onViewDetails: EventEmitter<PotentialAddon> = new EventEmitter();
|
||||
@Output() onViewDetails: EventEmitter<AddonSearchResult> = new EventEmitter();
|
||||
|
||||
constructor() {}
|
||||
public addonVersion: string = "";
|
||||
|
||||
ngOnInit(): void { }
|
||||
constructor(
|
||||
private _sessionService: SessionService,
|
||||
private _wowupService: WowUpService
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.channel) {
|
||||
const latestFile = this.addon.getLatestFile(this.channel);
|
||||
this.addonVersion = latestFile?.version;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// this._defaultChannel = this._wowupService.getDefaultAddonChannel(
|
||||
// this._sessionService.selectedClientType
|
||||
// );
|
||||
}
|
||||
|
||||
viewDetails() {
|
||||
this.onViewDetails.emit(this.addon);
|
||||
this.onViewDetails.emit(this.addon.searchResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div class="title-container">
|
||||
<div>WowUp.io</div>
|
||||
</div>
|
||||
<div *ngIf="isMac" class="window-control-container">
|
||||
<mat-icon class="debug-button" (click)="onClickDebug()">bug_report</mat-icon>
|
||||
<div *ngIf="isMac" class="window-control-container pointer">
|
||||
<mat-icon class="debug-button pointer" (click)="onClickDebug()">bug_report</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="isWindows" class="window-control-container">
|
||||
<mat-icon (click)="onClickDebug()">bug_report</mat-icon>
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
<mat-form-field class="grow folder-input">
|
||||
<mat-label>{{clientTypeName}} Path</mat-label>
|
||||
<input matInput disabled [value]="clientLocation">
|
||||
<mat-hint>The folder that contains the {{clientTypeName | lowercase}} client folder "{{clientFolderName}}"</mat-hint>
|
||||
<mat-hint>The folder that contains the {{clientTypeName | lowercase}} client folder "{{clientFolderName}}"
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" class="select-button" (click)="onSelectClientPath()">Select</button>
|
||||
<button mat-flat-button color="primary" class="select-button" (click)="onSelectClientPath()" appUserActionTracker
|
||||
category="Options" [action]="clientTypeName + 'SelectPath'">Select</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="grow">
|
||||
@@ -14,7 +16,9 @@
|
||||
</div>
|
||||
<mat-form-field class="light-select">
|
||||
<mat-label>{{'PAGES.OPTIONS.WOW.DEFAULT_ADDON_CHANNEL_SELECT_LABEL' | translate}}</mat-label>
|
||||
<mat-select [(value)]="selectedAddonChannelType" (selectionChange)="onDefaultAddonChannelChange($event)">
|
||||
<mat-select [(value)]="selectedAddonChannelType" (selectionChange)="onDefaultAddonChannelChange($event)"
|
||||
appUserActionTracker category="Options" [action]="clientTypeName + 'DefaultChannel'"
|
||||
[label]="selectedAddonChannelType">
|
||||
<mat-option *ngFor="let channel of addonChannelInfos" [value]="channel.type">{{channel.name}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@@ -24,6 +28,7 @@
|
||||
<p>{{'PAGES.OPTIONS.WOW.AUTO_UPDATE_LABEL' | translate}}</p>
|
||||
<small class="hint">{{'PAGES.OPTIONS.WOW.AUTO_UPDATE_DESCRIPTION' | translate}}</small>
|
||||
</div>
|
||||
<mat-slide-toggle [(checked)]="clientAutoUpdate" (change)="onDefaultAutoUpdateChange($event)">
|
||||
<mat-slide-toggle [(checked)]="clientAutoUpdate" (change)="onDefaultAutoUpdateChange($event)" appUserActionTracker
|
||||
category="Options" [action]="clientTypeName + 'DefaultAutoUpdate'" [label]="clientAutoUpdate">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { ExternalLinkDirective } from "./directives/external-link.directive";
|
||||
import { UserActionTrackerDirective } from "./directives/user-action-tracker.directive";
|
||||
|
||||
@NgModule({
|
||||
declarations: [ExternalLinkDirective],
|
||||
exports: [ExternalLinkDirective],
|
||||
declarations: [
|
||||
ExternalLinkDirective,
|
||||
UserActionTrackerDirective
|
||||
],
|
||||
exports: [
|
||||
ExternalLinkDirective,
|
||||
UserActionTrackerDirective
|
||||
],
|
||||
})
|
||||
export class DirectiveModule {}
|
||||
export class DirectiveModule { }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { UserActionTrackerDirective } from './user-action-tracker.directive';
|
||||
|
||||
describe('UserActionTrackerDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new UserActionTrackerDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { AnalyticsService } from '../services/analytics/analytics.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[appUserActionTracker]'
|
||||
})
|
||||
export class UserActionTrackerDirective {
|
||||
|
||||
@Input() appUserActionTracker: string;
|
||||
@Input() category: string;
|
||||
@Input() action: string;
|
||||
@Input() label: string;
|
||||
|
||||
@HostListener('click', ['$event']) onClick($event) {
|
||||
this._analyticsService.trackUserAction(this.category, this.action, this.label);
|
||||
}
|
||||
|
||||
constructor(private _analyticsService: AnalyticsService) { }
|
||||
|
||||
}
|
||||
@@ -25,4 +25,7 @@ export interface Addon {
|
||||
patreonFundingLink?: string;
|
||||
githubFundingLink?: string;
|
||||
customFundingLink?: string;
|
||||
downloadCount?: number;
|
||||
summary?: string;
|
||||
screenshotUrls?: string[];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ErrorHandler } from "@angular/core";
|
||||
import { AnalyticsService } from "app/services/analytics/analytics.service";
|
||||
import { AppConfig } from "environments/environment";
|
||||
import * as Rollbar from "rollbar";
|
||||
|
||||
export class ErrorHandlerIntercepter implements ErrorHandler {
|
||||
|
||||
private readonly rollbarConfig = {
|
||||
accessToken: AppConfig.rollbarAccessKey,
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
};
|
||||
|
||||
private _rollbar: Rollbar;
|
||||
private get rollbar() {
|
||||
if (!this._analytics.telemetryEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this._rollbar) {
|
||||
this._rollbar = new Rollbar(this.rollbarConfig);
|
||||
}
|
||||
return this._rollbar;
|
||||
}
|
||||
|
||||
constructor(private _analytics: AnalyticsService) {
|
||||
|
||||
}
|
||||
|
||||
// ErrorHandler
|
||||
handleError(error: any): void {
|
||||
console.error("Caught error", error);
|
||||
|
||||
this.rollbar?.error(error.originalError || error);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum AddonInstallState {
|
||||
Pending,
|
||||
Downloading,
|
||||
BackingUp,
|
||||
Installing,
|
||||
Complete
|
||||
}
|
||||
Pending,
|
||||
Downloading,
|
||||
BackingUp,
|
||||
Installing,
|
||||
Complete,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { AddonSearchResultFile } from "./addon-search-result-file";
|
||||
|
||||
export interface AddonSearchResult {
|
||||
name: string;
|
||||
author: string;
|
||||
thumbnailUrl: string;
|
||||
downloadCount?: number;
|
||||
externalId: string;
|
||||
externalUrl: string;
|
||||
files?: AddonSearchResultFile[];
|
||||
name: string;
|
||||
providerName: string;
|
||||
files: AddonSearchResultFile[];
|
||||
}
|
||||
screenshotUrl?: string;
|
||||
screenshotUrls?: string[];
|
||||
summary?: string;
|
||||
thumbnailUrl: string;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export interface PotentialAddon {
|
||||
name: string;
|
||||
providerName: string;
|
||||
thumbnailUrl: string;
|
||||
screenshotUrl?: string;
|
||||
externalId: string;
|
||||
externalUrl: string;
|
||||
author: string;
|
||||
downloadCount: number;
|
||||
summary?: string;
|
||||
screenshotUrls?: string[];
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<h2>{{'PAGES.ABOUT.TITLE' | translate}}</h2>
|
||||
<div class="version">v{{version}}</div>
|
||||
<div class="link-container">
|
||||
<a appExternalLink href="https://wowup.io">
|
||||
<a appExternalLink href="https://wowup.io" appUserActionTracker category="About" action="ViewWebsite">
|
||||
{{'PAGES.ABOUT.WEBSITE_LINK_LABEL' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -17,10 +17,10 @@
|
||||
</h2>
|
||||
<ul class="change-log-list">
|
||||
<li class="changelog" *ngFor="let cl of changeLogs">
|
||||
<div class="version">{{cl.Version}}</div>
|
||||
<div class="version mat-subheading-2">{{cl.Version}}</div>
|
||||
<pre class="description">{{cl.Description}}</pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
background-color: $dark-4;
|
||||
|
||||
.version {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
// font-size: 1em;
|
||||
// font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button mat-flat-button color="primary" (click)="onRefresh()">
|
||||
<button mat-flat-button color="primary" (click)="onRefresh()" appUserActionTracker category="GetAddons"
|
||||
action="Refresh">
|
||||
{{'PAGES.GET_ADDONS.REFRESH_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" (click)="onInstallFromUrl()">
|
||||
<button mat-flat-button color="primary" (click)="onInstallFromUrl()" appUserActionTracker category="GetAddons"
|
||||
action="InstallFromUrl">
|
||||
{{'PAGES.GET_ADDONS.INSTALL_FROM_URL_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -36,18 +38,28 @@
|
||||
<app-progress-spinner *ngIf="isBusy === true"></app-progress-spinner>
|
||||
|
||||
<div class="table-container flex-grow-1" [hidden]="isBusy === true">
|
||||
<table mat-table matSort [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<table mat-table matSort [dataSource]="dataSource" matSortActive="downloadCount" matSortDirection="desc"
|
||||
class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.GET_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<app-potential-addon-table-column [addon]="element" (onViewDetails)="openDetailDialog($event)">
|
||||
<app-potential-addon-table-column [addon]="element" [channel]="defaultAddonChannel" (onViewDetails)="openDetailDialog($event)">
|
||||
</app-potential-addon-table-column>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="provider">
|
||||
<ng-container matColumnDef="downloadCount">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.GET_ADDONS.TABLE.DOWNLOAD_COUNT_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{element.downloadCount | downloadCount}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="providerName">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef class="provider-column">
|
||||
{{'PAGES.GET_ADDONS.TABLE.PROVIDER_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
@@ -60,7 +72,7 @@
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef class="author-column">
|
||||
{{'PAGES.GET_ADDONS.TABLE.AUTHOR_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<td mat-cell *matCellDef="let element" class="cell-padding">
|
||||
{{element.author}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -70,7 +82,7 @@
|
||||
{{'PAGES.GET_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<app-addon-install-button [addon]="element"> </app-addon-install-button>
|
||||
<app-get-addon-status-column [listItem]="element" ></app-get-addon-status-column>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -78,4 +90,4 @@
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;let i = index" (dblclick)="openDetailDialog(row)"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,15 +64,20 @@
|
||||
}
|
||||
|
||||
.addon-title {
|
||||
font-weight: bold;
|
||||
font-weight: 400;
|
||||
font-size: 1.1em;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.cell-padding {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.author-column {
|
||||
width: 100px;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.provider-column {
|
||||
min-width: 60px;
|
||||
|
||||
@@ -5,16 +5,19 @@ import { InstallFromUrlDialogComponent } from "app/components/install-from-url-d
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { AddonDetailModel } from "app/models/wowup/addon-detail.model";
|
||||
import { ColumnState } from "app/models/wowup/column-state";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { ElectronService } from "app/services";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { WarcraftService } from "app/services/warcraft/warcraft.service";
|
||||
import { BehaviorSubject, Subject, Subscription } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { MatSort } from "@angular/material/sort";
|
||||
import * as _ from "lodash";
|
||||
import { GetAddonListItem } from "app/business-objects/get-addon-list-item";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { WowUpService } from "app/services/wowup/wowup.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-get-addons",
|
||||
@@ -26,19 +29,21 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<PotentialAddon[]>(
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<GetAddonListItem[]>(
|
||||
[]
|
||||
);
|
||||
private readonly _destroyed$ = new Subject<void>();
|
||||
private subscriptions: Subscription[] = [];
|
||||
private isSelectedTab: boolean = false;
|
||||
private channelTypeKey: string = "";
|
||||
|
||||
public dataSource = new MatTableDataSource<PotentialAddon>([]);
|
||||
public dataSource = new MatTableDataSource<GetAddonListItem>([]);
|
||||
|
||||
columns: ColumnState[] = [
|
||||
{ name: "name", display: "Addon", visible: true },
|
||||
{ name: "downloadCount", display: "Downloads", visible: true },
|
||||
{ name: "author", display: "Author", visible: true },
|
||||
{ name: "provider", display: "Provider", visible: true },
|
||||
{ name: "providerName", display: "Provider", visible: true },
|
||||
{ name: "status", display: "Status", visible: true },
|
||||
];
|
||||
|
||||
@@ -46,6 +51,18 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
return this.columns.filter((col) => col.visible).map((col) => col.name);
|
||||
}
|
||||
|
||||
public get defaultAddonChannelKey() {
|
||||
return this._wowUpService.getClientDefaultAddonChannelKey(
|
||||
this._sessionService.selectedClientType
|
||||
);
|
||||
}
|
||||
|
||||
public get defaultAddonChannel() {
|
||||
return this._wowUpService.getDefaultAddonChannel(
|
||||
this._sessionService.selectedClientType
|
||||
);
|
||||
}
|
||||
|
||||
public query = "";
|
||||
public isBusy = false;
|
||||
public selectedClient = WowClientType.None;
|
||||
@@ -54,6 +71,7 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
private _addonService: AddonService,
|
||||
private _sessionService: SessionService,
|
||||
private _dialog: MatDialog,
|
||||
private _wowUpService: WowUpService,
|
||||
public electronService: ElectronService,
|
||||
public warcraftService: WarcraftService
|
||||
) {
|
||||
@@ -84,17 +102,24 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
.subscribe();
|
||||
|
||||
const displayAddonSubscription = this._displayAddonsSrc.subscribe(
|
||||
(items: PotentialAddon[]) => {
|
||||
(items: GetAddonListItem[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.dataSource.sortingDataAccessor = _.get;
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
);
|
||||
|
||||
const channelTypeSubscription = this._wowUpService.preferenceChange$
|
||||
.pipe(filter((change) => change.key === this.defaultAddonChannelKey))
|
||||
.subscribe((change) => {
|
||||
this.onSearch();
|
||||
});
|
||||
|
||||
this.subscriptions = [
|
||||
selectedClientSubscription,
|
||||
addonRemovedSubscription,
|
||||
displayAddonSubscription,
|
||||
channelTypeSubscription,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -136,16 +161,16 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
this.selectedClient
|
||||
);
|
||||
|
||||
searchResults = this.filterInstalledAddons(searchResults);
|
||||
this.formatAddons(searchResults);
|
||||
this._displayAddonsSrc.next(searchResults);
|
||||
this._displayAddonsSrc.next(
|
||||
this.formatAddons(this.filterInstalledAddons(searchResults))
|
||||
);
|
||||
this.isBusy = false;
|
||||
this.setPageContextText();
|
||||
}
|
||||
|
||||
openDetailDialog(addon: PotentialAddon) {
|
||||
openDetailDialog(addon: AddonSearchResult) {
|
||||
const dialogRef = this._dialog.open(AddonDetailComponent, {
|
||||
data: new AddonDetailModel(addon),
|
||||
data: addon,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe();
|
||||
@@ -160,9 +185,8 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
this._addonService.getFeaturedAddons(clientType).subscribe({
|
||||
next: (addons) => {
|
||||
addons = this.filterInstalledAddons(addons);
|
||||
this.formatAddons(addons);
|
||||
this._displayAddonsSrc.next(addons);
|
||||
const listItems = this.formatAddons(this.filterInstalledAddons(addons));
|
||||
this._displayAddonsSrc.next(listItems);
|
||||
this.isBusy = false;
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -171,7 +195,7 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private filterInstalledAddons(addons: PotentialAddon[]) {
|
||||
private filterInstalledAddons(addons: AddonSearchResult[]) {
|
||||
return addons.filter(
|
||||
(addon) =>
|
||||
!this._addonService.isInstalled(
|
||||
@@ -181,12 +205,14 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private formatAddons(addons: PotentialAddon[]) {
|
||||
private formatAddons(addons: AddonSearchResult[]): GetAddonListItem[] {
|
||||
addons.forEach((addon) => {
|
||||
if (!addon.thumbnailUrl) {
|
||||
addon.thumbnailUrl = "assets/wowup_logo_512np.png";
|
||||
}
|
||||
});
|
||||
|
||||
return addons.map((addon) => new GetAddonListItem(addon));
|
||||
}
|
||||
|
||||
private setPageContextText() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="page-container">
|
||||
<div class="tabs">
|
||||
<mat-tab-group mat-align-tabs="center" [backgroundColor]="'primary'" [disablePagination]="true"
|
||||
<mat-tab-group mat-align-tabs="center" animationDuration="0ms" [backgroundColor]="'primary'" [disablePagination]="true"
|
||||
[(selectedIndex)]="selectedIndex" (selectedIndexChange)="onSelectedIndexChange($event)">
|
||||
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.MY_ADDONS_TAB_TITLE' | translate">
|
||||
<app-my-addons [tabIndex]="0"></app-my-addons>
|
||||
|
||||
@@ -24,6 +24,8 @@ import { AddonDetailComponent } from "app/components/addon-detail/addon-detail.c
|
||||
import { AddonProviderBadgeComponent } from "app/components/addon-provider-badge/addon-provider-badge.component";
|
||||
import { MatProgressButtonsModule } from "mat-progress-buttons";
|
||||
import { AddonInstallButtonComponent } from "app/components/addon-install-button/addon-install-button.component";
|
||||
import { GetAddonStatusColumnComponent } from "app/components/get-addon-status-column/get-addon-status-column.component";
|
||||
import { MyAddonStatusColumnComponent } from "app/components/my-addon-status-column/my-addon-status-column.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -44,6 +46,8 @@ import { AddonInstallButtonComponent } from "app/components/addon-install-button
|
||||
AddonDetailComponent,
|
||||
AddonProviderBadgeComponent,
|
||||
AddonInstallButtonComponent,
|
||||
GetAddonStatusColumnComponent,
|
||||
MyAddonStatusColumnComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="select-container ">
|
||||
<mat-form-field>
|
||||
<mat-label>{{'PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_LABEL' | translate}}</mat-label>
|
||||
<mat-select class="select" [(value)]="selectedClient" (selectionChange)="onClientChange()"
|
||||
<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)}}
|
||||
@@ -25,17 +25,20 @@
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON_TOOLTIP' | translate"
|
||||
[disabled]="enableControls === false" (click)="onUpdateAll()" (contextmenu)="onUpdateAllContext($event)">
|
||||
[disabled]="enableControls === false" (click)="onUpdateAll()" (contextmenu)="onUpdateAllContext($event)"
|
||||
appUserActionTracker category="MyAddons" action="UpdateAll">
|
||||
{{'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
|
||||
[disabled]="enableControls === false" (click)="onRefresh()">
|
||||
[disabled]="enableControls === false" (click)="onRefresh()" appUserActionTracker category="MyAddons"
|
||||
action="CheckUpdates">
|
||||
{{'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
|
||||
[disabled]="enableControls === false" (click)="onReScan()">
|
||||
[disabled]="enableControls === false" (click)="onReScan()" appUserActionTracker category="MyAddons"
|
||||
action="ReScanFolders">
|
||||
{{'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -52,35 +55,17 @@
|
||||
<ng-container matColumnDef="addon.name">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<app-my-addons-addon-cell [addon]="element"></app-my-addons-addon-cell>
|
||||
<td mat-cell *matCellDef="let element" class="cell-padding">
|
||||
<app-my-addons-addon-cell [addon]="element" (onViewDetails)="openDetailDialog($event)">
|
||||
</app-my-addons-addon-cell>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="displayState">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="status-column">
|
||||
<button *ngIf="element.needsInstall === true" mat-flat-button color="primary" (click)="onInstall()">
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON' | translate}}
|
||||
</button>
|
||||
<button *ngIf="element.needsUpdate === true" mat-flat-button color="primary"
|
||||
(click)="onUpdateAddon(element)">
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON' | translate}}
|
||||
</button>
|
||||
<div *ngIf="element.isUpToDate === true || element.isIgnored === true" class="status-text">
|
||||
{{element.statusText}}</div>
|
||||
<mat-icon *ngIf="element.addon.autoUpdateEnabled" class="auto-update-icon"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate">
|
||||
update
|
||||
</mat-icon>
|
||||
<div *ngIf="element.isInstalling === true" class="progress-container">
|
||||
<p class="progress-text">{{element.statusText}}</p>
|
||||
<mat-progress-bar class="addon-progress" mode="determinate" [value]="element.installProgress">
|
||||
</mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
<td mat-cell *matCellDef="let element" class="status-column cell-padding">
|
||||
<app-my-addon-status-column [listItem]="element"></app-my-addon-status-column>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -88,7 +73,7 @@
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.LATEST_VERSION_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<td mat-cell *matCellDef="let element" class="cell-padding">
|
||||
{{element.addon.latestVersion}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -106,7 +91,7 @@
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef class="provider-column">
|
||||
{{'PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<td mat-cell *matCellDef="let element" class="cell-padding">
|
||||
<div *ngIf="element.addon.providerName !== 'WowUp'">
|
||||
{{element.addon.providerName}}
|
||||
</div>
|
||||
@@ -122,7 +107,7 @@
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef class="author-column">
|
||||
{{'PAGES.MY_ADDONS.TABLE.AUTHOR_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<td mat-cell *matCellDef="let element" class="author-column">
|
||||
{{element.addon.author}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -132,7 +117,7 @@
|
||||
|
||||
<tr mat-row *matRowDef="let row; let i = index; columns: displayedColumns;"
|
||||
[ngClass]="{'selected-row': row.selected}" (click)="onRowClicked($event, row, i)"
|
||||
(contextmenu)="onCellContext($event, row)"></tr>
|
||||
(dblclick)="openDetailDialog(row)" (contextmenu)="onCellContext($event, row)"></tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
@@ -152,36 +137,44 @@
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-checkbox class="mat-menu-item" [checked]="listItem.addon.isIgnored"
|
||||
(change)="onClickIgnoreAddon($event, listItem)">
|
||||
(change)="onClickIgnoreAddon($event, listItem)" appUserActionTracker category="MyAddons" action="IgnoreAddon"
|
||||
[label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON' | translate}}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox *ngIf="listItem.addon.isIgnored === false" class="mat-menu-item"
|
||||
[checked]="listItem.addon.autoUpdateEnabled" (change)="onClickAutoUpdateAddon($event, listItem)">
|
||||
[checked]="listItem.addon.autoUpdateEnabled" (change)="onClickAutoUpdateAddon($event, listItem)"
|
||||
appUserActionTracker category="MyAddons" action="AutoUpdateAddon" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON' | translate}}
|
||||
</mat-checkbox>
|
||||
<button mat-menu-item [matMenuTriggerFor]="addonChannels">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.CHANNEL_SUBMENT_TITLE' | translate}}
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowfolder(listItem.addon)">
|
||||
<button mat-menu-item (click)="onShowfolder(listItem.addon)" appUserActionTracker category="MyAddons"
|
||||
action="ShowAddonFolder" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.SHOW_FOLDER' | translate}}
|
||||
</button>
|
||||
<button mat-menu-item (click)="onReInstallAddon(listItem)">
|
||||
<button mat-menu-item (click)="onReInstallAddon(listItem)" appUserActionTracker category="MyAddons"
|
||||
action="ReInstallAddon" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REINSTALL_ADDON_BUTTON' | translate}}
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="onRemoveAddon(listItem.addon)">
|
||||
<button mat-menu-item (click)="onRemoveAddon(listItem.addon)" appUserActionTracker category="MyAddons"
|
||||
action="RemoveAddon" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REMOVE_ADDON_BUTTON' | translate}}
|
||||
</button>
|
||||
<mat-menu #addonChannels="matMenu" class="addon-context-menu">
|
||||
<mat-radio-group class="vertical-radio-group" [ngModel]="listItem.addon.channelType"
|
||||
(change)="onSelectedAddonChannelChange($event, listItem)">
|
||||
<mat-radio-button class="mat-menu-item" [value]="0">
|
||||
<mat-radio-button class="mat-menu-item" [value]="0" appUserActionTracker category="MyAddons"
|
||||
action="SetStableAddonChannel" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL' | translate}}
|
||||
</mat-radio-button>
|
||||
<mat-radio-button class="mat-menu-item" [value]="1">
|
||||
<mat-radio-button class="mat-menu-item" [value]="1" appUserActionTracker category="MyAddons"
|
||||
action="SetBetaAddonChannel" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.BETA_ADDON_CHANNEL' | translate}}
|
||||
</mat-radio-button>
|
||||
<mat-radio-button class="mat-menu-item" [value]="2">
|
||||
<mat-radio-button class="mat-menu-item" [value]="2" appUserActionTracker category="MyAddons"
|
||||
action="SetAlphaAddonChannel" [label]="listItem.addon.name">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ALPHA_ADDON_CHANNEL' | translate}}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@@ -255,10 +248,12 @@
|
||||
</div>
|
||||
<mat-menu #updateAllContextMenu="matMenu" class="addon-context-menu">
|
||||
<ng-template matMenuContent let-columns="columns">
|
||||
<button mat-menu-item (click)="onUpdateAllRetailClassic()">
|
||||
<button mat-menu-item (click)="onUpdateAllRetailClassic()" appUserActionTracker category="MyAddons"
|
||||
action="UpdateAllClassicRetail">
|
||||
{{'PAGES.MY_ADDONS.UPDATE_ALL_CONTEXT_MENU.UPDATE_RETAIL_CLASSIC_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-menu-item (click)="onUpdateAllClients()">
|
||||
<button mat-menu-item (click)="onUpdateAllClients()" appUserActionTracker category="MyAddons"
|
||||
action="UpdateAllClients">
|
||||
{{'PAGES.MY_ADDONS.UPDATE_ALL_CONTEXT_MENU.UPDATE_ALL_CLIENTS_BUTTON' | translate}}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
|
||||
.addon-title {
|
||||
font-weight: bold;
|
||||
font-weight: 400;
|
||||
font-size: 1.1em;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
@@ -80,9 +80,9 @@
|
||||
}
|
||||
|
||||
.status-column {
|
||||
display: flex;
|
||||
// display: flex;
|
||||
width: 130px;
|
||||
align-items: center;
|
||||
// align-items: center;
|
||||
|
||||
.auto-update-icon {
|
||||
margin-left: 0.5em;
|
||||
@@ -114,6 +114,10 @@
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-padding {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.author-column {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SessionService } from "app/services/session/session.service";
|
||||
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { ColumnState } from "app/models/wowup/column-state";
|
||||
import { MatCheckboxChange } from "@angular/material/checkbox";
|
||||
import { MyAddonsListItem } from "app/business-objects/my-addons-list-item";
|
||||
import { AddonViewModel } from "app/business-objects/my-addon-list-item";
|
||||
import * as _ from "lodash";
|
||||
import { ElectronService } from "app/services";
|
||||
import { AddonDisplayState } from "app/models/wowup/addon-display-state";
|
||||
@@ -30,6 +30,8 @@ import { getEnumName } from "app/utils/enum.utils";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { MatSort } from "@angular/material/sort";
|
||||
import { stringIncludes } from "app/utils/string.utils";
|
||||
import { GetAddonListItem } from "app/business-objects/get-addon-list-item";
|
||||
import { AddonDetailComponent } from "app/components/addon-detail/addon-detail.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-my-addons",
|
||||
@@ -46,20 +48,20 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
updateAllContextMenu: MatMenuTrigger;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<MyAddonsListItem[]>(
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<AddonViewModel[]>(
|
||||
[]
|
||||
);
|
||||
private readonly _destroyed$ = new Subject<void>();
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
private isSelectedTab: boolean = false;
|
||||
private sortedListItems: MyAddonsListItem[] = [];
|
||||
private sortedListItems: AddonViewModel[] = [];
|
||||
|
||||
public spinnerMessage = "Loading...";
|
||||
|
||||
contextMenuPosition = { x: "0px", y: "0px" };
|
||||
|
||||
public dataSource = new MatTableDataSource<MyAddonsListItem>([]);
|
||||
public dataSource = new MatTableDataSource<AddonViewModel>([]);
|
||||
public filter = "";
|
||||
|
||||
columns: ColumnState[] = [
|
||||
@@ -120,7 +122,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
const addonInstalledSubscription = this.addonService.addonInstalled$.subscribe(
|
||||
(evt) => {
|
||||
let listItems: MyAddonsListItem[] = [].concat(
|
||||
let listItems: AddonViewModel[] = [].concat(
|
||||
this._displayAddonsSrc.value
|
||||
);
|
||||
|
||||
@@ -138,7 +140,8 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
if (listItemIdx === -1) {
|
||||
listItems.push(listItem);
|
||||
} else {
|
||||
listItems[listItemIdx] = listItem;
|
||||
return;
|
||||
// listItems[listItemIdx] = listItem;
|
||||
}
|
||||
|
||||
listItems = this.sortListItems(listItems);
|
||||
@@ -151,7 +154,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
const addonRemovedSubscription = this.addonService.addonRemoved$.subscribe(
|
||||
(addonId) => {
|
||||
const addons: MyAddonsListItem[] = [].concat(
|
||||
const addons: AddonViewModel[] = [].concat(
|
||||
this._displayAddonsSrc.value
|
||||
);
|
||||
const listItemIdx = addons.findIndex((li) => li.addon.id === addonId);
|
||||
@@ -164,7 +167,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
const displayAddonSubscription = this._displayAddonsSrc.subscribe(
|
||||
(items: MyAddonsListItem[]) => {
|
||||
(items: AddonViewModel[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.dataSource.sortingDataAccessor = _.get;
|
||||
this.dataSource.filterPredicate = this.filterListItem;
|
||||
@@ -209,8 +212,9 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
this.loadAddons(this.selectedClient);
|
||||
}
|
||||
|
||||
onRowClicked(event: MouseEvent, row: MyAddonsListItem, index: number) {
|
||||
console.log("index clicked: " + index, row.addon.name);
|
||||
onRowClicked(event: MouseEvent, row: AddonViewModel, index: number) {
|
||||
console.log(row.displayState);
|
||||
console.log("index clicked: " + index);
|
||||
|
||||
if (event.ctrlKey) {
|
||||
row.selected = !row.selected;
|
||||
@@ -238,6 +242,14 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
openDetailDialog(listItem: AddonViewModel) {
|
||||
const dialogRef = this._dialog.open(AddonDetailComponent, {
|
||||
data: listItem.addon,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
filterAddons(): void {
|
||||
this.dataSource.filter = this.filter.trim().toLowerCase();
|
||||
}
|
||||
@@ -295,7 +307,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
this.columnContextMenu.openMenu();
|
||||
}
|
||||
|
||||
onCellContext(event: MouseEvent, listItem: MyAddonsListItem) {
|
||||
onCellContext(event: MouseEvent, listItem: AddonViewModel) {
|
||||
event.preventDefault();
|
||||
this.updateContextMenuPosition(event);
|
||||
|
||||
@@ -319,11 +331,11 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
this.updateAllContextMenu.openMenu();
|
||||
}
|
||||
|
||||
async onReInstallAddon(listItems: MyAddonsListItem) {
|
||||
async onReInstallAddon(listItems: AddonViewModel) {
|
||||
await this.onReInstallAddons([listItems]);
|
||||
}
|
||||
|
||||
async onReInstallAddons(listItems: MyAddonsListItem[]) {
|
||||
async onReInstallAddons(listItems: AddonViewModel[]) {
|
||||
for (let listItem of listItems) {
|
||||
try {
|
||||
await this.addonService.installAddon(listItem.addon.id);
|
||||
@@ -342,7 +354,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateAddon(listItem: MyAddonsListItem) {
|
||||
onUpdateAddon(listItem: AddonViewModel) {
|
||||
listItem.isInstalling = true;
|
||||
|
||||
this.addonService.installAddon(listItem.addon.id);
|
||||
@@ -393,7 +405,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveAddons(listItems: MyAddonsListItem[]) {
|
||||
onRemoveAddons(listItems: AddonViewModel[]) {
|
||||
let message = "";
|
||||
if (listItems.length > 3) {
|
||||
message = `Are you sure you want to remove the selected ${listItems.length} addons?`;
|
||||
@@ -427,11 +439,11 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
onInstall() {}
|
||||
|
||||
onClickIgnoreAddon(evt: MatCheckboxChange, listItem: MyAddonsListItem) {
|
||||
onClickIgnoreAddon(evt: MatCheckboxChange, listItem: AddonViewModel) {
|
||||
this.onClickIgnoreAddons(evt, [listItem]);
|
||||
}
|
||||
|
||||
onClickIgnoreAddons(evt: MatCheckboxChange, listItems: MyAddonsListItem[]) {
|
||||
onClickIgnoreAddons(evt: MatCheckboxChange, listItems: AddonViewModel[]) {
|
||||
listItems.forEach((listItem) => {
|
||||
listItem.addon.isIgnored = evt.checked;
|
||||
if (evt.checked) {
|
||||
@@ -446,13 +458,13 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
onClickAutoUpdateAddon(evt: MatCheckboxChange, listItem: MyAddonsListItem) {
|
||||
onClickAutoUpdateAddon(evt: MatCheckboxChange, listItem: AddonViewModel) {
|
||||
this.onClickAutoUpdateAddons(evt, [listItem]);
|
||||
}
|
||||
|
||||
onClickAutoUpdateAddons(
|
||||
evt: MatCheckboxChange,
|
||||
listItems: MyAddonsListItem[]
|
||||
listItems: AddonViewModel[]
|
||||
) {
|
||||
listItems.forEach((listItem) => {
|
||||
listItem.addon.autoUpdateEnabled = evt.checked;
|
||||
@@ -469,14 +481,14 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
onSelectedAddonChannelChange(
|
||||
evt: MatRadioChange,
|
||||
listItem: MyAddonsListItem
|
||||
listItem: AddonViewModel
|
||||
) {
|
||||
this.onSelectedAddonsChannelChange(evt, [listItem]);
|
||||
}
|
||||
|
||||
onSelectedAddonsChannelChange(
|
||||
evt: MatRadioChange,
|
||||
listItems: MyAddonsListItem[]
|
||||
listItems: AddonViewModel[]
|
||||
) {
|
||||
listItems.forEach((listItem) => {
|
||||
listItem.addon.channelType = evt.value;
|
||||
@@ -485,11 +497,11 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
this.loadAddons(this.selectedClient);
|
||||
}
|
||||
|
||||
isSelectedItemsProp(listItems: MyAddonsListItem[], prop: string) {
|
||||
isSelectedItemsProp(listItems: AddonViewModel[], prop: string) {
|
||||
return _.some(listItems, prop);
|
||||
}
|
||||
|
||||
private sortTable(dataSource: MatTableDataSource<MyAddonsListItem>) {
|
||||
private sortTable(dataSource: MatTableDataSource<AddonViewModel>) {
|
||||
this.dataSource.data = this.sortListItems(dataSource.data, dataSource.sort);
|
||||
}
|
||||
|
||||
@@ -506,7 +518,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Only care about the ones that need to be updated/installed
|
||||
addons = addons
|
||||
.map((addon) => new MyAddonsListItem(addon))
|
||||
.map((addon) => new AddonViewModel(addon))
|
||||
.filter((listItem) => listItem.needsUpdate || listItem.needsInstall)
|
||||
.map((listItem) => listItem.addon);
|
||||
|
||||
@@ -556,13 +568,13 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private formatAddons(addons: Addon[]): MyAddonsListItem[] {
|
||||
private formatAddons(addons: Addon[]): AddonViewModel[] {
|
||||
const listItems = addons.map((addon) => this.createAddonListItem(addon));
|
||||
|
||||
return this.sortListItems(listItems);
|
||||
}
|
||||
|
||||
private sortListItems(listItems: MyAddonsListItem[], sort?: MatSort) {
|
||||
private sortListItems(listItems: AddonViewModel[], sort?: MatSort) {
|
||||
if (!sort || !sort.active || sort.direction === "") {
|
||||
return _.orderBy(listItems, ["displayState", "addon.name"]);
|
||||
}
|
||||
@@ -573,7 +585,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private filterListItem(item: MyAddonsListItem, filter: string) {
|
||||
private filterListItem(item: AddonViewModel, filter: string) {
|
||||
if (
|
||||
stringIncludes(item.addon.name, filter) ||
|
||||
stringIncludes(item.addon.latestVersion, filter) ||
|
||||
@@ -585,7 +597,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createAddonListItem(addon: Addon) {
|
||||
const listItem = new MyAddonsListItem(addon);
|
||||
const listItem = new AddonViewModel(addon);
|
||||
|
||||
if (!listItem.addon.thumbnailUrl) {
|
||||
listItem.addon.thumbnailUrl = "assets/wowup_logo_512np.png";
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<div class="grow">{{'PAGES.OPTIONS.WOW.RESCAN_CLIENTS_LABEL' | translate}}</div>
|
||||
<button mat-flat-button color="primary" (click)="onReScan()">
|
||||
<button mat-flat-button color="primary" (click)="onReScan()" appUserActionTracker category="Options"
|
||||
action="ScanInstalls">
|
||||
{{'PAGES.OPTIONS.WOW.RESCAN_CLIENTS_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -45,7 +46,8 @@
|
||||
<p>{{'PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_LABEL' | translate}}</p>
|
||||
<small class="hint">{{'PAGES.OPTIONS.APPLICATION.MINIMIZE_ON_CLOSE_DESCRIPTION' | translate}}</small>
|
||||
</div>
|
||||
<mat-slide-toggle [(checked)]="collapseToTray" (change)="onCollapseChange($event)">
|
||||
<mat-slide-toggle [(checked)]="collapseToTray" (change)="onCollapseChange($event)" appUserActionTracker
|
||||
category="Options" action="CollapseToTray" [label]="collapseToTray">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@@ -60,7 +62,8 @@
|
||||
<p>{{'PAGES.OPTIONS.DEBUG.LOG_FILES_LABEL' | translate}}</p>
|
||||
<small class="hint">{{'PAGES.OPTIONS.DEBUG.LOG_FILES_DESCRIPTION' | translate}}</small>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" (click)="onShowLogs()">
|
||||
<button mat-flat-button color="primary" (click)="onShowLogs()" appUserActionTracker category="Options"
|
||||
action="ShowLogFiles">
|
||||
{{'PAGES.OPTIONS.DEBUG.LOG_FILES_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -69,11 +72,12 @@
|
||||
<p>{{'PAGES.OPTIONS.DEBUG.DEBUG_DATA_LABEL' | translate}}</p>
|
||||
<small class="hint">{{'PAGES.OPTIONS.DEBUG.DEBUG_DATA_DESCRIPTION' | translate}}</small>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" (click)="onShowLogs()">
|
||||
<button mat-flat-button color="primary" (click)="onLogDebugData()" appUserActionTracker category="Options"
|
||||
action="LogDebugData">
|
||||
{{'PAGES.OPTIONS.DEBUG.DEBUG_DATA_BUTTON' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,60 @@
|
||||
import { Component, OnInit, NgZone, OnChanges, SimpleChanges, Input } from '@angular/core';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { WowClientType } from 'app/models/warcraft/wow-client-type';
|
||||
import { ElectronService } from 'app/services';
|
||||
import { WarcraftService } from 'app/services/warcraft/warcraft.service';
|
||||
import { WowUpService } from 'app/services/wowup/wowup.service';
|
||||
import { telemetryEnabledKey } from '../../../constants';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AlertDialogComponent } from 'app/components/alert-dialog/alert-dialog.component';
|
||||
import { getEnumList, getEnumName } from 'app/utils/enum.utils';
|
||||
import { WowUpReleaseChannelType } from 'app/models/wowup/wowup-release-channel-type';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
Input,
|
||||
} from "@angular/core";
|
||||
import { MatSlideToggleChange } from "@angular/material/slide-toggle";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { ElectronService } from "app/services";
|
||||
import { WarcraftService } from "app/services/warcraft/warcraft.service";
|
||||
import { WowUpService } from "app/services/wowup/wowup.service";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import * as _ from "lodash";
|
||||
import * as path from "path";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { AlertDialogComponent } from "app/components/alert-dialog/alert-dialog.component";
|
||||
import { getEnumList, getEnumName } from "app/utils/enum.utils";
|
||||
import { WowUpReleaseChannelType } from "app/models/wowup/wowup-release-channel-type";
|
||||
import { MatSelectChange } from "@angular/material/select";
|
||||
import { AnalyticsService } from "app/services/analytics/analytics.service";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { GET_ASSET_FILE_PATH } from "common/constants";
|
||||
|
||||
@Component({
|
||||
selector: 'app-options',
|
||||
templateUrl: './options.component.html',
|
||||
styleUrls: ['./options.component.scss']
|
||||
selector: "app-options",
|
||||
templateUrl: "./options.component.html",
|
||||
styleUrls: ["./options.component.scss"],
|
||||
})
|
||||
export class OptionsComponent implements OnInit, OnChanges {
|
||||
@Input("tabIndex") tabIndex: number;
|
||||
|
||||
@Input('tabIndex') tabIndex: number;
|
||||
|
||||
public retailLocation = '';
|
||||
public classicLocation = '';
|
||||
public retailPtrLocation = '';
|
||||
public classicPtrLocation = '';
|
||||
public betaLocation = '';
|
||||
public retailLocation = "";
|
||||
public classicLocation = "";
|
||||
public retailPtrLocation = "";
|
||||
public classicPtrLocation = "";
|
||||
public betaLocation = "";
|
||||
public collapseToTray = false;
|
||||
public telemetryEnabled = false;
|
||||
public wowClientTypes: WowClientType[] = getEnumList(WowClientType).filter(clientType => clientType !== WowClientType.None) as WowClientType[];
|
||||
public wowClientTypes: WowClientType[] = getEnumList(WowClientType).filter(
|
||||
(clientType) => clientType !== WowClientType.None
|
||||
) as WowClientType[];
|
||||
public wowUpReleaseChannel: WowUpReleaseChannelType;
|
||||
public wowUpReleaseChannels: { type: WowUpReleaseChannelType, name: string }[] = getEnumList(WowUpReleaseChannelType)
|
||||
.map((type: WowUpReleaseChannelType) => ({ type, name: getEnumName(WowUpReleaseChannelType, type) }));
|
||||
public wowUpReleaseChannels: {
|
||||
type: WowUpReleaseChannelType;
|
||||
name: string;
|
||||
}[] = getEnumList(WowUpReleaseChannelType).map(
|
||||
(type: WowUpReleaseChannelType) => ({
|
||||
type,
|
||||
name: getEnumName(WowUpReleaseChannelType, type),
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _analyticsService: AnalyticsService,
|
||||
private warcraft: WarcraftService,
|
||||
private _electronService: ElectronService,
|
||||
private _warcraftService: WarcraftService,
|
||||
@@ -44,15 +63,13 @@ export class OptionsComponent implements OnInit, OnChanges {
|
||||
private zone: NgZone,
|
||||
public electronService: ElectronService
|
||||
) {
|
||||
_wowUpService.preferenceChange$
|
||||
.pipe(filter(change => change.key === telemetryEnabledKey))
|
||||
.subscribe(change => {
|
||||
this.telemetryEnabled = change.value === true.toString()
|
||||
})
|
||||
_analyticsService.telemetryEnabled$.subscribe((enabled) => {
|
||||
this.telemetryEnabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
console.log(changes)
|
||||
console.log(changes);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -63,25 +80,29 @@ export class OptionsComponent implements OnInit, OnChanges {
|
||||
|
||||
onShowLogs = () => {
|
||||
this._wowUpService.showLogsFolder();
|
||||
}
|
||||
};
|
||||
|
||||
onReScan = () => {
|
||||
this.warcraft.scanProducts();
|
||||
this.loadData();
|
||||
}
|
||||
};
|
||||
|
||||
onTelemetryChange = (evt: MatSlideToggleChange) => {
|
||||
this._wowUpService.telemetryEnabled = evt.checked;
|
||||
}
|
||||
this._analyticsService.telemetryEnabled = evt.checked;
|
||||
};
|
||||
|
||||
onCollapseChange = (evt: MatSlideToggleChange) => {
|
||||
this._wowUpService.collapseToTray = evt.checked;
|
||||
}
|
||||
};
|
||||
|
||||
onWowUpChannelChange(evt: MatSelectChange) {
|
||||
this._wowUpService.wowUpReleaseChannel = evt.value;
|
||||
}
|
||||
|
||||
async onLogDebugData() {
|
||||
await this._addonService.logDebugData();
|
||||
}
|
||||
|
||||
async onSelectRetailClientPath() {
|
||||
const selectedPath = await this.selectWowClientPath(WowClientType.Retail);
|
||||
if (selectedPath) {
|
||||
@@ -90,7 +111,9 @@ export class OptionsComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
async onSelectRetailPtrClientPath() {
|
||||
const selectedPath = await this.selectWowClientPath(WowClientType.RetailPtr);
|
||||
const selectedPath = await this.selectWowClientPath(
|
||||
WowClientType.RetailPtr
|
||||
);
|
||||
if (selectedPath) {
|
||||
this.retailPtrLocation = selectedPath;
|
||||
}
|
||||
@@ -104,7 +127,9 @@ export class OptionsComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
async onSelectClassicPtrClientPath() {
|
||||
const selectedPath = await this.selectWowClientPath(WowClientType.ClassicPtr);
|
||||
const selectedPath = await this.selectWowClientPath(
|
||||
WowClientType.ClassicPtr
|
||||
);
|
||||
if (selectedPath) {
|
||||
this.classicPtrLocation = selectedPath;
|
||||
}
|
||||
@@ -117,52 +142,74 @@ export class OptionsComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
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 '';
|
||||
return "";
|
||||
}
|
||||
|
||||
const selectedPath = _.first(dialogResult.filePaths);
|
||||
if (!selectedPath) {
|
||||
console.warn('No path selected')
|
||||
return '';
|
||||
console.warn("No path selected");
|
||||
return "";
|
||||
}
|
||||
|
||||
console.log('dialogResult', selectedPath);
|
||||
console.log("dialogResult", selectedPath);
|
||||
|
||||
if (this._warcraftService.setWowFolderPath(clientType, selectedPath)) {
|
||||
return selectedPath;
|
||||
}
|
||||
|
||||
const clientFolderName = this._warcraftService.getClientFolderName(clientType);
|
||||
const clientExecutableName = this._warcraftService.getExecutableName(clientType);
|
||||
const clientExecutablePath = path.join(selectedPath, clientFolderName, clientExecutableName);
|
||||
const clientFolderName = this._warcraftService.getClientFolderName(
|
||||
clientType
|
||||
);
|
||||
const clientExecutableName = this._warcraftService.getExecutableName(
|
||||
clientType
|
||||
);
|
||||
const clientExecutablePath = path.join(
|
||||
selectedPath,
|
||||
clientFolderName,
|
||||
clientExecutableName
|
||||
);
|
||||
const dialogRef = this._dialog.open(AlertDialogComponent, {
|
||||
data: {
|
||||
title: `Alert`,
|
||||
message: `Unable to set "${selectedPath}" as your ${getEnumName(WowClientType, clientType)} folder.\nPath not found: "${clientExecutablePath}".`
|
||||
}
|
||||
message: `Unable to set "${selectedPath}" as your ${getEnumName(
|
||||
WowClientType,
|
||||
clientType
|
||||
)} folder.\nPath not found: "${clientExecutablePath}".`,
|
||||
},
|
||||
});
|
||||
|
||||
await dialogRef.afterClosed().toPromise();
|
||||
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
private loadData() {
|
||||
this.zone.run(() => {
|
||||
this.telemetryEnabled = this._wowUpService.telemetryEnabled;
|
||||
this.telemetryEnabled = this._analyticsService.telemetryEnabled;
|
||||
this.collapseToTray = this._wowUpService.collapseToTray;
|
||||
this.retailLocation = this.warcraft.getClientLocation(WowClientType.Retail);
|
||||
this.classicLocation = this.warcraft.getClientLocation(WowClientType.Classic);
|
||||
this.retailPtrLocation = this.warcraft.getClientLocation(WowClientType.RetailPtr);
|
||||
this.classicPtrLocation = this.warcraft.getClientLocation(WowClientType.ClassicPtr);
|
||||
this.retailLocation = this.warcraft.getClientLocation(
|
||||
WowClientType.Retail
|
||||
);
|
||||
this.classicLocation = this.warcraft.getClientLocation(
|
||||
WowClientType.Classic
|
||||
);
|
||||
this.retailPtrLocation = this.warcraft.getClientLocation(
|
||||
WowClientType.RetailPtr
|
||||
);
|
||||
this.classicPtrLocation = this.warcraft.getClientLocation(
|
||||
WowClientType.ClassicPtr
|
||||
);
|
||||
this.betaLocation = this.warcraft.getClientLocation(WowClientType.Beta);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import { Injectable, Injector } from "@angular/core";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AddonStorageService } from "../storage/addon-storage.service";
|
||||
import { Addon } from "../../entities/addon";
|
||||
import { WarcraftService } from "../warcraft/warcraft.service";
|
||||
import { AddonProvider } from "../../addon-providers/addon-provider";
|
||||
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import * as _ from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { WowUpApiService } from "../wowup-api/wowup-api.service";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AddonSearchResult } from "app/models/wowup/addon-search-result";
|
||||
import { AddonSearchResultFile } from "app/models/wowup/addon-search-result-file";
|
||||
import { forkJoin, Observable, Subject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { AddonInstallState } from "app/models/wowup/addon-install-state";
|
||||
import { DownloadSevice } from "../download/download.service";
|
||||
import { WowUpService } from "../wowup/wowup.service";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { TocService } from "../toc/toc.service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
|
||||
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
|
||||
import { AddonProviderFactory } from "./addon.provider.factory";
|
||||
import { AnalyticsService } from "../analytics/analytics.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@@ -44,6 +37,7 @@ export class AddonService {
|
||||
|
||||
constructor(
|
||||
private _addonStorage: AddonStorageService,
|
||||
private _analyticsService: AnalyticsService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _downloadService: DownloadSevice,
|
||||
@@ -52,7 +46,6 @@ export class AddonService {
|
||||
private _addonProviderFactory: AddonProviderFactory
|
||||
) {
|
||||
this._addonProviders = [
|
||||
this._addonProviderFactory.createWowUpAddonProvider(),
|
||||
this._addonProviderFactory.createCurseAddonProvider(),
|
||||
this._addonProviderFactory.createTukUiAddonProvider(),
|
||||
this._addonProviderFactory.createWowInterfaceAddonProvider(),
|
||||
@@ -67,20 +60,25 @@ export class AddonService {
|
||||
public async search(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
): Promise<AddonSearchResult[]> {
|
||||
var searchTasks = this._addonProviders.map((p) =>
|
||||
p.searchByQuery(query, clientType)
|
||||
);
|
||||
var searchResults = await Promise.all(searchTasks);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "Search", $"{clientType}|{query}");
|
||||
await this._analyticsService.trackUserAction(
|
||||
"Addons",
|
||||
"Search",
|
||||
`${clientType}|${query}`
|
||||
);
|
||||
|
||||
const flatResults = searchResults.flat(1);
|
||||
|
||||
return _.orderBy(flatResults, "downloadCount").reverse();
|
||||
}
|
||||
|
||||
public async installPotentialAddon(
|
||||
potentialAddon: PotentialAddon,
|
||||
potentialAddon: AddonSearchResult,
|
||||
clientType: WowClientType,
|
||||
onUpdate: (
|
||||
installState: AddonInstallState,
|
||||
@@ -101,6 +99,7 @@ export class AddonService {
|
||||
clientType
|
||||
).toPromise();
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
|
||||
await this.installAddon(addon.id, onUpdate);
|
||||
}
|
||||
|
||||
@@ -114,7 +113,6 @@ export class AddonService {
|
||||
|
||||
for (let clientTypeStr in clientTypeGroups) {
|
||||
const clientType: WowClientType = parseInt(clientTypeStr, 10);
|
||||
// console.log('clientType', clientType, clientTypeGroups[clientType]);
|
||||
|
||||
const synced = await this.syncAddons(
|
||||
clientType,
|
||||
@@ -133,7 +131,7 @@ export class AddonService {
|
||||
await this.installAddon(addon.id);
|
||||
updateCt += 1;
|
||||
} catch (err) {
|
||||
// _analyticsService.Track(ex, "Failed to install addon");
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,7 +172,6 @@ export class AddonService {
|
||||
|
||||
let downloadedFilePath = "";
|
||||
let unzippedDirectory = "";
|
||||
let downloadedThumbnail = "";
|
||||
try {
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(
|
||||
addon.downloadUrl,
|
||||
@@ -182,6 +179,7 @@ export class AddonService {
|
||||
);
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Installing, 75);
|
||||
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Installing,
|
||||
@@ -192,6 +190,7 @@ export class AddonService {
|
||||
this._wowUpService.applicationDownloadsFolderPath,
|
||||
uuidv4()
|
||||
);
|
||||
|
||||
unzippedDirectory = await this._downloadService.unzipFile(
|
||||
downloadedFilePath,
|
||||
unzipPath
|
||||
@@ -215,7 +214,11 @@ export class AddonService {
|
||||
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "InstallById", $"{addon.ClientType}|{addon.Name}");
|
||||
await this._analyticsService.trackUserAction(
|
||||
"Addons",
|
||||
"InstallById",
|
||||
`${addon.clientType}|${addon.name}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -238,6 +241,28 @@ export class AddonService {
|
||||
});
|
||||
}
|
||||
|
||||
public async logDebugData() {
|
||||
const curseProvider = this._addonProviders.find(
|
||||
(p) => p.name === "Curse"
|
||||
) as CurseAddonProvider;
|
||||
|
||||
const clientTypes = await this._warcraftService.getWowClientTypes();
|
||||
for (let clientType of clientTypes) {
|
||||
const addonFolders = await this._warcraftService.listAddons(clientType);
|
||||
const scanResults = await curseProvider.getScanResults(addonFolders);
|
||||
const map = {};
|
||||
|
||||
scanResults.forEach((sr) => (map[sr.folderName] = sr.fingerprint));
|
||||
|
||||
console.log(
|
||||
`clientType ${this._warcraftService.getClientDisplayName(
|
||||
clientType
|
||||
)} addon fingerprints`
|
||||
);
|
||||
console.log(map);
|
||||
}
|
||||
}
|
||||
|
||||
private async getLatestGameVersion(
|
||||
baseDir: string,
|
||||
installedFolders: string[]
|
||||
@@ -370,7 +395,7 @@ export class AddonService {
|
||||
}
|
||||
|
||||
public async removeAddon(addon: Addon) {
|
||||
const installedDirectories = addon.installedFolders.split(",");
|
||||
const installedDirectories = addon.installedFolders?.split(",") ?? [];
|
||||
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
addon.clientType
|
||||
@@ -389,10 +414,13 @@ export class AddonService {
|
||||
rescan = false
|
||||
): Promise<Addon[]> {
|
||||
let addons = this._addonStorage.getAllForClientType(clientType);
|
||||
if (rescan ) {
|
||||
if (rescan) {
|
||||
const newAddons = await this.scanAddons(clientType);
|
||||
console.log(newAddons)
|
||||
this.updateAddons(addons, newAddons);
|
||||
console.log(newAddons);
|
||||
|
||||
this._addonStorage.removeAllForClientType(clientType);
|
||||
addons = this.updateAddons(addons, newAddons);
|
||||
this._addonStorage.saveAll(addons);
|
||||
}
|
||||
|
||||
await this.syncAddons(clientType, addons);
|
||||
@@ -400,51 +428,24 @@ export class AddonService {
|
||||
return addons;
|
||||
}
|
||||
|
||||
private updateAddons(existingAddons: Addon[], newAddons: Addon[]): Addon[] {
|
||||
const removedAddons = existingAddons.filter(
|
||||
(existingAddon) =>
|
||||
!newAddons.some((newAddon) => this.addonsMatch(existingAddon, newAddon))
|
||||
);
|
||||
|
||||
const addedAddons = newAddons.filter(
|
||||
(newAddon) =>
|
||||
!existingAddons.some((existingAddon) =>
|
||||
this.addonsMatch(existingAddon, newAddon)
|
||||
)
|
||||
);
|
||||
|
||||
_.remove(existingAddons, (addon) =>
|
||||
removedAddons.some((removedAddon) => removedAddon.id === addon.id)
|
||||
);
|
||||
|
||||
existingAddons.push(...addedAddons);
|
||||
|
||||
for (let existingAddon of existingAddons) {
|
||||
var matchingAddon = newAddons.find((newAddon) =>
|
||||
this.addonsMatch(newAddon, existingAddon)
|
||||
private updateAddons(existingAddons: Addon[], newAddons: Addon[]) {
|
||||
_.forEach(newAddons, (newAddon) => {
|
||||
const existingAddon = _.find(
|
||||
existingAddons,
|
||||
(ea) =>
|
||||
ea.externalId == newAddon.externalId &&
|
||||
ea.providerName == newAddon.providerName
|
||||
);
|
||||
if (!matchingAddon) {
|
||||
continue;
|
||||
|
||||
if (!existingAddon) {
|
||||
return;
|
||||
}
|
||||
|
||||
existingAddon.name = matchingAddon.name;
|
||||
existingAddon.folderName = matchingAddon.folderName;
|
||||
existingAddon.downloadUrl = matchingAddon.downloadUrl;
|
||||
existingAddon.installedVersion = matchingAddon.installedVersion;
|
||||
existingAddon.externalUrl = matchingAddon.externalUrl;
|
||||
existingAddon.latestVersion = matchingAddon.latestVersion;
|
||||
existingAddon.thumbnailUrl = matchingAddon.thumbnailUrl;
|
||||
existingAddon.gameVersion = matchingAddon.gameVersion;
|
||||
existingAddon.author = matchingAddon.author;
|
||||
existingAddon.patreonFundingLink = matchingAddon.patreonFundingLink;
|
||||
existingAddon.githubFundingLink = matchingAddon.githubFundingLink;
|
||||
existingAddon.customFundingLink = matchingAddon.customFundingLink;
|
||||
}
|
||||
newAddon.autoUpdateEnabled = existingAddon.autoUpdateEnabled;
|
||||
newAddon.isIgnored = existingAddon.isIgnored;
|
||||
});
|
||||
|
||||
this._addonStorage.removeAll(...removedAddons);
|
||||
this._addonStorage.setAll(existingAddons);
|
||||
|
||||
return existingAddons;
|
||||
return newAddons;
|
||||
}
|
||||
|
||||
private addonsMatch(addon1: Addon, addon2: Addon): boolean {
|
||||
@@ -561,7 +562,7 @@ export class AddonService {
|
||||
|
||||
public getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Observable<PotentialAddon[]> {
|
||||
): Observable<AddonSearchResult[]> {
|
||||
return forkJoin(
|
||||
this._addonProviders.map((p) => p.getFeaturedAddons(clientType))
|
||||
).pipe(
|
||||
|
||||
@@ -1,47 +1,124 @@
|
||||
import { ErrorHandler, Injectable } from "@angular/core";
|
||||
import * as Rollbar from 'rollbar';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PreferenceStorageService } from "../storage/preference-storage.service";
|
||||
import { telemetryEnabledKey } from "../../../constants";
|
||||
import { AppConfig } from "environments/environment";
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AnalyticsService implements ErrorHandler {
|
||||
private rollbarConfig = {
|
||||
accessToken: 'd01c11314a064572b11acee18d880650',
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
};
|
||||
export class AnalyticsService {
|
||||
private readonly analyticsUrl = "https://www.google-analytics.com";
|
||||
private readonly installIdPreferenceKey = "install_id";
|
||||
private readonly _installId: string;
|
||||
private readonly _appVersion: string;
|
||||
private readonly _telemetryEnabledSrc = new BehaviorSubject(false);
|
||||
|
||||
private _rollbar: Rollbar;
|
||||
private get rollbar() {
|
||||
if (!this.telemetryEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
public readonly telemetryPromptUsedKey = "telemetry_prompt_sent";
|
||||
public readonly telemetryEnabledKey = "telemetry_enabled";
|
||||
public readonly telemetryEnabled$ = this._telemetryEnabledSrc.asObservable();
|
||||
|
||||
if (!this._rollbar) {
|
||||
this._rollbar = new Rollbar(this.rollbarConfig);
|
||||
}
|
||||
return this._rollbar;
|
||||
private get installId() {
|
||||
return this._installId;
|
||||
}
|
||||
|
||||
public get shouldPromptTelemetry() {
|
||||
return (
|
||||
this._preferenceStorageService.get(this.telemetryEnabledKey) === undefined
|
||||
);
|
||||
}
|
||||
|
||||
public get telemetryEnabled() {
|
||||
const preference = this._preferenceStorageService.findByKey(
|
||||
this.telemetryEnabledKey
|
||||
);
|
||||
return preference === true.toString();
|
||||
}
|
||||
|
||||
public set telemetryEnabled(value: boolean) {
|
||||
this._preferenceStorageService.set(this.telemetryEnabledKey, value);
|
||||
this._telemetryEnabledSrc.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _httpClient: HttpClient,
|
||||
private _preferenceStorageService: PreferenceStorageService
|
||||
) {
|
||||
this._appVersion = _electronService.remote.app.getVersion();
|
||||
this._installId = this.loadInstallId();
|
||||
this._telemetryEnabledSrc.next(this.telemetryEnabled);
|
||||
console.log("installId", this._installId);
|
||||
}
|
||||
|
||||
public async trackStartup() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
private async track(action: (params: HttpParams) => void = undefined) {
|
||||
if (!this.telemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
private get telemetryEnabled() {
|
||||
return this._preferenceStorageService.get(telemetryEnabledKey) === true.toString();
|
||||
var url = `${this.analyticsUrl}/collect`;
|
||||
|
||||
try {
|
||||
let params = new URLSearchParams();
|
||||
params.set("v", "1");
|
||||
params.set("tid", AppConfig.googleAnalyticsId);
|
||||
params.set("cid", this._installId);
|
||||
params.set("ua", window.navigator.userAgent);
|
||||
params.set("an", "WowUp Client");
|
||||
params.set("av", this._appVersion);
|
||||
|
||||
action?.call(this, params);
|
||||
|
||||
const fullUrl = `${url}?${params}`;
|
||||
|
||||
const response = await this._httpClient
|
||||
.post(
|
||||
fullUrl,
|
||||
{},
|
||||
{
|
||||
responseType: "text",
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
} catch (e) {
|
||||
// eat
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private loadInstallId() {
|
||||
let installId = this._preferenceStorageService.findByKey(
|
||||
this.installIdPreferenceKey
|
||||
);
|
||||
if (installId) {
|
||||
return installId;
|
||||
}
|
||||
|
||||
installId = uuidv4();
|
||||
this._preferenceStorageService.set(this.installIdPreferenceKey, installId);
|
||||
|
||||
public get shouldPromptTelemetry() {
|
||||
return this._preferenceStorageService.get(telemetryEnabledKey) === undefined;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _preferenceStorageService: PreferenceStorageService
|
||||
) { }
|
||||
|
||||
// ErrorHandler
|
||||
handleError(error: any): void {
|
||||
console.error('Caught error', error);
|
||||
|
||||
this.rollbar?.error(error.originalError || error);
|
||||
}
|
||||
}
|
||||
return installId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { COPY_FILE_CHANNEL, DOWNLOAD_FILE_CHANNEL, UNZIP_FILE_CHANNEL } from "common/constants";
|
||||
import {
|
||||
COPY_FILE_CHANNEL,
|
||||
DOWNLOAD_FILE_CHANNEL,
|
||||
UNZIP_FILE_CHANNEL,
|
||||
} from "common/constants";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { DownloadRequest } from "common/models/download-request";
|
||||
import { DownloadStatus } from "common/models/download-status";
|
||||
import { DownloadStatusType } from "common/models/download-status-type";
|
||||
@@ -10,78 +15,93 @@ import { ElectronService } from "../electron/electron.service";
|
||||
import { CopyFileRequest } from "common/models/copy-file-request";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class DownloadSevice {
|
||||
constructor(private _electronService: ElectronService) {}
|
||||
|
||||
constructor(
|
||||
private _electronService: ElectronService
|
||||
) { }
|
||||
public downloadZipFile(
|
||||
url: string,
|
||||
outputFolder: string,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request: DownloadRequest = {
|
||||
url,
|
||||
outputFolder,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
public downloadZipFile(url: string, outputFolder: string, onProgress?: (progress: number) => void): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: DownloadStatus) => {
|
||||
switch (arg.type) {
|
||||
case DownloadStatusType.Complete:
|
||||
resolve(arg.savePath);
|
||||
this._electronService.ipcRenderer.off(url, eventHandler);
|
||||
break;
|
||||
case DownloadStatusType.Error:
|
||||
reject(arg.error);
|
||||
this._electronService.ipcRenderer.off(url, eventHandler);
|
||||
break;
|
||||
case DownloadStatusType.Progress:
|
||||
onProgress?.call(null, arg.progress);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const eventHandler = (_evt: any, arg: DownloadStatus) => {
|
||||
switch (arg.type) {
|
||||
case DownloadStatusType.Complete:
|
||||
this._electronService.ipcRenderer.off(
|
||||
request.responseKey,
|
||||
eventHandler
|
||||
);
|
||||
resolve(arg.savePath);
|
||||
break;
|
||||
case DownloadStatusType.Error:
|
||||
this._electronService.ipcRenderer.off(
|
||||
request.responseKey,
|
||||
eventHandler
|
||||
);
|
||||
reject(arg.error);
|
||||
break;
|
||||
case DownloadStatusType.Progress:
|
||||
onProgress?.call(null, arg.progress);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.on(url, eventHandler);
|
||||
this._electronService.ipcRenderer.send(DOWNLOAD_FILE_CHANNEL, { url, outputFolder } as DownloadRequest);
|
||||
})
|
||||
}
|
||||
this._electronService.ipcRenderer.on(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(DOWNLOAD_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
public unzipFile(zipFilePath: string, outputFolder: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: UnzipStatus) => {
|
||||
this._electronService.ipcRenderer.off(zipFilePath, eventHandler);
|
||||
public unzipFile(zipFilePath: string, outputFolder: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: UnzipStatus) => {
|
||||
if (arg.type === UnzipStatusType.Error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
resolve(arg.outputFolder);
|
||||
};
|
||||
|
||||
if (arg.type === UnzipStatusType.Error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
resolve(arg.outputFolder);
|
||||
}
|
||||
const request: UnzipRequest = {
|
||||
outputFolder,
|
||||
zipFilePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
const request: UnzipRequest = {
|
||||
outputFolder,
|
||||
zipFilePath
|
||||
};
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(UNZIP_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
this._electronService.ipcRenderer.on(zipFilePath, eventHandler);
|
||||
this._electronService.ipcRenderer.send(UNZIP_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
public copyFile(
|
||||
sourceFilePath: string,
|
||||
destinationFilePath: string
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: { error?: Error }) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
public copyFile(sourceFilePath: string, destinationFilePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: { error?: Error }) => {
|
||||
this._electronService.ipcRenderer.off(destinationFilePath, eventHandler);
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
resolve(destinationFilePath);
|
||||
};
|
||||
|
||||
resolve(destinationFilePath);
|
||||
};
|
||||
const request: CopyFileRequest = {
|
||||
destinationFilePath,
|
||||
sourceFilePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
const request: CopyFileRequest = {
|
||||
destinationFilePath,
|
||||
sourceFilePath
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.on(destinationFilePath, eventHandler);
|
||||
this._electronService.ipcRenderer.send(COPY_FILE_CHANNEL, request);
|
||||
})
|
||||
}
|
||||
}
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(COPY_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// If you import a module but never use any of the imported values other than as TypeScript types,
|
||||
// the resulting javascript file will look as if you never imported the module at all.
|
||||
import { ipcRenderer, webFrame, remote, shell } from 'electron';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ipcRenderer, webFrame, remote, shell } from "electron";
|
||||
import * as childProcess from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { ValueResponse } from "common/models/value-response";
|
||||
import { ValueRequest } from "common/models/value-request";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class ElectronService {
|
||||
private readonly _windowMaximizedSrc = new BehaviorSubject(false);
|
||||
@@ -31,7 +34,7 @@ export class ElectronService {
|
||||
}
|
||||
|
||||
get locale(): string {
|
||||
return this.remote.app.getLocale().split('-')[0];
|
||||
return this.remote.app.getLocale().split("-")[0];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -39,27 +42,27 @@ export class ElectronService {
|
||||
if (!this.isElectron) {
|
||||
return;
|
||||
}
|
||||
this.ipcRenderer = window.require('electron').ipcRenderer;
|
||||
this.webFrame = window.require('electron').webFrame;
|
||||
this.remote = window.require('electron').remote;
|
||||
this.shell = window.require('electron').shell;
|
||||
this.ipcRenderer = window.require("electron").ipcRenderer;
|
||||
this.webFrame = window.require("electron").webFrame;
|
||||
this.remote = window.require("electron").remote;
|
||||
this.shell = window.require("electron").shell;
|
||||
|
||||
this.childProcess = window.require('child_process');
|
||||
this.fs = window.require('fs');
|
||||
this.childProcess = window.require("child_process");
|
||||
this.fs = window.require("fs");
|
||||
|
||||
this.remote.getCurrentWindow().on('minimize', () => {
|
||||
this.remote.getCurrentWindow().on("minimize", () => {
|
||||
this._windowMinimizedSrc.next(true);
|
||||
});
|
||||
|
||||
this.remote.getCurrentWindow().on('restore', () => {
|
||||
this.remote.getCurrentWindow().on("restore", () => {
|
||||
this._windowMinimizedSrc.next(false);
|
||||
});
|
||||
|
||||
this.remote.getCurrentWindow().on('maximize', () => {
|
||||
this.remote.getCurrentWindow().on("maximize", () => {
|
||||
this._windowMaximizedSrc.next(true);
|
||||
});
|
||||
|
||||
this.remote.getCurrentWindow().on('unmaximize', () => {
|
||||
this.remote.getCurrentWindow().on("unmaximize", () => {
|
||||
this._windowMaximizedSrc.next(false);
|
||||
});
|
||||
|
||||
@@ -86,4 +89,31 @@ export class ElectronService {
|
||||
this.remote.getCurrentWindow().close();
|
||||
this.remote.app.quit();
|
||||
}
|
||||
|
||||
public showNotification(title: string, options?: NotificationOptions) {
|
||||
const myNotification = new Notification(title, options);
|
||||
}
|
||||
|
||||
public sendIpcValueMessage<TIN, TOUT>(
|
||||
channel: string,
|
||||
value: TIN
|
||||
): Promise<TOUT> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: ValueResponse<TOUT>) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
resolve(arg.value);
|
||||
};
|
||||
|
||||
const request: ValueRequest<TIN> = {
|
||||
value,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this.ipcRenderer.send(channel, request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, LIST_DIRECTORIES_CHANNEL, LIST_FILES_CHANNEL, PATH_EXISTS_CHANNEL, READ_FILE_CHANNEL, RENAME_DIRECTORY_CHANNEL, SHOW_DIRECTORY } from "common/constants";
|
||||
import {
|
||||
COPY_DIRECTORY_CHANNEL,
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
GET_ASSET_FILE_PATH,
|
||||
LIST_DIRECTORIES_CHANNEL,
|
||||
LIST_FILES_CHANNEL,
|
||||
PATH_EXISTS_CHANNEL,
|
||||
READ_FILE_CHANNEL,
|
||||
RENAME_DIRECTORY_CHANNEL,
|
||||
SHOW_DIRECTORY,
|
||||
} from "common/constants";
|
||||
import { CopyDirectoryRequest } from "common/models/copy-directory-request";
|
||||
import { DeleteDirectoryRequest } from "common/models/delete-directory-request";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import * as fs from 'fs';
|
||||
import * as globrex from 'globrex';
|
||||
import * as fs from "fs";
|
||||
import * as globrex from "globrex";
|
||||
import { ReadFileResponse } from "common/models/read-file-response";
|
||||
import { ReadFileRequest } from "common/models/read-file-request";
|
||||
import { ListFilesResponse } from "common/models/list-files-response";
|
||||
import { ListFilesRequest } from "common/models/list-files-request";
|
||||
import { ShowDirectoryRequest } from "common/models/show-directory-request";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ValueRequest } from "common/models/value-request";
|
||||
import { ValueResponse } from "common/models/value-response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class FileService {
|
||||
constructor(private _electronService: ElectronService) {}
|
||||
|
||||
constructor(
|
||||
private _electronService: ElectronService
|
||||
) { }
|
||||
public async getAssetFilePath(fileName: string) {
|
||||
return await this._electronService.sendIpcValueMessage<string, string>(
|
||||
GET_ASSET_FILE_PATH,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
public showDirectory(sourceDir: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -29,11 +43,14 @@ export class FileService {
|
||||
resolve(arg);
|
||||
};
|
||||
|
||||
const request: ShowDirectoryRequest = { sourceDir, responseKey: uuidv4() };
|
||||
const request: ShowDirectoryRequest = {
|
||||
sourceDir,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(SHOW_DIRECTORY, request);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public pathExists(sourcePath: string) {
|
||||
@@ -45,11 +62,14 @@ export class FileService {
|
||||
resolve(arg.value);
|
||||
};
|
||||
|
||||
const request: ValueRequest<string> = { value: sourcePath, responseKey: uuidv4() };
|
||||
const request: ValueRequest<string> = {
|
||||
value: sourcePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(PATH_EXISTS_CHANNEL, request);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public deleteDirectory(sourcePath: string) {
|
||||
@@ -61,11 +81,14 @@ export class FileService {
|
||||
resolve(sourcePath);
|
||||
};
|
||||
|
||||
const request: DeleteDirectoryRequest = { sourcePath };
|
||||
const request: DeleteDirectoryRequest = {
|
||||
sourcePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(sourcePath, eventHandler);
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(DELETE_DIRECTORY_CHANNEL, request);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public copyDirectory(sourcePath: string, destinationPath: string) {
|
||||
@@ -78,11 +101,15 @@ export class FileService {
|
||||
resolve(destinationPath);
|
||||
};
|
||||
|
||||
const request: CopyDirectoryRequest = { sourcePath, destinationPath };
|
||||
const request: CopyDirectoryRequest = {
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(destinationPath, eventHandler);
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(COPY_DIRECTORY_CHANNEL, request);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public renameDirectory(sourcePath: string, destinationPath: string) {
|
||||
@@ -95,9 +122,13 @@ export class FileService {
|
||||
resolve(destinationPath);
|
||||
};
|
||||
|
||||
const request: CopyDirectoryRequest = { sourcePath, destinationPath };
|
||||
const request: CopyDirectoryRequest = {
|
||||
sourcePath,
|
||||
destinationPath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(destinationPath, eventHandler);
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(RENAME_DIRECTORY_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
@@ -112,11 +143,14 @@ export class FileService {
|
||||
resolve(arg.data);
|
||||
};
|
||||
|
||||
const request: ReadFileRequest = { sourcePath };
|
||||
const request: ReadFileRequest = {
|
||||
sourcePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(sourcePath, eventHandler);
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(READ_FILE_CHANNEL, request);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public listDirectories(sourcePath: string): Promise<string[]> {
|
||||
@@ -129,7 +163,10 @@ export class FileService {
|
||||
resolve(arg.value);
|
||||
};
|
||||
|
||||
const request: ValueRequest<string> = { value: sourcePath, responseKey: uuidv4() };
|
||||
const request: ValueRequest<string> = {
|
||||
value: sourcePath,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(LIST_DIRECTORIES_CHANNEL, request);
|
||||
@@ -139,12 +176,16 @@ export class FileService {
|
||||
public listFiles(sourcePath: string, filter: string) {
|
||||
const globFilter = globrex(filter);
|
||||
|
||||
return fs.readdirSync(sourcePath, { withFileTypes: true })
|
||||
.filter(entry => !!globFilter.regex.test(entry.name))
|
||||
.map(entry => entry.name);
|
||||
return fs
|
||||
.readdirSync(sourcePath, { withFileTypes: true })
|
||||
.filter((entry) => !!globFilter.regex.test(entry.name))
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
|
||||
public listAllFiles(sourcePath: string, recursive: boolean = true): Promise<string[]> {
|
||||
public listAllFiles(
|
||||
sourcePath: string,
|
||||
recursive: boolean = true
|
||||
): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: ListFilesResponse) => {
|
||||
if (arg.error) {
|
||||
@@ -154,10 +195,14 @@ export class FileService {
|
||||
resolve(arg.files);
|
||||
};
|
||||
|
||||
const request: ListFilesRequest = { sourcePath, recursive, responseKey: uuidv4() };
|
||||
const request: ListFilesRequest = {
|
||||
sourcePath,
|
||||
recursive,
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(sourcePath, eventHandler);
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(LIST_FILES_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AddonStorageService {
|
||||
return addons;
|
||||
}
|
||||
|
||||
public setAll(addons: Addon[]) {
|
||||
public saveAll(addons: Addon[]) {
|
||||
addons.forEach(addon => this.set(addon.id, addon));
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class AddonStorageService {
|
||||
this._store.delete(addon.id);
|
||||
}
|
||||
|
||||
public removeForClientType(clientType: WowClientType) {
|
||||
public removeAllForClientType(clientType: WowClientType) {
|
||||
const addons = this.getAllForClientType(clientType);
|
||||
addons.forEach(addon => this._store.delete(addon.id));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { remote } from "electron";
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PreferenceStorageService } from "../storage/preference-storage.service";
|
||||
import { WowUpReleaseChannelType } from "app/models/wowup/wowup-release-channel-type";
|
||||
import { getEnumList, getEnumName } from "app/utils/enum.utils";
|
||||
@@ -15,7 +15,7 @@ import { from, Observable, of, Subject } from "rxjs";
|
||||
import { LatestVersionResponse } from "app/models/wowup-api/latest-version-response";
|
||||
import { map, switchMap } from "rxjs/operators";
|
||||
import { LatestVersion } from "app/models/wowup-api/latest-version";
|
||||
import * as compareVersions from 'compare-versions';
|
||||
import * as compareVersions from "compare-versions";
|
||||
import { DownloadSevice } from "../download/download.service";
|
||||
import { PreferenceChange } from "app/models/wowup/preference-change";
|
||||
import { FileService } from "../files/file.service";
|
||||
@@ -24,24 +24,32 @@ import {
|
||||
defaultAutoUpdateKeySuffix,
|
||||
defaultChannelKeySuffix,
|
||||
lastSelectedWowClientTypeKey,
|
||||
telemetryEnabledKey,
|
||||
wowupReleaseChannelKey
|
||||
wowupReleaseChannelKey,
|
||||
} from "../../../constants";
|
||||
|
||||
const LATEST_VERSION_CACHE_KEY = 'latest-version-response';
|
||||
const LATEST_VERSION_CACHE_KEY = "latest-version-response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class WowUpService {
|
||||
|
||||
private readonly _preferenceChangeSrc = new Subject<PreferenceChange>();
|
||||
|
||||
public readonly updaterName = 'WowUpUpdater.exe';
|
||||
public readonly applicationFolderPath: string = remote.app.getPath('userData');
|
||||
public readonly applicationLogsFolderPath: string = remote.app.getPath('logs');
|
||||
public readonly applicationDownloadsFolderPath: string = join(this.applicationFolderPath, 'downloads');
|
||||
public readonly applicationUpdaterPath: string = join(this.applicationFolderPath, this.updaterName);
|
||||
public readonly updaterName = "WowUpUpdater.exe";
|
||||
public readonly applicationFolderPath: string = remote.app.getPath(
|
||||
"userData"
|
||||
);
|
||||
public readonly applicationLogsFolderPath: string = remote.app.getPath(
|
||||
"logs"
|
||||
);
|
||||
public readonly applicationDownloadsFolderPath: string = join(
|
||||
this.applicationFolderPath,
|
||||
"downloads"
|
||||
);
|
||||
public readonly applicationUpdaterPath: string = join(
|
||||
this.applicationFolderPath,
|
||||
this.updaterName
|
||||
);
|
||||
public readonly applicationVersion: string;
|
||||
public readonly isBetaBuild: boolean;
|
||||
public readonly preferenceChange$ = this._preferenceChangeSrc.asObservable();
|
||||
@@ -57,7 +65,8 @@ export class WowUpService {
|
||||
this.setDefaultPreferences();
|
||||
|
||||
this.applicationVersion = _electronService.remote.app.getVersion();
|
||||
this.isBetaBuild = this.applicationVersion.toLowerCase().indexOf('beta') != -1;
|
||||
this.isBetaBuild =
|
||||
this.applicationVersion.toLowerCase().indexOf("beta") != -1;
|
||||
}
|
||||
|
||||
public get updaterExists() {
|
||||
@@ -65,29 +74,22 @@ export class WowUpService {
|
||||
}
|
||||
|
||||
public get collapseToTray() {
|
||||
const preference = this._preferenceStorageService.findByKey(collapseToTrayKey);
|
||||
return preference === 'true';
|
||||
const preference = this._preferenceStorageService.findByKey(
|
||||
collapseToTrayKey
|
||||
);
|
||||
return preference === "true";
|
||||
}
|
||||
|
||||
public set collapseToTray(value: boolean) {
|
||||
const key = collapseToTrayKey;
|
||||
this._preferenceStorageService.set(key, value);
|
||||
this._preferenceChangeSrc.next({ key, value: value.toString() })
|
||||
}
|
||||
|
||||
public get telemetryEnabled() {
|
||||
const preference = this._preferenceStorageService.findByKey(telemetryEnabledKey);
|
||||
return preference === 'true';
|
||||
}
|
||||
|
||||
public set telemetryEnabled(value: boolean) {
|
||||
const key = telemetryEnabledKey;
|
||||
this._preferenceStorageService.set(key, value);
|
||||
this._preferenceChangeSrc.next({ key, value: value.toString() })
|
||||
this._preferenceChangeSrc.next({ key, value: value.toString() });
|
||||
}
|
||||
|
||||
public get wowUpReleaseChannel() {
|
||||
const preference = this._preferenceStorageService.findByKey(wowupReleaseChannelKey);
|
||||
const preference = this._preferenceStorageService.findByKey(
|
||||
wowupReleaseChannelKey
|
||||
);
|
||||
return parseInt(preference, 10) as WowUpReleaseChannelType;
|
||||
}
|
||||
|
||||
@@ -96,15 +98,23 @@ export class WowUpService {
|
||||
}
|
||||
|
||||
public get lastSelectedClientType(): WowClientType {
|
||||
const preference = this._preferenceStorageService.findByKey(lastSelectedWowClientTypeKey);
|
||||
const preference = this._preferenceStorageService.findByKey(
|
||||
lastSelectedWowClientTypeKey
|
||||
);
|
||||
const value = parseInt(preference, 10);
|
||||
return isNaN(value)
|
||||
? WowClientType.None
|
||||
: value as WowClientType;
|
||||
return isNaN(value) ? WowClientType.None : (value as WowClientType);
|
||||
}
|
||||
|
||||
public set lastSelectedClientType(clientType: WowClientType) {
|
||||
this._preferenceStorageService.set(lastSelectedWowClientTypeKey, clientType);
|
||||
this._preferenceStorageService.set(
|
||||
lastSelectedWowClientTypeKey,
|
||||
clientType
|
||||
);
|
||||
}
|
||||
|
||||
public getClientDefaultAddonChannelKey(clientType: WowClientType) {
|
||||
const typeName = getEnumName(WowClientType, clientType);
|
||||
return `${typeName}${defaultChannelKeySuffix}`.toLowerCase();
|
||||
}
|
||||
|
||||
public getDefaultAddonChannel(clientType: WowClientType): AddonChannelType {
|
||||
@@ -113,9 +123,13 @@ export class WowUpService {
|
||||
return parseInt(preference, 10) as AddonChannelType;
|
||||
}
|
||||
|
||||
public setDefaultAddonChannel(clientType: WowClientType, channelType: AddonChannelType) {
|
||||
public setDefaultAddonChannel(
|
||||
clientType: WowClientType,
|
||||
channelType: AddonChannelType
|
||||
) {
|
||||
const key = this.getClientDefaultAddonChannelKey(clientType);
|
||||
this._preferenceStorageService.set(key, channelType);
|
||||
this._preferenceChangeSrc.next({ key, value: channelType.toString() });
|
||||
}
|
||||
|
||||
public getDefaultAutoUpdate(clientType: WowClientType): boolean {
|
||||
@@ -136,51 +150,68 @@ export class WowUpService {
|
||||
public isUpdateAvailable(): Observable<boolean> {
|
||||
const releaseChannel = this.wowUpReleaseChannel;
|
||||
|
||||
return this.getLatestWowUpVersion(releaseChannel)
|
||||
.pipe(
|
||||
map(response => {
|
||||
if (!response?.version) {
|
||||
console.error("Got empty WowUp version");
|
||||
return false;
|
||||
}
|
||||
return this.getLatestWowUpVersion(releaseChannel).pipe(
|
||||
map((response) => {
|
||||
if (!response?.version) {
|
||||
console.error("Got empty WowUp version");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isBetaBuild && releaseChannel != WowUpReleaseChannelType.Beta) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.isBetaBuild &&
|
||||
releaseChannel != WowUpReleaseChannelType.Beta
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return compareVersions(response.version, this._electronService.remote.app.getVersion()) > 0;
|
||||
})
|
||||
);
|
||||
return (
|
||||
compareVersions(
|
||||
response.version,
|
||||
this._electronService.remote.app.getVersion()
|
||||
) > 0
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getLatestWowUpVersion(channel: WowUpReleaseChannelType): Observable<LatestVersion> {
|
||||
const cachedResponse = this._cacheService.get<LatestVersionResponse>(LATEST_VERSION_CACHE_KEY);
|
||||
public getLatestWowUpVersion(
|
||||
channel: WowUpReleaseChannelType
|
||||
): Observable<LatestVersion> {
|
||||
const cachedResponse = this._cacheService.get<LatestVersionResponse>(
|
||||
LATEST_VERSION_CACHE_KEY
|
||||
);
|
||||
if (cachedResponse) {
|
||||
return of(channel === WowUpReleaseChannelType.Beta ? cachedResponse.beta : cachedResponse.stable);
|
||||
}
|
||||
return this._wowUpApiService.getLatestVersion()
|
||||
.pipe(
|
||||
map(response => {
|
||||
this._cacheService.set(LATEST_VERSION_CACHE_KEY, response);
|
||||
return channel === WowUpReleaseChannelType.Beta ? response.beta : response.stable;
|
||||
})
|
||||
return of(
|
||||
channel === WowUpReleaseChannelType.Beta
|
||||
? cachedResponse.beta
|
||||
: cachedResponse.stable
|
||||
);
|
||||
}
|
||||
return this._wowUpApiService.getLatestVersion().pipe(
|
||||
map((response) => {
|
||||
this._cacheService.set(LATEST_VERSION_CACHE_KEY, response);
|
||||
return channel === WowUpReleaseChannelType.Beta
|
||||
? response.beta
|
||||
: response.stable;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getLatestUpdaterVersion() {
|
||||
return this._wowUpApiService.getLatestVersion()
|
||||
.pipe(
|
||||
map(response => {
|
||||
return response.updater;
|
||||
})
|
||||
);
|
||||
return this._wowUpApiService.getLatestVersion().pipe(
|
||||
map((response) => {
|
||||
return response.updater;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public installUpdate() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public checkUpdaterApp(onProgress?: (progress: number) => void): Observable<void> {
|
||||
public checkUpdaterApp(
|
||||
onProgress?: (progress: number) => void
|
||||
): Observable<void> {
|
||||
if (this.updaterExists) {
|
||||
return of(undefined);
|
||||
} else {
|
||||
@@ -188,23 +219,37 @@ export class WowUpService {
|
||||
}
|
||||
}
|
||||
|
||||
private installUpdater(onProgress?: (progress: number) => void): Observable<void> {
|
||||
return this.getLatestUpdaterVersion()
|
||||
.pipe(
|
||||
switchMap(response => from(this._downloadService.downloadZipFile(response.url, this.applicationDownloadsFolderPath, onProgress))),
|
||||
switchMap(downloadedPath => {
|
||||
const unzipPath = join(this.applicationDownloadsFolderPath, uuidv4());
|
||||
return from(this._downloadService.unzipFile(downloadedPath, unzipPath));
|
||||
}),
|
||||
switchMap(unzippedDir => {
|
||||
console.log(unzippedDir);
|
||||
const newUpdaterPath = join(unzippedDir, this.updaterName);
|
||||
return from(this._downloadService.copyFile(newUpdaterPath, this.applicationUpdaterPath));
|
||||
}),
|
||||
map(() => {
|
||||
console.log('DOWNLOAD COMPLETE')
|
||||
})
|
||||
)
|
||||
private installUpdater(
|
||||
onProgress?: (progress: number) => void
|
||||
): Observable<void> {
|
||||
return this.getLatestUpdaterVersion().pipe(
|
||||
switchMap((response) =>
|
||||
from(
|
||||
this._downloadService.downloadZipFile(
|
||||
response.url,
|
||||
this.applicationDownloadsFolderPath,
|
||||
onProgress
|
||||
)
|
||||
)
|
||||
),
|
||||
switchMap((downloadedPath) => {
|
||||
const unzipPath = join(this.applicationDownloadsFolderPath, uuidv4());
|
||||
return from(this._downloadService.unzipFile(downloadedPath, unzipPath));
|
||||
}),
|
||||
switchMap((unzippedDir) => {
|
||||
console.log(unzippedDir);
|
||||
const newUpdaterPath = join(unzippedDir, this.updaterName);
|
||||
return from(
|
||||
this._downloadService.copyFile(
|
||||
newUpdaterPath,
|
||||
this.applicationUpdaterPath
|
||||
)
|
||||
);
|
||||
}),
|
||||
map(() => {
|
||||
console.log("DOWNLOAD COMPLETE");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private setDefaultPreference(key: string, defaultValue: any) {
|
||||
@@ -214,11 +259,6 @@ export class WowUpService {
|
||||
}
|
||||
}
|
||||
|
||||
private getClientDefaultAddonChannelKey(clientType: WowClientType) {
|
||||
const typeName = getEnumName(WowClientType, clientType);
|
||||
return `${typeName}${defaultChannelKeySuffix}`.toLowerCase();
|
||||
}
|
||||
|
||||
private getClientDefaultAutoUpdateKey(clientType: WowClientType): string {
|
||||
const typeName = getEnumName(WowClientType, clientType);
|
||||
return `${typeName}${defaultAutoUpdateKeySuffix}`.toLowerCase();
|
||||
@@ -226,13 +266,18 @@ export class WowUpService {
|
||||
|
||||
private setDefaultPreferences() {
|
||||
this.setDefaultPreference(collapseToTrayKey, true);
|
||||
this.setDefaultPreference(wowupReleaseChannelKey, this.getDefaultReleaseChannel());
|
||||
this.setDefaultPreference(
|
||||
wowupReleaseChannelKey,
|
||||
this.getDefaultReleaseChannel()
|
||||
);
|
||||
this.setDefaultClientPreferences();
|
||||
}
|
||||
|
||||
private setDefaultClientPreferences() {
|
||||
const keys = getEnumList<WowClientType>(WowClientType).filter(key => key !== WowClientType.None);
|
||||
keys.forEach(key => {
|
||||
const keys = getEnumList<WowClientType>(WowClientType).filter(
|
||||
(key) => key !== WowClientType.None
|
||||
);
|
||||
keys.forEach((key) => {
|
||||
const preferenceKey = this.getClientDefaultAddonChannelKey(key);
|
||||
this.setDefaultPreference(preferenceKey, AddonChannelType.Stable);
|
||||
|
||||
@@ -242,6 +287,8 @@ export class WowUpService {
|
||||
}
|
||||
|
||||
private getDefaultReleaseChannel() {
|
||||
return this.isBetaBuild ? WowUpReleaseChannelType.Beta : WowUpReleaseChannelType.Stable;
|
||||
return this.isBetaBuild
|
||||
? WowUpReleaseChannelType.Beta
|
||||
: WowUpReleaseChannelType.Stable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"ChangeLogs": [
|
||||
{
|
||||
"Version": "2.0.0-alpha.11",
|
||||
"Description": "Add version label to the 'Get Addons' page.\nUpdates to the addon detail page (by Flippey).\nRussian local updates (by Medok).\nAdd the system notification for auto updates.\nFix some column spacing.\nTelemetry for user actions added.\nFix a bug not allowing users to uninstall an addon with no 'installed folders'.\nHopefully less issues with installing multiple addons at once.\nFix some errors related to addons imported from GitHub."
|
||||
},
|
||||
{
|
||||
"Version": "2.0.0-alpha.10",
|
||||
"Description": "Temporary CF fingerprint endpoint patch."
|
||||
},
|
||||
{
|
||||
"Version": "2.0.0-alpha.9",
|
||||
"Description": "Update Russian locale.\nTry to fix some font blurriness.\nRemove debug error in WowInterface provider."
|
||||
},
|
||||
{
|
||||
"Version": "2.0.0-alpha.8",
|
||||
"Description": "Localize most static text (by Pansa and Medok).\nImplemented addon detail view in my-addons (by Flippeey).\nWindow position/size/maximized state should now be restored when starting the app (by Chops).\nDouble clicking the titlebar on mac should perform the action set in your system settings (by Chops).\nAddon scanning is now much faster.\nWhen scanning addons, the matching channel type should be applied not the default.\nRe-scan is now less complicated and should prevent duplicates.\nAdd the 'Show Folder' button to the addon context menu.\nAdd http circuit breakers to the providers.\nFix issue with My Addons Provider column not sorting.\nPage context data on the footer.\nRemove the windows menu bar buttons for mac.\nAdd WowUp addon provider prototype"
|
||||
},
|
||||
{
|
||||
"Version": "2.0.0-alpha.7",
|
||||
"Description": "Stuff?"
|
||||
},
|
||||
{
|
||||
"Version": "1.16.1",
|
||||
"Description": "Default addon channel can now be set separately per World of Warcraft client.\nUI updates.\nBug Fixes."
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Anbieter",
|
||||
"STATUS_COLUMN_HEADER": "Status"
|
||||
"STATUS_COLUMN_HEADER": "Status",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -119,5 +120,21 @@
|
||||
"POSITIVE_BUTTON": "Sicher!",
|
||||
"TITLE": "WowUp Telemetrie"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Author",
|
||||
"PROVIDER_COLUMN_HEADER": "Provider",
|
||||
"STATUS_COLUMN_HEADER": "Status"
|
||||
"STATUS_COLUMN_HEADER": "Status",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "Downloads"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -120,5 +121,23 @@
|
||||
"POSITIVE_BUTTON": "Sure!",
|
||||
"TITLE": "WowUp Telemetry"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"BACKINGUP": "Backing Up",
|
||||
"COMPLETE": "Installed",
|
||||
"DOWNLOADING": "Downloading",
|
||||
"INSTALLING": "Installing",
|
||||
"PENDING": "Pending",
|
||||
"UNINSTALLING": "Uninstalling",
|
||||
"UPDATING": "Updating..."
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "Uninstall",
|
||||
"IGNORED": "Ignored",
|
||||
"UPDATE": "Update",
|
||||
"INSTALL": "Install",
|
||||
"UPTODATE": "Up to date"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Proveedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
"STATUS_COLUMN_HEADER": "Estado",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -119,5 +120,21 @@
|
||||
"POSITIVE_BUTTON": "¡Seguro!",
|
||||
"TITLE": "Telemetría WowUp"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Auteur",
|
||||
"PROVIDER_COLUMN_HEADER": "Fournisseur",
|
||||
"STATUS_COLUMN_HEADER": "Statut"
|
||||
"STATUS_COLUMN_HEADER": "Statut",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -119,5 +120,21 @@
|
||||
"POSITIVE_BUTTON": "Bien sûr!",
|
||||
"TITLE": "Télémétrie WowUp"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autore",
|
||||
"PROVIDER_COLUMN_HEADER": "Provveditore",
|
||||
"STATUS_COLUMN_HEADER": "Stato"
|
||||
"STATUS_COLUMN_HEADER": "Stato",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -119,5 +120,21 @@
|
||||
"POSITIVE_BUTTON": "Certo!",
|
||||
"TITLE": "Telemetria WowUp"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,141 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Registro de Alterações",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Conheça o nosso site!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Atualizar",
|
||||
"INSTALL_FROM_URL_BUTTON": "Instalar pela URL",
|
||||
"SEARCH_LABEL": "Procurar",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "App funciona!",
|
||||
"GO_TO_DETAIL": "Ir para Detalhes",
|
||||
"MY_ADDONS_TAB_TITLE": "Meus Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Obtenha Addons",
|
||||
"ABOUT_TAB_TITLE": "Sobre",
|
||||
"OPTIONS_TAB_TITLE": "Opções"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Verificar Atualizações",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Verificar atualizações recentes",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Re-escanear pastas",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Procura por Addons instalados",
|
||||
"UPDATE_ALL_BUTTON": "Actualizar todos",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Atualizar todos os Addons",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Instalar",
|
||||
"ADDON_UPDATE_BUTTON": "Atualizar",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Atualização automática habilitada",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Versão do Jogo",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Ultima versão",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorar",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Atualização Automática",
|
||||
"CHANNEL_SUBMENT_TITLE": "Canal",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Reinstalar",
|
||||
"REMOVE_ADDON_BUTTON": "Remover",
|
||||
"STABLE_ADDON_CHANNEL": "Estável",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alfa"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Exibir Colunas"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Atualizar Retail/Clássico",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Atualizar todos os clientes"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimizar ao Fechar",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Ao fechar a janela do WowUp, minimize para a bandeja do sistema.",
|
||||
"TELEMETRY_DESCRIPTION": "Ajude a melhorar o WowUp enviando dados e/ou erros de instalação anônimamente.",
|
||||
"TELEMETRY_LABEL": "Telemetria",
|
||||
"TITLE": "Aplicativo"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Esvaziar log de depuração de dados",
|
||||
"DEBUG_DATA_DESCRIPTION": "Registra os dados de depuração e ajuda a diagnosticar problemas potenciais. Apenas por o curiosidade, isso pode ser encontrado em seu último arquivo de registro.",
|
||||
"DEBUG_DATA_LABEL": "Depurar Dados",
|
||||
"LOG_FILES_BUTTON": "Mostrar Arquivos de Registro",
|
||||
"LOG_FILES_DESCRIPTION": "Abre a pasta que contém seus últimos arquivos de registro.",
|
||||
"LOG_FILES_LABEL": "Arquivos de Registro",
|
||||
"TITLE": "Depurar"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Addons recém-instalados serão definidos para atualizar automáticamente por padrão",
|
||||
"AUTO_UPDATE_LABEL": "Atualização Automática",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canal de Addon Padrão",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Re-escanear",
|
||||
"RESCAN_CLIENTS_LABEL": "Reescanear World of Warcraft instalados"
|
||||
}
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Registro de Alterações",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Conheça o nosso site!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Atualizar",
|
||||
"INSTALL_FROM_URL_BUTTON": "Instalar pela URL",
|
||||
"SEARCH_LABEL": "Procurar",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Visualizar no navegador"
|
||||
"HOME": {
|
||||
"TITLE": "App funciona!",
|
||||
"GO_TO_DETAIL": "Ir para Detalhes",
|
||||
"MY_ADDONS_TAB_TITLE": "Meus Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Obtenha Addons",
|
||||
"ABOUT_TAB_TITLE": "Sobre",
|
||||
"OPTIONS_TAB_TITLE": "Opções"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Verificar Atualizações",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Verificar atualizações recentes",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Re-escanear pastas",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Procura por Addons instalados",
|
||||
"UPDATE_ALL_BUTTON": "Actualizar todos",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Atualizar todos os Addons",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Instalar",
|
||||
"ADDON_UPDATE_BUTTON": "Atualizar",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Atualização automática habilitada",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Versão do Jogo",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Ultima versão",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Ok"
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorar",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Atualização Automática",
|
||||
"CHANNEL_SUBMENT_TITLE": "Canal",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Reinstalar",
|
||||
"REMOVE_ADDON_BUTTON": "Remover",
|
||||
"STABLE_ADDON_CHANNEL": "Estável",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alfa"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Não",
|
||||
"POSITIVE_BUTTON": "Sim"
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Exibir Colunas"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub ou WowInterface URL",
|
||||
"CLOSE_BUTTON": "Fechar",
|
||||
"IMPORT_BUTTON": "Importar",
|
||||
"INSTALL_BUTTON": "Instalar",
|
||||
"INSTALL_SUCCESS_LABEL": "Instalado!",
|
||||
"TITLE": "Instalar Addon pela URL",
|
||||
"DESCRIPTION": "Se você deseja instalar um addon diretamente de uma URL, cole-a abaixo para iniciar.",
|
||||
"SUPPORTED_SOURCES": "Suporta WowInterface e GitHub*"
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Atualizar Retail/Clássico",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Atualizar todos os clientes"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimizar ao Fechar",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Ao fechar a janela do WowUp, minimize para a bandeja do sistema.",
|
||||
"TELEMETRY_DESCRIPTION": "Ajude a melhorar o WowUp enviando dados e/ou erros de instalação anônimamente.",
|
||||
"TELEMETRY_LABEL": "Telemetria",
|
||||
"TITLE": "Aplicativo"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Ajude-nos a melhorar o WowUp enviando dados e/ou erros de instalação do aplicativo anônimamente?",
|
||||
"NEGATIVE_BUTTON": "Não obrigado",
|
||||
"POSITIVE_BUTTON": "Claro!",
|
||||
"TITLE": "Telemetria do WowUp"
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Esvaziar log de depuração de dados",
|
||||
"DEBUG_DATA_DESCRIPTION": "Registra os dados de depuração e ajuda a diagnosticar problemas potenciais. Apenas por o curiosidade, isso pode ser encontrado em seu último arquivo de registro.",
|
||||
"DEBUG_DATA_LABEL": "Depurar Dados",
|
||||
"LOG_FILES_BUTTON": "Mostrar Arquivos de Registro",
|
||||
"LOG_FILES_DESCRIPTION": "Abre a pasta que contém seus últimos arquivos de registro.",
|
||||
"LOG_FILES_LABEL": "Arquivos de Registro",
|
||||
"TITLE": "Depurar"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Addons recém-instalados serão definidos para atualizar automáticamente por padrão",
|
||||
"AUTO_UPDATE_LABEL": "Atualização Automática",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canal de Addon Padrão",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Re-escanear",
|
||||
"RESCAN_CLIENTS_LABEL": "Reescanear World of Warcraft instalados"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Visualizar no navegador"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Ok"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Não",
|
||||
"POSITIVE_BUTTON": "Sim"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub ou WowInterface URL",
|
||||
"CLOSE_BUTTON": "Fechar",
|
||||
"IMPORT_BUTTON": "Importar",
|
||||
"INSTALL_BUTTON": "Instalar",
|
||||
"INSTALL_SUCCESS_LABEL": "Instalado!",
|
||||
"TITLE": "Instalar Addon pela URL",
|
||||
"DESCRIPTION": "Se você deseja instalar um addon diretamente de uma URL, cole-a abaixo para iniciar.",
|
||||
"SUPPORTED_SOURCES": "Suporta WowInterface e GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Ajude-nos a melhorar o WowUp enviando dados e/ou erros de instalação do aplicativo anônimamente?",
|
||||
"NEGATIVE_BUTTON": "Não obrigado",
|
||||
"POSITIVE_BUTTON": "Claro!",
|
||||
"TITLE": "Telemetria do WowUp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,55 +3,57 @@
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Журнал изменений",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Посмотрите на сайт!"
|
||||
"WEBSITE_LINK_LABEL": "Посетите наш сайт!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Обновить",
|
||||
"INSTALL_FROM_URL_BUTTON": "Установить из URL",
|
||||
"INSTALL_FROM_URL_BUTTON": "Установить по ссылке",
|
||||
"SEARCH_LABEL": "Искать",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_COLUMN_HEADER": "Модификация",
|
||||
"AUTHOR_COLUMN_HEADER": "Автор",
|
||||
"PROVIDER_COLUMN_HEADER": "Поставщик",
|
||||
"STATUS_COLUMN_HEADER": "Статус"
|
||||
"PROVIDER_COLUMN_HEADER": "Источник",
|
||||
"STATUS_COLUMN_HEADER": "Статус",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "Приложение работает !",
|
||||
"GO_TO_DETAIL": "Детали",
|
||||
"MY_ADDONS_TAB_TITLE": "Мои аддоны",
|
||||
"GET_ADDONS_TAB_TITLE": "Получить аддоны",
|
||||
"MY_ADDONS_TAB_TITLE": "Мои модификации",
|
||||
"GET_ADDONS_TAB_TITLE": "Получить модификации",
|
||||
"ABOUT_TAB_TITLE": "О программе",
|
||||
"OPTIONS_TAB_TITLE": "Варианты"
|
||||
"OPTIONS_TAB_TITLE": "Настройки"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Проверить обновления",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Проверить наличие последних обновлений аддона",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Проверить наличие последних обновлений модификации",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Пересканировать папки",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Сканирование клиентской папки для установленных аддонов",
|
||||
"UPDATE_ALL_BUTTON": "Обновить все",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Обновить все аддоны для этого клиента",
|
||||
"FILTER_LABEL": "Фильтр",
|
||||
"RESCAN_FOLDERS_BUTTON": "Сканировать папки",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Сканирование папки клиента на наличие установленных модификаций",
|
||||
"UPDATE_ALL_BUTTON": "Обновить всё",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Обновить все модификации для этого клиента",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_COLUMN_HEADER": "Модификация",
|
||||
"ADDON_INSTALL_BUTTON": "Установить",
|
||||
"ADDON_UPDATE_BUTTON": "Обновить",
|
||||
"AUTHOR_COLUMN_HEADER": "Автор",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Автообновление включено",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Версия игры",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Последняя версия",
|
||||
"PROVIDER_COLUMN_HEADER": "Поставщик",
|
||||
"PROVIDER_COLUMN_HEADER": "Источник",
|
||||
"STATUS_COLUMN_HEADER": "Статус"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Пропустить",
|
||||
"IGNORE_ADDON_BUTTON": "Пропускать",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Автообновление",
|
||||
"CHANNEL_SUBMENT_TITLE": "Канал",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"CHANNEL_SUBMENT_TITLE": "Тип выпуска",
|
||||
"SHOW_FOLDER": "Показать папку",
|
||||
"REINSTALL_ADDON_BUTTON": "Переустановить",
|
||||
"REMOVE_ADDON_BUTTON": "Удалить",
|
||||
"STABLE_ADDON_CHANNEL": "Конюшня",
|
||||
"STABLE_ADDON_CHANNEL": "Стабильная",
|
||||
"BETA_ADDON_CHANNEL": "Бета",
|
||||
"ALPHA_ADDON_CHANNEL": "Альфа"
|
||||
},
|
||||
@@ -59,41 +61,41 @@
|
||||
"TITLE": "Показать колонки"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Обновить Retail/Classic",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Обновить всех клиентов"
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Обновить Текущую/Classic",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Обновить все клиенты"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Свернуть при закрытии",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "При закрытии окна WowUp сворачиваем в системный трей.",
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Свернуть в трей при закрытии",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "При закрытии окна WowUp сворачивается в системный трей.",
|
||||
"TELEMETRY_DESCRIPTION": "Помогите улучшить WowUp, отправив анонимные данные об установке и/или ошибках.",
|
||||
"TELEMETRY_LABEL": "Телеметрия",
|
||||
"TITLE": "Приложение"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Дамп отладочных данных",
|
||||
"DEBUG_DATA_DESCRIPTION": "Записывать отладочные данные, чтобы помочь в диагностике потенциальных проблем. Это можно найти в последнем файле журнала для любопытства.",
|
||||
"DEBUG_DATA_DESCRIPTION": "Записывать отладочные данные, чтобы помочь в диагностике потенциальных проблем. Его можно найти в последнем лог-файле, если необходимо.",
|
||||
"DEBUG_DATA_LABEL": "Отладка данных",
|
||||
"LOG_FILES_BUTTON": "Показать лог-файлы",
|
||||
"LOG_FILES_DESCRIPTION": "Откройте папку, содержащую последние несколько лог файлов.",
|
||||
"LOG_FILES_DESCRIPTION": "Открыть папку, содержащую последние несколько лог-файлов.",
|
||||
"LOG_FILES_LABEL": "Файлы логов",
|
||||
"TITLE": "Debug"
|
||||
"TITLE": "Отладка"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Новые установленные дополнения будут автоматически обновляться по умолчанию",
|
||||
"AUTO_UPDATE_DESCRIPTION": "Новые установленные модификации будут автоматически обновляться по умолчанию",
|
||||
"AUTO_UPDATE_LABEL": "Автообновление",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Канал аддона по умолчанию",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Канал дополнения",
|
||||
"RESCAN_CLIENTS_BUTTON": "Пересканировать",
|
||||
"RESCAN_CLIENTS_LABEL": "Пересканируйте установленные продукты World of Warcraft"
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Тип выпуска модификации по умолчанию",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Тип выпуска модификации",
|
||||
"RESCAN_CLIENTS_BUTTON": "Повторное сканирование",
|
||||
"RESCAN_CLIENTS_LABEL": "Повторно найти установленные продукты World of Warcraft"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Просмотр в браузере"
|
||||
"VIEW_IN_BROWSER_BUTTON": "Посмотреть в браузере"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Окей"
|
||||
@@ -103,21 +105,37 @@
|
||||
"POSITIVE_BUTTON": "Да"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Пример URL-адреса GitHub или WowInterface",
|
||||
"ADDON_URL_INPUT_LABEL": "Ссылка на модификацию",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Например ссылки GitHub или WowInterface",
|
||||
"CLOSE_BUTTON": "Закрыть",
|
||||
"IMPORT_BUTTON": "Импорт",
|
||||
"INSTALL_BUTTON": "Установить",
|
||||
"INSTALL_SUCCESS_LABEL": "Установлено!",
|
||||
"TITLE": "Install Addon URL",
|
||||
"DESCRIPTION": "Если вы хотите установить аддон непосредственно с URL, вставьте его ниже, чтобы начать.",
|
||||
"SUPPORTED_SOURCES": "Поддерживает WowInterface и GitHub*"
|
||||
"TITLE": "Ссылка на установку модификации",
|
||||
"DESCRIPTION": "Если вы хотите установить модификацию непосредственно по ссылке, вставьте её ниже, чтобы начать.",
|
||||
"SUPPORTED_SOURCES": "Поддерживаются WowInterface и GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Помогите мне улучшить WowUp, отправив анонимные данные и/или ошибки в приложении?",
|
||||
"DESCRIPTION": "Хотите помочь мне улучшить WowUp, анонимно отправляя данные об установке и ошибках?",
|
||||
"NEGATIVE_BUTTON": "Нет, спасибо",
|
||||
"POSITIVE_BUTTON": "Конечно!",
|
||||
"TITLE": "WowUp Телеметрия"
|
||||
"TITLE": "Телеметрия WowUp"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "作者",
|
||||
"PROVIDER_COLUMN_HEADER": "提供商",
|
||||
"STATUS_COLUMN_HEADER": "状态"
|
||||
"STATUS_COLUMN_HEADER": "状态",
|
||||
"DOWNLOAD_COUNT_COLUMN_HEADER": "TEXT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
@@ -119,5 +120,21 @@
|
||||
"POSITIVE_BUTTON": "当然!",
|
||||
"TITLE": "WowUp遥测"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"ADDON_STATUS": {
|
||||
"COMPLETE": "COMPLETE",
|
||||
"DOWNLOADING": "DOWNLOADING",
|
||||
"INSTALLING": "INSTALLING",
|
||||
"UNINSTALLING": "UNINSTALLING",
|
||||
"UPDATING": "UPDATING"
|
||||
},
|
||||
"ADDON_STATE": {
|
||||
"UNINSTALL": "UNINSTALL",
|
||||
"IGNORED": "IGNORED",
|
||||
"UPDATE": "UPDATE",
|
||||
"INSTALL": "INSTALL",
|
||||
"UPTODATE": "UPTODATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +13,4 @@ export const CURSE_HASH_FILE_CHANNEL = "curse-hash-file";
|
||||
export const SHOW_DIRECTORY = "show-directory";
|
||||
export const CURSE_GET_SCAN_RESULTS = "curse-get-scan-results";
|
||||
export const WOWUP_GET_SCAN_RESULTS = "wowup-get-scan-results";
|
||||
export const GET_ASSET_FILE_PATH = "get-asset-file-path";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface CopyDirectoryRequest {
|
||||
sourcePath: string;
|
||||
destinationPath: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface CopyDirectoryRequest extends IpcRequest {
|
||||
sourcePath: string;
|
||||
destinationPath: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface CopyFileRequest {
|
||||
sourceFilePath: string;
|
||||
destinationFilePath: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface CopyFileRequest extends IpcRequest {
|
||||
sourceFilePath: string;
|
||||
destinationFilePath: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export interface DeleteDirectoryRequest {
|
||||
sourcePath: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface DeleteDirectoryRequest extends IpcRequest {
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface DownloadRequest {
|
||||
url: string;
|
||||
outputFolder: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface DownloadRequest extends IpcRequest {
|
||||
url: string;
|
||||
outputFolder: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export interface ReadFileRequest {
|
||||
sourcePath: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface ReadFileRequest extends IpcRequest {
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface UnzipRequest {
|
||||
zipFilePath: string;
|
||||
outputFolder: string;
|
||||
}
|
||||
import { IpcRequest } from "./ipc-request";
|
||||
|
||||
export interface UnzipRequest extends IpcRequest {
|
||||
zipFilePath: string;
|
||||
outputFolder: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ export const collapseToTrayKey = 'collapse_to_tray';
|
||||
export const wowupReleaseChannelKey = 'wowup_release_channel';
|
||||
export const defaultChannelKeySuffix = '_default_addon_channel';
|
||||
export const defaultAutoUpdateKeySuffix = '_default_auto_update';
|
||||
export const telemetryEnabledKey = 'telemetry_enabled';
|
||||
export const telemetryPromptSentKey = 'telemetry_prompt_sent';
|
||||
export const lastSelectedWowClientTypeKey = 'last_selected_client_type';
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV',
|
||||
wowUpApiUrl: 'https://api.dev.wowup.io',
|
||||
environment: "DEV",
|
||||
wowUpApiUrl: "https://api.dev.wowup.io",
|
||||
wowUpHubUrl: "https://hub.dev.wowup.io",
|
||||
rollbarAccessKey: "d01c11314a064572b11acee18d880650",
|
||||
googleAnalyticsId: "UA-92563227-4",
|
||||
};
|
||||
|
||||
@@ -3,4 +3,6 @@ export const AppConfig = {
|
||||
environment: "PROD",
|
||||
wowUpApiUrl: "https://api.wowup.io",
|
||||
wowUpHubUrl: "https://hub.wowup.io",
|
||||
rollbarAccessKey: "d01c11314a064572b11acee18d880650",
|
||||
googleAnalyticsId: "UA-92563227-4",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'LOCAL',
|
||||
wowUpApiUrl: 'https://api.dev.wowup.io',
|
||||
wowUpHubUrl: 'https://hub.dev.wowup.io'
|
||||
environment: "LOCAL",
|
||||
wowUpApiUrl: "https://api.dev.wowup.io",
|
||||
wowUpHubUrl: "https://hub.dev.wowup.io",
|
||||
rollbarAccessKey: "d01c11314a064572b11acee18d880650",
|
||||
googleAnalyticsId: "UA-92563227-4",
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV',
|
||||
environment: "DEV",
|
||||
wowUpApiUrl: "https://api.dev.wowup.io",
|
||||
wowUpHubUrl: "https://hub.dev.wowup.io",
|
||||
rollbarAccessKey: "d01c11314a064572b11acee18d880650",
|
||||
googleAnalyticsId: "UA-92563227-4",
|
||||
};
|
||||
|
||||
@@ -24,8 +24,7 @@ img:not([draggable="true"]) {
|
||||
|
||||
a[href^="http://"],
|
||||
a[href^="https://"],
|
||||
a[href^="ftp://"]
|
||||
{
|
||||
a[href^="ftp://"] {
|
||||
-webkit-user-drag: auto;
|
||||
user-drag: auto;
|
||||
/* Technically not supported in Electron yet */
|
||||
@@ -59,9 +58,11 @@ img {
|
||||
.mr-1 {
|
||||
margin-right: .25em !important;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: .5em !important;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 1em !important;
|
||||
}
|
||||
@@ -70,14 +71,29 @@ img {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.pointer:hover {
|
||||
cursor: pointer;
|
||||
.pointer {
|
||||
pointer-events: all !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-tab-label,
|
||||
.mat-tab-label-content,
|
||||
.mat-select-value,
|
||||
.mat-form-field-infix,
|
||||
.mat-button-wrapper span {
|
||||
&:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-slide-toggle-thumb,
|
||||
.mat-slide-toggle-bar,
|
||||
.mat-button-wrapper {
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -215,7 +231,7 @@ img {
|
||||
|
||||
.addon-name {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
// font-weight: bold;
|
||||
}
|
||||
|
||||
.addon-version {
|
||||
@@ -291,5 +307,5 @@ img {
|
||||
}
|
||||
|
||||
.install-button {
|
||||
min-width: 110px !important;
|
||||
min-width: 120px !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user