mirror of
https://github.com/WowUp/WowUp.git
synced 2026-05-24 22:46:45 -04:00
Merge branch 'electron' into fix/columns-overlapping
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,37 +46,37 @@ 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);
|
||||
@@ -161,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() };
|
||||
}
|
||||
@@ -209,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,
|
||||
@@ -264,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) => {
|
||||
@@ -383,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.9",
|
||||
"version": "2.0.0-alpha.10",
|
||||
"description": "Word of Warcraft addon updater",
|
||||
"homepage": "https://github.com/maximegris/angular-electron",
|
||||
"author": {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { CurseGetFeaturedResponse } from "app/models/curse/curse-get-featured-re
|
||||
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<
|
||||
@@ -62,7 +63,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async scan(
|
||||
public async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolders: AddonFolder[]
|
||||
@@ -111,12 +112,45 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
} catch (err) {
|
||||
console.error(scanResult);
|
||||
console.error(err);
|
||||
// TODO
|
||||
// _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<AppCurseScanResult[]> => {
|
||||
const t1 = Date.now();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseGetScanResultsResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
const appScanResults: AppCurseScanResult[] = arg.scanResults.map(
|
||||
(scanResult) => {
|
||||
const addonFolder = addonFolders.find(
|
||||
(af) => af.path === scanResult.directory
|
||||
);
|
||||
|
||||
return Object.assign({}, scanResult, { addonFolder });
|
||||
}
|
||||
);
|
||||
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
resolve(appScanResults);
|
||||
};
|
||||
|
||||
const request: CurseGetScanResultsRequest = {
|
||||
filePaths: addonFolders.map((addonFolder) => addonFolder.path),
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_GET_SCAN_RESULTS, request);
|
||||
});
|
||||
};
|
||||
|
||||
private async mapAddonFolders(
|
||||
scanResults: AppCurseScanResult[],
|
||||
clientType: WowClientType
|
||||
@@ -125,15 +159,15 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
scanResults.forEach(result => {
|
||||
scanResults.forEach((result) => {
|
||||
console.debug(result.folderName, result.fingerprint);
|
||||
});
|
||||
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(
|
||||
const fingerprintResponse = await this.getAddonsByFingerprintsW(
|
||||
scanResults.map((result) => result.fingerprint)
|
||||
);
|
||||
|
||||
console.log('fingerprintResponse', fingerprintResponse);
|
||||
console.log("fingerprintResponse", fingerprintResponse);
|
||||
|
||||
for (let scanResult of scanResults) {
|
||||
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
|
||||
@@ -146,7 +180,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
);
|
||||
|
||||
// If the addon does not have an exact match, check the partial matches.
|
||||
if (!scanResult.exactMatch) {
|
||||
if (!scanResult.exactMatch && fingerprintResponse.partialMatches) {
|
||||
scanResult.exactMatch = fingerprintResponse.partialMatches.find(
|
||||
(partialMatch) =>
|
||||
partialMatch.file?.modules?.some(
|
||||
@@ -173,6 +207,25 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
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> {
|
||||
@@ -207,41 +260,6 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return action.call(this);
|
||||
}
|
||||
|
||||
private getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<AppCurseScanResult[]> => {
|
||||
const t1 = Date.now();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseGetScanResultsResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
const appScanResults: AppCurseScanResult[] = arg.scanResults.map(
|
||||
(scanResult) => {
|
||||
const addonFolder = addonFolders.find(
|
||||
(af) => af.path === scanResult.directory
|
||||
);
|
||||
|
||||
return Object.assign({}, scanResult, { addonFolder });
|
||||
}
|
||||
);
|
||||
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
resolve(appScanResults);
|
||||
};
|
||||
|
||||
const request: CurseGetScanResultsRequest = {
|
||||
filePaths: addonFolders.map((addonFolder) => addonFolder.path),
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_GET_SCAN_RESULTS, request);
|
||||
});
|
||||
};
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
|
||||
@@ -31,7 +31,7 @@ export class GitHubAddonProvider implements AddonProvider {
|
||||
async getAll(clientType: WowClientType, addonIds: string[]): Promise<AddonSearchResult[]> {
|
||||
var searchResults: AddonSearchResult[] = []
|
||||
|
||||
for (let addonId in addonIds) {
|
||||
for (let addonId of addonIds) {
|
||||
var result = await this.getById(addonId, clientType).toPromise();
|
||||
if (result == null) {
|
||||
continue;
|
||||
@@ -74,7 +74,7 @@ export class GitHubAddonProvider implements AddonProvider {
|
||||
author: author,
|
||||
downloadCount: asset.download_count,
|
||||
externalId: repoPath,
|
||||
externalUrl: latestRelease.url,
|
||||
externalUrl: repository.html_url,
|
||||
name: repository.name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: authorImageUrl
|
||||
@@ -124,7 +124,7 @@ export class GitHubAddonProvider implements AddonProvider {
|
||||
var searchResult: AddonSearchResult = {
|
||||
author: author,
|
||||
externalId: addonId,
|
||||
externalUrl: asset.url,
|
||||
externalUrl: repository.html_url,
|
||||
files: [searchResultFile],
|
||||
name: addonName,
|
||||
providerName: this.name,
|
||||
@@ -203,7 +203,6 @@ export class GitHubAddonProvider implements AddonProvider {
|
||||
|
||||
private getReleases(repositoryPath: string): Observable<GitHubRelease[]> {
|
||||
const url = `${API_URL}${repositoryPath}/releases`;
|
||||
|
||||
return this._httpClient.get<GitHubRelease[]>(url.toString());
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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) { }
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -23,4 +23,4 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -70,7 +72,9 @@
|
||||
{{'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-addon-install-button [addon]="element" appUserActionTracker category="GetAddons" action="Install"
|
||||
[label]="element.name">
|
||||
</app-addon-install-button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -78,4 +82,4 @@
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;let i = index" (dblclick)="openDetailDialog(row)"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
|
||||
.author-column {
|
||||
width: 100px;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.provider-column {
|
||||
min-width: 60px;
|
||||
|
||||
@@ -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>
|
||||
@@ -62,11 +65,13 @@
|
||||
{{'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()">
|
||||
<button *ngIf="element.needsInstall === true" mat-flat-button color="primary" (click)="onInstall()"
|
||||
appUserActionTracker category="MyAddons" action="InstallAddon" [label]="element.addon.name">
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_INSTALL_BUTTON' | translate}}
|
||||
</button>
|
||||
<button *ngIf="element.needsUpdate === true" mat-flat-button color="primary"
|
||||
(click)="onUpdateAddon(element)">
|
||||
(click)="onUpdateAddon(element)" appUserActionTracker category="MyAddons" action="UpdateAddon"
|
||||
[label]="element.addon.name">
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_UPDATE_BUTTON' | translate}}
|
||||
</button>
|
||||
<div *ngIf="element.isUpToDate === true || element.isIgnored === true" class="status-text">
|
||||
@@ -122,7 +127,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>
|
||||
@@ -152,36 +157,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.addon)">
|
||||
[checked]="listItem.addon.autoUpdateEnabled" (change)="onClickAutoUpdateAddon($event, listItem.addon)"
|
||||
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.addon)">
|
||||
<button mat-menu-item (click)="onReInstallAddon(listItem.addon)" 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.addon)">
|
||||
<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>
|
||||
@@ -212,10 +225,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>
|
||||
|
||||
@@ -111,7 +111,6 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
_sessionService.selectedHomeTab$.subscribe((tabIndex) => {
|
||||
this.isSelectedTab = tabIndex === this.tabIndex;
|
||||
console.log("TAB CHANGE", tabIndex, this.tabIndex);
|
||||
if (this.isSelectedTab) {
|
||||
this.setPageContextText();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { FileService } from "../files/file.service";
|
||||
import { TocService } from "../toc/toc.service";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { AddonProviderFactory } from "./addon.provider.factory";
|
||||
import { AnalyticsService } from "../analytics/analytics.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@@ -37,6 +38,7 @@ export class AddonService {
|
||||
|
||||
constructor(
|
||||
private _addonStorage: AddonStorageService,
|
||||
private _analyticsService: AnalyticsService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _downloadService: DownloadSevice,
|
||||
@@ -65,7 +67,12 @@ export class AddonService {
|
||||
);
|
||||
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();
|
||||
@@ -93,6 +100,7 @@ export class AddonService {
|
||||
clientType
|
||||
).toPromise();
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
|
||||
await this.installAddon(addon.id, onUpdate);
|
||||
}
|
||||
|
||||
@@ -106,7 +114,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,
|
||||
@@ -125,7 +132,7 @@ export class AddonService {
|
||||
await this.installAddon(addon.id);
|
||||
updateCt += 1;
|
||||
} catch (err) {
|
||||
// _analyticsService.Track(ex, "Failed to install addon");
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +173,6 @@ export class AddonService {
|
||||
|
||||
let downloadedFilePath = "";
|
||||
let unzippedDirectory = "";
|
||||
let downloadedThumbnail = "";
|
||||
try {
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(
|
||||
addon.downloadUrl,
|
||||
@@ -174,6 +180,7 @@ export class AddonService {
|
||||
);
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Installing, 75);
|
||||
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Installing,
|
||||
@@ -184,6 +191,7 @@ export class AddonService {
|
||||
this._wowUpService.applicationDownloadsFolderPath,
|
||||
uuidv4()
|
||||
);
|
||||
|
||||
unzippedDirectory = await this._downloadService.unzipFile(
|
||||
downloadedFilePath,
|
||||
unzipPath
|
||||
@@ -207,7 +215,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);
|
||||
|
||||
@@ -230,6 +242,24 @@ 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[]
|
||||
@@ -362,7 +392,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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
defaultAutoUpdateKeySuffix,
|
||||
defaultChannelKeySuffix,
|
||||
lastSelectedWowClientTypeKey,
|
||||
telemetryEnabledKey,
|
||||
wowupReleaseChannelKey
|
||||
} from "../../../constants";
|
||||
|
||||
@@ -75,17 +74,6 @@ export class WowUpService {
|
||||
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() })
|
||||
}
|
||||
|
||||
public get wowUpReleaseChannel() {
|
||||
const preference = this._preferenceStorageService.findByKey(wowupReleaseChannelKey);
|
||||
return parseInt(preference, 10) as WowUpReleaseChannelType;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"ChangeLogs": [
|
||||
{
|
||||
"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."
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -3,4 +3,6 @@ export const AppConfig = {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user