Merge branch 'electron' into fix/columns-overlapping

This commit is contained in:
jliddev
2020-10-17 00:15:09 -05:00
43 changed files with 949 additions and 521 deletions

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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": {

View File

@@ -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[]

View File

@@ -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());
}

View File

@@ -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,
});
};
}

View File

@@ -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 { }

View File

@@ -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>

View File

@@ -37,6 +37,8 @@ footer {
}
.discord-link {
padding: 0 0.25em;
&:hover {
background-color: $dark-3;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 { }

View File

@@ -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();
});
});

View File

@@ -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) { }
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -77,6 +77,7 @@
.author-column {
width: 100px;
padding-right: 1em;
}
.provider-column {
min-width: 60px;

View File

@@ -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>

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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);
})
});
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}
}

View File

@@ -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;

View File

@@ -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."

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,3 +1,5 @@
export interface DeleteDirectoryRequest {
sourcePath: string;
}
import { IpcRequest } from "./ipc-request";
export interface DeleteDirectoryRequest extends IpcRequest {
sourcePath: string;
}

View File

@@ -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;
}

View File

@@ -1,3 +1,5 @@
export interface ReadFileRequest {
sourcePath: string;
}
import { IpcRequest } from "./ipc-request";
export interface ReadFileRequest extends IpcRequest {
sourcePath: string;
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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;
}