Start working on the backup system

Feature flag for account functions for now disabled
This commit is contained in:
jliddev
2021-10-29 17:30:05 -05:00
parent f911ae1cbb
commit bcebdf7f7d
54 changed files with 832 additions and 263 deletions

View File

@@ -1,11 +1,34 @@
import { exec } from 'child_process';
import * as log from 'electron-log';
import * as fsp from 'fs/promises';
import { max, sumBy } from 'lodash';
import * as path from 'path';
import { exec } from "child_process";
import * as log from "electron-log";
import * as fsp from "fs/promises";
import { max, sumBy } from "lodash";
import * as path from "path";
import * as crypto from "crypto";
import * as AdmZip from "adm-zip";
import { TreeNode } from '../src/common/models/ipc-events';
import { isWin } from './platform';
import { TreeNode } from "../src/common/models/ipc-events";
import { GetDirectoryTreeOptions } from "../src/common/models/ipc-request";
import { isWin } from "./platform";
export function zipFile(srcPath: string, outPath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const zip = new AdmZip();
zip.addLocalFolder(srcPath);
zip.writeZip(outPath, (e) => {
return e ? reject(e) : resolve(true);
});
});
}
export function readFileInZip(zipPath: string, filePath: string) {
return new Promise((resolve, reject) => {
const zip = new AdmZip(zipPath);
zip.readAsTextAsync(filePath, (data, err) => {
return err ? reject(err) : resolve(data);
});
});
}
export async function exists(path: string): Promise<boolean> {
try {
@@ -95,7 +118,7 @@ export async function readDirRecursive(sourcePath: string): Promise<string[]> {
return dirFiles;
}
export async function getDirTree(sourcePath: string): Promise<TreeNode> {
export async function getDirTree(sourcePath: string, opts?: GetDirectoryTreeOptions): Promise<TreeNode> {
const files = await fsp.readdir(sourcePath, { withFileTypes: true });
const node: TreeNode = {
@@ -109,10 +132,18 @@ export async function getDirTree(sourcePath: string): Promise<TreeNode> {
for (const file of files) {
const filePath = path.join(sourcePath, file.name);
if (file.isDirectory()) {
const nestedNode = await getDirTree(filePath);
const nestedNode = await getDirTree(filePath, opts);
node.children.push(nestedNode);
node.size = sumBy(node.children, (n) => n.size);
if (opts?.includeHash) {
node.hash = hashString(node.children.map((n) => n.hash).join(""), "sha256");
}
} else {
let hash = "";
if (opts?.includeHash) {
hash = await hashFile(filePath, "sha256");
}
const stats = await fsp.stat(filePath);
node.size += stats.size;
node.children.push({
@@ -121,10 +152,15 @@ export async function getDirTree(sourcePath: string): Promise<TreeNode> {
children: [],
isDirectory: false,
size: stats.size,
hash,
});
}
}
if (opts?.includeHash) {
node.hash = hashString(node.children.map((n) => n.hash).join(""), "sha256");
}
return node;
}
@@ -139,3 +175,14 @@ export async function getLastModifiedFileDate(sourcePath: string): Promise<numbe
const latest = max(dates);
return latest;
}
export function hashString(str: string | crypto.BinaryLike, alg = "md5") {
const md5 = crypto.createHash(alg);
md5.update(str);
return md5.digest("hex");
}
export async function hashFile(filePath: string, alg = "md5"): Promise<string> {
const text = await fsp.readFile(filePath);
return hashString(text, alg);
}

View File

@@ -89,11 +89,28 @@ import { createAppMenu } from "./app-menu";
import { CurseFolderScanner } from "./curse-folder-scanner";
import * as fsp from "fs/promises";
import { chmodDir, copyDir, getDirTree, getLastModifiedFileDate, readDirRecursive, remove } from "./file.utils";
import {
chmodDir,
copyDir,
exists,
getDirTree,
getLastModifiedFileDate,
readDirRecursive,
readFileInZip,
remove,
zipFile,
} from "./file.utils";
import { addonStore } from "./stores";
import { createTray, restoreWindow } from "./system-tray";
import { WowUpFolderScanner } from "./wowup-folder-scanner";
import * as push from "./push";
import { WowInstallation } from "../src/common/warcraft/wow-installation";
import {
BackupCreateRequest,
BackupGetExistingRequest,
GetDirectoryTreeRequest,
} from "../src/common/models/ipc-request";
import { BackupCreateResponse, BackupGetExistingResponse } from "../src/common/models/ipc-response";
let PENDING_OPEN_URLS: string[] = [];
@@ -310,6 +327,11 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
});
handle(IPC_LIST_FILES_CHANNEL, async (evt, sourcePath: string, filter: string) => {
const pathExists = await exists(sourcePath);
if (!pathExists) {
return [];
}
const globFilter = globrex(filter);
const results = await fsp.readdir(sourcePath, { withFileTypes: true });
const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
@@ -363,6 +385,14 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
return arg.outputFolder;
});
handle("zip-file", async (evt, srcPath: string, destPath: string) => {
return await zipFile(srcPath, destPath);
});
handle("zip-read-file", async (evt, zipPath: string, filePath: string) => {
return await readFileInZip(zipPath, filePath);
});
handle(IPC_COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest): Promise<boolean> => {
log.info(`[FileCopy] '${arg.sourceFilePath}' -> '${arg.destinationFilePath}'`);
const stat = await fsp.lstat(arg.sourceFilePath);
@@ -409,8 +439,9 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
return readDirRecursive(dirPath);
});
handle(IPC_GET_DIRECTORY_TREE, (evt, dirPath: string): Promise<TreeNode> => {
return getDirTree(dirPath);
handle(IPC_GET_DIRECTORY_TREE, (evt, args: GetDirectoryTreeRequest): Promise<TreeNode> => {
log.debug(IPC_GET_DIRECTORY_TREE, args);
return getDirTree(args.dirPath, args.opts);
});
handle(IPC_MINIMIZE_WINDOW, () => {
@@ -484,6 +515,37 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
return await push.subscribeToChannel(channel);
});
handle("backup-get-existing", async (evt, req: BackupGetExistingRequest) => {
const response: BackupGetExistingResponse = {
exists: false,
};
log.debug("backup-get-existing", req);
const backupFolder = path.join(req.backupPath, req.installation.id);
response.exists = await exists(backupFolder);
if (!response.exists) {
return response;
}
return response;
});
handle("backup-create", async (evt, req: BackupCreateRequest) => {
const response: BackupCreateResponse = {};
log.debug("backup-get-existing", req);
const backupFolder = path.join(req.backupPath, req.installation.id);
await fsp.mkdir(backupFolder, { recursive: true });
// TODO create backup tree
// TODO copy backup contents
return response;
});
ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => {
handleDownloadFile(arg).catch((e) => console.error(e.toString()));
});

View File

@@ -4,7 +4,7 @@ import * as path from "path";
import * as pLimit from "p-limit";
import * as log from "electron-log";
import { WowUpScanResult } from "../src/common/wowup/models";
import { exists, readDirRecursive } from "./file.utils";
import { exists, readDirRecursive, hashFile, hashString } from "./file.utils";
import * as fsp from "fs/promises";
const INVALID_PATH_CHARS = [
@@ -86,14 +86,14 @@ export class WowUpFolderScanner {
const limit = pLimit(4);
const tasks = _.map(matchingFiles, (file) =>
limit(async () => {
return { hash: await this.hashFile(file), file };
return { hash: await hashFile(file), file };
})
);
const fileFingerprints = await Promise.all(tasks);
const fingerprintList = _.map(fileFingerprints, (ff) => ff.hash);
const hashConcat = _.orderBy(fingerprintList).join("");
const fingerprint = this.hashString(hashConcat);
const fingerprint = hashString(hashConcat);
const result: WowUpScanResult = {
fileFingerprints: fingerprintList,
@@ -204,17 +204,6 @@ export class WowUpFolderScanner {
return matches;
}
private hashString(str: string | crypto.BinaryLike) {
const md5 = crypto.createHash("md5");
md5.update(str);
return md5.digest("hex");
}
private async hashFile(filePath: string): Promise<string> {
const text = await fsp.readFile(filePath);
return this.hashString(text);
}
private getRealPath(filePath: string) {
const lowerPath = filePath.toLowerCase();
const matchedPath = this._fileMap[lowerPath];

View File

@@ -1,12 +1,12 @@
{
"name": "wowup",
"version": "2.5.0-beta.20",
"version": "2.5.0-beta.21",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "wowup",
"version": "2.5.0-beta.20",
"version": "2.5.0-beta.21",
"hasInstallScript": true,
"dependencies": {
"@angular/animations": "12.2.10",
@@ -29,8 +29,10 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@microsoft/applicationinsights-web": "2.7.0",
"adm-zip": "0.5.9",
"ag-grid-angular": "26.1.0",
"ag-grid-community": "26.1.0",
"archiver": "5.3.0",
"auto-launch": "5.0.5",
"compare-versions": "3.6.0",
"electron-log": "4.4.1",
@@ -73,6 +75,8 @@
"@angular/cli": "12.2.10",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/adm-zip": "0.4.34",
"@types/archiver": "5.1.1",
"@types/globrex": "0.1.1",
"@types/jasmine": "3.10.0",
"@types/jasminewd2": "2.0.10",
@@ -3562,6 +3566,24 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"node_modules/@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/archiver": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.1.tgz",
"integrity": "sha512-heuaCk0YH5m274NOLSi66H1zX6GtZoMsdE6TYFcpFFjBjg0FoU4i4/M/a/kNlgNg26Xk3g364mNOYe1JaiEPOQ==",
"dev": true,
"dependencies": {
"@types/glob": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -4795,12 +4817,11 @@
}
},
"node_modules/adm-zip": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
"dev": true,
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"engines": {
"node": ">=0.3.0"
"node": ">=6.0"
}
},
"node_modules/ag-grid-angular": {
@@ -5094,7 +5115,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz",
"integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==",
"dev": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.0",
@@ -5112,7 +5132,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
@@ -5132,14 +5151,12 @@
"node_modules/archiver/node_modules/async": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz",
"integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==",
"dev": true
"integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg=="
},
"node_modules/archiver/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -5620,8 +5637,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base": {
"version": "0.11.2",
@@ -5666,7 +5682,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -5737,7 +5752,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -5748,7 +5762,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -5991,7 +6004,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6040,7 +6052,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -6955,7 +6966,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
"integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==",
"dev": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
@@ -6970,7 +6980,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -7037,8 +7046,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/concat-stream": {
"version": "1.6.2",
@@ -7651,8 +7659,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cors": {
"version": "2.8.5",
@@ -7697,7 +7704,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
"dev": true,
"dependencies": {
"exit-on-epipe": "~1.0.1",
"printj": "~1.1.0"
@@ -7713,7 +7719,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz",
"integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==",
"dev": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
@@ -7726,7 +7731,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -9544,7 +9548,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -10675,7 +10678,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
"dev": true,
"engines": {
"node": ">=0.8"
}
@@ -11407,8 +11409,7 @@
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": {
"version": "9.1.0",
@@ -11446,8 +11447,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fsevents": {
"version": "2.3.2",
@@ -11788,7 +11788,6 @@
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -12882,7 +12881,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -13068,7 +13066,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -13077,8 +13074,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "2.0.0",
@@ -13828,8 +13824,7 @@
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/isbinaryfile": {
"version": "4.0.8",
@@ -14574,7 +14569,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
"integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
"dev": true,
"dependencies": {
"readable-stream": "^2.0.5"
},
@@ -14878,14 +14872,12 @@
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=",
"dev": true
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
},
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
@@ -14895,8 +14887,7 @@
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
@@ -14918,8 +14909,7 @@
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
"dev": true
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"node_modules/lodash.memoize": {
"version": "3.0.4",
@@ -14942,8 +14932,7 @@
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
"dev": true
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
},
"node_modules/lodash.uniq": {
"version": "4.5.0",
@@ -15747,7 +15736,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -17009,7 +16997,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -19675,7 +19662,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"dev": true,
"bin": {
"printj": "bin/printj.njs"
},
@@ -19686,8 +19672,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/progress": {
"version": "2.0.3",
@@ -21621,7 +21606,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -21636,7 +21620,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
"integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==",
"dev": true,
"dependencies": {
"minimatch": "^3.0.4"
}
@@ -23660,7 +23643,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -24112,7 +24094,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@@ -24128,7 +24109,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -25255,8 +25235,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/utils-merge": {
"version": "1.0.1",
@@ -25455,6 +25434,15 @@
"node": ">=6.9.x"
}
},
"node_modules/webdriver-manager/node_modules/adm-zip": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
"dev": true,
"engines": {
"node": ">=0.3.0"
}
},
"node_modules/webdriver-manager/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@@ -27042,8 +27030,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"node_modules/write-file-atomic": {
"version": "3.0.3",
@@ -27286,7 +27273,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
"integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==",
"dev": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"compress-commons": "^4.1.0",
@@ -27300,7 +27286,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -29810,6 +29795,24 @@
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/archiver": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.1.tgz",
"integrity": "sha512-heuaCk0YH5m274NOLSi66H1zX6GtZoMsdE6TYFcpFFjBjg0FoU4i4/M/a/kNlgNg26Xk3g364mNOYe1JaiEPOQ==",
"dev": true,
"requires": {
"@types/glob": "*"
}
},
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -30789,10 +30792,9 @@
}
},
"adm-zip": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
"dev": true
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"ag-grid-angular": {
"version": "26.1.0",
@@ -31023,7 +31025,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz",
"integrity": "sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==",
"dev": true,
"requires": {
"archiver-utils": "^2.1.0",
"async": "^3.2.0",
@@ -31037,14 +31038,12 @@
"async": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz",
"integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==",
"dev": true
"integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -31057,7 +31056,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"requires": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
@@ -31429,8 +31427,7 @@
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base": {
"version": "0.11.2",
@@ -31467,8 +31464,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"base64id": {
"version": "2.0.0",
@@ -31516,7 +31512,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
@@ -31527,7 +31522,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -31722,7 +31716,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -31758,7 +31751,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
@@ -32483,7 +32475,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
"integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==",
"dev": true,
"requires": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
@@ -32495,7 +32486,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -32554,8 +32544,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.2",
@@ -33029,8 +33018,7 @@
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"cors": {
"version": "2.8.5",
@@ -33069,7 +33057,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
"dev": true,
"requires": {
"exit-on-epipe": "~1.0.1",
"printj": "~1.1.0"
@@ -33079,7 +33066,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz",
"integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==",
"dev": true,
"requires": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
@@ -33089,7 +33075,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -34481,7 +34466,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
@@ -35310,8 +35294,7 @@
"exit-on-epipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==",
"dev": true
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
},
"expand-brackets": {
"version": "2.1.4",
@@ -35902,8 +35885,7 @@
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "9.1.0",
@@ -35935,8 +35917,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.3.2",
@@ -36200,7 +36181,6 @@
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -37084,8 +37064,7 @@
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
"version": "5.1.8",
@@ -37211,7 +37190,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -37220,8 +37198,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "2.0.0",
@@ -37764,8 +37741,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "4.0.8",
@@ -38361,7 +38337,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
"integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
"dev": true,
"requires": {
"readable-stream": "^2.0.5"
}
@@ -38599,14 +38574,12 @@
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
},
"lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=",
"dev": true
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
},
"lodash.escaperegexp": {
"version": "4.1.2",
@@ -38616,8 +38589,7 @@
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.isequal": {
"version": "4.5.0",
@@ -38639,8 +38611,7 @@
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
"dev": true
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.memoize": {
"version": "3.0.4",
@@ -38663,8 +38634,7 @@
"lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=",
"dev": true
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
},
"lodash.uniq": {
"version": "4.5.0",
@@ -39278,7 +39248,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -40240,7 +40209,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
@@ -42161,14 +42129,12 @@
"printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"dev": true
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"progress": {
"version": "2.0.3",
@@ -43996,7 +43962,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -44011,7 +43976,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz",
"integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==",
"dev": true,
"requires": {
"minimatch": "^3.0.4"
}
@@ -45622,7 +45586,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -45964,7 +45927,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
@@ -45977,7 +45939,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -46844,8 +46805,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
@@ -47134,6 +47094,12 @@
"xml2js": "^0.4.17"
},
"dependencies": {
"adm-zip": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
"integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
"dev": true
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@@ -48240,8 +48206,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
"version": "3.0.3",
@@ -48413,7 +48378,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
"integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==",
"dev": true,
"requires": {
"archiver-utils": "^2.1.0",
"compress-commons": "^4.1.0",
@@ -48424,7 +48388,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",

View File

@@ -59,6 +59,8 @@
"@angular/cli": "12.2.10",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/adm-zip": "0.4.34",
"@types/archiver": "5.1.1",
"@types/globrex": "0.1.1",
"@types/jasmine": "3.10.0",
"@types/jasminewd2": "2.0.10",
@@ -131,8 +133,10 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@microsoft/applicationinsights-web": "2.7.0",
"adm-zip": "0.5.9",
"ag-grid-angular": "26.1.0",
"ag-grid-community": "26.1.0",
"archiver": "5.3.0",
"auto-launch": "5.0.5",
"compare-versions": "3.6.0",
"electron-log": "4.4.1",

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { Observable, of } from "rxjs";
import { Addon } from "../../common/entities/addon";

View File

@@ -34,7 +34,7 @@ import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultDependency } from "../models/wowup/addon-search-result-dependency";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { ProtocolSearchResult } from "../models/wowup/protocol-search-result";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { ElectronService } from "../services";
import { CachingService } from "../services/caching/caching-service";
import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service";

View File

@@ -22,7 +22,7 @@ import { AddonChannelType } from "../../common/wowup/models";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { AddonProvider, GetAllResult } from "./addon-provider";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { convertMarkdown } from "../utils/markdown.utlils";
import { strictFilterBy } from "../utils/array.utils";

View File

@@ -1,4 +1,4 @@
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import * as _ from "lodash";
import { v4 as uuidv4 } from "uuid";

View File

@@ -11,7 +11,7 @@ import { TukUiAddon } from "../models/tukui/tukui-addon";
import { AddonFolder } from "../models/wowup/addon-folder";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { CachingService } from "../services/caching/caching-service";
import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service";
import { getGameVersion } from "../utils/addon.utils";

View File

@@ -13,7 +13,7 @@ import { AddonDetailsResponse } from "../models/wow-interface/addon-details-resp
import { AddonFolder } from "../models/wowup/addon-folder";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { CachingService } from "../services/caching/caching-service";
import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service";
import { getGameVersion } from "../utils/addon.utils";

View File

@@ -23,7 +23,7 @@ import { AddonFolder } from "../models/wowup/addon-folder";
import { AddonSearchResult } from "../models/wowup/addon-search-result";
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
import { AppWowUpScanResult } from "../models/wowup/app-wowup-scan-result";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { ElectronService } from "../services";
import { CachingService } from "../services/caching/caching-service";
import { CircuitBreakerWrapper, NetworkService } from "../services/network/network.service";

View File

@@ -4,7 +4,7 @@ import { v4 as uuidv4 } from "uuid";
import { ADDON_PROVIDER_WOWUP_COMPANION, WOWUP_DATA_ADDON_FOLDER_NAME } from "../../common/constants";
import { AddonChannelType } from "../../common/wowup/models";
import { AddonFolder } from "../models/wowup/addon-folder";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
import { FileService } from "../services/files/file.service";
import { TocService } from "../services/toc/toc.service";
import { getGameVersion } from "../utils/addon.utils";

View File

@@ -15,7 +15,7 @@ import { FileService } from "../services/files/file.service";
import { TocService } from "../services/toc/toc.service";
import { WarcraftService } from "../services/warcraft/warcraft.service";
import { AddonProvider } from "./addon-provider";
import { WowInstallation } from "../models/wowup/wow-installation";
import { WowInstallation } from "../../common/warcraft/wow-installation";
const VALID_ZIP_CONTENT_TYPES = ["application/zip", "application/x-zip-compressed", "application/octet-stream"];

View File

@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { BehaviorSubject, Subscription } from "rxjs";
import { map } from "rxjs/operators";
import { AddonInstallState } from "../../../models/wowup/addon-install-state";
import { WowInstallation } from "../../../models/wowup/wow-installation";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
import {
AddonBrokerService,
ExportPayload,

View File

@@ -7,10 +7,10 @@ import { FormControl } from "@angular/forms";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { ProtocolSearchResult } from "../../../models/wowup/protocol-search-result";
import { WowInstallation } from "../../../models/wowup/wow-installation";
import { AddonService } from "../../../services/addons/addon.service";
import { SessionService } from "../../../services/session/session.service";
import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
export interface InstallFromProtocolDialogComponentData {
protocol: string;

View File

@@ -0,0 +1,55 @@
<div mat-dialog-title class="wtf-backup-dialog">
<div class="row align-items-center">
<h2 class="flex-grow-1 m-0">
{{ "WTF_BACKUP.DIALOG_TITLE" | translate: { clientType: selectedInstallation.label } }}
</h2>
<button mat-icon-button [mat-dialog-close]="true" color="accent" [disabled]="busy$ | async">
<mat-icon svgIcon="fas:times"> </mat-icon>
</button>
</div>
</div>
<mat-dialog-content>
<div *ngIf="(hasBackups$ | async) === false">
<h4 class="mb-0">No backups were found at:</h4>
<p class="text-2">{{ backupPath }}</p>
</div>
<div *ngIf="(hasBackups$ | async) === true">
<p class="text-2">
{{ "WTF_BACKUP.BACKUP_COUNT_TEXT" | translate: { count: backupCt$ | async } }}
</p>
<ul class="backup-list rounded">
<li *ngFor="let backup of backups$ | async" class="backup-list-item">
<div class="row align-items-center">
<div class="flex-grow-1">
<div class="row">
<div class="title ml-1 flex-grow-1" [ngClass]="{ 'text-warning': backup.error }">{{ backup.title }}</div>
</div>
<div class="row text-2">
<div class="mr-3">{{ backup.date | localeDate: "medium" }}</div>
<div class="mr-3">{{ backup.size }}</div>
</div>
</div>
<!-- ACTIONS -->
<div *ngIf="backup.error" class="flex-shrink-0">{{ "WTF_BACKUP.ERROR." + backup.error | translate }}</div>
<div *ngIf="backup.error === undefined" class="flex-shrink-0">
<button mat-icon-button [matTooltip]="'WTF_BACKUP.TOOL_TIP.APPLY_BUTTON' | translate">
<mat-icon svgIcon="fas:history"> </mat-icon>
</button>
<button mat-icon-button color="warn" [matTooltip]="'WTF_BACKUP.TOOL_TIP.DELETE_BUTTON' | translate">
<mat-icon svgIcon="fas:trash"> </mat-icon>
</button>
</div>
</div>
</li>
</ul>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button color="primary" [disabled]="busy$ | async" (click)="onShowFolder()">
{{ "WTF_BACKUP.SHOW_FOLDER_BUTTON" | translate }}
</button>
<div class="flex-grow-1"></div>
<button mat-flat-button color="primary" [disabled]="busy$ | async" (click)="onCreateBackup()">
{{ "WTF_BACKUP.CREATE_BACKUP_BUTTON" | translate }}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,28 @@
.wtf-backup-dialog {
min-width: 600px;
}
.backup-list {
background-color: var(--background-secondary-2);
margin: 0;
overflow: auto;
padding: 0 1em;
.backup-list-item:not(:last-child) {
border-bottom: 1px solid var(--background-secondary-1);
}
.backup-list-item {
padding: 1em 0;
list-style: none;
.status-badge {
width: 20px;
height: 20px;
}
.title {
font-size: 1.2em;
}
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WtfBackupComponent } from './wtf-backup.component';
describe('WtfBackupComponent', () => {
let component: WtfBackupComponent;
let fixture: ComponentFixture<WtfBackupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WtfBackupComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(WtfBackupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,85 @@
import { Component, OnInit } from "@angular/core";
import { BehaviorSubject } from "rxjs";
import { map } from "rxjs/operators";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
import { ElectronService } from "../../../services";
import { SessionService } from "../../../services/session/session.service";
import { WtfBackup, WtfService } from "../../../services/wtf/wtf.service";
import { formatSize } from "../../../utils/number.utils";
interface WtfBackupViewModel {
title: string;
size: string;
date: number;
error?: string;
}
@Component({
selector: "app-wtf-backup",
templateUrl: "./wtf-backup.component.html",
styleUrls: ["./wtf-backup.component.scss"],
})
export class WtfBackupComponent implements OnInit {
public readonly busy$ = new BehaviorSubject<boolean>(false);
public readonly backups$ = new BehaviorSubject<WtfBackupViewModel[]>([]);
public readonly selectedInstallation: WowInstallation;
public readonly hasBackups$ = this.backups$.pipe(map((backups) => backups.length > 0));
public readonly backupCt$ = this.backups$.pipe(map((backups) => backups.length));
public readonly backupPath: string;
constructor(
private _electronService: ElectronService,
private _sessionService: SessionService,
private _wtfService: WtfService
) {
this.selectedInstallation = this._sessionService.getSelectedWowInstallation();
this.backupPath = this._wtfService.getBackupPath(this.selectedInstallation);
}
ngOnInit(): void {
this.loadBackups();
}
async onShowFolder(): Promise<void> {
const backupPath = this._wtfService.getBackupPath(this.selectedInstallation);
await this._electronService.openPath(backupPath);
}
async onCreateBackup(): Promise<void> {
this.busy$.next(true);
try {
await this._wtfService.createBackup(this.selectedInstallation);
await this.loadBackups();
} catch (e) {
console.error(e);
} finally {
this.busy$.next(false);
}
}
private async loadBackups() {
this.busy$.next(true);
try {
const backups = await this._wtfService.getBackupList(this.selectedInstallation);
console.debug(backups);
const viewModels = backups.map((b) => this.toViewModel(b));
console.debug(viewModels);
this.backups$.next(viewModels);
} catch (e) {
console.error(e);
} finally {
this.busy$.next(false);
}
}
private toViewModel(backup: WtfBackup): WtfBackupViewModel {
return {
title: backup.fileName,
size: formatSize(backup.size),
date: backup.metadata?.createdAt ?? backup.birthtimeMs,
error: backup.error,
};
}
}

View File

@@ -7,13 +7,6 @@
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate }}">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fab:discord"></mat-icon>
</a>
<!-- <div *ngIf="(sessionService.wowUpAuthenticated$ | async) === false">
<div class="bnet-btn bnet-btn-sm ml-2" (click)="onClickAccount()">Login</div>
</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="btn" (click)="onClickAccount()"
matTooltip="{{ 'You\'re logged in!' | translate }}">
{{accountDisplayName$ | async}}
</div> -->
<p class="text-1">{{ sessionService.statusText$ | async }}</p>
<div class="flex-grow-1"></div>
<p class="mr-3">{{ sessionService.pageContextText$ | async }}</p>

View File

@@ -1,13 +1,22 @@
<div class="tab-strip bg-primary text-1" [ngClass]="{
mac: electronService.isMac,
windows: electronService.isWin,
linux: electronService.isLinux
}">
<div *ngFor="let tab of tabsTop" class="tab"
[ngClass]="{ selected: tab.isSelected$ | async, disabled: tab.isDisabled$ | async }" (click)="tab.onClick(tab)">
<mat-icon class="tab-icon tab-icon-inactive"
<div
class="tab-strip bg-primary text-1"
[ngClass]="{
mac: electronService.isMac,
windows: electronService.isWin,
linux: electronService.isLinux
}"
>
<div
*ngFor="let tab of tabsTop"
class="tab"
[ngClass]="{ selected: tab.isSelected$ | async, disabled: tab.isDisabled$ | async }"
(click)="tab.onClick(tab)"
>
<mat-icon
class="tab-icon tab-icon-inactive"
[ngClass]="{ 'tab-icon-active': tab.isSelected$ | async, 'tab-icon-disabled': tab.isDisabled$ | async }"
[svgIcon]="tab.icon">
[svgIcon]="tab.icon"
>
</mat-icon>
<div class="tab-title" *ngIf="tab.titleKey">{{ tab.titleKey | translate }}</div>
<!-- <mat-icon *ngIf="tab.badge === true" class="tab-icon-inactive z-badge clear-badge" matBadge="1" matBadgeColor="accent"
@@ -15,40 +24,49 @@
</mat-icon> -->
</div>
<div class="spacer"></div>
<a appExternalLink class="text-1 tab hover-bg-secondary-2" [href]="wowUpWebsiteUrl + '/guide'"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GUIDE' | translate }}">
<a
appExternalLink
class="text-1 tab hover-bg-secondary-2"
[href]="wowUpWebsiteUrl + '/guide'"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.VIEW_GUIDE' | translate }}"
>
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="far:question-circle"></mat-icon>
<div class="tab-title">{{ 'PAGES.HOME.GUIDE_TAB_TITLE' | translate }}</div>
<div class="tab-title">{{ "PAGES.HOME.GUIDE_TAB_TITLE" | translate }}</div>
</a>
<!-- <a appExternalLink class="patreon-link tab hover-bg-secondary-2" href="https://www.patreon.com/jliddev"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.PATREON_SUPPORT' | translate }}">
<img class="tab-icon patron-img" src="assets/images/patreon_logo_small.png" />
</a> -->
<!-- <a appExternalLink class="text-1 tab hover-bg-secondary-2" href="https://discord.gg/rk4F5aD"
matTooltip="{{ 'PAGES.MY_ADDONS.PAGE_CONTEXT_FOOTER.JOIN_DISCORD' | translate }}">
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fab:discord"></mat-icon>
</a> -->
<div class="tab hover-bg-secondary-2" [ngClass]="{ selected: isAccountSelected$ | async }"
(click)="onClickTab(TAB_INDEX_ACCOUNT)">
<!-- <div class="tab-icon"> -->
<mat-icon class="tab-icon tab-icon-inactive" svgIcon="fas:user-circle"
[ngClass]="{ 'tab-icon-active': isAccountSelected$ | async }">
<!-- ACCOUNT TAB -->
<div
*ngIf="FEATURE_ACCOUNTS_ENABLED"
class="tab hover-bg-secondary-2"
[ngClass]="{ selected: isAccountSelected$ | async }"
(click)="onClickTab(TAB_INDEX_ACCOUNT)"
>
<mat-icon
class="tab-icon tab-icon-inactive"
svgIcon="fas:user-circle"
[ngClass]="{ 'tab-icon-active': isAccountSelected$ | async }"
>
</mat-icon>
<!-- </div> -->
<div>
<div class="tab-title">{{ 'PAGES.HOME.ACCOUNT_TAB_TITLE' | translate }}
</div>
<div class="tab-title">{{ "PAGES.HOME.ACCOUNT_TAB_TITLE" | translate }}</div>
<div *ngIf="(sessionService.wowUpAuthenticated$ | async) === true" class="tab-subtitle">Logged in</div>
</div>
</div>
<div *ngFor="let tab of tabsBottom" class="tab hover-bg-secondary-2"
[ngClass]="{ selected: tab.isSelected$ | async, disabled: tab.isDisabled$ | async }" (click)="tab.onClick(tab)">
<div
*ngFor="let tab of tabsBottom"
class="tab hover-bg-secondary-2"
[ngClass]="{ selected: tab.isSelected$ | async, disabled: tab.isDisabled$ | async }"
(click)="tab.onClick(tab)"
>
<!-- <div class="tab-icon"> -->
<mat-icon class="tab-icon tab-icon-inactive" [svgIcon]="tab.icon"
[ngClass]="{ 'tab-icon-active': tab.isSelected$ | async, 'tab-icon-disabled': tab.isDisabled$ | async }">
<mat-icon
class="tab-icon tab-icon-inactive"
[svgIcon]="tab.icon"
[ngClass]="{ 'tab-icon-active': tab.isSelected$ | async, 'tab-icon-disabled': tab.isDisabled$ | async }"
>
</mat-icon>
<!-- </div> -->
<div class="tab-title" *ngIf="tab.titleKey">{{ tab.titleKey | translate }}</div>
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { map } from "rxjs/operators";
import { Component, OnInit } from "@angular/core";
import {
FEATURE_ACCOUNTS_ENABLED,
TAB_INDEX_ABOUT,
TAB_INDEX_GET_ADDONS,
TAB_INDEX_MY_ADDONS,
@@ -33,6 +34,7 @@ interface Tab {
export class HorizontalTabsComponent implements OnInit {
public wowUpWebsiteUrl = AppConfig.wowUpWebsiteUrl;
public TAB_INDEX_ACCOUNT = TAB_INDEX_ABOUT;
public FEATURE_ACCOUNTS_ENABLED = FEATURE_ACCOUNTS_ENABLED;
public isAccountSelected$ = this.sessionService.selectedHomeTab$.pipe(map((result) => result === TAB_INDEX_ABOUT));

View File

@@ -6,13 +6,13 @@ import { MatDialog } from "@angular/material/dialog";
import { TranslateService } from "@ngx-translate/core";
import { WowClientType } from "../../../../common/warcraft/wow-client-type";
import { WowInstallation } from "../../../models/wowup/wow-installation";
import { WowUpReleaseChannelType } from "../../../models/wowup/wowup-release-channel-type";
import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service";
import { WarcraftService } from "../../../services/warcraft/warcraft.service";
import { WowUpService } from "../../../services/wowup/wowup.service";
import { getEnumList, getEnumName } from "../../../utils/enum.utils";
import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
@Component({
selector: "app-options-wow-section",

View File

@@ -9,12 +9,12 @@ import { TranslateService } from "@ngx-translate/core";
import { WowClientType } from "../../../../common/warcraft/wow-client-type";
import { AddonChannelType } from "../../../../common/wowup/models";
import { WowInstallation } from "../../../models/wowup/wow-installation";
import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service";
import { getEnumList, getEnumName } from "../../../utils/enum.utils";
import { ConfirmDialogComponent } from "../../common/confirm-dialog/confirm-dialog.component";
import { WarcraftService } from "../../../services/warcraft/warcraft.service";
import { SessionService } from "../../../services/session/session.service";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
@Component({
selector: "app-wow-client-options",

View File

@@ -9,9 +9,9 @@ import { WarcraftService } from "../../../services/warcraft/warcraft.service";
import { WtfNode, WtfService } from "../../../services/wtf/wtf.service";
import { removeExtension } from "../../../utils/string.utils";
import { AddonFolder } from "../../../models/wowup/addon-folder";
import { WowInstallation } from "../../../models/wowup/wow-installation";
import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service";
import { formatSize } from "../../../utils/number.utils";
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
interface SavedVariable {
name: string;

View File

@@ -21,6 +21,7 @@ import { InstallFromUrlDialogComponent } from "../components/addons/install-from
import { DirectiveModule } from "./directive.module";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { AddonManageDialogComponent } from "../components/addons/addon-manage-dialog/addon-manage-dialog.component";
import { WtfBackupComponent } from "../components/addons/wtf-backup/wtf-backup.component";
@NgModule({
declarations: [
@@ -38,6 +39,7 @@ import { AddonManageDialogComponent } from "../components/addons/addon-manage-di
MyAddonsAddonCellComponent,
InstallFromUrlDialogComponent,
AddonManageDialogComponent,
WtfBackupComponent,
],
imports: [
CommonModule,
@@ -65,6 +67,7 @@ import { AddonManageDialogComponent } from "../components/addons/addon-manage-di
MyAddonsAddonCellComponent,
InstallFromUrlDialogComponent,
AddonManageDialogComponent,
WtfBackupComponent,
],
providers: [
{

View File

@@ -1,3 +1,4 @@
import { DatePipe } from "@angular/common";
import { NgModule } from "@angular/core";
import { DownloadCountPipe } from "../pipes/download-count.pipe";
import { GetAddonListItemFilePropPipe } from "../pipes/get-addon-list-item-file-prop.pipe";
@@ -29,6 +30,6 @@ import { TrustHtmlPipe } from "../pipes/trust-html.pipe";
InterfaceFormatPipe,
InvertBoolPipe,
],
providers: [RelativeDurationPipe, GetAddonListItemFilePropPipe, DownloadCountPipe],
providers: [RelativeDurationPipe, GetAddonListItemFilePropPipe, DownloadCountPipe, DatePipe],
})
export class PipesModule {}

View File

@@ -33,7 +33,6 @@ import { TableContextHeaderCellComponent } from "../../components/addons/table-c
import { GenericProviderError } from "../../errors";
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
import { ColumnState } from "../../models/wowup/column-state";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { DownloadCountPipe } from "../../pipes/download-count.pipe";
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
import { ElectronService } from "../../services";
@@ -46,6 +45,7 @@ import { WarcraftService } from "../../services/warcraft/warcraft.service";
import { WowUpService } from "../../services/wowup/wowup.service";
import { getEnumKeys } from "../../utils/enum.utils";
import { camelToSnakeCase } from "../../utils/string.utils";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
interface CategoryItem {
category: AddonCategory;

View File

@@ -22,7 +22,6 @@ import { PatchNotesDialogComponent } from "../../components/common/patch-notes-d
import { AddonScanError } from "../../errors";
import { AddonInstallState } from "../../models/wowup/addon-install-state";
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { ElectronService } from "../../services";
import { AddonService, ScanUpdate, ScanUpdateType } from "../../services/addons/addon.service";
import { DialogFactory } from "../../services/dialog/dialog.factory";
@@ -31,6 +30,7 @@ import { SnackbarService } from "../../services/snackbar/snackbar.service";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { WowUpService } from "../../services/wowup/wowup.service";
import { getProtocol } from "../../utils/string.utils";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
@Component({
selector: "app-home",

View File

@@ -297,6 +297,9 @@
<button mat-menu-item (click)="onClickImportExport()">
<span>Import/Export</span>
</button>
<button mat-menu-item (click)="onClickCreateBackup()">
<span>Create Backup</span>
</button>
</mat-menu>
<!-- MULTI ADDON CONTEXT MENU -->

View File

@@ -44,7 +44,7 @@ import { TableContextHeaderCellComponent } from "../../components/addons/table-c
import { AddonInstallState } from "../../models/wowup/addon-install-state";
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
import { ColumnState } from "../../models/wowup/column-state";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
import { ElectronService } from "../../services";
import { AddonService } from "../../services/addons/addon.service";
@@ -61,6 +61,8 @@ import { SortOrder } from "../../models/wowup/sort-order";
import { PushService } from "../../services/push/push.service";
import { AddonUiService } from "../../services/addons/addon-ui.service";
import { AddonManageDialogComponent } from "../../components/addons/addon-manage-dialog/addon-manage-dialog.component";
import { WtfService } from "../../services/wtf/wtf.service";
import { WtfBackupComponent } from "../../components/addons/wtf-backup/wtf-backup.component";
@Component({
selector: "app-my-addons",
@@ -241,6 +243,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
private _snackbarService: SnackbarService,
private _pushService: PushService,
private _addonUiService: AddonUiService,
private _wtfService: WtfService,
public addonService: AddonService,
public electronService: ElectronService,
public overlay: Overlay,
@@ -670,6 +673,15 @@ export class MyAddonsComponent implements OnInit, OnDestroy, AfterViewInit {
return data.addon.id;
};
public onClickCreateBackup(): void {
const dialogRef = this._dialogFactory.getDialog(WtfBackupComponent, {
disableClose: true,
data: {},
});
dialogRef.afterClosed().pipe(first()).subscribe();
}
public onClickImportExport(): void {
const dialogRef = this._dialogFactory.getDialog(AddonManageDialogComponent, {
disableClose: true,

View File

@@ -1,3 +1,4 @@
import { DatePipe } from "@angular/common";
import { Pipe, PipeTransform } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
@@ -5,9 +6,9 @@ import { TranslateService } from "@ngx-translate/core";
name: "localeDate",
})
export class NgxDatePipe implements PipeTransform {
public constructor(private translateService: TranslateService) {}
public constructor(private translateService: TranslateService, private datePipe: DatePipe) {}
public transform(value: string | number): any {
return new Date(value).toLocaleString(this.translateService.currentLang);
public transform(value: string | number, pattern: string = "short"): any {
return this.datePipe.transform(value, pattern, undefined, this.translateService.currentLang);
}
}

View File

@@ -5,7 +5,7 @@ import { nanoid } from "nanoid";
import { Addon } from "../../../common/entities/addon";
import { WowClientType } from "../../../common/warcraft/wow-client-type";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { getEnumName } from "../../utils/enum.utils";
import { AddonStorageService } from "../storage/addon-storage.service";
import { WarcraftService } from "../warcraft/warcraft.service";
@@ -129,28 +129,53 @@ export class AddonBrokerService {
}
public async installImportSummary(importSummary: ImportSummary, installation: WowInstallation): Promise<void> {
for (const comp of importSummary.comparisons) {
if (comp.state !== "added") {
continue;
}
const comps = importSummary.comparisons.filter((comp) => comp.state === "added");
try {
await this._addonService.installBaseAddon(
comp.imported.id,
comp.imported.provider_name,
installation,
(installState, progress) => {
this._addonInstallSrc.next({
comparisonId: comp.id,
installState,
progress,
});
}
);
} catch (e) {
console.error(`Failed to install imported addon`, e);
}
}
const tasks = comps.map((comp) => {
return (async (c) => {
try {
await this._addonService.installBaseAddon(
c.imported.id,
c.imported.provider_name,
installation,
(installState, progress) => {
this._addonInstallSrc.next({
comparisonId: c.id,
installState,
progress,
});
}
);
} catch (e) {
console.error(`Failed to install imported addon`, e);
}
})(comp);
});
await Promise.all(tasks);
// for (const comp of importSummary.comparisons) {
// if (comp.state !== "added") {
// continue;
// }
// try {
// await this._addonService.installBaseAddon(
// comp.imported.id,
// comp.imported.provider_name,
// installation,
// (installState, progress) => {
// this._addonInstallSrc.next({
// comparisonId: comp.id,
// installState,
// progress,
// });
// }
// );
// } catch (e) {
// console.error(`Failed to install imported addon`, e);
// }
// }
}
public getImportSummary(exportPayload: ExportPayload, installation: WowInstallation): ImportSummary {

View File

@@ -46,7 +46,7 @@ import { AddonSearchResultFile } from "../../models/wowup/addon-search-result-fi
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
import { ProtocolSearchResult } from "../../models/wowup/protocol-search-result";
import { Toc } from "../../models/wowup/toc";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import * as AddonUtils from "../../utils/addon.utils";
import { getEnumName } from "../../utils/enum.utils";
import * as SearchResults from "../../utils/search-result.utils";

View File

@@ -25,6 +25,7 @@ import { CopyFileRequest } from "../../../common/models/copy-file-request";
import { UnzipRequest } from "../../../common/models/unzip-request";
import { FsDirent, FsStats, TreeNode } from "../../../common/models/ipc-events";
import { ElectronService } from "../electron/electron.service";
import { GetDirectoryTreeOptions, GetDirectoryTreeRequest } from "../../../common/models/ipc-request";
@Injectable({
providedIn: "root",
@@ -127,8 +128,12 @@ export class FileService {
return await this._electronService.invoke(IPC_LIST_DIR_RECURSIVE, dirPath);
}
public async getDirectoryTree(dirPath: string): Promise<TreeNode> {
return await this._electronService.invoke(IPC_GET_DIRECTORY_TREE, dirPath);
public async getDirectoryTree(dirPath: string, opts?: GetDirectoryTreeOptions): Promise<TreeNode> {
const request: GetDirectoryTreeRequest = {
dirPath,
opts,
};
return await this._electronService.invoke(IPC_GET_DIRECTORY_TREE, request);
}
public async writeFile(sourcePath: string, contents: string): Promise<string> {
@@ -155,6 +160,10 @@ export class FileService {
return this._electronService.invoke<string[]>(IPC_LIST_FILES_CHANNEL, sourcePath, filter);
}
public readFileInZip(zipPath: string, filePath: string) {
return this._electronService.invoke<any>("zip-read-file", zipPath, filePath);
}
public async unzipFile(zipFilePath: string, outputFolder: string): Promise<string> {
console.log("unzipFile", zipFilePath);
@@ -166,4 +175,8 @@ export class FileService {
return await this._electronService.invoke(IPC_UNZIP_FILE_CHANNEL, request);
}
public async zipFile(srcPath: string, destPath: string) {
await this._electronService.invoke("zip-file", srcPath, destPath);
}
}

View File

@@ -14,6 +14,7 @@ import {
faInfoCircle,
faCodeBranch,
faCaretDown,
faExclamation,
faExclamationTriangle,
faCode,
faCoins,
@@ -31,6 +32,8 @@ import {
faUserCircle,
faEllipsisV,
faCopy,
faTrash,
faHistory,
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faClock, faCheckCircle as farCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { faDiscord, faGithub, faPatreon } from "@fortawesome/free-brands-svg-icons";
@@ -75,6 +78,9 @@ export class IconService {
this.addSvg(faEllipsisV);
this.addSvg(faCopy);
this.addSvg(farCheckCircle);
this.addSvg(faExclamation);
this.addSvg(faTrash);
this.addSvg(faHistory);
}
private addSvg(icon: IconDefinition): void {

View File

@@ -4,7 +4,7 @@ import { BehaviorSubject, Subject } from "rxjs";
import { Injectable } from "@angular/core";
import { SELECTED_DETAILS_TAB_KEY, TAB_INDEX_SETTINGS } from "../../../common/constants";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { PreferenceStorageService } from "../storage/preference-storage.service";
import { WarcraftInstallationService } from "../warcraft/warcraft-installation.service";
import { ColumnState } from "../../models/wowup/column-state";

View File

@@ -14,7 +14,7 @@ import {
} from "../../../common/constants";
import { WowClientType } from "../../../common/warcraft/wow-client-type";
import { AddonChannelType } from "../../../common/wowup/models";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { getEnumName } from "../../utils/enum.utils";
import { ElectronService } from "../electron/electron.service";
import { FileService } from "../files/file.service";

View File

@@ -11,7 +11,7 @@ import { InstalledProduct } from "../../models/warcraft/installed-product";
import { ProductDb } from "../../models/warcraft/product-db";
import { AddonFolder } from "../../models/wowup/addon-folder";
import { SelectItem } from "../../models/wowup/select-item";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { getEnumList, getEnumName } from "../../utils/enum.utils";
import { FileService } from "../files/file.service";
import { PreferenceStorageService } from "../storage/preference-storage.service";

View File

@@ -25,11 +25,11 @@ const CHANGELOGS: ChangeLog[] = [
<li>Added the ability to remove addons from the details dialog on Get Addons tab (tellier-dev)</li>
<li>
<div>The first stage of a set of cloud based features you've been asking for</div>
<img style="width: 80%;" loading="lazy" src="/assets/images/patch/account-beta.png">
<img style="width: 70%; border: 1px solid #666666; border-radius: 4px;" loading="lazy" src="assets/images/patch/account-beta.png">
</li>
<li>
<div>Added the ability to import/export a list of addons for a WoW client. The first step in backing up your list of addons or sharing them with your friends!</div>
<img style="width: 80%;" loading="lazy" src="/assets/images/patch/import-export-preview.png">
<img style="width: 70%; border: 1px solid #666666; border-radius: 4px;" loading="lazy" src="assets/images/patch/import-export-preview.png">
</li>
<li>Added WowUpHub category support</li>
<li>Added WowUpHub preview support</li>
@@ -79,7 +79,7 @@ const CHANGELOGS: ChangeLog[] = [
<div>
<h4 style="margin-top: 1em;">Fixes</h4>
<ul>
<li>Update Electron to 13.5.1 to fix the <a appExternalLink href="https://github.com/electron/electron/issues/31212">TukUI download issue</a></li>
<li>Update Electron to 13.5.1 to fix the <a appExternalLink style="" href="https://github.com/electron/electron/issues/31212">TukUI download issue</a></li>
</ul>
</div>`,
},

View File

@@ -7,6 +7,7 @@ import {
ACCT_FEATURE_KEYS,
ACCT_PUSH_ENABLED_KEY,
APP_PROTOCOL_NAME,
FEATURE_ACCOUNTS_ENABLED,
IPC_PUSH_INIT,
IPC_PUSH_REGISTER,
IPC_PUSH_SUBSCRIBE,
@@ -52,6 +53,10 @@ export class WowUpAccountService {
private _preferenceStorageService: PreferenceStorageService,
private _wowUpApiService: WowUpApiService
) {
if (!FEATURE_ACCOUNTS_ENABLED) {
return;
}
this.wowUpAuthTokenSrc
.pipe(
filter((token) => !!token && token.length > 10),

View File

@@ -11,7 +11,7 @@ import {
import { Addon } from "../../../common/entities/addon";
import { AddonChannelType } from "../../../common/wowup/models";
import { AddonInstallState } from "../../models/wowup/addon-install-state";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { toInterfaceVersion } from "../../utils/addon.utils";
import { AddonProviderFactory } from "../addons/addon.provider.factory";
import { AddonService } from "../addons/addon.service";

View File

@@ -61,6 +61,7 @@ export class WowUpService {
public readonly applicationLogsFolderPath: string = window.logPath;
public readonly applicationDownloadsFolderPath: string = join(this.applicationFolderPath, "downloads");
public readonly applicationUpdaterPath: string = join(this.applicationFolderPath, this.updaterName);
public readonly wtfBackupFolder: string = join(this.applicationFolderPath, "wtf_backups");
public readonly preferenceChange$ = this._preferenceChangeSrc.asObservable();

View File

@@ -1,14 +1,19 @@
import { Injectable } from "@angular/core";
import * as path from "path";
import { ElectronService } from "..";
import { Addon } from "../../../common/entities/addon";
import { FsStats, TreeNode } from "../../../common/models/ipc-events";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { BackupGetExistingRequest } from "../../../common/models/ipc-request";
import { BackupGetExistingResponse } from "../../../common/models/ipc-response";
import { WowInstallation } from "../../../common/warcraft/wow-installation";
import { FileService } from "../files/file.service";
import { WowUpService } from "../wowup/wowup.service";
const WTF_FOLDER = "WTF";
const ACCOUNT_FOLDER = "Account";
const SAVED_VARIABLES_FOLDER = "SavedVariables";
const BACKUP_META_FILENAME = "wowup-meta.json";
export interface FileStats {
name: string;
@@ -27,15 +32,42 @@ export interface WtfNode extends TreeNode {
children: WtfNode[];
}
export interface WtfBackupMetadataFile {
createdBy: string;
createdAt: number;
contents: WtfBackupMeta[];
}
export interface WtfBackupMeta {
path: string;
isDirectory: boolean;
size: number;
hash: string;
}
export interface WtfBackup {
location: string;
fileName: string;
size: number;
birthtimeMs: number;
error?: string;
metadata?: WtfBackupMetadataFile;
}
@Injectable({
providedIn: "root",
})
export class WtfService {
public constructor(private _fileService: FileService) {}
public constructor(
private _fileService: FileService,
private _electronService: ElectronService,
private _wowUpService: WowUpService
) {}
public async getWtfContents(installation: WowInstallation): Promise<WtfNode> {
// including the hash will make this operation much slower
public async getWtfContents(installation: WowInstallation, includeHash = false): Promise<WtfNode> {
const wtfPath = this.getWtfPath(installation);
const tree = await this._fileService.getDirectoryTree(wtfPath);
const tree = await this._fileService.getDirectoryTree(wtfPath, { includeHash });
return this.getWtfNode(tree);
}
@@ -136,4 +168,139 @@ export class WtfService {
return fsStats;
}
public async getBackupList(installation: WowInstallation): Promise<WtfBackup[]> {
const wtfBackups: WtfBackup[] = [];
const backupZipFiles = await this.listBackupFiles(installation);
const fsStats = await this._fileService.statFiles(backupZipFiles);
for (let i = 0; i < backupZipFiles.length; i++) {
const zipFile = backupZipFiles[i];
const stat = fsStats[zipFile];
const wtfBackup: WtfBackup = {
location: zipFile,
fileName: path.basename(zipFile),
size: stat.size,
birthtimeMs: stat.birthtimeMs,
};
try {
const zipMetaTxt = await this._fileService.readFileInZip(zipFile, BACKUP_META_FILENAME);
const zipMetaData: WtfBackupMetadataFile = JSON.parse(zipMetaTxt);
if (!Array.isArray(zipMetaData.contents)) {
wtfBackup.error = "INVALID_CONTENTS";
} else if (typeof zipMetaData.createdAt !== "number") {
wtfBackup.error = "INVALID_CREATED_AT";
} else if (typeof zipMetaData.createdBy !== "string") {
wtfBackup.error = "INVALID_CREATED_BY";
}
} catch (e) {
console.error("Failed to process backup metadata", zipFile, e);
wtfBackup.error = "GENERIC_ERROR";
} finally {
wtfBackups.push(wtfBackup);
}
}
return wtfBackups;
}
public async createBackup(installation: WowInstallation): Promise<void> {
await this.createBackupDirectory(installation);
const metadataFilePath = await this.createBackupMetadataFile(installation);
try {
await this.createBackupZip(installation);
} finally {
// always delete the metadata file
await this._fileService.remove(metadataFilePath);
}
}
private async createBackupZip(installation: WowInstallation): Promise<void> {
const wtfPath = this.getWtfPath(installation);
const zipPath = path.join(this.getBackupPath(installation), `wtf_${Date.now()}.zip`);
this._fileService.zipFile(wtfPath, zipPath);
}
private async createBackupDirectory(installation: WowInstallation): Promise<void> {
const backupPath = this.getBackupPath(installation);
await this._fileService.createDirectory(backupPath);
}
private async createBackupMetadataFile(installation: WowInstallation): Promise<string> {
const wtfTree = await this.getWtfContents(installation, true);
const wtfList = this.flattenTree([wtfTree]);
console.debug(wtfList);
const backupMetadata: WtfBackupMetadataFile = {
contents: this.toBackupMeta(wtfList, installation),
createdAt: Date.now(),
createdBy: "manual",
};
return await this.writeWtfMetadataFile(backupMetadata, installation);
}
private async writeWtfMetadataFile(
backupMetadata: WtfBackupMetadataFile,
installation: WowInstallation
): Promise<string> {
const wtfPath = this.getWtfPath(installation);
const metaPath = path.join(wtfPath, BACKUP_META_FILENAME);
await this._fileService.writeFile(metaPath, JSON.stringify(backupMetadata, null, 2));
return metaPath;
}
private async getBackup(installation: WowInstallation): Promise<BackupGetExistingResponse> {
const args: BackupGetExistingRequest = {
backupPath: this._wowUpService.wtfBackupFolder,
installation,
};
return await this._electronService.invoke("backup-get-existing", args);
}
private async listBackupFiles(installation: WowInstallation) {
const backupPath = this.getBackupPath(installation);
const zipFiles = await this._fileService.listFiles(backupPath, "*.zip");
return zipFiles.map((f) => path.join(backupPath, f));
}
private async createFullBackup(installation: WowInstallation): Promise<void> {}
private async backupNode(node: WtfNode, installation: WowInstallation) {
const wtfPath = this.getWtfPath(installation);
const nodeBase = node.path.replace(wtfPath, "");
}
public getBackupPath(installation: WowInstallation): string {
return path.join(this._wowUpService.wtfBackupFolder, installation.id);
}
private flattenTree(nodes: WtfNode[]): WtfNode[] {
return Array.prototype.concat.apply(
nodes,
nodes.map((n) => this.flattenTree(n.children))
);
}
private toBackupMeta(nodes: WtfNode[], installation: WowInstallation): WtfBackupMeta[] {
const wtfPath = this.getWtfPath(installation);
return nodes.map((n) => {
const nodeBase = n.path.replace(wtfPath, "");
return {
hash: n.hash,
isDirectory: n.isDirectory,
path: nodeBase,
size: n.size,
};
});
}
}

View File

@@ -29,6 +29,22 @@
"RESET_BUTTON": "Reset",
"VERSION_MISMATCH": "This addon is already installed, but the versions do not match"
},
"WTF_BACKUP": {
"DIALOG_TITLE": "WTF Settings Backup: {clientType}",
"CREATE_BACKUP_BUTTON": "Create Backup",
"SHOW_FOLDER_BUTTON": "Show Folder",
"BACKUP_COUNT_TEXT": "Found {count} {count, plural, =1{backup} other{backups}}",
"TOOL_TIP": {
"APPLY_BUTTON": "Apply this backup",
"DELETE_BUTTON": "Delete this backup"
},
"ERROR": {
"INVALID_CONTENTS": "There was an issue processing this backup",
"INVALID_CREATED_AT": "There was an issue processing this backup",
"INVALID_CREATED_BY": "There was an issue processing this backup",
"GENERIC_ERROR": "There was an issue processing this backup"
}
},
"APP": {
"APP_MENU": {
"EDIT": {

View File

@@ -1,5 +1,8 @@
export const APP_USER_MODEL_ID = "io.wowup.jliddev"; // Bundle ID
// FEATURES
export const FEATURE_ACCOUNTS_ENABLED = false;
export const ADDON_PROVIDER_WOWINTERFACE = "WowInterface";
export const ADDON_PROVIDER_CURSEFORGE = "Curse";
export const ADDON_PROVIDER_GITHUB = "GitHub";

View File

@@ -44,4 +44,5 @@ export interface TreeNode {
isDirectory: boolean;
children: TreeNode[];
size: number;
hash?: string;
}

View File

@@ -1,3 +1,24 @@
import { WowInstallation } from "../warcraft/wow-installation";
export interface IpcRequest {
responseKey: string;
}
export interface BackupGetExistingRequest {
backupPath: string;
installation: WowInstallation;
}
export interface BackupCreateRequest {
backupPath: string;
installation: WowInstallation;
}
export interface GetDirectoryTreeOptions {
includeHash?: boolean;
}
export interface GetDirectoryTreeRequest {
dirPath: string;
opts?: GetDirectoryTreeOptions;
}

View File

@@ -1,3 +1,11 @@
export interface IpcResponse {
error?: Error;
}
export interface BackupGetExistingResponse {
exists: boolean;
}
export interface BackupCreateResponse {
error?: string;
}

View File

@@ -1,5 +1,5 @@
import { WowClientType } from "../../../common/warcraft/wow-client-type";
import { AddonChannelType } from "../../../common/wowup/models";
import { WowClientType } from "./wow-client-type";
import { AddonChannelType } from "../wowup/models";
export interface WowInstallation {
id: string;

View File

@@ -38,6 +38,8 @@ declare type RendererChannels =
| "curse-get-scan-results"
| "wowup-get-scan-results"
| "unzip-file"
| "zip-file"
| "zip-read-file"
| "copy-file"
| "delete-directory"
| "list-disks-win32"
@@ -79,7 +81,9 @@ declare type RendererChannels =
| "push-init"
| "push-register"
| "push-unregister"
| "push-subscribe";
| "push-subscribe"
| "backup-get-existing"
| "backup-create";
declare global {
interface Window {

View File

@@ -535,6 +535,11 @@ body {
}
}
.changelog {
a {
color: var(--control-color);
}
}
.tab-icon-inactive {
svg {
transition: fill 0.3s ease 0.1s;

View File

@@ -162,6 +162,9 @@ img {
margin-top: 1em !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.mb-1 {
margin-bottom: 0.25em !important;
}