mirror of
https://github.com/WowUp/WowUp.git
synced 2026-04-24 07:47:29 -04:00
Speed up scanning some
Add some status output
This commit is contained in:
@@ -52,7 +52,6 @@ ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => {
|
||||
});
|
||||
|
||||
ipcMain.on(LIST_FILES_CHANNEL, async (evt, arg: ListFilesRequest) => {
|
||||
console.log('list files', arg);
|
||||
const response: ListFilesResponse = {
|
||||
files: []
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@types/adm-zip": "0.4.33",
|
||||
"@types/async": "3.2.3",
|
||||
"@types/globrex": "0.1.0",
|
||||
"@types/jasmine": "3.5.14",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
@@ -109,6 +110,7 @@
|
||||
"@angular/material": "10.2.3",
|
||||
"@types/lodash": "4.14.161",
|
||||
"adm-zip": "0.4.16",
|
||||
"async": "3.2.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"conf": "7.1.2",
|
||||
"electron-dl": "3.0.2",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { map, mergeMap } from "rxjs/operators";
|
||||
import { CurseFile } from "../models/curse/curse-file";
|
||||
import * as _ from "lodash";
|
||||
import * as fp from "lodash/fp";
|
||||
import * as path from "path";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
@@ -23,6 +24,10 @@ import { CurseScanResult } from "../models/curse/curse-scan-result";
|
||||
import { CurseFingerprintsResponse } from "app/models/curse/curse-fingerprint-response";
|
||||
import { CurseMatch } from "app/models/curse/curse-match";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as async from "async";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { Inject } from "@angular/core";
|
||||
import { Session } from "inspector";
|
||||
|
||||
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
|
||||
|
||||
@@ -33,8 +38,9 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _sessionService: SessionService,
|
||||
private _fileService: FileService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
@@ -157,30 +163,33 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return this._httpClient.post<CurseSearchResult[]>(url, addonIds);
|
||||
}
|
||||
|
||||
private async getScanResults(
|
||||
private getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<CurseScanResult[]> {
|
||||
const scanResults: CurseScanResult[] = [];
|
||||
): Promise<CurseScanResult[]> => {
|
||||
// const scanResults: CurseScanResult[] = [];
|
||||
|
||||
const t1 = Date.now();
|
||||
|
||||
// Scan addon folders in parallel for speed!?
|
||||
for (let folder of addonFolders) {
|
||||
const scanResult = await new CurseFolderScanner(
|
||||
this._electronService,
|
||||
this._fileService
|
||||
).scanFolder(folder);
|
||||
scanResults.push(scanResult);
|
||||
}
|
||||
const scanResults = await async.mapLimit<AddonFolder, CurseScanResult>(
|
||||
addonFolders,
|
||||
2,
|
||||
async (folder, callback) => {
|
||||
this._sessionService.statusText = `Scanning ${folder.name}`;
|
||||
const scanResult = await new CurseFolderScanner(
|
||||
this._electronService,
|
||||
this._fileService
|
||||
).scanFolder(folder);
|
||||
|
||||
callback(undefined, scanResult);
|
||||
}
|
||||
);
|
||||
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
|
||||
// const str = _.orderBy(scanResults, sr => sr.folderName.toLowerCase())
|
||||
// .map(sr => `${sr.fingerprint} ${sr.folderName}`).join('\n');
|
||||
// console.log(str);
|
||||
this._sessionService.statusText = "";
|
||||
|
||||
return scanResults;
|
||||
}
|
||||
};
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
|
||||
@@ -1,215 +1,234 @@
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as _ from "lodash";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { ElectronService } from "app/services";
|
||||
import { CurseHashFileResponse } from "common/models/curse-hash-file-response";
|
||||
import { CurseHashFileRequest } from "common/models/curse-hash-file-request";
|
||||
import { CURSE_HASH_FILE_CHANNEL } from "common/constants";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { CurseScanResult } from "../../models/curse/curse-scan-result";
|
||||
import { from } from "rxjs";
|
||||
import { mergeMap } from "rxjs/operators";
|
||||
import * as async from "async";
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
|
||||
export class CurseFolderScanner {
|
||||
constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) { }
|
||||
private get tocFileCommentsRegex() {
|
||||
return /\s*#.*$/gm;
|
||||
}
|
||||
|
||||
private get tocFileCommentsRegex() {
|
||||
return /\s*#.*$/mg;
|
||||
private get tocFileIncludesRegex() {
|
||||
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/gim;
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^\/]+)[\\\/]\1\.toc$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlIncludesRegex() {
|
||||
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/gi;
|
||||
}
|
||||
|
||||
private get bindingsXmlCommentsRegex() {
|
||||
return /<!--.*?-->/gs;
|
||||
}
|
||||
|
||||
async scanFolder(addonFolder: AddonFolder): Promise<CurseScanResult> {
|
||||
const folderPath = addonFolder.path;
|
||||
|
||||
const files = await this._fileService.listAllFiles(folderPath);
|
||||
console.log("listAllFiles", folderPath, files.length);
|
||||
|
||||
let matchingFiles = await this.getMatchingFiles(folderPath, files);
|
||||
matchingFiles = _.sortBy(matchingFiles, (f) => f.toLowerCase());
|
||||
|
||||
// console.log('matching files', matchingFiles.length)
|
||||
// const fst = matchingFiles.map(f => f.toLowerCase()).join('\n');
|
||||
|
||||
const individualFingerprints = await async.mapLimit<string, number>(
|
||||
matchingFiles,
|
||||
2,
|
||||
async (path, callback) => {
|
||||
const normalizedFileHash = await this.computeNormalizedFileHash(path);
|
||||
callback(undefined, normalizedFileHash);
|
||||
}
|
||||
);
|
||||
|
||||
// const individualFingerprints: number[] = [];
|
||||
// for (let path of matchingFiles) {
|
||||
// const normalizedFileHash = await this.computeNormalizedFileHash(path);
|
||||
// individualFingerprints.push(normalizedFileHash);
|
||||
// }
|
||||
|
||||
const hashConcat = _.orderBy(individualFingerprints).join("");
|
||||
const fingerprint = await this.computeStringHash(hashConcat);
|
||||
console.log("fingerprint", fingerprint);
|
||||
|
||||
return {
|
||||
directory: folderPath,
|
||||
fileCount: matchingFiles.length,
|
||||
fingerprint,
|
||||
folderName: path.basename(folderPath),
|
||||
individualFingerprints,
|
||||
addonFolder,
|
||||
};
|
||||
}
|
||||
|
||||
private async getMatchingFiles(
|
||||
folderPath: string,
|
||||
filePaths: string[]
|
||||
): Promise<string[]> {
|
||||
const parentDir = path.dirname(folderPath) + path.sep;
|
||||
const matchingFileList: string[] = [];
|
||||
const fileInfoList: string[] = [];
|
||||
for (let filePath of filePaths) {
|
||||
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), "");
|
||||
|
||||
if (this.tocFileRegex.test(input)) {
|
||||
fileInfoList.push(filePath);
|
||||
} else if (this.bindingsXmlRegex.test(input)) {
|
||||
matchingFileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private get tocFileIncludesRegex() {
|
||||
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/mig;
|
||||
// console.log('fileInfoList', fileInfoList.length)
|
||||
for (let fileInfo of fileInfoList) {
|
||||
await this.processIncludeFile(matchingFileList, fileInfo);
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^\/]+)[\\\/]\1\.toc$/i;
|
||||
return matchingFileList;
|
||||
}
|
||||
|
||||
private async processIncludeFile(
|
||||
matchingFileList: string[],
|
||||
fileInfo: string
|
||||
) {
|
||||
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
|
||||
matchingFileList.push(fileInfo);
|
||||
|
||||
let input = await this._fileService.readFile(fileInfo);
|
||||
input = this.removeComments(fileInfo, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(fileInfo, input);
|
||||
if (!inclusions || !inclusions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
private get bindingsXmlIncludesRegex() {
|
||||
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/ig;
|
||||
const dirname = path.dirname(fileInfo);
|
||||
for (let include of inclusions) {
|
||||
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
|
||||
await this.processIncludeFile(matchingFileList, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private get bindingsXmlCommentsRegex() {
|
||||
return /<!--.*?-->/gs;
|
||||
private getFileInclusionMatches(
|
||||
fileInfo: string,
|
||||
fileContent: string
|
||||
): string[] | null {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case ".xml":
|
||||
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
|
||||
case ".toc":
|
||||
return this.matchAll(fileContent, this.tocFileIncludesRegex);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async scanFolder(addonFolder: AddonFolder): Promise<CurseScanResult> {
|
||||
const folderPath = addonFolder.path;
|
||||
const files = await this._fileService.listAllFiles(folderPath);
|
||||
console.log('listAllFiles', folderPath, files.length);
|
||||
private removeComments(fileInfo: string, fileContent: string): string {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case ".xml":
|
||||
return fileContent.replace(this.bindingsXmlCommentsRegex, "");
|
||||
case ".toc":
|
||||
return fileContent.replace(this.tocFileCommentsRegex, "");
|
||||
default:
|
||||
return fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
let matchingFiles = await this.getMatchingFiles(folderPath, files);
|
||||
matchingFiles = _.sortBy(matchingFiles, f => f.toLowerCase());
|
||||
private matchAll(str: string, regex: RegExp): string[] {
|
||||
const matches: string[] = [];
|
||||
let currentMatch: RegExpExecArray;
|
||||
do {
|
||||
currentMatch = regex.exec(str);
|
||||
if (currentMatch) {
|
||||
matches.push(currentMatch[1]);
|
||||
}
|
||||
} while (currentMatch);
|
||||
|
||||
// console.log('matching files', matchingFiles.length)
|
||||
// const fst = matchingFiles.map(f => f.toLowerCase()).join('\n');
|
||||
return matches;
|
||||
}
|
||||
|
||||
const individualFingerprints: number[] = [];
|
||||
for (let path of matchingFiles) {
|
||||
const normalizedFileHash = await this.computeNormalizedFileHash(path);
|
||||
individualFingerprints.push(normalizedFileHash);
|
||||
private computeNormalizedFileHash = (filePath: string) => {
|
||||
return this.computeFileHash(filePath, true);
|
||||
};
|
||||
|
||||
private computeFileHash = (
|
||||
filePath: string,
|
||||
normalizeWhitespace: boolean
|
||||
) => {
|
||||
return this.computeHash(filePath, 0, normalizeWhitespace);
|
||||
};
|
||||
|
||||
private computeStringHash = (str: string): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
const hashConcat = _.orderBy(individualFingerprints).join('');
|
||||
const fingerprint = await this.computeStringHash(hashConcat);
|
||||
console.log('fingerprint', fingerprint);
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
return {
|
||||
directory: folderPath,
|
||||
fileCount: matchingFiles.length,
|
||||
fingerprint,
|
||||
folderName: path.basename(folderPath),
|
||||
individualFingerprints,
|
||||
addonFolder
|
||||
};
|
||||
}
|
||||
const request: CurseHashFileRequest = {
|
||||
targetString: str,
|
||||
targetStringEncoding: "ascii",
|
||||
responseKey: uuidv4(),
|
||||
normalizeWhitespace: false,
|
||||
precomputedLength: 0,
|
||||
};
|
||||
|
||||
private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise<string[]> {
|
||||
const parentDir = path.dirname(folderPath) + path.sep;
|
||||
const matchingFileList: string[] = [];
|
||||
const fileInfoList: string[] = [];
|
||||
for (let filePath of filePaths) {
|
||||
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), '');
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.tocFileRegex.test(input)) {
|
||||
fileInfoList.push(filePath);
|
||||
} else if (this.bindingsXmlRegex.test(input)) {
|
||||
matchingFileList.push(filePath);
|
||||
}
|
||||
private computeHash = (
|
||||
filePath: string,
|
||||
precomputedLength: number = 0,
|
||||
normalizeWhitespace: boolean = false
|
||||
): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
// console.log('fileInfoList', fileInfoList.length)
|
||||
for (let fileInfo of fileInfoList) {
|
||||
await this.processIncludeFile(matchingFileList, fileInfo);
|
||||
}
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
return matchingFileList;
|
||||
}
|
||||
const request: CurseHashFileRequest = {
|
||||
responseKey: uuidv4(),
|
||||
filePath,
|
||||
normalizeWhitespace,
|
||||
precomputedLength,
|
||||
};
|
||||
|
||||
private async processIncludeFile(matchingFileList: string[], fileInfo: string) {
|
||||
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchingFileList.push(fileInfo);
|
||||
|
||||
let input = await this._fileService.readFile(fileInfo);
|
||||
input = this.removeComments(fileInfo, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(fileInfo, input);
|
||||
if (!inclusions || !inclusions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dirname = path.dirname(fileInfo);
|
||||
for (let include of inclusions) {
|
||||
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
|
||||
await this.processIncludeFile(matchingFileList, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private getFileInclusionMatches(fileInfo: string, fileContent: string): string[] | null {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case '.xml':
|
||||
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
|
||||
case '.toc':
|
||||
return this.matchAll(fileContent, this.tocFileIncludesRegex);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private removeComments(fileInfo: string, fileContent: string): string {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case '.xml':
|
||||
return fileContent.replace(this.bindingsXmlCommentsRegex, '');
|
||||
case '.toc':
|
||||
return fileContent.replace(this.tocFileCommentsRegex, '');
|
||||
default:
|
||||
return fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
private matchAll(str: string, regex: RegExp): string[] {
|
||||
const matches: string[] = [];
|
||||
let currentMatch: RegExpExecArray;
|
||||
do {
|
||||
currentMatch = regex.exec(str);
|
||||
if (currentMatch) {
|
||||
matches.push(currentMatch[1]);
|
||||
}
|
||||
} while (currentMatch);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private computeNormalizedFileHash(filePath: string) {
|
||||
return this.computeFileHash(filePath, true);
|
||||
}
|
||||
|
||||
private computeFileHash(filePath: string, normalizeWhitespace: boolean) {
|
||||
return this.computeHash(filePath, 0, normalizeWhitespace);
|
||||
}
|
||||
|
||||
private computeStringHash(str: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
const request: CurseHashFileRequest = {
|
||||
targetString: str,
|
||||
targetStringEncoding: 'ascii',
|
||||
responseKey: uuidv4(),
|
||||
normalizeWhitespace: false,
|
||||
precomputedLength: 0
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
private computeHash(
|
||||
filePath: string,
|
||||
precomputedLength: number = 0,
|
||||
normalizeWhitespace: boolean = false
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
const request: CurseHashFileRequest = {
|
||||
responseKey: uuidv4(),
|
||||
filePath,
|
||||
normalizeWhitespace,
|
||||
precomputedLength
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { AfterViewInit, Component } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AppConfig } from '../environments/environment';
|
||||
import { TelemetryDialogComponent } from './components/telemetry-dialog/telemetry-dialog.component';
|
||||
import { ElectronService } from './services';
|
||||
import { AnalyticsService } from './services/analytics/analytics.service';
|
||||
import { WarcraftService } from './services/warcraft/warcraft.service';
|
||||
import { WowUpService } from './services/wowup/wowup.service';
|
||||
import { AfterViewInit, Component } from "@angular/core";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { AppConfig } from "../environments/environment";
|
||||
import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetry-dialog.component";
|
||||
import { ElectronService } from "./services";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { WarcraftService } from "./services/warcraft/warcraft.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
|
||||
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
selector: "app-root",
|
||||
templateUrl: "./app.component.html",
|
||||
styleUrls: ["./app.component.scss"],
|
||||
})
|
||||
export class AppComponent implements AfterViewInit {
|
||||
private _autoUpdateInterval?: number;
|
||||
|
||||
constructor(
|
||||
private _analyticsService: AnalyticsService,
|
||||
private electronService: ElectronService,
|
||||
private translate: TranslateService,
|
||||
private warcraft: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _dialog: MatDialog
|
||||
private _dialog: MatDialog,
|
||||
private _addonService: AddonService
|
||||
) {
|
||||
this.translate.setDefaultLang('en');
|
||||
this.translate.setDefaultLang("en");
|
||||
|
||||
this.translate.use(this.electronService.locale);
|
||||
}
|
||||
@@ -33,15 +39,26 @@ export class AppComponent implements AfterViewInit {
|
||||
} else {
|
||||
// TODO track startup
|
||||
}
|
||||
|
||||
this.onAutoUpdateInterval();
|
||||
this._autoUpdateInterval = window.setInterval(
|
||||
this.onAutoUpdateInterval,
|
||||
AUTO_UPDATE_PERIOD_MS
|
||||
);
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this._dialog.open(TelemetryDialogComponent, {
|
||||
disableClose: true
|
||||
disableClose: true,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this._wowUpService.telemetryEnabled = result;
|
||||
});
|
||||
}
|
||||
|
||||
private onAutoUpdateInterval = async () => {
|
||||
console.log("Auto update");
|
||||
const updateCount = await this._addonService.processAutoUpdates();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import 'reflect-metadata';
|
||||
import '../polyfills';
|
||||
import "reflect-metadata";
|
||||
import "../polyfills";
|
||||
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ErrorHandler, InjectionToken, NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { ErrorHandler, NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import {
|
||||
HttpClientModule,
|
||||
HttpClient,
|
||||
HTTP_INTERCEPTORS,
|
||||
} from "@angular/common/http";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
// NG Translate
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
|
||||
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
||||
|
||||
import { HomeModule } from './pages/home/home.module';
|
||||
import { HomeModule } from "./pages/home/home.module";
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TitlebarComponent } from './components/titlebar/titlebar.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { DefaultHeadersInterceptor } from './interceptors/default-headers.interceptor';
|
||||
import { AnalyticsService } from './services/analytics/analytics.service';
|
||||
import { DirectiveModule } from './directive.module';
|
||||
import { MatModule } from './mat-module';
|
||||
import { MatProgressButtonsModule } from 'mat-progress-buttons';
|
||||
import { AppComponent } from "./app.component";
|
||||
import { TitlebarComponent } from "./components/titlebar/titlebar.component";
|
||||
import { FooterComponent } from "./components/footer/footer.component";
|
||||
import { DefaultHeadersInterceptor } from "./interceptors/default-headers.interceptor";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { DirectiveModule } from "./directive.module";
|
||||
import { MatModule } from "./mat-module";
|
||||
import { MatProgressButtonsModule } from "mat-progress-buttons";
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
TitlebarComponent,
|
||||
FooterComponent,
|
||||
],
|
||||
declarations: [AppComponent, TitlebarComponent, FooterComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
@@ -50,16 +50,19 @@ export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: httpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
|
||||
],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true },
|
||||
{ provide: ErrorHandler, useClass: AnalyticsService }
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DefaultHeadersInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: AnalyticsService },
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev">
|
||||
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
|
||||
</a>
|
||||
<p>{{sessionService.statusText$ | async}}</p>
|
||||
<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>
|
||||
|
||||
@@ -7,7 +7,6 @@ footer {
|
||||
height: 25px;
|
||||
padding: 0.25em 0.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
|
||||
|
||||
@@ -15,21 +15,18 @@ export class HomeComponent implements OnInit {
|
||||
private _sessionService: SessionService,
|
||||
private _warcraftService: WarcraftService
|
||||
) {
|
||||
this._warcraftService.installedClientTypes$
|
||||
.subscribe((clientTypes) => {
|
||||
if(clientTypes === undefined){
|
||||
this.hasWowClient = false;
|
||||
this.selectedIndex = 3;
|
||||
} else {
|
||||
this.hasWowClient = clientTypes.length > 0;
|
||||
this.selectedIndex = this.hasWowClient ? 0 : 3;
|
||||
}
|
||||
});
|
||||
this._warcraftService.installedClientTypes$.subscribe((clientTypes) => {
|
||||
if (clientTypes === undefined) {
|
||||
this.hasWowClient = false;
|
||||
this.selectedIndex = 3;
|
||||
} else {
|
||||
this.hasWowClient = clientTypes.length > 0;
|
||||
this.selectedIndex = this.hasWowClient ? 0 : 3;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._sessionService.appLoaded();
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
onSelectedIndexChange(index: number) {
|
||||
this._sessionService.selectedHomeTab = index;
|
||||
|
||||
@@ -43,7 +43,7 @@ import { AddonInstallButtonComponent } from "app/components/addon-install-button
|
||||
InstallFromUrlDialogComponent,
|
||||
AddonDetailComponent,
|
||||
AddonProviderBadgeComponent,
|
||||
AddonInstallButtonComponent
|
||||
AddonInstallButtonComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AddonProvider } from "app/addon-providers/addon-provider";
|
||||
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
|
||||
import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
|
||||
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
|
||||
|
||||
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { SessionService } from "../session/session.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AddonProviderFactory {
|
||||
constructor(
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _httpClient: HttpClient,
|
||||
private _sessionService: SessionService,
|
||||
private _fileService: FileService
|
||||
) {}
|
||||
|
||||
public getAddonProvider<T extends object>(providerType: T & AddonProvider) {
|
||||
switch (providerType.name) {
|
||||
case CurseAddonProvider.name:
|
||||
return this.createCurseAddonProvider();
|
||||
case TukUiAddonProvider.name:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public createCurseAddonProvider(): CurseAddonProvider {
|
||||
return new CurseAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._sessionService,
|
||||
this._fileService
|
||||
);
|
||||
}
|
||||
|
||||
public createTukUiAddonProvider(): TukUiAddonProvider {
|
||||
return new TukUiAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._fileService
|
||||
);
|
||||
}
|
||||
|
||||
public createWowInterfaceAddonProvider(): WowInterfaceAddonProvider {
|
||||
return new WowInterfaceAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._fileService
|
||||
);
|
||||
}
|
||||
|
||||
public createGitHubAddonProvider(): GitHubAddonProvider {
|
||||
return new GitHubAddonProvider(this._httpClient);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Injectable, Injector } from "@angular/core";
|
||||
import { AddonStorageService } from "../storage/addon-storage.service";
|
||||
import { Addon } from "../../entities/addon";
|
||||
import { WarcraftService } from "../warcraft/warcraft.service";
|
||||
import { AddonProvider } from "../../addon-providers/addon-provider";
|
||||
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import * as _ from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { WowUpApiService } from "../wowup-api/wowup-api.service";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
@@ -29,12 +29,12 @@ import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
|
||||
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
|
||||
import { AddonProviderFactory } from "./addon.provider.factory";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AddonService {
|
||||
|
||||
private readonly _addonProviders: AddonProvider[];
|
||||
private readonly _addonInstalledSrc = new Subject<AddonUpdateEvent>();
|
||||
private readonly _addonRemovedSrc = new Subject<string>();
|
||||
@@ -44,21 +44,18 @@ export class AddonService {
|
||||
|
||||
constructor(
|
||||
private _addonStorage: AddonStorageService,
|
||||
private _cachingService: CachingService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _wowupApiService: WowUpApiService,
|
||||
private _downloadService: DownloadSevice,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService,
|
||||
private _tocService: TocService,
|
||||
httpClient: HttpClient
|
||||
private _addonProviderFactory: AddonProviderFactory
|
||||
) {
|
||||
this._addonProviders = [
|
||||
new CurseAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new TukUiAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new WowInterfaceAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new GitHubAddonProvider(httpClient),
|
||||
this._addonProviderFactory.createCurseAddonProvider(),
|
||||
this._addonProviderFactory.createTukUiAddonProvider(),
|
||||
this._addonProviderFactory.createWowInterfaceAddonProvider(),
|
||||
this._addonProviderFactory.createGitHubAddonProvider(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,41 +63,62 @@ export class AddonService {
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
}
|
||||
|
||||
public async search(query: string, clientType: WowClientType): Promise<PotentialAddon[]> {
|
||||
var searchTasks = this._addonProviders.map(p => p.searchByQuery(query, clientType));
|
||||
public async search(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
var searchTasks = this._addonProviders.map((p) =>
|
||||
p.searchByQuery(query, clientType)
|
||||
);
|
||||
var searchResults = await Promise.all(searchTasks);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "Search", $"{clientType}|{query}");
|
||||
const flatResults = searchResults.flat(1);
|
||||
|
||||
return _.orderBy(flatResults, 'downloadCount').reverse();
|
||||
return _.orderBy(flatResults, "downloadCount").reverse();
|
||||
}
|
||||
|
||||
public async installPotentialAddon(
|
||||
potentialAddon: PotentialAddon,
|
||||
clientType: WowClientType,
|
||||
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
|
||||
onUpdate: (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => void = undefined
|
||||
) {
|
||||
var existingAddon = this._addonStorage.getByExternalId(potentialAddon.externalId, clientType);
|
||||
var existingAddon = this._addonStorage.getByExternalId(
|
||||
potentialAddon.externalId,
|
||||
clientType
|
||||
);
|
||||
if (existingAddon) {
|
||||
throw new Error('Addon already installed');
|
||||
throw new Error("Addon already installed");
|
||||
}
|
||||
|
||||
const addon = await this.getAddon(potentialAddon.externalId, potentialAddon.providerName, clientType).toPromise();
|
||||
const addon = await this.getAddon(
|
||||
potentialAddon.externalId,
|
||||
potentialAddon.providerName,
|
||||
clientType
|
||||
).toPromise();
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
await this.installAddon(addon.id, onUpdate);
|
||||
}
|
||||
|
||||
public async processAutoUpdates(): Promise<number> {
|
||||
const autoUpdateAddons = this.getAutoUpdateEnabledAddons();
|
||||
const clientTypeGroups = _.groupBy(autoUpdateAddons, addon => addon.clientType);
|
||||
const clientTypeGroups = _.groupBy(
|
||||
autoUpdateAddons,
|
||||
(addon) => addon.clientType
|
||||
);
|
||||
let updateCt = 0;
|
||||
|
||||
for (let clientTypeStr in clientTypeGroups) {
|
||||
const clientType: WowClientType = parseInt(clientTypeStr, 10);
|
||||
// console.log('clientType', clientType, clientTypeGroups[clientType]);
|
||||
|
||||
const synced = await this.syncAddons(clientType, clientTypeGroups[clientType]);
|
||||
const synced = await this.syncAddons(
|
||||
clientType,
|
||||
clientTypeGroups[clientType]
|
||||
);
|
||||
if (!synced) {
|
||||
continue;
|
||||
}
|
||||
@@ -113,30 +131,33 @@ export class AddonService {
|
||||
try {
|
||||
await this.installAddon(addon.id);
|
||||
updateCt += 1;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
} catch (err) {
|
||||
// _analyticsService.Track(ex, "Failed to install addon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return updateCt;
|
||||
}
|
||||
|
||||
public canUpdateAddon(addon: Addon) {
|
||||
return addon.installedVersion && addon.installedVersion !== addon.latestVersion;
|
||||
return (
|
||||
addon.installedVersion && addon.installedVersion !== addon.latestVersion
|
||||
);
|
||||
}
|
||||
|
||||
public getAutoUpdateEnabledAddons() {
|
||||
return this._addonStorage.queryAll(addon => {
|
||||
return this._addonStorage.queryAll((addon) => {
|
||||
return addon.isIgnored !== true && addon.autoUpdateEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
public async installAddon(
|
||||
addonId: string,
|
||||
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
|
||||
onUpdate: (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => void = undefined
|
||||
) {
|
||||
const addon = this.getAddonById(addonId);
|
||||
if (addon == null || !addon.downloadUrl) {
|
||||
@@ -144,35 +165,56 @@ export class AddonService {
|
||||
}
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Downloading, 25);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Downloading, progress: 25 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Downloading,
|
||||
progress: 25,
|
||||
});
|
||||
|
||||
let downloadedFilePath = '';
|
||||
let unzippedDirectory = '';
|
||||
let downloadedThumbnail = '';
|
||||
let downloadedFilePath = "";
|
||||
let unzippedDirectory = "";
|
||||
let downloadedThumbnail = "";
|
||||
try {
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(addon.downloadUrl, this._wowUpService.applicationDownloadsFolderPath);
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(
|
||||
addon.downloadUrl,
|
||||
this._wowUpService.applicationDownloadsFolderPath
|
||||
);
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Installing, 75);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Installing, progress: 75 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Installing,
|
||||
progress: 75,
|
||||
});
|
||||
|
||||
const unzipPath = path.join(this._wowUpService.applicationDownloadsFolderPath, uuidv4());
|
||||
unzippedDirectory = await this._downloadService.unzipFile(downloadedFilePath, unzipPath);
|
||||
const unzipPath = path.join(
|
||||
this._wowUpService.applicationDownloadsFolderPath,
|
||||
uuidv4()
|
||||
);
|
||||
unzippedDirectory = await this._downloadService.unzipFile(
|
||||
downloadedFilePath,
|
||||
unzipPath
|
||||
);
|
||||
|
||||
await this.installUnzippedDirectory(unzippedDirectory, addon.clientType);
|
||||
const unzippedDirectoryNames = await this._fileService.listDirectories(unzippedDirectory);
|
||||
const unzippedDirectoryNames = await this._fileService.listDirectories(
|
||||
unzippedDirectory
|
||||
);
|
||||
|
||||
addon.installedVersion = addon.latestVersion;
|
||||
addon.installedAt = new Date();
|
||||
addon.installedFolders = unzippedDirectoryNames.join(',');
|
||||
addon.installedFolders = unzippedDirectoryNames.join(",");
|
||||
|
||||
if (!!addon.gameVersion) {
|
||||
addon.gameVersion = await this.getLatestGameVersion(unzippedDirectory, unzippedDirectoryNames);
|
||||
addon.gameVersion = await this.getLatestGameVersion(
|
||||
unzippedDirectory,
|
||||
unzippedDirectoryNames
|
||||
);
|
||||
}
|
||||
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "InstallById", $"{addon.ClientType}|{addon.Name}");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -188,10 +230,17 @@ export class AddonService {
|
||||
}
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Complete, 100);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Complete, progress: 100 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Complete,
|
||||
progress: 100,
|
||||
});
|
||||
}
|
||||
|
||||
private async getLatestGameVersion(baseDir: string, installedFolders: string[]) {
|
||||
private async getLatestGameVersion(
|
||||
baseDir: string,
|
||||
installedFolders: string[]
|
||||
) {
|
||||
const versions = [];
|
||||
|
||||
for (let dir of installedFolders) {
|
||||
@@ -211,31 +260,44 @@ export class AddonService {
|
||||
versions.push(toc.interface);
|
||||
}
|
||||
|
||||
return _.orderBy(versions)[0] || '';
|
||||
return _.orderBy(versions)[0] || "";
|
||||
}
|
||||
|
||||
private async installUnzippedDirectory(unzippedDirectory: string, clientType: WowClientType) {
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(clientType);
|
||||
const unzippedFolders = await this._fileService.listDirectories(unzippedDirectory);
|
||||
private async installUnzippedDirectory(
|
||||
unzippedDirectory: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
clientType
|
||||
);
|
||||
const unzippedFolders = await this._fileService.listDirectories(
|
||||
unzippedDirectory
|
||||
);
|
||||
for (let unzippedFolder of unzippedFolders) {
|
||||
const unzippedFilePath = path.join(unzippedDirectory, unzippedFolder);
|
||||
const unzipLocation = path.join(addonFolderPath, unzippedFolder);
|
||||
const unzipBackupLocation = path.join(addonFolderPath, `${unzippedFolder}-bak`);
|
||||
const unzipBackupLocation = path.join(
|
||||
addonFolderPath,
|
||||
`${unzippedFolder}-bak`
|
||||
);
|
||||
|
||||
try {
|
||||
// If the user already has the addon installed, create a temporary backup
|
||||
if (fs.existsSync(unzipLocation)) {
|
||||
console.log('BACKING UP', unzipLocation);
|
||||
await this._fileService.renameDirectory(unzipLocation, unzipBackupLocation);
|
||||
console.log("BACKING UP", unzipLocation);
|
||||
await this._fileService.renameDirectory(
|
||||
unzipLocation,
|
||||
unzipBackupLocation
|
||||
);
|
||||
}
|
||||
|
||||
// Copy contents from unzipped new directory to existing addon folder location
|
||||
console.log('COPY', unzipLocation);
|
||||
console.log("COPY", unzipLocation);
|
||||
await this._fileService.copyDirectory(unzippedFilePath, unzipLocation);
|
||||
|
||||
// If the copy succeeds, delete the backup
|
||||
if (fs.existsSync(unzipBackupLocation)) {
|
||||
console.log('DELETE BKUP', unzipLocation);
|
||||
console.log("DELETE BKUP", unzipLocation);
|
||||
await this._fileService.deleteDirectory(unzipBackupLocation);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -251,7 +313,10 @@ export class AddonService {
|
||||
|
||||
// Move the backup folder into the original location
|
||||
console.log(`Attempting to roll back ${unzipBackupLocation}`);
|
||||
await this._fileService.copyDirectory(unzipBackupLocation, unzipLocation);
|
||||
await this._fileService.copyDirectory(
|
||||
unzipBackupLocation,
|
||||
unzipLocation
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -269,27 +334,39 @@ export class AddonService {
|
||||
return await provider.searchByUrl(url, clientType);
|
||||
}
|
||||
|
||||
public getAddon(externalId: string, providerName: string, clientType: WowClientType) {
|
||||
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(clientType);
|
||||
public getAddon(
|
||||
externalId: string,
|
||||
providerName: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(
|
||||
clientType
|
||||
);
|
||||
const provider = this.getProvider(providerName);
|
||||
return provider.getById(externalId, clientType)
|
||||
.pipe(
|
||||
map(searchResult => {
|
||||
console.log('SEARCH RES', searchResult);
|
||||
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
|
||||
if (!latestFile) {
|
||||
latestFile = searchResult.files[0];
|
||||
}
|
||||
return provider.getById(externalId, clientType).pipe(
|
||||
map((searchResult) => {
|
||||
console.log("SEARCH RES", searchResult);
|
||||
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
|
||||
if (!latestFile) {
|
||||
latestFile = searchResult.files[0];
|
||||
}
|
||||
|
||||
return this.createAddon(latestFile.folders[0], searchResult, latestFile, clientType);
|
||||
})
|
||||
)
|
||||
return this.createAddon(
|
||||
latestFile.folders[0],
|
||||
searchResult,
|
||||
latestFile,
|
||||
clientType
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async removeAddon(addon: Addon) {
|
||||
const installedDirectories = addon.installedFolders.split(',');
|
||||
const installedDirectories = addon.installedFolders.split(",");
|
||||
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(addon.clientType);
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
addon.clientType
|
||||
);
|
||||
for (let directory of installedDirectories) {
|
||||
const addonDirectory = path.join(addonFolderPath, directory);
|
||||
await this._fileService.deleteDirectory(addonDirectory);
|
||||
@@ -299,7 +376,10 @@ export class AddonService {
|
||||
this._addonRemovedSrc.next(addon.id);
|
||||
}
|
||||
|
||||
public async getAddons(clientType: WowClientType, rescan = false): Promise<Addon[]> {
|
||||
public async getAddons(
|
||||
clientType: WowClientType,
|
||||
rescan = false
|
||||
): Promise<Addon[]> {
|
||||
let addons = this._addonStorage.getAllForClientType(clientType);
|
||||
if (rescan || !addons.length) {
|
||||
const newAddons = await this.scanAddons(clientType);
|
||||
@@ -312,18 +392,28 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private updateAddons(existingAddons: Addon[], newAddons: Addon[]): Addon[] {
|
||||
const removedAddons = existingAddons
|
||||
.filter(existingAddon => !newAddons.some(newAddon => this.addonsMatch(existingAddon, newAddon)));
|
||||
const removedAddons = existingAddons.filter(
|
||||
(existingAddon) =>
|
||||
!newAddons.some((newAddon) => this.addonsMatch(existingAddon, newAddon))
|
||||
);
|
||||
|
||||
const addedAddons = newAddons
|
||||
.filter(newAddon => !existingAddons.some(existingAddon => this.addonsMatch(existingAddon, newAddon)));
|
||||
const addedAddons = newAddons.filter(
|
||||
(newAddon) =>
|
||||
!existingAddons.some((existingAddon) =>
|
||||
this.addonsMatch(existingAddon, newAddon)
|
||||
)
|
||||
);
|
||||
|
||||
_.remove(existingAddons, addon => removedAddons.some(removedAddon => removedAddon.id === addon.id));
|
||||
_.remove(existingAddons, (addon) =>
|
||||
removedAddons.some((removedAddon) => removedAddon.id === addon.id)
|
||||
);
|
||||
|
||||
existingAddons.push(...addedAddons);
|
||||
|
||||
for (let existingAddon of existingAddons) {
|
||||
var matchingAddon = newAddons.find(newAddon => this.addonsMatch(newAddon, existingAddon));
|
||||
var matchingAddon = newAddons.find((newAddon) =>
|
||||
this.addonsMatch(newAddon, existingAddon)
|
||||
);
|
||||
if (!matchingAddon) {
|
||||
continue;
|
||||
}
|
||||
@@ -346,9 +436,11 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private addonsMatch(addon1: Addon, addon2: Addon): boolean {
|
||||
return addon1.externalId == addon2.externalId &&
|
||||
return (
|
||||
addon1.externalId == addon2.externalId &&
|
||||
addon1.providerName == addon2.providerName &&
|
||||
addon1.clientType == addon2.clientType;
|
||||
addon1.clientType == addon2.clientType
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAddons(clientType: WowClientType, addons: Addon[]) {
|
||||
@@ -358,25 +450,40 @@ export class AddonService {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async syncProviderAddons(clientType: WowClientType, addons: Addon[], addonProvider: AddonProvider) {
|
||||
const providerAddonIds = this.getExternalIdsForProvider(addonProvider, addons);
|
||||
private async syncProviderAddons(
|
||||
clientType: WowClientType,
|
||||
addons: Addon[],
|
||||
addonProvider: AddonProvider
|
||||
) {
|
||||
const providerAddonIds = this.getExternalIdsForProvider(
|
||||
addonProvider,
|
||||
addons
|
||||
);
|
||||
if (!providerAddonIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResults = await addonProvider.getAll(clientType, providerAddonIds);
|
||||
const searchResults = await addonProvider.getAll(
|
||||
clientType,
|
||||
providerAddonIds
|
||||
);
|
||||
for (let result of searchResults) {
|
||||
const addon = addons.find(addon => addon.externalId === result?.externalId);
|
||||
const addon = addons.find(
|
||||
(addon) => addon.externalId === result?.externalId
|
||||
);
|
||||
const latestFile = this.getLatestFile(result, addon?.channelType);
|
||||
|
||||
if (!result || !latestFile || latestFile.version === addon.latestVersion) {
|
||||
if (
|
||||
!result ||
|
||||
!latestFile ||
|
||||
latestFile.version === addon.latestVersion
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -396,37 +503,57 @@ export class AddonService {
|
||||
}
|
||||
}
|
||||
|
||||
private getExternalIdsForProvider(addonProvider: AddonProvider, addons: Addon[]): string[] {
|
||||
return addons.filter(addon => addon.providerName === addonProvider.name)
|
||||
.map(addon => addon.externalId);
|
||||
private getExternalIdsForProvider(
|
||||
addonProvider: AddonProvider,
|
||||
addons: Addon[]
|
||||
): string[] {
|
||||
return addons
|
||||
.filter((addon) => addon.providerName === addonProvider.name)
|
||||
.map((addon) => addon.externalId);
|
||||
}
|
||||
|
||||
private async scanAddons(clientType: WowClientType): Promise<Addon[]> {
|
||||
const addonFolders = await this._warcraftService.listAddons(clientType);
|
||||
for (let provider of this._addonProviders) {
|
||||
try {
|
||||
const validFolders = addonFolders.filter(af => !af.matchingAddon && af.toc)
|
||||
await provider.scan(clientType, this._wowUpService.getDefaultAddonChannel(clientType), validFolders);
|
||||
const validFolders = addonFolders.filter(
|
||||
(af) => !af.matchingAddon && af.toc
|
||||
);
|
||||
await provider.scan(
|
||||
clientType,
|
||||
this._wowUpService.getDefaultAddonChannel(clientType),
|
||||
validFolders
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const matchedAddonFolders = addonFolders.filter(addonFolder => !!addonFolder.matchingAddon);
|
||||
const matchedGroups = _.groupBy(matchedAddonFolders, addonFolder => `${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`);
|
||||
const matchedAddonFolders = addonFolders.filter(
|
||||
(addonFolder) => !!addonFolder.matchingAddon
|
||||
);
|
||||
const matchedGroups = _.groupBy(
|
||||
matchedAddonFolders,
|
||||
(addonFolder) =>
|
||||
`${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`
|
||||
);
|
||||
|
||||
console.log(Object.keys(matchedGroups));
|
||||
console.log(matchedGroups['Curse2382'])
|
||||
return Object.values(matchedGroups).map(value => value[0].matchingAddon);
|
||||
console.log(matchedGroups["Curse2382"]);
|
||||
|
||||
return Object.values(matchedGroups).map((value) => value[0].matchingAddon);
|
||||
}
|
||||
|
||||
public getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return forkJoin(this._addonProviders.map(p => p.getFeaturedAddons(clientType)))
|
||||
.pipe(
|
||||
map(results => {
|
||||
return _.orderBy(results.flat(1), ['downloadCount']).reverse();
|
||||
})
|
||||
);
|
||||
public getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Observable<PotentialAddon[]> {
|
||||
return forkJoin(
|
||||
this._addonProviders.map((p) => p.getFeaturedAddons(clientType))
|
||||
).pipe(
|
||||
map((results) => {
|
||||
return _.orderBy(results.flat(1), ["downloadCount"]).reverse();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public isInstalled(externalId: string, clientType: WowClientType) {
|
||||
@@ -434,17 +561,19 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private getProvider(providerName: string) {
|
||||
return this._addonProviders.find(provider => provider.name === providerName);
|
||||
return this._addonProviders.find(
|
||||
(provider) => provider.name === providerName
|
||||
);
|
||||
}
|
||||
|
||||
private getAllStoredAddons(clientType: WowClientType) {
|
||||
const addons: Addon[] = [];
|
||||
|
||||
this._addonStorage.query(store => {
|
||||
this._addonStorage.query((store) => {
|
||||
for (const result of store) {
|
||||
addons.push(result[1] as Addon);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return addons;
|
||||
}
|
||||
@@ -452,7 +581,7 @@ export class AddonService {
|
||||
private async getLocalAddons(clientType: WowClientType): Promise<any> {
|
||||
const addonFolders = await this._warcraftService.listAddons(clientType);
|
||||
const addons: Addon[] = [];
|
||||
console.log('addonFolders', addonFolders);
|
||||
console.log("addonFolders", addonFolders);
|
||||
|
||||
for (const folder of addonFolders) {
|
||||
try {
|
||||
@@ -461,7 +590,6 @@ export class AddonService {
|
||||
if (folder.toc.curseProjectId) {
|
||||
addon = await this.getCurseAddonById(folder, clientType);
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
if (!addon) {
|
||||
@@ -478,22 +606,42 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private getAddonProvider(addonUri: URL): AddonProvider {
|
||||
return this._addonProviders.find(provider => provider.isValidAddonUri(addonUri));
|
||||
return this._addonProviders.find((provider) =>
|
||||
provider.isValidAddonUri(addonUri)
|
||||
);
|
||||
}
|
||||
|
||||
private async getCurseAddonById(
|
||||
addonFolder: AddonFolder,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const curseProvider = this._addonProviders.find(p => p instanceof CurseAddonProvider);
|
||||
const searchResult = await curseProvider.getById(addonFolder.toc.curseProjectId, clientType).toPromise();
|
||||
const latestFile = this.getLatestFile(searchResult, AddonChannelType.Stable);
|
||||
return this.createAddon(addonFolder.name, searchResult, latestFile, clientType);
|
||||
const curseProvider = this._addonProviders.find(
|
||||
(p) => p instanceof CurseAddonProvider
|
||||
);
|
||||
const searchResult = await curseProvider
|
||||
.getById(addonFolder.toc.curseProjectId, clientType)
|
||||
.toPromise();
|
||||
const latestFile = this.getLatestFile(
|
||||
searchResult,
|
||||
AddonChannelType.Stable
|
||||
);
|
||||
return this.createAddon(
|
||||
addonFolder.name,
|
||||
searchResult,
|
||||
latestFile,
|
||||
clientType
|
||||
);
|
||||
}
|
||||
|
||||
private getLatestFile(searchResult: AddonSearchResult, channelType: AddonChannelType): AddonSearchResultFile {
|
||||
let files = _.filter(searchResult.files, (f: AddonSearchResultFile) => f.channelType <= channelType);
|
||||
files = _.orderBy(files, ['releaseDate']).reverse();
|
||||
private getLatestFile(
|
||||
searchResult: AddonSearchResult,
|
||||
channelType: AddonChannelType
|
||||
): AddonSearchResultFile {
|
||||
let files = _.filter(
|
||||
searchResult.files,
|
||||
(f: AddonSearchResultFile) => f.channelType <= channelType
|
||||
);
|
||||
files = _.orderBy(files, ["releaseDate"]).reverse();
|
||||
return _.first(files);
|
||||
}
|
||||
|
||||
@@ -525,4 +673,4 @@ export class AddonService {
|
||||
autoUpdateEnabled: this._wowUpService.getDefaultAutoUpdate(clientType),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Injectable, InjectionToken } from "@angular/core";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { filter, first, map } from "rxjs/operators";
|
||||
import { AddonService } from "../addons/addon.service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { WarcraftService } from "../warcraft/warcraft.service";
|
||||
import { WowUpService } from "../wowup/wowup.service";
|
||||
|
||||
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
@@ -20,28 +16,29 @@ export class SessionService {
|
||||
private readonly _statusTextSrc = new BehaviorSubject(""); // left side bar text, context to the app
|
||||
private readonly _selectedHomeTabSrc = new BehaviorSubject(0);
|
||||
|
||||
private _autoUpdateInterval?: number;
|
||||
|
||||
public readonly selectedClientType$ = this._selectedClientTypeSrc.asObservable();
|
||||
public readonly statusText$ = this._statusTextSrc.asObservable();
|
||||
public readonly selectedHomeTab$ = this._selectedHomeTabSrc.asObservable();
|
||||
public readonly pageContextText$ = this._pageContextTextSrc.asObservable();
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService
|
||||
) {
|
||||
this.loadInitialClientType().pipe(first()).subscribe();
|
||||
}
|
||||
|
||||
public set contextText(text: string){
|
||||
public set contextText(text: string) {
|
||||
this._pageContextTextSrc.next(text);
|
||||
}
|
||||
|
||||
public set statusText(text: string) {
|
||||
this._statusTextSrc.next(text);
|
||||
}
|
||||
|
||||
public set selectedHomeTab(tabIndex: number) {
|
||||
this._selectedHomeTabSrc.next(tabIndex);
|
||||
this.contextText = '';
|
||||
this.contextText = "";
|
||||
}
|
||||
|
||||
public set selectedClientType(clientType: WowClientType) {
|
||||
@@ -53,25 +50,10 @@ export class SessionService {
|
||||
return this._selectedClientTypeSrc.value;
|
||||
}
|
||||
|
||||
public appLoaded() {
|
||||
if (!this._autoUpdateInterval) {
|
||||
this.onAutoUpdateInterval();
|
||||
this._autoUpdateInterval = window.setInterval(
|
||||
this.onAutoUpdateInterval,
|
||||
AUTO_UPDATE_PERIOD_MS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public startUpdaterCheck() {
|
||||
this.checkUpdaterApp();
|
||||
}
|
||||
|
||||
private onAutoUpdateInterval = async () => {
|
||||
console.log("Auto update");
|
||||
const updateCount = await this._addonService.processAutoUpdates();
|
||||
};
|
||||
|
||||
private loadInitialClientType() {
|
||||
return this._warcraftService.installedClientTypes$.pipe(
|
||||
filter((clientTypes) => clientTypes !== undefined),
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import { remote } from 'electron'
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { remote } from "electron";
|
||||
import { ListFilesResponse } from "common/models/list-files-response";
|
||||
import { ListFilesRequest } from "common/models/list-files-request";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { LIST_FILES_CHANNEL, READ_FILE_CHANNEL } from "common/constants";
|
||||
import { ReadFileResponse } from "common/models/read-file-response";
|
||||
import { ReadFileRequest } from "common/models/read-file-request";
|
||||
|
||||
const fsAccess = util.promisify(fs.access)
|
||||
const fsReadFile = util.promisify(fs.readFile)
|
||||
const userDataPath = remote.app.getPath('userData');
|
||||
const fsAccess = util.promisify(fs.access);
|
||||
const fsReadFile = util.promisify(fs.readFile);
|
||||
const userDataPath = remote.app.getPath("userData");
|
||||
|
||||
export class FileUtils {
|
||||
static async exists(path: string) {
|
||||
try {
|
||||
await fsAccess(path, fs.constants.F_OK)
|
||||
return true
|
||||
await fsAccess(path, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static readFile(path: string) {
|
||||
return fsReadFile(path)
|
||||
return fsReadFile(path);
|
||||
}
|
||||
|
||||
static readFileSync(path: string) {
|
||||
@@ -27,4 +33,4 @@ export class FileUtils {
|
||||
static getUserDataPath() {
|
||||
return userDataPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user