mirror of
https://github.com/WowUp/WowUp.git
synced 2026-04-22 15:00:38 -04:00
4
.github/workflows/electron-mac-build.yml
vendored
4
.github/workflows/electron-mac-build.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: ["macos-latest"]
|
||||
os: ["macos-11"]
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: npm install -g @angular/cli
|
||||
|
||||
- name: Build Mac App
|
||||
if: matrix.os == 'macos-latest'
|
||||
if: matrix.os == 'macos-11'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
CSC_LINK: ${{ secrets.MACOS_CERT }}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": ["src/assets"],
|
||||
"styles": ["./node_modules/ngx-lightbox/lightbox.css", "src/styles.scss"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as path from "path";
|
||||
import * as fs from "fs-extra";
|
||||
import * as _ from "lodash";
|
||||
import * as log from "electron-log";
|
||||
import * as pLimit from "p-limit";
|
||||
import { CurseFolderScanResult } from "../src/common/curse/curse-folder-scan-result";
|
||||
import { readDirRecursive } from "./file.utils";
|
||||
import { exists, readDirRecursive } from "./file.utils";
|
||||
import * as fsp from "fs/promises";
|
||||
|
||||
const nativeAddon = require("../build/Release/addon.node");
|
||||
|
||||
@@ -56,7 +56,7 @@ export class CurseFolderScanner {
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^/]+)[\\/]\1(-mainline|-bcc|-classic)?\.toc$/i;
|
||||
return /^([^/]+)[\\/]\1([-|_](mainline|bcc|tbc|classic|vanilla))?\.toc$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
@@ -139,13 +139,14 @@ export class CurseFolderScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(nativePath) || matchingFileList.indexOf(nativePath) !== -1) {
|
||||
const pathExists = await exists(nativePath);
|
||||
if (!pathExists || matchingFileList.indexOf(nativePath) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchingFileList.push(nativePath);
|
||||
|
||||
let input = await fs.readFile(nativePath, { encoding: "utf-8" });
|
||||
let input = await fsp.readFile(nativePath, { encoding: "utf-8" });
|
||||
input = this.removeComments(nativePath, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(nativePath, input);
|
||||
@@ -242,24 +243,10 @@ export class CurseFolderScanner {
|
||||
}
|
||||
}
|
||||
|
||||
private getFileHash(filePath: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
fs.readFile(filePath, (err, buffer) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const hash = nativeAddon.computeHash(buffer, buffer.length);
|
||||
|
||||
return resolve(hash);
|
||||
});
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
log.info(filePath);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
private async getFileHash(filePath: string): Promise<number> {
|
||||
const buffer = await fsp.readFile(filePath);
|
||||
const hash = nativeAddon.computeHash(buffer, buffer.length);
|
||||
return hash;
|
||||
}
|
||||
|
||||
private getRealPath(filePath: string) {
|
||||
|
||||
@@ -1,39 +1,74 @@
|
||||
import { exec } from "child_process";
|
||||
import * as log from "electron-log";
|
||||
import * as fs from "fs-extra";
|
||||
import * as fsp from "fs/promises";
|
||||
import { max } from "lodash";
|
||||
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 { GetDirectoryTreeOptions } from "../src/common/models/ipc-request";
|
||||
import { isWin } from "./platform";
|
||||
|
||||
export async function readDirRecursive(sourcePath: string): Promise<string[]> {
|
||||
const dirFiles: string[] = [];
|
||||
const files = await fs.readdir(sourcePath, { withFileTypes: true });
|
||||
export function zipFile(srcPath: string, outPath: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const zip = new AdmZip();
|
||||
zip.addLocalFolder(srcPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(sourcePath, file.name);
|
||||
if (file.isDirectory()) {
|
||||
const nestedFiles = await readDirRecursive(filePath);
|
||||
dirFiles.push(...nestedFiles);
|
||||
} else {
|
||||
dirFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return dirFiles;
|
||||
zip.writeZip(outPath, (e) => {
|
||||
return e ? reject(e) : resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLastModifiedFileDate(sourcePath: string): Promise<number> {
|
||||
const dirFiles = await readDirRecursive(sourcePath);
|
||||
const dates: number[] = [];
|
||||
for (const file of dirFiles) {
|
||||
const stat = await fs.stat(file);
|
||||
dates.push(stat.mtimeMs);
|
||||
}
|
||||
export function readFileInZip(zipPath: string, filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.readAsTextAsync(filePath, (data, err) => {
|
||||
return err ? reject(err) : resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const latest = max(dates);
|
||||
return latest;
|
||||
export async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.access(path);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn(`File does not exist: ${path}`);
|
||||
log.warn(e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function chmodDir(dirPath: string, mode: number | string): Promise<void> {
|
||||
const entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(dirPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await chmodDir(srcPath, mode);
|
||||
} else {
|
||||
await fsp.chmod(srcPath, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyDir(src: string, dest: string): Promise<void> {
|
||||
await fsp.mkdir(dest, { recursive: true });
|
||||
const entries = await fsp.readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDir(srcPath, destPath);
|
||||
} else {
|
||||
await fsp.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(path: string): Promise<void> {
|
||||
@@ -65,3 +100,89 @@ async function rmdir(path: string): Promise<void> {
|
||||
await fsp.rm(path, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function readDirRecursive(sourcePath: string): Promise<string[]> {
|
||||
const dirFiles: string[] = [];
|
||||
const files = await fsp.readdir(sourcePath, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(sourcePath, file.name);
|
||||
if (file.isDirectory()) {
|
||||
const nestedFiles = await readDirRecursive(filePath);
|
||||
dirFiles.push(...nestedFiles);
|
||||
} else {
|
||||
dirFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return dirFiles;
|
||||
}
|
||||
|
||||
export async function getDirTree(sourcePath: string, opts?: GetDirectoryTreeOptions): Promise<TreeNode> {
|
||||
const files = await fsp.readdir(sourcePath, { withFileTypes: true });
|
||||
|
||||
const node: TreeNode = {
|
||||
name: path.basename(sourcePath),
|
||||
path: sourcePath,
|
||||
children: [],
|
||||
isDirectory: true,
|
||||
size: 0,
|
||||
};
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(sourcePath, file.name);
|
||||
if (file.isDirectory()) {
|
||||
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({
|
||||
name: file.name,
|
||||
path: filePath,
|
||||
children: [],
|
||||
isDirectory: false,
|
||||
size: stats.size,
|
||||
hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (opts?.includeHash) {
|
||||
node.hash = hashString(node.children.map((n) => n.hash).join(""), "sha256");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
export async function getLastModifiedFileDate(sourcePath: string): Promise<number> {
|
||||
const dirFiles = await readDirRecursive(sourcePath);
|
||||
const dates: number[] = [];
|
||||
for (const file of dirFiles) {
|
||||
const stat = await fsp.stat(file);
|
||||
dates.push(stat.mtimeMs);
|
||||
}
|
||||
|
||||
const latest = max(dates);
|
||||
return latest;
|
||||
}
|
||||
|
||||
export function hashString(str: string | crypto.BinaryLike, alg = "md5"): string {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import axios from "axios";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
dialog,
|
||||
ipcMain,
|
||||
IpcMainInvokeEvent,
|
||||
net,
|
||||
OpenDialogOptions,
|
||||
Settings,
|
||||
shell,
|
||||
systemPreferences,
|
||||
} from "electron";
|
||||
import * as log from "electron-log";
|
||||
import * as fs from "fs-extra";
|
||||
import * as globrex from "globrex";
|
||||
import * as _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -20,6 +20,7 @@ import * as pLimit from "p-limit";
|
||||
import * as path from "path";
|
||||
import { Transform } from "stream";
|
||||
import * as yauzl from "yauzl";
|
||||
import * as fs from "fs";
|
||||
|
||||
import {
|
||||
IPC_ADDONS_SAVE_ALL,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
IPC_FOCUS_WINDOW,
|
||||
IPC_GET_APP_VERSION,
|
||||
IPC_GET_ASSET_FILE_PATH,
|
||||
IPC_GET_DIRECTORY_TREE,
|
||||
IPC_GET_LATEST_DIR_UPDATE_TIME,
|
||||
IPC_GET_LAUNCH_ARGS,
|
||||
IPC_GET_LOCALE,
|
||||
@@ -41,6 +43,7 @@ import {
|
||||
IPC_GET_PENDING_OPEN_URLS,
|
||||
IPC_GET_ZOOM_FACTOR,
|
||||
IPC_IS_DEFAULT_PROTOCOL_CLIENT,
|
||||
IPC_LIST_DIR_RECURSIVE,
|
||||
IPC_LIST_DIRECTORIES_CHANNEL,
|
||||
IPC_LIST_DISKS_WIN32,
|
||||
IPC_LIST_ENTRIES,
|
||||
@@ -67,6 +70,11 @@ import {
|
||||
IPC_WINDOW_LEAVE_FULLSCREEN,
|
||||
IPC_WOWUP_GET_SCAN_RESULTS,
|
||||
IPC_WRITE_FILE_CHANNEL,
|
||||
DEFAULT_FILE_MODE,
|
||||
IPC_PUSH_INIT,
|
||||
IPC_PUSH_REGISTER,
|
||||
IPC_PUSH_UNREGISTER,
|
||||
IPC_PUSH_SUBSCRIBE,
|
||||
} from "../src/common/constants";
|
||||
import { CurseFolderScanResult } from "../src/common/curse/curse-folder-scan-result";
|
||||
import { Addon } from "../src/common/entities/addon";
|
||||
@@ -74,18 +82,31 @@ import { CopyFileRequest } from "../src/common/models/copy-file-request";
|
||||
import { DownloadRequest } from "../src/common/models/download-request";
|
||||
import { DownloadStatus } from "../src/common/models/download-status";
|
||||
import { DownloadStatusType } from "../src/common/models/download-status-type";
|
||||
import { FsDirent, FsStats } from "../src/common/models/ipc-events";
|
||||
import { FsDirent, FsStats, TreeNode } from "../src/common/models/ipc-events";
|
||||
import { UnzipRequest } from "../src/common/models/unzip-request";
|
||||
import { RendererChannels } from "../src/common/wowup";
|
||||
import { MenuConfig, SystemTrayConfig, WowUpScanResult } from "../src/common/wowup/models";
|
||||
import { createAppMenu } from "./app-menu";
|
||||
import { CurseFolderScanner } from "./curse-folder-scanner";
|
||||
import { getLastModifiedFileDate, remove } from "./file.utils";
|
||||
import * as fsp from "fs/promises";
|
||||
|
||||
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 { GetDirectoryTreeRequest } from "../src/common/models/ipc-request";
|
||||
|
||||
let USER_AGENT = "";
|
||||
let PENDING_OPEN_URLS: string[] = [];
|
||||
|
||||
interface SymlinkDir {
|
||||
@@ -95,6 +116,8 @@ interface SymlinkDir {
|
||||
isDir: boolean;
|
||||
}
|
||||
|
||||
const _dlMap = new Map<string, (evt: Electron.Event, item: Electron.DownloadItem, ec: Electron.WebContents) => void>();
|
||||
|
||||
async function getSymlinkDirs(basePath: string, files: fs.Dirent[]): Promise<SymlinkDir[]> {
|
||||
// Find and resolve symlinks found and return the folder names as
|
||||
const symlinks = _.filter(files, (file) => file.isSymbolicLink());
|
||||
@@ -108,8 +131,8 @@ async function getSymlinkDirs(basePath: string, files: fs.Dirent[]): Promise<Sym
|
||||
});
|
||||
|
||||
for (const symlinkDir of symlinkDirs) {
|
||||
const realPath = await fs.realpath(symlinkDir.originalPath);
|
||||
const lstat = await fs.lstat(realPath);
|
||||
const realPath = await fsp.realpath(symlinkDir.originalPath);
|
||||
const lstat = await fsp.lstat(realPath);
|
||||
|
||||
symlinkDir.realPath = realPath;
|
||||
symlinkDir.isDir = lstat.isDirectory();
|
||||
@@ -131,7 +154,7 @@ export function setPendingOpenUrl(...openUrls: string[]): void {
|
||||
}
|
||||
|
||||
export function initializeIpcHandlers(window: BrowserWindow, userAgent: string): void {
|
||||
USER_AGENT = userAgent;
|
||||
log.info("process.versions", process.versions);
|
||||
|
||||
// Remove the pending URLs once read so they are only able to be gotten once
|
||||
handle(IPC_GET_PENDING_OPEN_URLS, (): string[] => {
|
||||
@@ -151,6 +174,10 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
}
|
||||
);
|
||||
|
||||
handle("clipboard-read-text", (evt) => {
|
||||
return clipboard.readText();
|
||||
});
|
||||
|
||||
handle(IPC_SHOW_DIRECTORY, async (evt, filePath: string): Promise<string> => {
|
||||
return await shell.openPath(filePath);
|
||||
});
|
||||
@@ -161,7 +188,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
|
||||
handle(IPC_CREATE_DIRECTORY_CHANNEL, async (evt, directoryPath: string): Promise<boolean> => {
|
||||
log.info(`[CreateDirectory] '${directoryPath}'`);
|
||||
await fs.ensureDir(directoryPath);
|
||||
await fsp.mkdir(directoryPath, { recursive: true });
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -177,6 +204,10 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
return window.webContents?.setVisualZoomLevelLimits(minimumLevel, maximumLevel);
|
||||
});
|
||||
|
||||
handle("show-item-in-folder", (evt, path: string) => {
|
||||
shell.showItemInFolder(path);
|
||||
});
|
||||
|
||||
handle(IPC_SET_ZOOM_FACTOR, (evt, zoomFactor: number) => {
|
||||
if (window?.webContents) {
|
||||
window.webContents.zoomFactor = zoomFactor;
|
||||
@@ -208,7 +239,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
});
|
||||
|
||||
handle(IPC_READDIR, async (evt, dirPath: string): Promise<string[]> => {
|
||||
return await fs.readdir(dirPath);
|
||||
return await fsp.readdir(dirPath);
|
||||
});
|
||||
|
||||
handle(IPC_IS_DEFAULT_PROTOCOL_CLIENT, (evt, protocol: string) => {
|
||||
@@ -224,7 +255,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
});
|
||||
|
||||
handle(IPC_LIST_DIRECTORIES_CHANNEL, async (evt, filePath: string, scanSymlinks: boolean) => {
|
||||
const files = await fs.readdir(filePath, { withFileTypes: true });
|
||||
const files = await fsp.readdir(filePath, { withFileTypes: true });
|
||||
let symlinkNames: string[] = [];
|
||||
if (scanSymlinks === true) {
|
||||
log.info("Scanning symlinks");
|
||||
@@ -241,7 +272,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
const limit = pLimit(3);
|
||||
const tasks = _.map(filePaths, (path) =>
|
||||
limit(async () => {
|
||||
const stats = await fs.stat(path);
|
||||
const stats = await fsp.stat(path);
|
||||
const fsStats: FsStats = {
|
||||
atime: stats.atime,
|
||||
atimeMs: stats.atimeMs,
|
||||
@@ -281,7 +312,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
|
||||
handle(IPC_LIST_ENTRIES, async (evt, sourcePath: string, filter: string) => {
|
||||
const globFilter = globrex(filter);
|
||||
const results = await fs.readdir(sourcePath, { withFileTypes: true });
|
||||
const results = await fsp.readdir(sourcePath, { withFileTypes: true });
|
||||
const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
|
||||
return _.map(matches, (match) => {
|
||||
const dirEnt: FsDirent = {
|
||||
@@ -299,8 +330,13 @@ 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 fs.readdir(sourcePath, { withFileTypes: true });
|
||||
const results = await fsp.readdir(sourcePath, { withFileTypes: true });
|
||||
const matches = _.filter(results, (entry) => globFilter.regex.test(entry.name));
|
||||
return _.map(matches, (match) => match.name);
|
||||
});
|
||||
@@ -311,7 +347,7 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await fsp.access(filePath);
|
||||
} catch (e) {
|
||||
if (e.code !== "ENOENT") {
|
||||
log.error(e);
|
||||
@@ -347,13 +383,36 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
});
|
||||
});
|
||||
|
||||
await chmodDir(arg.outputFolder, DEFAULT_FILE_MODE);
|
||||
|
||||
return arg.outputFolder;
|
||||
});
|
||||
|
||||
handle("zip-file", async (evt, srcPath: string, destPath: string) => {
|
||||
log.info(`[ZipFile]: '${srcPath} -> ${destPath}`);
|
||||
return await zipFile(srcPath, destPath);
|
||||
});
|
||||
|
||||
handle("zip-read-file", async (evt, zipPath: string, filePath: string) => {
|
||||
log.info(`[ZipReadFile]: '${zipPath} : ${filePath}`);
|
||||
return await readFileInZip(zipPath, filePath);
|
||||
});
|
||||
|
||||
handle("rename-file", async (evt, srcPath: string, destPath: string) => {
|
||||
log.info(`[RenameFile]: '${srcPath} -> ${destPath}`);
|
||||
return await fsp.rename(srcPath, destPath);
|
||||
});
|
||||
|
||||
handle(IPC_COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest): Promise<boolean> => {
|
||||
log.info(`[FileCopy] '${arg.sourceFilePath}' -> '${arg.destinationFilePath}'`);
|
||||
await fs.copy(arg.sourceFilePath, arg.destinationFilePath);
|
||||
await fs.chmod(arg.destinationFilePath, arg.destinationFileChmod);
|
||||
const stat = await fsp.lstat(arg.sourceFilePath);
|
||||
if (stat.isDirectory()) {
|
||||
await copyDir(arg.sourceFilePath, arg.destinationFilePath);
|
||||
await chmodDir(arg.destinationFilePath, DEFAULT_FILE_MODE);
|
||||
} else {
|
||||
await fsp.copyFile(arg.sourceFilePath, arg.destinationFilePath);
|
||||
await fsp.chmod(arg.destinationFilePath, DEFAULT_FILE_MODE);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -363,15 +422,15 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
});
|
||||
|
||||
handle(IPC_READ_FILE_CHANNEL, async (evt, filePath: string) => {
|
||||
return await fs.readFile(filePath, { encoding: "utf-8" });
|
||||
return await fsp.readFile(filePath, { encoding: "utf-8" });
|
||||
});
|
||||
|
||||
handle(IPC_READ_FILE_BUFFER_CHANNEL, async (evt, filePath: string) => {
|
||||
return await fs.readFile(filePath);
|
||||
return await fsp.readFile(filePath);
|
||||
});
|
||||
|
||||
handle(IPC_WRITE_FILE_CHANNEL, async (evt, filePath: string, contents: string) => {
|
||||
return await fs.writeFile(filePath, contents, { encoding: "utf-8" });
|
||||
return await fsp.writeFile(filePath, contents, { encoding: "utf-8", mode: DEFAULT_FILE_MODE });
|
||||
});
|
||||
|
||||
handle(IPC_CREATE_TRAY_MENU_CHANNEL, (evt, config: SystemTrayConfig) => {
|
||||
@@ -386,6 +445,15 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
return getLastModifiedFileDate(dirPath);
|
||||
});
|
||||
|
||||
handle(IPC_LIST_DIR_RECURSIVE, (evt, dirPath: string): Promise<string[]> => {
|
||||
return readDirRecursive(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, () => {
|
||||
if (window?.minimizable) {
|
||||
window.minimize();
|
||||
@@ -441,55 +509,111 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
|
||||
return await dialog.showOpenDialog(options);
|
||||
});
|
||||
|
||||
handle(IPC_PUSH_INIT, () => {
|
||||
return push.startPushService();
|
||||
});
|
||||
|
||||
handle(IPC_PUSH_REGISTER, async (evt, appId: string) => {
|
||||
return await push.registerForPush(appId);
|
||||
});
|
||||
|
||||
handle(IPC_PUSH_UNREGISTER, async () => {
|
||||
return await push.unregisterPush();
|
||||
});
|
||||
|
||||
handle(IPC_PUSH_SUBSCRIBE, async (evt, channel) => {
|
||||
return await push.subscribeToChannel(channel);
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_DOWNLOAD_FILE_CHANNEL, (evt, arg: DownloadRequest) => {
|
||||
handleDownloadFile(arg).catch((e) => console.error(e));
|
||||
handleDownloadFile(arg).catch((e) => console.error(e.toString()));
|
||||
});
|
||||
|
||||
// In order to allow concurrent downloads, we have to get creative with this session handler
|
||||
window.webContents.session.on("will-download", (evt, item, wc) => {
|
||||
for (const key of _dlMap.keys()) {
|
||||
log.info(`will-download: ${key}`);
|
||||
if (!item.getURLChain().includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const action = _dlMap.get(key);
|
||||
action.call(null, evt, item, wc);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
} finally {
|
||||
_dlMap.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function handleDownloadFile(arg: DownloadRequest) {
|
||||
const status: DownloadStatus = {
|
||||
type: DownloadStatusType.Pending,
|
||||
savePath: "",
|
||||
};
|
||||
|
||||
try {
|
||||
await fs.ensureDir(arg.outputFolder, 0o666);
|
||||
await fsp.mkdir(arg.outputFolder, { recursive: true });
|
||||
|
||||
const savePath = path.join(arg.outputFolder, `${nanoid()}-${arg.fileName}`);
|
||||
log.info(`[DownloadFile] '${arg.url}' -> '${savePath}'`);
|
||||
|
||||
const { data } = await axios({
|
||||
url: arg.url,
|
||||
method: "GET",
|
||||
responseType: "stream",
|
||||
headers: {
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
});
|
||||
|
||||
// const totalLength = headers["content-length"];
|
||||
// Progress is not shown anywhere
|
||||
// data.on("data", (chunk) => {
|
||||
// log.info("DLPROG", arg.responseKey);
|
||||
// });
|
||||
|
||||
const url = new URL(arg.url).toString();
|
||||
const writer = fs.createWriteStream(savePath);
|
||||
data.pipe(writer);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on("finish", resolve);
|
||||
writer.on("error", reject);
|
||||
});
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
let size = 0;
|
||||
let percentMod = -1;
|
||||
|
||||
const req = net.request(url);
|
||||
req.on("response", (response) => {
|
||||
const fileLength = parseInt((response.headers["content-length"] as string) ?? "0", 10);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return reject(new Error(`Invalid response (${response.statusCode}): ${url}`));
|
||||
}
|
||||
|
||||
response.on("data", (data) => {
|
||||
writer.write(data, () => {
|
||||
size += data.length;
|
||||
const percent = fileLength <= 0 ? 0 : Math.floor((size / fileLength) * 100);
|
||||
if (percent % 5 === 0 && percentMod !== percent) {
|
||||
percentMod = percent;
|
||||
log.debug(`Write: [${percent}] ${size}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
response.on("end", () => {
|
||||
console.log("No more data in response.");
|
||||
return resolve(undefined);
|
||||
});
|
||||
response.on("error", (err) => {
|
||||
return reject(err);
|
||||
});
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
} finally {
|
||||
// always close stream
|
||||
writer.end();
|
||||
}
|
||||
|
||||
status.type = DownloadStatusType.Complete;
|
||||
status.savePath = savePath;
|
||||
|
||||
const status: DownloadStatus = {
|
||||
type: DownloadStatusType.Complete,
|
||||
savePath,
|
||||
};
|
||||
window.webContents.send(arg.responseKey, status);
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
const status: DownloadStatus = {
|
||||
type: DownloadStatusType.Error,
|
||||
error: err,
|
||||
};
|
||||
status.type = DownloadStatusType.Error;
|
||||
status.error = err;
|
||||
window.webContents.send(arg.responseKey, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/thejoshwolfe/yauzl/blob/96f0eb552c560632a754ae0e1701a7edacbda389/examples/unzip.js#L124
|
||||
function handleZipFile(err: Error, zipfile: yauzl.ZipFile, targetDir: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -510,7 +634,7 @@ function handleZipFile(err: Error, zipfile: yauzl.ZipFile, targetDir: string): P
|
||||
if (/\/$/.test(entry.fileName)) {
|
||||
// directory file names end with '/'
|
||||
const dirPath = path.join(targetDir, entry.fileName);
|
||||
fs.mkdirp(dirPath, function () {
|
||||
fs.mkdir(dirPath, { recursive: true }, function () {
|
||||
if (err) throw err;
|
||||
zipfile.readEntry();
|
||||
});
|
||||
@@ -518,7 +642,7 @@ function handleZipFile(err: Error, zipfile: yauzl.ZipFile, targetDir: string): P
|
||||
// ensure parent directory exists
|
||||
const filePath = path.join(targetDir, entry.fileName);
|
||||
const parentPath = path.join(targetDir, path.dirname(entry.fileName));
|
||||
fs.mkdirp(parentPath, function () {
|
||||
fs.mkdir(parentPath, { recursive: true }, function () {
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AppUpdater } from "./app-updater";
|
||||
import { initializeIpcHandlers, setPendingOpenUrl } from "./ipc-events";
|
||||
import * as platform from "./platform";
|
||||
import {
|
||||
APP_PROTOCOL_NAME,
|
||||
APP_USER_MODEL_ID,
|
||||
COLLAPSE_TO_TRAY_PREFERENCE_KEY,
|
||||
CURRENT_THEME_KEY,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
IPC_POWER_MONITOR_RESUME,
|
||||
IPC_POWER_MONITOR_SUSPEND,
|
||||
IPC_POWER_MONITOR_UNLOCK,
|
||||
IPC_PUSH_NOTIFICATION,
|
||||
IPC_WINDOW_ENTER_FULLSCREEN,
|
||||
IPC_WINDOW_LEAVE_FULLSCREEN,
|
||||
IPC_WINDOW_MAXIMIZED,
|
||||
@@ -39,6 +41,7 @@ import { MainChannels } from "../src/common/wowup";
|
||||
import { AppOptions } from "../src/common/wowup/models";
|
||||
import { initializeStoreIpcHandlers, preferenceStore } from "./stores";
|
||||
import { windowStateManager } from "./window-state";
|
||||
import { pushEvents, PUSH_NOTIFICATION_EVENT } from "./push";
|
||||
|
||||
// LOGGING SETUP
|
||||
// Override the default log path so they aren't a pain to find on Mac
|
||||
@@ -63,6 +66,11 @@ process.on("unhandledRejection", (error) => {
|
||||
log.error("unhandledRejection", error);
|
||||
});
|
||||
|
||||
// WINDOWS CERTS
|
||||
if (platform.isWin) {
|
||||
require("win-ca");
|
||||
}
|
||||
|
||||
// VARIABLES
|
||||
const startedAt = Date.now();
|
||||
const argv = minimist(process.argv.slice(1), {
|
||||
@@ -81,6 +89,9 @@ let loadFailCount = 0;
|
||||
// APP MENU SETUP
|
||||
createAppMenu(win);
|
||||
|
||||
// WowUp Protocol Handler
|
||||
app.setAsDefaultProtocolClient(APP_PROTOCOL_NAME);
|
||||
|
||||
// Set the app ID so that our notifications work correctly on Windows
|
||||
app.setAppUserModelId(APP_USER_MODEL_ID);
|
||||
|
||||
@@ -92,8 +103,6 @@ if (preferenceStore.get(USE_HARDWARE_ACCELERATION_PREFERENCE_KEY) === "false") {
|
||||
log.info("Hardware acceleration enabled");
|
||||
}
|
||||
|
||||
app.allowRendererProcessReuse = false;
|
||||
|
||||
// Some servers don't supply good CORS headers for us, so we ignore them.
|
||||
app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
|
||||
|
||||
@@ -152,6 +161,10 @@ if (app.isReady()) {
|
||||
// setTimeout(() => {
|
||||
createWindow();
|
||||
// }, 400);
|
||||
|
||||
// Preload native lib
|
||||
const nativeAddon = require("../build/Release/addon.node");
|
||||
nativeAddon.hello();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,8 +263,11 @@ function createWindow(): BrowserWindow {
|
||||
allowRunningInsecureContent: argv.serve,
|
||||
webSecurity: false,
|
||||
nativeWindowOpen: true,
|
||||
enableRemoteModule: false, // This is only required for electron store https://github.com/sindresorhus/electron-store/issues/152,
|
||||
additionalArguments: [`--log-path=${LOG_PATH}`, `--user-data-path=${app.getPath("userData")}`],
|
||||
additionalArguments: [
|
||||
`--log-path=${LOG_PATH}`,
|
||||
`--user-data-path=${app.getPath("userData")}`,
|
||||
`--base-bg-color=${getBackgroundColor()}`,
|
||||
],
|
||||
},
|
||||
show: false,
|
||||
};
|
||||
@@ -273,6 +289,10 @@ function createWindow(): BrowserWindow {
|
||||
initializeIpcHandlers(win, USER_AGENT);
|
||||
initializeStoreIpcHandlers();
|
||||
|
||||
pushEvents.on(PUSH_NOTIFICATION_EVENT, (data) => {
|
||||
win.webContents.send(IPC_PUSH_NOTIFICATION, data);
|
||||
});
|
||||
|
||||
// Keep track of window state
|
||||
mainWindowManager.monitorState(win);
|
||||
|
||||
@@ -342,6 +362,7 @@ function createWindow(): BrowserWindow {
|
||||
|
||||
win.on("close", (e) => {
|
||||
if (appIsQuitting || preferenceStore.get(COLLAPSE_TO_TRAY_PREFERENCE_KEY) !== "true") {
|
||||
pushEvents.removeAllListeners(PUSH_NOTIFICATION_EVENT);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
@@ -36,6 +36,7 @@ function getArg(argKey: string): string {
|
||||
|
||||
const LOG_PATH = getArg("log-path");
|
||||
const USER_DATA_PATH = getArg("user-data-path");
|
||||
const BASE_BG_COLOR = getArg("base-bg-color");
|
||||
|
||||
log.transports.file.resolvePath = (variables: log.PathVariables) => {
|
||||
return join(LOG_PATH, variables.fileName);
|
||||
@@ -73,25 +74,30 @@ function openPath(path: string): Promise<string> {
|
||||
return shell.openPath(path);
|
||||
}
|
||||
|
||||
if (window.opener === null) {
|
||||
window.log = log;
|
||||
window.libs = {
|
||||
handlebars: require("handlebars"),
|
||||
autoLaunch: require("auto-launch"),
|
||||
};
|
||||
window.userDataPath = USER_DATA_PATH;
|
||||
window.logPath = LOG_PATH;
|
||||
window.platform = process.platform;
|
||||
window.wowup = {
|
||||
onRendererEvent,
|
||||
onceRendererEvent,
|
||||
rendererSend,
|
||||
rendererInvoke,
|
||||
rendererOff,
|
||||
rendererOn,
|
||||
openExternal,
|
||||
openPath,
|
||||
};
|
||||
} else {
|
||||
console.log("HAS OPENER");
|
||||
try {
|
||||
if (window.opener === null) {
|
||||
window.log = log;
|
||||
window.baseBgColor = BASE_BG_COLOR;
|
||||
window.libs = {
|
||||
handlebars: require("handlebars"),
|
||||
autoLaunch: require("auto-launch"),
|
||||
};
|
||||
window.userDataPath = USER_DATA_PATH;
|
||||
window.logPath = LOG_PATH;
|
||||
window.platform = process.platform;
|
||||
window.wowup = {
|
||||
onRendererEvent,
|
||||
onceRendererEvent,
|
||||
rendererSend,
|
||||
rendererInvoke,
|
||||
rendererOff,
|
||||
rendererOn,
|
||||
openExternal,
|
||||
openPath,
|
||||
};
|
||||
} else {
|
||||
console.log("HAS OPENER");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
|
||||
64
wowup-electron/app/push.ts
Normal file
64
wowup-electron/app/push.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as log from "electron-log";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export const PUSH_NOTIFICATION_EVENT = "push-notification";
|
||||
export const pushEvents = new EventEmitter();
|
||||
|
||||
const channelSubscriptions = new Map<string, boolean>();
|
||||
|
||||
let Pushy: any;
|
||||
export function startPushService(): boolean {
|
||||
if (!Pushy) {
|
||||
Pushy = require("pushy-electron");
|
||||
}
|
||||
|
||||
// Listen for push notifications
|
||||
Pushy.setNotificationListener((data) => {
|
||||
pushEvents.emit(PUSH_NOTIFICATION_EVENT, data);
|
||||
});
|
||||
|
||||
Pushy.listen();
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function registerForPush(appId: string): Promise<string> {
|
||||
if (!Pushy) {
|
||||
throw new Error("Push not started");
|
||||
}
|
||||
if (!appId) {
|
||||
throw new Error("Invalid push app id");
|
||||
}
|
||||
|
||||
return await Pushy.register({ appId });
|
||||
}
|
||||
|
||||
export async function unregisterPush(): Promise<void> {
|
||||
for (const [key] of channelSubscriptions) {
|
||||
try {
|
||||
await Pushy.unsubscribe(key);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
channelSubscriptions.clear();
|
||||
|
||||
Pushy.disconnect();
|
||||
}
|
||||
|
||||
export async function subscribeToChannel(channel: string): Promise<void> {
|
||||
// Make sure the user is registered
|
||||
if (!Pushy.isRegistered()) {
|
||||
throw new Error("Push services not registered");
|
||||
}
|
||||
|
||||
if (channelSubscriptions.has(channel)) {
|
||||
log.warn(`Already listening: ${channel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe the user to a topic
|
||||
await Pushy.subscribe(channel);
|
||||
channelSubscriptions.set(channel, true);
|
||||
log.debug(`Subscribed: ${channel}`);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import * as _ from "lodash";
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs-extra";
|
||||
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 { readDirRecursive } from "./file.utils";
|
||||
import { exists, readDirRecursive, hashFile, hashString } from "./file.utils";
|
||||
import * as fsp from "fs/promises";
|
||||
|
||||
const INVALID_PATH_CHARS = [
|
||||
"|",
|
||||
@@ -61,7 +60,7 @@ export class WowUpFolderScanner {
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^/]+)[\\/]\1(-mainline|-bcc|-classic)?\.toc$/i;
|
||||
return /^([^/]+)[\\/]\1([-|_](mainline|bcc|tbc|classic|vanilla))?\.toc$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
@@ -86,14 +85,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,
|
||||
@@ -136,13 +135,14 @@ export class WowUpFolderScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(nativePath) || matchingFileList.indexOf(nativePath) !== -1) {
|
||||
const pathExists = await exists(nativePath);
|
||||
if (!pathExists || matchingFileList.indexOf(nativePath) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchingFileList.push(nativePath);
|
||||
|
||||
let input = await fs.readFile(nativePath, { encoding: "utf-8" });
|
||||
let input = await fsp.readFile(nativePath, { encoding: "utf-8" });
|
||||
input = this.removeComments(nativePath, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(nativePath, input);
|
||||
@@ -203,17 +203,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 fs.readFile(filePath);
|
||||
return this.hashString(text);
|
||||
}
|
||||
|
||||
private getRealPath(filePath: string) {
|
||||
const lowerPath = filePath.toLowerCase();
|
||||
const matchedPath = this._fileMap[lowerPath];
|
||||
|
||||
@@ -55,15 +55,22 @@
|
||||
"mac": {
|
||||
"icon": "electron-build/icon.icns",
|
||||
"category": "public.app-category.games",
|
||||
"target": ["default"],
|
||||
"target": [
|
||||
{
|
||||
"target": "default",
|
||||
"arch": ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./electron-build/entitlements.mac.plist",
|
||||
"extendInfo": {
|
||||
"CFBundleURLTypes": [{
|
||||
"CFBundleTypeRole": "Shell",
|
||||
"CFBundleURLName": "CurseForge",
|
||||
"CFBundleURLSchemes": "curseforge"
|
||||
}]
|
||||
"CFBundleURLTypes": [
|
||||
{
|
||||
"CFBundleTypeRole": "Shell",
|
||||
"CFBundleURLName": "CurseForge",
|
||||
"CFBundleURLSchemes": "curseforge"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
|
||||
31163
wowup-electron/package-lock.json
generated
31163
wowup-electron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wowup",
|
||||
"productName": "WowUp",
|
||||
"version": "2.4.7",
|
||||
"version": "2.5.0",
|
||||
"description": "World of Warcraft addon updater",
|
||||
"homepage": "https://wowup.io",
|
||||
"author": {
|
||||
@@ -49,115 +49,121 @@
|
||||
"pretty": "npx prettier --write . && ng lint --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "12.1.0",
|
||||
"@angular-devkit/build-angular": "12.1.0",
|
||||
"@angular-eslint/builder": "12.1.0",
|
||||
"@angular-eslint/eslint-plugin": "12.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "12.1.0",
|
||||
"@angular-eslint/schematics": "12.2.0",
|
||||
"@angular-eslint/template-parser": "12.1.0",
|
||||
"@angular/cli": "12.1.0",
|
||||
"@angular-builders/custom-webpack": "12.1.3",
|
||||
"@angular-devkit/build-angular": "12.2.12",
|
||||
"@angular-eslint/builder": "12.6.1",
|
||||
"@angular-eslint/eslint-plugin": "12.6.1",
|
||||
"@angular-eslint/eslint-plugin-template": "file:latest@angular-eslint/template-parser@latest",
|
||||
"@angular-eslint/schematics": "12.6.1",
|
||||
"@angular-eslint/template-parser": "12.6.1",
|
||||
"@angular/cli": "12.2.12",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@types/globrex": "0.1.0",
|
||||
"@types/jasmine": "3.7.6",
|
||||
"@types/jasminewd2": "2.0.9",
|
||||
"@types/lodash": "4.14.170",
|
||||
"@types/markdown-it": "12.0.1",
|
||||
"@types/mocha": "8.2.2",
|
||||
"@types/node": "15.9.0",
|
||||
"@types/opossum": "4.1.1",
|
||||
"@types/slug": "5.0.0",
|
||||
"@types/adm-zip": "0.4.34",
|
||||
"@types/archiver": "5.1.1",
|
||||
"@types/flat": "5.0.2",
|
||||
"@types/globrex": "0.1.1",
|
||||
"@types/jasmine": "3.10.0",
|
||||
"@types/jasminewd2": "2.0.10",
|
||||
"@types/lodash": "4.14.176",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/mocha": "9.0.0",
|
||||
"@types/node": "14.17.15",
|
||||
"@types/object-hash": "2.2.1",
|
||||
"@types/opossum": "6.2.0",
|
||||
"@types/slug": "5.0.2",
|
||||
"@types/string-similarity": "4.0.0",
|
||||
"@types/uuid": "8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.26.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "4.26.0",
|
||||
"@typescript-eslint/parser": "4.26.0",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "4.33.0",
|
||||
"@typescript-eslint/parser": "4.33.0",
|
||||
"chai": "4.3.4",
|
||||
"conventional-changelog-cli": "2.1.1",
|
||||
"core-js": "3.17.2",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "10.0.0",
|
||||
"electron": "13.5.1",
|
||||
"electron-builder": "22.11.7",
|
||||
"electron-notarize": "1.0.0",
|
||||
"electron-reload": "1.5.0",
|
||||
"eslint": "7.27.0",
|
||||
"electron": "15.3.0",
|
||||
"electron-builder": "22.13.1",
|
||||
"electron-notarize": "1.1.1",
|
||||
"electron-reload": "2.0.0-alpha.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-jsdoc": "35.1.3",
|
||||
"eslint-plugin-import": "2.24.2",
|
||||
"eslint-plugin-jsdoc": "36.1.0",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"flat": "5.0.2",
|
||||
"i18next-json-sync": "2.3.1",
|
||||
"jasmine-core": "3.7.1",
|
||||
"jasmine-core": "3.9.0",
|
||||
"jasmine-spec-reporter": "7.0.0",
|
||||
"karma": "6.3.3",
|
||||
"karma": "6.3.4",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-electron": "7.0.0",
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-jasmine-html-reporter": "1.6.0",
|
||||
"mocha": "8.4.0",
|
||||
"nan": "2.14.2",
|
||||
"node-addon-api": "4.0.0",
|
||||
"node-gyp": "8.1.0",
|
||||
"karma-jasmine-html-reporter": "1.7.0",
|
||||
"mocha": "9.1.1",
|
||||
"node-addon-api": "4.2.0",
|
||||
"node-gyp": "8.3.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.3.0",
|
||||
"prettier": "2.4.1",
|
||||
"spectron": "15.0.0",
|
||||
"ts-node": "9.1.1",
|
||||
"typescript": "4.2.4",
|
||||
"wait-on": "5.3.0",
|
||||
"ts-node": "10.3.0",
|
||||
"typescript": "4.3.5",
|
||||
"wait-on": "6.0.0",
|
||||
"webdriver-manager": "12.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~12.1.0",
|
||||
"@angular/cdk": "12.1.0",
|
||||
"@angular/common": "12.1.0",
|
||||
"@angular/compiler": "12.1.0",
|
||||
"@angular/compiler-cli": "12.1.0",
|
||||
"@angular/core": "12.1.0",
|
||||
"@angular/forms": "12.1.0",
|
||||
"@angular/material": "12.1.0",
|
||||
"@angular/platform-browser": "12.1.0",
|
||||
"@angular/platform-browser-dynamic": "12.1.0",
|
||||
"@angular/router": "12.1.0",
|
||||
"@angular/animations": "12.2.12",
|
||||
"@angular/cdk": "12.2.12",
|
||||
"@angular/common": "12.2.12",
|
||||
"@angular/compiler": "12.2.12",
|
||||
"@angular/compiler-cli": "12.2.12",
|
||||
"@angular/core": "12.2.12",
|
||||
"@angular/forms": "12.2.12",
|
||||
"@angular/material": "12.2.12",
|
||||
"@angular/platform-browser": "12.2.12",
|
||||
"@angular/platform-browser-dynamic": "12.2.12",
|
||||
"@angular/router": "12.2.12",
|
||||
"@bbob/core": "2.7.0",
|
||||
"@bbob/html": "2.7.0",
|
||||
"@bbob/preset-html5": "2.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@microsoft/applicationinsights-web": "2.6.2",
|
||||
"ag-grid-angular": "25.3.0",
|
||||
"ag-grid-community": "25.3.0",
|
||||
"@circlon/angular-tree-component": "11.0.4",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@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",
|
||||
"axios": "0.21.1",
|
||||
"compare-versions": "3.6.0",
|
||||
"core-js": "3.13.1",
|
||||
"electron-log": "4.3.5",
|
||||
"electron-store": "8.0.0",
|
||||
"electron-log": "4.4.1",
|
||||
"electron-store": "8.0.1",
|
||||
"electron-updater": "4.3.9",
|
||||
"fs-extra": "10.0.0",
|
||||
"globrex": "0.1.2",
|
||||
"handlebars": "4.7.7",
|
||||
"lodash": "4.17.21",
|
||||
"markdown-it": "12.0.6",
|
||||
"markdown-it": "12.2.0",
|
||||
"messageformat": "2.3.0",
|
||||
"minimist": "1.2.5",
|
||||
"nanoid": "3.1.23",
|
||||
"ngx-lightbox": "2.4.1",
|
||||
"nanoid": "3.1.30",
|
||||
"ng-gallery": "5.0.0",
|
||||
"ngx-translate-messageformat-compiler": "4.10.0",
|
||||
"node-cache": "5.1.2",
|
||||
"node-disk-info": "1.3.0",
|
||||
"opossum": "6.1.0",
|
||||
"object-hash": "2.2.0",
|
||||
"opossum": "6.2.1",
|
||||
"p-limit": "3.1.0",
|
||||
"protobufjs": "6.11.2",
|
||||
"pushy-electron": "1.0.8",
|
||||
"rxjs": "6.6.7",
|
||||
"slug": "5.1.0",
|
||||
"string-similarity": "4.0.4",
|
||||
"ts-custom-error": "3.2.0",
|
||||
"tslib": "2.2.0",
|
||||
"tslib": "2.3.1",
|
||||
"uuid": "8.3.2",
|
||||
"win-ca": "3.4.5",
|
||||
"yauzl": "2.10.0",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
@@ -65,14 +65,17 @@ const FEATURED_ADDONS_CACHE_TTL_SEC = AppConfig.featuredAddonsCacheTimeSec;
|
||||
const GAME_TYPE_LISTS = [
|
||||
{
|
||||
flavor: "wow_classic",
|
||||
typeId: 67408,
|
||||
matches: [WowClientType.ClassicEra, WowClientType.ClassicEraPtr],
|
||||
},
|
||||
{
|
||||
flavor: "wow_burning_crusade",
|
||||
typeId: 73246,
|
||||
matches: [WowClientType.Classic, WowClientType.ClassicPtr, WowClientType.ClassicBeta],
|
||||
},
|
||||
{
|
||||
flavor: "wow_retail",
|
||||
typeId: 517,
|
||||
matches: [WowClientType.Retail, WowClientType.RetailPtr, WowClientType.Beta],
|
||||
},
|
||||
];
|
||||
@@ -144,6 +147,7 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("addonResult", addonResult);
|
||||
|
||||
const addonFileResponse = await this.getAddonFileById(protocolData.addonId, protocolData.fileId).toPromise();
|
||||
@@ -312,8 +316,10 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
|
||||
for (const scanResult of scanResults) {
|
||||
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
|
||||
scanResult.exactMatch = fingerprintResponse.exactMatches.find((exactMatch) =>
|
||||
this.hasMatchingFingerprint(scanResult, exactMatch)
|
||||
scanResult.exactMatch = fingerprintResponse.exactMatches.find(
|
||||
(exactMatch) =>
|
||||
this.hasMatchingFingerprint(scanResult, exactMatch) &&
|
||||
this.isCompatible(installation.clientType, exactMatch.file)
|
||||
);
|
||||
|
||||
// If the addon does not have an exact match, check the partial matches.
|
||||
@@ -339,6 +345,7 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
{
|
||||
fingerprints,
|
||||
},
|
||||
undefined,
|
||||
AppConfig.wowUpHubHttpTimeoutMs
|
||||
);
|
||||
|
||||
@@ -686,7 +693,7 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
|
||||
try {
|
||||
const blockList = await this._wowupApiService.getBlockList().toPromise();
|
||||
const blockedAuthorIds = _.map(blockList.curse.authors, (author) => author.authorId);
|
||||
const blockedAuthorIds = blockList.curse.authors.map((author) => author.authorId);
|
||||
return blockedAuthorIds.includes(author.id.toString()) || blockedAuthorIds.includes(author.userId.toString());
|
||||
} catch (e) {
|
||||
return false;
|
||||
@@ -813,61 +820,38 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
}
|
||||
|
||||
private isClientType(file: CurseFile, clientType: WowClientType) {
|
||||
const clientTypeStr = this.getGameVersionFlavor(clientType);
|
||||
if (file.isAlternate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the version flavor is an exact match, use that
|
||||
if (file.gameVersionFlavor === clientTypeStr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This check was a workaround for CF not supporting multi toc, it causes odd behavior with legacy (12 year old) addons showing up as valid.
|
||||
// Otherwise check if the game version array is close enough
|
||||
// const gameVersionRegex = this.getGameVersionRegex(clientType);
|
||||
// return file.gameVersion.some((gameVersion) => gameVersionRegex.test(gameVersion));
|
||||
return false;
|
||||
return this.isCompatible(clientType, file);
|
||||
}
|
||||
|
||||
private getGameVersionRegex(clientType: WowClientType): RegExp {
|
||||
switch (clientType) {
|
||||
case WowClientType.ClassicEra:
|
||||
case WowClientType.ClassicEraPtr:
|
||||
return /^1.\d+.\d+$/;
|
||||
case WowClientType.Classic:
|
||||
case WowClientType.ClassicPtr:
|
||||
case WowClientType.ClassicBeta:
|
||||
return /^2.\d+.\d+$/;
|
||||
case WowClientType.Retail:
|
||||
case WowClientType.RetailPtr:
|
||||
case WowClientType.Beta:
|
||||
default:
|
||||
return /^[3-9].\d+.\d+$/;
|
||||
private getGameVersionTypeId(clientType: WowClientType): number {
|
||||
const gameType = GAME_TYPE_LISTS.find((gtl) => gtl.matches.includes(clientType));
|
||||
if (!gameType) {
|
||||
throw new Error(`Game type not found: ${clientType}`);
|
||||
}
|
||||
|
||||
return gameType.typeId;
|
||||
}
|
||||
|
||||
private getGameVersionFlavor(clientType: WowClientType): CurseGameVersionFlavor {
|
||||
switch (clientType) {
|
||||
case WowClientType.ClassicEra:
|
||||
case WowClientType.ClassicEraPtr:
|
||||
return "wow_classic";
|
||||
case WowClientType.Classic:
|
||||
case WowClientType.ClassicPtr:
|
||||
case WowClientType.ClassicBeta:
|
||||
return "wow_burning_crusade";
|
||||
case WowClientType.Retail:
|
||||
case WowClientType.RetailPtr:
|
||||
case WowClientType.Beta:
|
||||
default:
|
||||
return "wow_retail";
|
||||
const gameType = GAME_TYPE_LISTS.find((gtl) => gtl.matches.includes(clientType));
|
||||
if (!gameType) {
|
||||
throw new Error(`Game type not found: ${clientType}`);
|
||||
}
|
||||
|
||||
return gameType.flavor as CurseGameVersionFlavor;
|
||||
}
|
||||
|
||||
private getValidClientTypes(file: CurseAddonFileResponse): WowClientType[] {
|
||||
const gameVersions: WowClientType[] = [];
|
||||
|
||||
const flavorMatches = GAME_TYPE_LISTS.find((list) => list.flavor === file.gameVersionFlavor)?.matches ?? [];
|
||||
const flavorMatches =
|
||||
GAME_TYPE_LISTS.find(
|
||||
(list) => file.sortableGameVersion.find((sgv) => sgv.gameVersionTypeId === list.typeId) !== undefined
|
||||
)?.matches ?? [];
|
||||
gameVersions.push(...flavorMatches);
|
||||
|
||||
if (!Array.isArray(file.gameVersion) || file.gameVersion.length === 0) {
|
||||
@@ -897,6 +881,24 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private isCompatible(clientType: WowClientType, file: CurseFile): boolean {
|
||||
if (Array.isArray(file.sortableGameVersion) && file.sortableGameVersion.length > 0) {
|
||||
const gameVersionTypeId = this.getGameVersionTypeId(clientType);
|
||||
return this.hasSortableGameVersion(file, gameVersionTypeId);
|
||||
}
|
||||
|
||||
const gameVersionFlavor = this.getGameVersionFlavor(clientType);
|
||||
console.debug(`Checking via game version flavor fallback`, gameVersionFlavor, file.displayName);
|
||||
return file.gameVersionFlavor === gameVersionFlavor;
|
||||
}
|
||||
|
||||
private hasSortableGameVersion(file: CurseFile, typeId: number): boolean {
|
||||
if (!file.sortableGameVersion) {
|
||||
console.debug(file);
|
||||
}
|
||||
return file.sortableGameVersion.find((sgv) => sgv.gameVersionTypeId === typeId) !== undefined;
|
||||
}
|
||||
|
||||
private getAddon(installation: WowInstallation, scanResult: AppCurseScanResult): Addon {
|
||||
if (!scanResult.exactMatch || !scanResult.searchResult) {
|
||||
throw new Error("No scan result exact match");
|
||||
@@ -911,7 +913,12 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
|
||||
const latestFiles = this.getLatestFiles(scanResult.searchResult, installation.clientType);
|
||||
|
||||
const targetToc = this._tocService.getTocForGameType2(scanResult.addonFolder.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(scanResult.addonFolder, installation.clientType);
|
||||
if (!targetToc) {
|
||||
console.error(scanResult.addonFolder.tocs);
|
||||
throw new Error("Target toc not found");
|
||||
}
|
||||
|
||||
const gameVersion = AddonUtils.getGameVersion(targetToc.interface);
|
||||
|
||||
let channelType = this.getChannelType(scanResult.exactMatch.file.releaseType);
|
||||
@@ -934,6 +941,7 @@ export class CurseAddonProvider extends AddonProvider {
|
||||
name: scanResult.searchResult?.name ?? "unknown",
|
||||
channelType,
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
clientType: installation.clientType,
|
||||
downloadUrl: latestVersion?.downloadUrl ?? scanResult.exactMatch?.file.downloadUrl ?? "",
|
||||
externalUrl: scanResult.searchResult?.websiteUrl ?? "",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -359,11 +359,11 @@ export class GitHubAddonProvider extends AddonProvider {
|
||||
}
|
||||
|
||||
private isClassicAsset(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().endsWith("-classic.zip");
|
||||
return /[-](classic|vanilla)\.zip$/i.test(asset.name);
|
||||
}
|
||||
|
||||
private isBurningCrusadeAsset(asset: GitHubAsset): boolean {
|
||||
return asset.name.toLowerCase().endsWith("-bc.zip") || asset.name.toLowerCase().endsWith("-bcc.zip");
|
||||
return /[-](bc|bcc|tbc)\.zip$/i.test(asset.name);
|
||||
}
|
||||
|
||||
private getAddonName(addonId: string): string {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -37,7 +37,7 @@ export class RaiderIoAddonProvider extends AddonProvider {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const targetToc = this._tocService.getTocForGameType2(raiderIo.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(raiderIo, installation.clientType);
|
||||
const dependencies = _.filter(addonFolders, (addonFolder) => this.isRaiderIoDependant(addonFolder));
|
||||
console.debug("RAIDER IO CLIENT FOUND", dependencies);
|
||||
|
||||
@@ -46,10 +46,11 @@ export class RaiderIoAddonProvider extends AddonProvider {
|
||||
const installedFolders = installedFolderList.join(",");
|
||||
|
||||
for (const rioAddonFolder of rioAddonFolders) {
|
||||
const subTargetToc = this._tocService.getTocForGameType2(rioAddonFolder.tocs, installation.clientType);
|
||||
const subTargetToc = this._tocService.getTocForGameType2(rioAddonFolder, installation.clientType);
|
||||
|
||||
rioAddonFolder.matchingAddon = {
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
channelType: AddonChannelType.Stable,
|
||||
clientType: installation.clientType,
|
||||
id: uuidv4(),
|
||||
|
||||
@@ -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";
|
||||
@@ -61,45 +61,6 @@ export class TukUiAddonProvider extends AddonProvider {
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
private mapAddonCategory(category: AddonCategory): string[] {
|
||||
switch (category) {
|
||||
case AddonCategory.Achievements:
|
||||
return ["Achievements"];
|
||||
case AddonCategory.ActionBars:
|
||||
return ["Action Bars"];
|
||||
case AddonCategory.BagsInventory:
|
||||
return ["Bags & Inventory"];
|
||||
case AddonCategory.BuffsDebuffs:
|
||||
return ["Buffs & Debuffs"];
|
||||
case AddonCategory.Bundles:
|
||||
return ["Edited UIs & Compilations", "Full UI Replacements"];
|
||||
case AddonCategory.ChatCommunication:
|
||||
return ["Chat & Communication"];
|
||||
case AddonCategory.Class:
|
||||
return ["Class"];
|
||||
case AddonCategory.Combat:
|
||||
return ["Combat"];
|
||||
case AddonCategory.Guild:
|
||||
return ["Guild"];
|
||||
case AddonCategory.MapMinimap:
|
||||
return ["Map & Minimap"];
|
||||
case AddonCategory.Miscellaneous:
|
||||
return ["Miscellaneous"];
|
||||
case AddonCategory.Plugins:
|
||||
return ["Plugins: ElvUI", "Plugins: Tukui", "Plugins: Other", "Skins"];
|
||||
case AddonCategory.Professions:
|
||||
return ["Professions"];
|
||||
case AddonCategory.Roleplay:
|
||||
return ["Roleplay"];
|
||||
case AddonCategory.Tooltips:
|
||||
return ["Tooltips"];
|
||||
case AddonCategory.UnitFrames:
|
||||
return ["Unit Frames"];
|
||||
default:
|
||||
throw new Error("Unhandled addon category");
|
||||
}
|
||||
}
|
||||
|
||||
public async getDescription(installation: WowInstallation, externalId: string): Promise<string> {
|
||||
const addons = await this.getAllAddons(installation.clientType);
|
||||
const addonMatch = _.find(addons, (addon) => addon.id.toString() === externalId.toString());
|
||||
@@ -175,12 +136,12 @@ export class TukUiAddonProvider extends AddonProvider {
|
||||
const matches: TukUiAddon[] = [];
|
||||
|
||||
// Sort folders to prioritize ones with a toc id
|
||||
const tukProjectAddonFolders = addonFolders.filter((folder) =>
|
||||
const tukProjectAddonFolders = _.sortBy(addonFolders, (folder) =>
|
||||
folder.tocs.some((toc) => !!toc.tukUiProjectId && toc.loadOnDemand !== "1")
|
||||
);
|
||||
).reverse();
|
||||
|
||||
for (const addonFolder of tukProjectAddonFolders) {
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder, installation.clientType);
|
||||
|
||||
let tukUiAddon: TukUiAddon;
|
||||
if (targetToc?.tukUiProjectId) {
|
||||
@@ -220,6 +181,7 @@ export class TukUiAddonProvider extends AddonProvider {
|
||||
|
||||
addonFolder.matchingAddon = {
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
channelType: addonChannelType,
|
||||
clientType: installation.clientType,
|
||||
id: uuidv4(),
|
||||
@@ -299,16 +261,14 @@ export class TukUiAddonProvider extends AddonProvider {
|
||||
const canonAddonName = addonName.toLowerCase();
|
||||
const addons = await this.getAllAddons(clientType);
|
||||
|
||||
let matches = _.orderBy(
|
||||
addons
|
||||
.map((addon) => {
|
||||
const similarity = stringSimilarity.compareTwoStrings(canonAddonName, addon.name.toLowerCase());
|
||||
return { addon, similarity };
|
||||
})
|
||||
.filter((result) => result.similarity > 0.7),
|
||||
(match) => match.similarity,
|
||||
"desc"
|
||||
).map((result) => result.addon);
|
||||
const similarity = addons
|
||||
.map((addon) => {
|
||||
const similarity = stringSimilarity.compareTwoStrings(canonAddonName, addon.name.toLowerCase());
|
||||
return { addon, similarity };
|
||||
})
|
||||
.filter((result) => result.similarity > 0.7);
|
||||
|
||||
let matches = _.orderBy(similarity, (match) => match.similarity, "desc").map((result) => result.addon);
|
||||
|
||||
// If we didnt get any similarity matches
|
||||
if (allowContain && matches.length === 0) {
|
||||
@@ -449,4 +409,43 @@ export class TukUiAddonProvider extends AddonProvider {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private mapAddonCategory(category: AddonCategory): string[] {
|
||||
switch (category) {
|
||||
case AddonCategory.Achievements:
|
||||
return ["Achievements"];
|
||||
case AddonCategory.ActionBars:
|
||||
return ["Action Bars"];
|
||||
case AddonCategory.BagsInventory:
|
||||
return ["Bags & Inventory"];
|
||||
case AddonCategory.BuffsDebuffs:
|
||||
return ["Buffs & Debuffs"];
|
||||
case AddonCategory.Bundles:
|
||||
return ["Edited UIs & Compilations", "Full UI Replacements"];
|
||||
case AddonCategory.ChatCommunication:
|
||||
return ["Chat & Communication"];
|
||||
case AddonCategory.Class:
|
||||
return ["Class"];
|
||||
case AddonCategory.Combat:
|
||||
return ["Combat"];
|
||||
case AddonCategory.Guild:
|
||||
return ["Guild"];
|
||||
case AddonCategory.MapMinimap:
|
||||
return ["Map & Minimap"];
|
||||
case AddonCategory.Miscellaneous:
|
||||
return ["Miscellaneous"];
|
||||
case AddonCategory.Plugins:
|
||||
return ["Plugins: ElvUI", "Plugins: Tukui", "Plugins: Other", "Skins"];
|
||||
case AddonCategory.Professions:
|
||||
return ["Professions"];
|
||||
case AddonCategory.Roleplay:
|
||||
return ["Roleplay"];
|
||||
case AddonCategory.Tooltips:
|
||||
return ["Tooltips"];
|
||||
case AddonCategory.UnitFrames:
|
||||
return ["Unit Frames"];
|
||||
default:
|
||||
throw new Error("Unhandled addon category");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -149,7 +149,7 @@ export class WowInterfaceAddonProvider extends AddonProvider {
|
||||
const addonDetails = await this.getAllAddonDetails(addonIds);
|
||||
|
||||
for (const addonFolder of wowiFolders) {
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder, installation.clientType);
|
||||
if (!targetToc?.wowInterfaceId) {
|
||||
continue;
|
||||
}
|
||||
@@ -223,12 +223,13 @@ export class WowInterfaceAddonProvider extends AddonProvider {
|
||||
addonChannelType: AddonChannelType,
|
||||
addonFolder: AddonFolder
|
||||
): Addon {
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(addonFolder, installation.clientType);
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
author: response.author,
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
channelType: addonChannelType,
|
||||
clientType: installation.clientType,
|
||||
downloadUrl: response.downloadUri,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { ADDON_PROVIDER_HUB, IPC_WOWUP_GET_SCAN_RESULTS } from "../../common/constants";
|
||||
import { Addon } from "../../common/entities/addon";
|
||||
import { WowClientType } from "../../common/warcraft/wow-client-type";
|
||||
import { AddonChannelType, WowUpScanResult } from "../../common/wowup/models";
|
||||
import { AddonCategory, AddonChannelType, WowUpScanResult } from "../../common/wowup/models";
|
||||
import { AppConfig } from "../../environments/environment";
|
||||
import { SourceRemovedAddonError } from "../errors";
|
||||
import { WowUpAddonReleaseRepresentation, WowUpAddonRepresentation } from "../models/wowup-api/addon-representations";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
GetFeaturedAddonsResponse,
|
||||
WowUpGetAddonReleaseResponse,
|
||||
WowUpGetAddonResponse,
|
||||
WowUpGetAddonsResponse,
|
||||
WowUpSearchAddonsResponse,
|
||||
} from "../models/wowup-api/api-responses";
|
||||
import { GetAddonsByFingerprintResponse } from "../models/wowup-api/get-addons-by-fingerprint.response";
|
||||
@@ -22,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";
|
||||
@@ -232,6 +233,17 @@ export class WowUpAddonProvider extends AddonProvider {
|
||||
);
|
||||
}
|
||||
|
||||
public async getCategory(category: AddonCategory, installation: WowInstallation): Promise<AddonSearchResult[]> {
|
||||
const gameType = this.getWowGameType(installation.clientType);
|
||||
const response = await this.getAddonsByCategory(gameType, category);
|
||||
|
||||
const searchResults = _.map(response?.addons, (addon) => this.getSearchResult(addon, gameType)).filter(
|
||||
(sr) => sr !== undefined
|
||||
);
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
public async scan(
|
||||
installation: WowInstallation,
|
||||
addonChannelType: AddonChannelType,
|
||||
@@ -303,6 +315,15 @@ export class WowUpAddonProvider extends AddonProvider {
|
||||
return scanResults;
|
||||
};
|
||||
|
||||
private async getAddonsByCategory(gameType: WowGameType, category: AddonCategory) {
|
||||
const url = new URL(`${API_URL}/addons/category/${category}/${gameType}`);
|
||||
return await this._cachingService.transaction(
|
||||
url.toString(),
|
||||
() => this._circuitBreaker.getJson<WowUpGetAddonsResponse>(url),
|
||||
CHANGELOG_CACHE_TTL_SEC
|
||||
);
|
||||
}
|
||||
|
||||
private async getAddonById(addonId: number | string) {
|
||||
const url = new URL(`${API_URL}/addons/${addonId}`);
|
||||
return await this._cachingService.transaction(
|
||||
@@ -400,20 +421,30 @@ export class WowUpAddonProvider extends AddonProvider {
|
||||
return {
|
||||
author: authors,
|
||||
externalId: representation.id.toString(),
|
||||
externalUrl: representation.repository,
|
||||
externalUrl: `${AppConfig.wowUpWebsiteUrl}/addons/${representation.id}`,
|
||||
name,
|
||||
providerName: this.name,
|
||||
thumbnailUrl: representation.image_url || representation.owner_image_url || "",
|
||||
downloadCount: representation.total_download_count,
|
||||
files: searchResultFiles,
|
||||
releasedAt: new Date(),
|
||||
screenshotUrl: "",
|
||||
screenshotUrls: [],
|
||||
summary: representation.description,
|
||||
fundingLinks: [...(representation?.funding_links ?? [])],
|
||||
screenshotUrls: this.getScreenshotUrls(clientReleases),
|
||||
};
|
||||
}
|
||||
|
||||
// Currently we only support images, so we filter for those
|
||||
private getScreenshotUrls(releases: WowUpAddonReleaseRepresentation[]): string[] {
|
||||
const urls = _.flatten(
|
||||
releases.map((release) =>
|
||||
release.previews?.filter((preview) => preview.preview_type === "image").map((preview) => preview.url)
|
||||
)
|
||||
).filter((url) => !!url);
|
||||
|
||||
return _.uniq(urls);
|
||||
}
|
||||
|
||||
private getAddon(
|
||||
installation: WowInstallation,
|
||||
addonChannelType: AddonChannelType,
|
||||
@@ -448,15 +479,21 @@ export class WowUpAddonProvider extends AddonProvider {
|
||||
throw new Error("Invalid matching version data");
|
||||
}
|
||||
|
||||
const screenshotUrls = this.getScreenshotUrls([matchedRelease]);
|
||||
const externalUrl = scanResult.exactMatch
|
||||
? `${AppConfig.wowUpWebsiteUrl}/addons/${scanResult.exactMatch.id}`
|
||||
: "unknown";
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
author: authors,
|
||||
name,
|
||||
channelType,
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
clientType: installation.clientType,
|
||||
downloadUrl: scanResult.exactMatch?.matched_release?.download_url ?? "",
|
||||
externalUrl: scanResult.exactMatch?.repository ?? "unknown",
|
||||
externalUrl,
|
||||
externalId: scanResult.exactMatch?.id.toString() ?? "unknown",
|
||||
gameVersion: getGameVersion(interfaceVer),
|
||||
installedAt: new Date(),
|
||||
@@ -476,6 +513,7 @@ export class WowUpAddonProvider extends AddonProvider {
|
||||
latestChangelog: scanResult.exactMatch?.matched_release?.body,
|
||||
externalLatestReleaseId: scanResult?.exactMatch?.matched_release?.id?.toString(),
|
||||
installationId: installation.id,
|
||||
screenshotUrls,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -36,11 +36,12 @@ export class WowUpCompanionAddonProvider extends AddonProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetToc = this._tocService.getTocForGameType2(companion.tocs, installation.clientType);
|
||||
const targetToc = this._tocService.getTocForGameType2(companion, installation.clientType);
|
||||
const lastUpdatedAt = await this._fileService.getLatestDirUpdateTime(companion.path);
|
||||
|
||||
companion.matchingAddon = {
|
||||
autoUpdateEnabled: false,
|
||||
autoUpdateNotificationsEnabled: false,
|
||||
channelType: AddonChannelType.Stable,
|
||||
clientType: installation.clientType,
|
||||
id: uuidv4(),
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ import { OverlayContainer, OverlayModule } from "@angular/cdk/overlay";
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { httpLoaderFactory } from "./app.module";
|
||||
import { MatModule } from "./mat-module";
|
||||
import { AnimatedLogoComponent } from "./components/common/animated-logo/animated-logo.component";
|
||||
import { MatModule } from "./modules/mat-module";
|
||||
import { PreferenceChange } from "./models/wowup/preference-change";
|
||||
import { ElectronService } from "./services";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
@@ -18,11 +20,9 @@ import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { FileService } from "./services/files/file.service";
|
||||
import { SessionService } from "./services/session/session.service";
|
||||
import { PreferenceStorageService } from "./services/storage/preference-storage.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
import { WowUpAddonService } from "./services/wowup/wowup-addon.service";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { AnimatedLogoComponent } from "./components/animated-logo/animated-logo.component";
|
||||
import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service";
|
||||
import { WowUpAddonService } from "./services/wowup/wowup-addon.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
import { ZoomService } from "./services/zoom/zoom.service";
|
||||
|
||||
describe("AppComponent", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
WOWUP_LOGO_FILENAME,
|
||||
} from "../common/constants";
|
||||
import { AppUpdateState, MenuConfig, SystemTrayConfig } from "../common/wowup/models";
|
||||
import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetry-dialog.component";
|
||||
import { TelemetryDialogComponent } from "./components/common/telemetry-dialog/telemetry-dialog.component";
|
||||
import { ElectronService } from "./services";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
@@ -44,13 +44,13 @@ import { ZoomDirection } from "./utils/zoom.utils";
|
||||
import { Addon } from "../common/entities/addon";
|
||||
import { AppConfig } from "../environments/environment";
|
||||
import { PreferenceStorageService } from "./services/storage/preference-storage.service";
|
||||
import { InstallFromUrlDialogComponent } from "./components/install-from-url-dialog/install-from-url-dialog.component";
|
||||
import { InstallFromUrlDialogComponent } from "./components/addons/install-from-url-dialog/install-from-url-dialog.component";
|
||||
import { WowUpAddonService } from "./services/wowup/wowup-addon.service";
|
||||
import { AddonSyncError, GitHubFetchReleasesError, GitHubFetchRepositoryError, GitHubLimitError } from "./errors";
|
||||
import { SnackbarService } from "./services/snackbar/snackbar.service";
|
||||
import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service";
|
||||
import { ZoomService } from "./services/zoom/zoom.service";
|
||||
import { AlertDialogComponent } from "./components/alert-dialog/alert-dialog.component";
|
||||
import { AlertDialogComponent } from "./components/common/alert-dialog/alert-dialog.component";
|
||||
import { AddonInstallState } from "./models/wowup/addon-install-state";
|
||||
|
||||
@Component({
|
||||
@@ -217,11 +217,11 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap(() => from(this.createSystemTray())),
|
||||
map(() => {
|
||||
switchMap(() => {
|
||||
if (this._analyticsService.shouldPromptTelemetry) {
|
||||
this.openDialog();
|
||||
return of(this.openDialog());
|
||||
} else {
|
||||
this._analyticsService.trackStartup();
|
||||
return from(this._analyticsService.trackStartup());
|
||||
}
|
||||
}),
|
||||
catchError((e) => {
|
||||
@@ -259,12 +259,18 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
disableClose: true,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this._analyticsService.telemetryEnabled = result;
|
||||
if (result) {
|
||||
this._analyticsService.trackStartup();
|
||||
}
|
||||
});
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
switchMap((result) => {
|
||||
this._analyticsService.telemetryEnabled = result;
|
||||
if (result) {
|
||||
return from(this._analyticsService.trackStartup());
|
||||
}
|
||||
return of(undefined);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private openInstallFromUrlDialog(path?: string) {
|
||||
@@ -305,11 +311,15 @@ export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
if (this.wowUpService.enableSystemNotifications) {
|
||||
const addonsWithNotificationsEnabled = updatedAddons.filter(
|
||||
(addon) => addon.autoUpdateNotificationsEnabled === true
|
||||
);
|
||||
|
||||
// Windows notification only shows so many chars
|
||||
if (this.getAddonNamesLength(updatedAddons) > 60) {
|
||||
await this.showManyAddonsAutoUpdated(updatedAddons);
|
||||
if (this.getAddonNamesLength(addonsWithNotificationsEnabled) > 60) {
|
||||
await this.showManyAddonsAutoUpdated(addonsWithNotificationsEnabled);
|
||||
} else {
|
||||
await this.showFewAddonsAutoUpdated(updatedAddons);
|
||||
await this.showFewAddonsAutoUpdated(addonsWithNotificationsEnabled);
|
||||
}
|
||||
} else {
|
||||
await this.checkQuitEnabled();
|
||||
|
||||
@@ -10,25 +10,25 @@ import { BrowserModule } from "@angular/platform-browser";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
||||
import { GalleryModule } from "ng-gallery";
|
||||
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { FooterComponent } from "./components/footer/footer.component";
|
||||
import { TitlebarComponent } from "./components/titlebar/titlebar.component";
|
||||
import { DirectiveModule } from "./directive.module";
|
||||
import { TitlebarComponent } from "./components/common/titlebar/titlebar.component";
|
||||
import { DirectiveModule } from "./modules/directive.module";
|
||||
import { DefaultHeadersInterceptor } from "./interceptors/default-headers.interceptor";
|
||||
import { ErrorHandlerInterceptor } from "./interceptors/error-handler-interceptor";
|
||||
import { MatModule } from "./mat-module";
|
||||
import { MatModule } from "./modules/mat-module";
|
||||
import { HomeModule } from "./pages/home/home.module";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { WowUpApiService } from "./services/wowup-api/wowup-api.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
import { WarcraftInstallationService } from "./services/warcraft/warcraft-installation.service";
|
||||
import { SharedModule } from "./shared.module";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
import { IconService } from "./services/icons/icon.service";
|
||||
import { VerticalTabsComponent } from "./components/vertical-tabs/vertical-tabs.component";
|
||||
import { HorizontalTabsComponent } from "./components/horizontal-tabs/horizontal-tabs.component";
|
||||
import { HorizontalTabsComponent } from "./components/common/horizontal-tabs/horizontal-tabs.component";
|
||||
import { CommonUiModule } from "./modules/common-ui.module";
|
||||
import { FooterComponent } from "./components/common/footer/footer.component";
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
@@ -42,12 +42,11 @@ export function initializeApp(wowupService: WowUpService) {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, TitlebarComponent, FooterComponent, VerticalTabsComponent, HorizontalTabsComponent],
|
||||
declarations: [AppComponent, TitlebarComponent, FooterComponent, HorizontalTabsComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
SharedModule,
|
||||
HomeModule,
|
||||
AppRoutingModule,
|
||||
DirectiveModule,
|
||||
@@ -64,6 +63,8 @@ export function initializeApp(wowupService: WowUpService) {
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
GalleryModule,
|
||||
CommonUiModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AddonInstallState } from "../models/wowup/addon-install-state";
|
||||
import { AddonStatusSortOrder } from "../models/wowup/addon-status-sort-order";
|
||||
import * as AddonUtils from "../utils/addon.utils";
|
||||
import { ADDON_PROVIDER_UNKNOWN } from "../../common/constants";
|
||||
import * as objectHash from "object-hash";
|
||||
|
||||
export class AddonViewModel {
|
||||
public addon: Addon | undefined;
|
||||
@@ -19,9 +20,12 @@ export class AddonViewModel {
|
||||
public isLoadOnDemand = false;
|
||||
public hasThumbnail = false;
|
||||
public thumbnailLetter = "";
|
||||
public showUpdate = false;
|
||||
public canonicalName = "";
|
||||
|
||||
public get isIgnored(): boolean {
|
||||
return this.addon?.isIgnored ?? false;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.addon?.name ?? "";
|
||||
}
|
||||
@@ -46,6 +50,10 @@ export class AddonViewModel {
|
||||
return this.addon?.author ?? "";
|
||||
}
|
||||
|
||||
public get hash(): string {
|
||||
return objectHash(this.addon);
|
||||
}
|
||||
|
||||
public constructor(addon: Addon | undefined) {
|
||||
this.addon = addon;
|
||||
this.installedAt = addon?.installedAt ? new Date(addon?.installedAt).getTime() : 0;
|
||||
|
||||
@@ -63,11 +63,11 @@
|
||||
<div #changelogContainer class="markdown-body addon-changelog text-1 mt-3" [innerHTML]="changelog$ | async"></div>
|
||||
</mat-tab>
|
||||
<!-- SCREENSHOTS -->
|
||||
<mat-tab *ngIf="imageUrls.length > 0" [label]="'DIALOGS.ADDON_DETAILS.IMAGES_TAB' | translate">
|
||||
<mat-tab *ngIf="previewItems.length > 0" [label]="'DIALOGS.ADDON_DETAILS.IMAGES_TAB' | translate">
|
||||
<mat-grid-list class="image-grid pt-3" cols="4" rowHeight="1:1" gutterSize="3">
|
||||
<mat-grid-tile *ngFor="let image of imageUrls">
|
||||
<mat-grid-tile *ngFor="let image of previewItems; index as i">
|
||||
<div class="image-thumb-container ">
|
||||
<img class="image-thumb mat-elevation-z8" [src]="image.src" (click)="onClickImage(image.src)">
|
||||
<img class="image-thumb mat-elevation-z8" [src]="image.data.thumb" [lightbox]="i">
|
||||
</div>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
@@ -76,7 +76,7 @@
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions *ngIf="isUnknownProvider === false">
|
||||
<div class="row w-100">
|
||||
<button *ngIf="showUpdateButton" mat-button color="warn" (click)="onClickRemoveAddon()"> {{
|
||||
<button *ngIf="showRemoveButton" mat-button color="warn" (click)="onClickRemoveAddon()"> {{
|
||||
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REMOVE_ADDON_BUTTON" | translate }}</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<a #providerLink mat-button class="mr-3 text-1" appExternalLink [href]="externalUrl"
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.image-grid {
|
||||
width: 70vw;
|
||||
.image-thumb-container {
|
||||
@@ -8,19 +8,21 @@ import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { AddonViewModel } from "../../business-objects/addon-view-model";
|
||||
import { Addon } from "../../../common/entities/addon";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { overrideIconModule } from "../../tests/mock-mat-icon";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { Addon } from "../../../../common/entities/addon";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { overrideIconModule } from "../../../tests/mock-mat-icon";
|
||||
import { AddonDetailComponent, AddonDetailModel } from "./addon-detail.component";
|
||||
import { mockPreload } from "../../tests/test-helpers";
|
||||
import { WowUpService } from "../../services/wowup/wowup.service";
|
||||
import { LightboxModule } from "ngx-lightbox";
|
||||
import { LinkService } from "../../services/links/link.service";
|
||||
import { mockPreload } from "../../../tests/test-helpers";
|
||||
import { WowUpService } from "../../../services/wowup/wowup.service";
|
||||
import { LinkService } from "../../../services/links/link.service";
|
||||
import { GalleryModule } from "ng-gallery";
|
||||
import { LightboxModule } from "ng-gallery/lightbox";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { AddonUiService } from "../../../services/addons/addon-ui.service";
|
||||
|
||||
describe("AddonDetailComponent", () => {
|
||||
let dialogModel: AddonDetailModel;
|
||||
@@ -28,6 +30,7 @@ describe("AddonDetailComponent", () => {
|
||||
let sessionServiceSpy: SessionService;
|
||||
let wowUpService: WowUpService;
|
||||
let linkService: any;
|
||||
let addonUiService: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockPreload();
|
||||
@@ -42,6 +45,7 @@ describe("AddonDetailComponent", () => {
|
||||
}
|
||||
);
|
||||
|
||||
addonUiService = jasmine.createSpyObj("AddonUiService", [""], {});
|
||||
wowUpService = jasmine.createSpyObj("WowUpService", [""], {});
|
||||
linkService = jasmine.createSpyObj("LinkService", [""], {});
|
||||
|
||||
@@ -73,6 +77,7 @@ describe("AddonDetailComponent", () => {
|
||||
useClass: TranslateMessageFormatCompiler,
|
||||
},
|
||||
}),
|
||||
GalleryModule,
|
||||
LightboxModule,
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
@@ -90,6 +95,7 @@ describe("AddonDetailComponent", () => {
|
||||
provide: WowUpService,
|
||||
useValue: wowUpService,
|
||||
},
|
||||
{ provide: AddonUiService, useValue: addonUiService },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { last } from "lodash";
|
||||
import { BehaviorSubject, from, of, Subscription } from "rxjs";
|
||||
import { filter, first, map, switchMap, tap } from "rxjs/operators";
|
||||
import { IAlbum, Lightbox } from "ngx-lightbox";
|
||||
import { Gallery, GalleryItem, ImageItem } from "ng-gallery";
|
||||
import { BehaviorSubject, from, Subscription } from "rxjs";
|
||||
import { filter, first, map, tap } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
AfterViewChecked,
|
||||
@@ -15,24 +15,24 @@ import {
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatTabChangeEvent, MatTabGroup } from "@angular/material/tabs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { ADDON_PROVIDER_GITHUB, ADDON_PROVIDER_UNKNOWN } from "../../../common/constants";
|
||||
import { AddonFundingLink } from "../../../common/entities/addon";
|
||||
import { AddonChannelType, AddonDependency, AddonDependencyType } from "../../../common/wowup/models";
|
||||
import { AddonViewModel } from "../../business-objects/addon-view-model";
|
||||
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultDependency } from "../../models/wowup/addon-search-result-dependency";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { SnackbarService } from "../../services/snackbar/snackbar.service";
|
||||
import * as SearchResult from "../../utils/search-result.utils";
|
||||
import { ConfirmDialogComponent } from "../confirm-dialog/confirm-dialog.component";
|
||||
import { formatDynamicLinks } from "../../utils/dom.utils";
|
||||
import { LinkService } from "../../services/links/link.service";
|
||||
import { ADDON_PROVIDER_GITHUB, ADDON_PROVIDER_UNKNOWN, TAB_INDEX_MY_ADDONS } from "../../../../common/constants";
|
||||
import { Addon, AddonFundingLink } from "../../../../common/entities/addon";
|
||||
import { AddonChannelType, AddonDependency, AddonDependencyType } from "../../../../common/wowup/models";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { AddonSearchResult } from "../../../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultDependency } from "../../../models/wowup/addon-search-result-dependency";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { LinkService } from "../../../services/links/link.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { SnackbarService } from "../../../services/snackbar/snackbar.service";
|
||||
import { formatDynamicLinks } from "../../../utils/dom.utils";
|
||||
import * as SearchResult from "../../../utils/search-result.utils";
|
||||
import { AddonUiService } from "../../../services/addons/addon-ui.service";
|
||||
|
||||
export interface AddonDetailModel {
|
||||
listItem?: AddonViewModel;
|
||||
@@ -69,6 +69,7 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
public hasChangeLog = false;
|
||||
public showInstallButton = false;
|
||||
public showUpdateButton = false;
|
||||
public showRemoveButton = false;
|
||||
public hasRequiredDependencies = false;
|
||||
public title = "";
|
||||
public subtitle = "";
|
||||
@@ -85,19 +86,19 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
public isUnknownProvider = false;
|
||||
public isMissingUnknownDependencies = false;
|
||||
public missingDependencies: string[] = [];
|
||||
public imageUrls: IAlbum[] = [];
|
||||
public previewItems: GalleryItem[] = [];
|
||||
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public model: AddonDetailModel,
|
||||
private _dialogRef: MatDialogRef<AddonDetailComponent>,
|
||||
private _dialog: MatDialog,
|
||||
private _addonService: AddonService,
|
||||
private _cdRef: ChangeDetectorRef,
|
||||
private _snackbarService: SnackbarService,
|
||||
private _translateService: TranslateService,
|
||||
private _sessionService: SessionService,
|
||||
private _lightbox: Lightbox,
|
||||
private _linkService: LinkService
|
||||
private _linkService: LinkService,
|
||||
private _addonUiService: AddonUiService,
|
||||
public gallery: Gallery
|
||||
) {
|
||||
this._dependencies = this.getDependencies();
|
||||
|
||||
@@ -130,6 +131,8 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
|
||||
this.showUpdateButton = !!this.model.listItem;
|
||||
|
||||
this.showRemoveButton = this.isAddonInstalled();
|
||||
|
||||
this.title = this.model.listItem?.addon?.name || this.model.searchResult?.name || "UNKNOWN";
|
||||
|
||||
this.subtitle = this.model.listItem?.addon?.author || this.model.searchResult?.author || "UNKNOWN";
|
||||
@@ -169,12 +172,12 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
this.isMissingUnknownDependencies = !!this.missingDependencies.length;
|
||||
|
||||
const imageUrlList = this.model.listItem?.addon?.screenshotUrls ?? this.model.searchResult?.screenshotUrls ?? [];
|
||||
this.imageUrls = imageUrlList.map((url) => {
|
||||
return {
|
||||
src: url,
|
||||
thumb: url,
|
||||
};
|
||||
|
||||
this.previewItems = imageUrlList.map((url) => {
|
||||
return new ImageItem({ src: url, thumb: url });
|
||||
});
|
||||
|
||||
this.gallery.ref().load(this.previewItems);
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {}
|
||||
@@ -190,15 +193,6 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
public onClickImage(url: string): void {
|
||||
const idx = this.imageUrls.findIndex((album) => album.src === url);
|
||||
if (idx >= 0) {
|
||||
this._lightbox.open(this.imageUrls, idx, {
|
||||
centerVertically: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onInstallUpdated(): void {
|
||||
this._cdRef.detectChanges();
|
||||
}
|
||||
@@ -214,39 +208,44 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
}
|
||||
|
||||
public onClickRemoveAddon(): void {
|
||||
if (!this.model.listItem?.addon?.name) {
|
||||
console.warn("Invalid model list item addon");
|
||||
let addon: Addon = null;
|
||||
|
||||
// Addon is expected to be available through the model when browsing My Addons tab
|
||||
if (this._sessionService.getSelectedHomeTab() === TAB_INDEX_MY_ADDONS) {
|
||||
if (!this.model.listItem?.addon.name) {
|
||||
console.warn("Invalid model list item addon");
|
||||
return;
|
||||
}
|
||||
|
||||
addon = this.model.listItem?.addon;
|
||||
} else {
|
||||
const selectedInstallation = this._sessionService.getSelectedWowInstallation();
|
||||
const externalId = this.model.searchResult?.externalId ?? "";
|
||||
const providerName = this.model.searchResult?.providerName ?? "";
|
||||
|
||||
if (!externalId || !providerName || !selectedInstallation) {
|
||||
console.warn("Invalid search result when identifying which addon to remove", {
|
||||
selectedInstallation,
|
||||
externalId,
|
||||
providerName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
addon = this._addonService.getByExternalId(externalId, providerName, selectedInstallation.id);
|
||||
}
|
||||
|
||||
if (!addon) {
|
||||
console.warn("Invalid addon when attempting removal");
|
||||
return;
|
||||
}
|
||||
|
||||
this.getRemoveAddonPrompt(this.model.listItem.addon.name)
|
||||
.afterClosed()
|
||||
this._addonUiService
|
||||
.handleRemoveAddon(addon)
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((result) => {
|
||||
if (!result) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
const addon = this.model.listItem?.addon;
|
||||
if (!addon) {
|
||||
console.warn(`Invalid addon`);
|
||||
return of(false);
|
||||
}
|
||||
|
||||
if (this._addonService.getRequiredDependencies(addon).length === 0) {
|
||||
return from(this._addonService.removeAddon(addon)).pipe(map(() => true));
|
||||
} else {
|
||||
return this.getRemoveDependenciesPrompt(addon.name, (addon.dependencies ?? []).length)
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
switchMap((result) => from(this._addonService.removeAddon(addon, result))),
|
||||
map(() => true)
|
||||
);
|
||||
}
|
||||
}),
|
||||
map((shouldClose) => {
|
||||
if (shouldClose) {
|
||||
map((result) => {
|
||||
if (result.removed) {
|
||||
this._dialogRef.close();
|
||||
}
|
||||
})
|
||||
@@ -254,44 +253,6 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private getRemoveAddonPrompt(addonName: string): MatDialogRef<ConfirmDialogComponent, any> {
|
||||
const title = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.TITLE", { count: 1 });
|
||||
const message1: string = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ONE", {
|
||||
addonName,
|
||||
});
|
||||
const message2: string = this._translateService.instant(
|
||||
"PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ACTION_EXPLANATION"
|
||||
);
|
||||
|
||||
return this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title,
|
||||
message: `${message1}\n\n${message2}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getRemoveDependenciesPrompt(
|
||||
addonName: string,
|
||||
dependencyCount: number
|
||||
): MatDialogRef<ConfirmDialogComponent, any> {
|
||||
const title = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.DEPENDENCY_TITLE");
|
||||
const message1: string = this._translateService.instant("PAGES.MY_ADDONS.UNINSTALL_POPUP.DEPENDENCY_MESSAGE", {
|
||||
addonName,
|
||||
dependencyCount,
|
||||
});
|
||||
const message2: string = this._translateService.instant(
|
||||
"PAGES.MY_ADDONS.UNINSTALL_POPUP.CONFIRMATION_ACTION_EXPLANATION"
|
||||
);
|
||||
|
||||
return this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title,
|
||||
message: `${message1}\n\n${message2}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedTabTypeFromIndex(index: number): DetailsTabType {
|
||||
return index === 0 ? "description" : "changelog";
|
||||
}
|
||||
@@ -432,4 +393,21 @@ export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked
|
||||
private getLatestSearchResultFile() {
|
||||
return SearchResult.getLatestFile(this.model.searchResult, this.model.channelType ?? AddonChannelType.Stable);
|
||||
}
|
||||
|
||||
private isAddonInstalled(): boolean {
|
||||
const selectedInstallation = this._sessionService.getSelectedWowInstallation();
|
||||
if (!selectedInstallation) {
|
||||
console.warn("No selected installation");
|
||||
return;
|
||||
}
|
||||
|
||||
const externalId = this.model.searchResult?.externalId ?? this.model.listItem?.addon?.externalId ?? "";
|
||||
const providerName = this.model.searchResult?.providerName ?? this.model.listItem?.addon?.providerName ?? "";
|
||||
|
||||
if (externalId && providerName) {
|
||||
return this._addonService.isInstalled(externalId, providerName, selectedInstallation);
|
||||
}
|
||||
|
||||
console.warn("Invalid list item addon when verifying if installed");
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
|
||||
import { WowClientType } from "../../../../common/warcraft/wow-client-type";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { ProgressButtonComponent } from "../../common/progress-button/progress-button.component";
|
||||
import { AddonInstallButtonComponent } from "./addon-install-button.component";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { WowClientType } from "../../../common/warcraft/wow-client-type";
|
||||
import { Subject } from "rxjs";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { ProgressButtonComponent } from "../progress-button/progress-button.component";
|
||||
|
||||
describe("AddonInstallButtonComponent", () => {
|
||||
let addonServiceSpy: AddonService;
|
||||
@@ -4,11 +4,11 @@ import { filter } from "rxjs/operators";
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { AddonInstallState } from "../../models/wowup/addon-install-state";
|
||||
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { AddonInstallState } from "../../../models/wowup/addon-install-state";
|
||||
import { AddonSearchResult } from "../../../models/wowup/addon-search-result";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-addon-install-button",
|
||||
@@ -0,0 +1,162 @@
|
||||
<div mat-dialog-title class="addon-manage-dialog">
|
||||
<div class="row align-items-center">
|
||||
<h2 class="flex-grow-1 m-0">
|
||||
{{ "ADDON_IMPORT.DIALOG_TITLE" | translate: { clientType: selectedInstallation.label } }}
|
||||
</h2>
|
||||
<button mat-icon-button [mat-dialog-close]="true" color="accent" [disabled]="(installing$ | async) === true">
|
||||
<mat-icon svgIcon="fas:times"> </mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group (selectedIndexChange)="selectedTab$.next($event)">
|
||||
<!-- EXPORT -->
|
||||
<mat-tab [label]="'ADDON_IMPORT.EXPORT_TAB_LABEL' | translate" [disabled]="(installing$ | async) === true">
|
||||
<div class="row align-items-center">
|
||||
<p *ngIf="exportSummary !== undefined" class="mt-3 flex-grow-1">
|
||||
<span>{{ "ADDON_IMPORT.ACTIVE_ADDON_COUNT" | translate: { count: exportSummary.activeCount } }}</span>
|
||||
<span *ngIf="exportSummary.ignoreCount > 0" class="text-warning">
|
||||
{{ "ADDON_IMPORT.IGNORED_ADDON_COUNT" | translate: { count: exportSummary.ignoreCount } }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div *ngIf="exportPayload !== undefined">
|
||||
<mat-form-field appearance="fill" class="w-100">
|
||||
<mat-label>{{ "ADDON_IMPORT.EXPORT_TEXT_LABEL" | translate }}</mat-label>
|
||||
<textarea matInput class="export-content" spellcheck="false" readonly="readonly">{{
|
||||
exportPayload
|
||||
}}</textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<!-- IMPORT -->
|
||||
<mat-tab [label]="'ADDON_IMPORT.IMPORT_TAB_LABEL' | translate" [disabled]="(installing$ | async) === true">
|
||||
<!-- IMPORT FORM -->
|
||||
<div *ngIf="(hasImportSummary$ | async) === false">
|
||||
<div class="row align-items-center mb-1">
|
||||
<p class="mt-3 flex-grow-1">
|
||||
<span>{{ "ADDON_IMPORT.IMPORT_TEXT_INSTRUCTIONS" | translate }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<mat-form-field appearance="fill" class="w-100">
|
||||
<mat-label>{{ "ADDON_IMPORT.IMPORT_TEXT_LABEL" | translate }}</mat-label>
|
||||
<textarea matInput class="import-content" spellcheck="false" [(ngModel)]="importData"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<!-- IMPORT RESULT -->
|
||||
<div *ngIf="hasImportSummary$ | async">
|
||||
<p class="m-0 mt-3">
|
||||
{{ "ADDON_IMPORT.IMPORT_TOTAL_COUNT" | translate: { count: importSummaryComparisonCt$ | async } }}
|
||||
</p>
|
||||
<p class="text-2">
|
||||
<span *ngIf="(importSummaryConflictCt$ | async) > 0"
|
||||
>{{ "ADDON_IMPORT.IMPORT_CONFLICT_COUNT" | translate: { count: importSummaryConflictCt$ | async } }}
|
||||
</span>
|
||||
<span *ngIf="(importSummaryAddedCt$ | async) > 0"
|
||||
>{{ "ADDON_IMPORT.IMPORT_ADDED_COUNT" | translate: { count: importSummaryAddedCt$ | async } }}
|
||||
</span>
|
||||
<span *ngIf="(importSummaryNoChangeCt$ | async) > 0">
|
||||
{{ "ADDON_IMPORT.IMPORT_NO_CHANGE_COUNT" | translate: { count: importSummaryNoChangeCt$ | async } }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="bg-secondary-2 rounded p-3">
|
||||
<div *ngFor="let comp of importSummaryComparisons$ | async">
|
||||
<div class="comparison-row" [ngClass]="[comp.state, comp.state === 'no-change' ? 'text-3' : '']">
|
||||
<div *ngIf="comp.isInstalling === false">
|
||||
<div
|
||||
*ngIf="comp.state === 'no-change'"
|
||||
class="rounded comp-badge no-change-badge"
|
||||
[matTooltip]="'ADDON_IMPORT.NO_CHANGE_BADGE_TOOLTIP' | translate"
|
||||
>
|
||||
{{ "ADDON_IMPORT.IMPORT_BADGE_NO_CHANGE" | translate }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="comp.state === 'added'"
|
||||
class="rounded comp-badge added-badge"
|
||||
[matTooltip]="'ADDON_IMPORT.ADDED_BADGE_TOOLTIP' | translate"
|
||||
>
|
||||
{{ "ADDON_IMPORT.IMPORT_BADGE_ADDED" | translate }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="comp.state === 'conflict'"
|
||||
class="rounded comp-badge conflict-badge"
|
||||
[matTooltip]="'ADDON_IMPORT.CONFLICT_BADGE_TOOLTIP' | translate"
|
||||
>
|
||||
{{ "ADDON_IMPORT.IMPORT_BADGE_CONFLICT" | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="comp.isInstalling === true">
|
||||
<div *ngIf="comp.isCompleted === false" class="rounded comp-badge">
|
||||
<mat-spinner [diameter]="20"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="comp.isCompleted === true" class="rounded comp-badge">
|
||||
<mat-icon class="badge-icon success-icon" svgIcon="far:check-circle"> </mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="comp-text">{{ comp.imported.name }}</div>
|
||||
<small *ngIf="comp.state === 'conflict'" class="text-2">{{
|
||||
"ADDON_IMPORT." + comp.conflictReason | translate
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
<!-- EXPORT ACTIONS -->
|
||||
<mat-dialog-actions *ngIf="(selectedTab$ | async) === TAB_IDX_EXPORT">
|
||||
<div class="flex-grow-1"></div>
|
||||
<button
|
||||
*ngIf="exportPayload !== undefined"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[cdkCopyToClipboard]="exportPayload"
|
||||
[disabled]="(installing$ | async) === true"
|
||||
(click)="onClickCopy()"
|
||||
>
|
||||
{{ "ADDON_IMPORT.COPY_BUTTON" | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
<!-- IMPORT ACTIONS -->
|
||||
<mat-dialog-actions *ngIf="(selectedTab$ | async) === TAB_IDX_IMPORT">
|
||||
<button
|
||||
*ngIf="hasImportSummary$ | async"
|
||||
mat-button
|
||||
color="warn"
|
||||
[disabled]="(installing$ | async) === true"
|
||||
(click)="importSummary$.next(undefined)"
|
||||
>
|
||||
{{ "ADDON_IMPORT.RESET_BUTTON" | translate }}
|
||||
</button>
|
||||
<div class="flex-grow-1"></div>
|
||||
<button
|
||||
*ngIf="hasImportSummary$ | async"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="(canInstall$ | async) === false || (installing$ | async) === true"
|
||||
(click)="onClickInstall()"
|
||||
>
|
||||
{{ "ADDON_IMPORT.INSTALL_BUTTON" | translate }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="(hasImportSummary$ | async) === false"
|
||||
mat-button
|
||||
color="accent"
|
||||
[disabled]="(installing$ | async) === true"
|
||||
(click)="onClickPaste()"
|
||||
>
|
||||
{{ "ADDON_IMPORT.PASTE_BUTTON" | translate }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="(hasImportSummary$ | async) === false"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="(installing$ | async) === true"
|
||||
(click)="onClickImport()"
|
||||
>
|
||||
{{ "ADDON_IMPORT.IMPORT_BUTTON" | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,61 @@
|
||||
.addon-manage-dialog {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.export-content {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.import-content {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.comparison-row {
|
||||
margin: 0.5em 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--background-secondary-1);
|
||||
padding-bottom: 0.25em;
|
||||
// align-items: center;
|
||||
}
|
||||
|
||||
.comp-badge {
|
||||
display: flex;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
background-color: var(--background-secondary-1);
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.comp-text {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
.added-badge {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.conflict-badge {
|
||||
background-color: var(--warning-color);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.no-change-badge {
|
||||
background-color: var(--background-secondary-1);
|
||||
}
|
||||
.success-icon {
|
||||
text-align: center;
|
||||
filter: invert(23%) sepia(99%) saturate(1821%) hue-rotate(104deg) brightness(96%) contrast(106%);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
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 "../../../../common/warcraft/wow-installation";
|
||||
import {
|
||||
AddonBrokerService,
|
||||
ExportPayload,
|
||||
ExportSummary,
|
||||
ImportComparison,
|
||||
ImportSummary,
|
||||
} from "../../../services/addons/addon-broker.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { SnackbarService } from "../../../services/snackbar/snackbar.service";
|
||||
import { ElectronService } from "../../../services";
|
||||
|
||||
interface ImportComparisonViewModel extends ImportComparison {
|
||||
isInstalling?: boolean;
|
||||
isCompleted?: boolean;
|
||||
didError?: boolean;
|
||||
}
|
||||
|
||||
interface ImportSummaryViewModel extends ImportSummary {
|
||||
comparisons: ImportComparisonViewModel[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-addon-manage-dialog",
|
||||
templateUrl: "./addon-manage-dialog.component.html",
|
||||
styleUrls: ["./addon-manage-dialog.component.scss"],
|
||||
})
|
||||
export class AddonManageDialogComponent implements OnInit, OnDestroy {
|
||||
private readonly _subscriptions: Subscription[] = [];
|
||||
|
||||
public readonly selectedTab$ = new BehaviorSubject<number>(0);
|
||||
|
||||
public readonly TAB_IDX_EXPORT = 0;
|
||||
public readonly TAB_IDX_IMPORT = 1;
|
||||
public readonly selectedInstallation: WowInstallation;
|
||||
|
||||
public exportSummary: ExportSummary | undefined;
|
||||
public exportPayload!: string;
|
||||
public importData = "";
|
||||
public installing$ = new BehaviorSubject<boolean>(false);
|
||||
public importSummary$ = new BehaviorSubject<ImportSummaryViewModel | undefined>(undefined);
|
||||
public hasImportSummary$ = this.importSummary$.pipe(map((summary) => summary !== undefined));
|
||||
public importSummaryAddedCt$ = this.importSummary$.pipe(map((summary) => summary?.addedCt ?? 0));
|
||||
public importSummaryConflictCt$ = this.importSummary$.pipe(map((summary) => summary?.conflictCt ?? 0));
|
||||
public importSummaryNoChangeCt$ = this.importSummary$.pipe(map((summary) => summary?.noChangeCt ?? 0));
|
||||
public importSummaryComparisons$ = this.importSummary$.pipe(map((summary) => summary?.comparisons ?? []));
|
||||
public importSummaryComparisonCt$ = this.importSummary$.pipe(map((summary) => summary?.comparisons?.length ?? 0));
|
||||
public canInstall$ = this.importSummary$.pipe(
|
||||
map((summary) => {
|
||||
if (!summary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if there are any new addons, we can install
|
||||
return summary.comparisons.some((comp) => comp.state === "added");
|
||||
})
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _addonBrokerService: AddonBrokerService,
|
||||
private _sessionService: SessionService,
|
||||
private _snackbarService: SnackbarService
|
||||
) {
|
||||
this.selectedInstallation = this._sessionService.getSelectedWowInstallation();
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.exportSummary = this._addonBrokerService.getExportSummary(this.selectedInstallation);
|
||||
|
||||
const payload = this._addonBrokerService.getExportPayload(this.selectedInstallation);
|
||||
this.exportPayload = btoa(JSON.stringify(payload));
|
||||
|
||||
const installSub = this._addonBrokerService.addonInstall$.subscribe((evt) => {
|
||||
console.log("Install", evt);
|
||||
|
||||
const viewModel = { ...this.importSummary$.value };
|
||||
const compVm = viewModel.comparisons.find((comp) => comp.id === evt.comparisonId);
|
||||
compVm.isInstalling = true;
|
||||
compVm.isCompleted = evt.installState === AddonInstallState.Complete;
|
||||
compVm.didError = evt.installState === AddonInstallState.Error;
|
||||
|
||||
this.importSummary$.next(viewModel);
|
||||
});
|
||||
|
||||
this._subscriptions.push(installSub);
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this._subscriptions.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
public onClickCopy(): void {
|
||||
this._snackbarService.showSuccessSnackbar("ADDON_IMPORT.EXPORT_STRING_COPIED", {
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
public async onClickPaste(): Promise<void> {
|
||||
try {
|
||||
const txt = await this._electronService.readClipboardText();
|
||||
this.importData = txt;
|
||||
|
||||
this._snackbarService.showSuccessSnackbar("ADDON_IMPORT.EXPORT_STRING_PASTED", {
|
||||
timeout: 2000,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async onClickInstall(): Promise<void> {
|
||||
try {
|
||||
this.installing$.next(true);
|
||||
await this._addonBrokerService.installImportSummary(this.importSummary$.value, this.selectedInstallation);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.installing$.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
public onClickImport(): void {
|
||||
let importJson: ExportPayload;
|
||||
try {
|
||||
importJson = this._addonBrokerService.parseImportString(this.importData);
|
||||
console.debug(importJson);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._snackbarService.showErrorSnackbar("ADDON_IMPORT.IMPORT_STRING_INVALID", {
|
||||
timeout: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const importSummary = this._addonBrokerService.getImportSummary(importJson, this.selectedInstallation);
|
||||
console.debug(importSummary);
|
||||
|
||||
if (importSummary.errorCode !== undefined) {
|
||||
this._snackbarService.showErrorSnackbar(`ADDON_IMPORT.${importSummary.errorCode}`, {
|
||||
timeout: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const viewModel = this.getImportSummaryViewModel(importSummary);
|
||||
this.importSummary$.next(viewModel);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._snackbarService.showErrorSnackbar("ADDON_IMPORT.GENERIC_IMPORT_ERROR", {
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getImportSummaryViewModel(importSummary: ImportSummary): ImportSummaryViewModel {
|
||||
const viewModel: ImportSummaryViewModel = { ...importSummary };
|
||||
|
||||
viewModel.comparisons.forEach((comp) => {
|
||||
comp.isInstalling = false;
|
||||
comp.didError = false;
|
||||
comp.isCompleted = false;
|
||||
});
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<div *ngIf="hasUrl() === true" class="addon-logo-container bg-secondary-3" [style.width]="size + 'px'"
|
||||
<div *ngIf="hasUrl() === true" class="addon-logo-container bg-secondary-3 rounded" [style.width]="size + 'px'"
|
||||
[style.height]="size + 'px'">
|
||||
<img [src]="url" loading="lazy" />
|
||||
</div>
|
||||
<div *ngIf="hasUrl() === false" class="addon-logo-container">
|
||||
<div *ngIf="hasUrl() === false" class="addon-logo-container bg-secondary-3 rounded">
|
||||
<div class="addon-logo-letter text-3">
|
||||
{{ getLetter() }}
|
||||
</div>
|
||||
@@ -1,18 +1,21 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { AnalyticsService } from "../../services/analytics/analytics.service";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { ElectronService } from "../../services";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { AddonUpdateButtonComponent } from "./addon-update-button.component";
|
||||
import { Subject } from "rxjs";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { ProgressButtonComponent } from "../progress-button/progress-button.component";
|
||||
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { ElectronService } from "../../../services";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { AnalyticsService } from "../../../services/analytics/analytics.service";
|
||||
import { ProgressButtonComponent } from "../../common/progress-button/progress-button.component";
|
||||
import { AddonUpdateButtonComponent } from "./addon-update-button.component";
|
||||
|
||||
describe("AddonUpdateButtonComponent", () => {
|
||||
let addonServiceSpy: AddonService;
|
||||
let analyticsServiceSpy: AnalyticsService;
|
||||
@@ -4,13 +4,13 @@ import { filter } from "rxjs/operators";
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { WowClientType } from "../../../common/warcraft/wow-client-type";
|
||||
import { AddonViewModel } from "../../business-objects/addon-view-model";
|
||||
import { AddonInstallState } from "../../models/wowup/addon-install-state";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { getEnumName } from "../../utils/enum.utils";
|
||||
import { ADDON_PROVIDER_UNKNOWN } from "../../../common/constants";
|
||||
import { WowClientType } from "../../../../common/warcraft/wow-client-type";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { AddonInstallState } from "../../../models/wowup/addon-install-state";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { getEnumName } from "../../../utils/enum.utils";
|
||||
import { ADDON_PROVIDER_UNKNOWN } from "../../../../common/constants";
|
||||
|
||||
@Component({
|
||||
selector: "app-addon-update-button",
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NgxDatePipe } from "../../pipes/ngx-date.pipe";
|
||||
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
|
||||
import { getStandardTestImports } from "../../utils/test.utils";
|
||||
import { NgxDatePipe } from "../../../pipes/ngx-date.pipe";
|
||||
import { RelativeDurationPipe } from "../../../pipes/relative-duration-pipe";
|
||||
import { getStandardTestImports } from "../../../utils/test.utils";
|
||||
|
||||
import { DateTooltipCellComponent } from "./date-tooltip-cell.component";
|
||||
|
||||
@@ -14,7 +15,7 @@ describe("DateTooltipCellComponent", () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DateTooltipCellComponent, RelativeDurationPipe, NgxDatePipe],
|
||||
imports: [...getStandardTestImports()],
|
||||
providers: [RelativeDurationPipe, NgxDatePipe],
|
||||
providers: [RelativeDurationPipe, NgxDatePipe, DatePipe],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
</a>
|
||||
<a *ngIf="size === 'small'" class="funding-button small text-1 hover-primary-2 text-1-hover" appExternalLink
|
||||
[matTooltip]="tooltipKey | translate:{platform:fundingName}" [href]="funding.url">
|
||||
<mat-icon *ngIf="isFontIcon === true" [svgIcon]="iconSrc"></mat-icon>
|
||||
<mat-icon *ngIf="isFontIcon === true" [class]="getClassName()" [svgIcon]="iconSrc">
|
||||
</mat-icon>
|
||||
<img *ngIf="isFontIcon === false" class="funding-icon" [src]="iconSrc" />
|
||||
</a>
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
:host {
|
||||
margin-right: 0.5em;
|
||||
|
||||
@@ -41,3 +39,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.patreon-icon {
|
||||
color: var(--patreon-color);
|
||||
}
|
||||
.custom-icon {
|
||||
color: var(--wow-gold-color);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { MatIcon } from "@angular/material/icon";
|
||||
import { MatIconTestingModule } from "@angular/material/icon/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { FundingButtonComponent } from "./funding-button.component";
|
||||
|
||||
describe("FundingButtonComponent", () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { AddonFundingLink } from "../../../common/entities/addon";
|
||||
import { AddonFundingLink } from "../../../../common/entities/addon";
|
||||
|
||||
@Component({
|
||||
selector: "app-funding-button",
|
||||
@@ -26,6 +26,16 @@ export class FundingButtonComponent implements OnInit {
|
||||
return `PAGES.MY_ADDONS.FUNDING_TOOLTIP.${this.funding.platform.toUpperCase()}`;
|
||||
}
|
||||
|
||||
public getClassName(): string {
|
||||
switch (this.funding.platform) {
|
||||
case "PATREON":
|
||||
return "patreon-icon";
|
||||
case "GITHUB":
|
||||
return "github-icon";
|
||||
default:
|
||||
return "custom-icon";
|
||||
}
|
||||
}
|
||||
private getIsFontIcon(): boolean {
|
||||
switch (this.funding.platform) {
|
||||
case "PATREON":
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { AgRendererComponent } from "ag-grid-angular";
|
||||
import { ICellRendererParams } from "ag-grid-community";
|
||||
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
|
||||
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { AddonSearchResult } from "../../../models/wowup/addon-search-result";
|
||||
|
||||
@Component({
|
||||
selector: "app-get-addon-status-column",
|
||||
@@ -5,13 +5,13 @@ import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { IconService } from "../../services/icons/icon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
|
||||
import { overrideIconModule } from "../../tests/mock-mat-icon";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { IconService } from "../../../services/icons/icon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { WarcraftInstallationService } from "../../../services/warcraft/warcraft-installation.service";
|
||||
import { overrideIconModule } from "../../../tests/mock-mat-icon";
|
||||
|
||||
import {
|
||||
InstallFromProtocolDialogComponent,
|
||||
@@ -6,11 +6,11 @@ import { AfterViewInit, Component, Inject, OnInit } from "@angular/core";
|
||||
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 { ProtocolSearchResult } from "../../../models/wowup/protocol-search-result";
|
||||
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;
|
||||
@@ -6,10 +6,23 @@
|
||||
<p>{{ "DIALOGS.INSTALL_FROM_URL.SUPPORTED_SOURCES" | translate }}</p>
|
||||
<mat-form-field class="url-input-container">
|
||||
<mat-label>{{ "DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_LABEL" | translate }}</mat-label>
|
||||
<input matInput [placeholder]="'DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_PLACEHOLDER' | translate"
|
||||
[(ngModel)]="query" (keyup.enter)="onImportUrl()" [disabled]="isBusy === true" />
|
||||
<button mat-button color="accent" *ngIf="query" matSuffix mat-icon-button aria-label="Clear"
|
||||
(click)="onClearSearch()" [disabled]="isBusy === true">
|
||||
<input
|
||||
matInput
|
||||
[placeholder]="'DIALOGS.INSTALL_FROM_URL.ADDON_URL_INPUT_PLACEHOLDER' | translate"
|
||||
[(ngModel)]="query"
|
||||
(keyup.enter)="onImportUrl()"
|
||||
[disabled]="isBusy === true"
|
||||
/>
|
||||
<button
|
||||
mat-button
|
||||
color="accent"
|
||||
*ngIf="query"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Clear"
|
||||
(click)="onClearSearch()"
|
||||
[disabled]="isBusy === true"
|
||||
>
|
||||
<mat-icon svgIcon="fas:times"></mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
@@ -17,8 +30,11 @@
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="isBusy === false && addon !== undefined" class="addon-container">
|
||||
<div *ngIf="hasThumbnail === true" class="addon-thumb" [style.backgroundImage]="'url(' + addon.thumbnailUrl + ')'">
|
||||
</div>
|
||||
<div
|
||||
*ngIf="hasThumbnail === true"
|
||||
class="addon-thumb"
|
||||
[style.backgroundImage]="'url(' + addon.thumbnailUrl + ')'"
|
||||
></div>
|
||||
<div *ngIf="hasThumbnail === false" class="addon-thumb">
|
||||
<div class="addon-logo-letter text-3">
|
||||
{{ thumbnailLetter }}
|
||||
@@ -27,7 +43,7 @@
|
||||
<div class="addon-info">
|
||||
<h4>{{ addon.name }}</h4>
|
||||
<p>{{ addon.author }}</p>
|
||||
<p>{{ addon.files?.length ? addon?.files[0]?.version ?? '' : '' }}</p>
|
||||
<p>{{ addon.files?.length ? addon?.files[0]?.version ?? "" : "" }}</p>
|
||||
<p class="addon-download-count">
|
||||
{{ "DIALOGS.INSTALL_FROM_URL.DOWNLOAD_COUNT" | translate: getDownloadCountParams() }}
|
||||
</p>
|
||||
@@ -55,4 +71,4 @@
|
||||
<button mat-flat-button color="primary" cdkFocusInitial (click)="onImportUrl()">
|
||||
{{ "DIALOGS.INSTALL_FROM_URL.IMPORT_BUTTON" | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.url-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import { MatDialogRef } from "@angular/material/dialog";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { DownloadCountPipe } from "../../pipes/download-count.pipe";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { DownloadCountPipe } from "../../../pipes/download-count.pipe";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { InstallFromUrlDialogComponent } from "./install-from-url-dialog.component";
|
||||
import { IconService } from "../../services/icons/icon.service";
|
||||
import { overrideIconModule } from "../../tests/mock-mat-icon";
|
||||
import { IconService } from "../../../services/icons/icon.service";
|
||||
import { overrideIconModule } from "../../../tests/mock-mat-icon";
|
||||
|
||||
describe("InstallFromUrlDialogComponent", () => {
|
||||
console.log("InstallFromUrlDialogComponent");
|
||||
@@ -2,21 +2,21 @@ import { HttpErrorResponse } from "@angular/common/http";
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { from, Subscription } from "rxjs";
|
||||
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { AlertDialogComponent } from "../alert-dialog/alert-dialog.component";
|
||||
import { AddonSearchResult } from "../../../models/wowup/addon-search-result";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { roundDownloadCount, shortenDownloadCount } from "../../utils/number.utils";
|
||||
import { DownloadCountPipe } from "../../pipes/download-count.pipe";
|
||||
import { NO_SEARCH_RESULTS_ERROR } from "../../../common/constants";
|
||||
import { roundDownloadCount, shortenDownloadCount } from "../../../utils/number.utils";
|
||||
import { DownloadCountPipe } from "../../../pipes/download-count.pipe";
|
||||
import { NO_SEARCH_RESULTS_ERROR } from "../../../../common/constants";
|
||||
import {
|
||||
AssetMissingError,
|
||||
BurningCrusadeAssetMissingError,
|
||||
ClassicAssetMissingError,
|
||||
GitHubLimitError,
|
||||
NoReleaseFoundError,
|
||||
} from "../../errors";
|
||||
} from "../../../errors";
|
||||
|
||||
interface DownloadCounts {
|
||||
count: number;
|
||||
@@ -6,10 +6,10 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { MatModule } from "../../mat-module";
|
||||
import { AddonUpdateEvent } from "../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { AddonUpdateEvent } from "../../../models/wowup/addon-update-event";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import { MyAddonStatusColumnComponent } from "./my-addon-status-column.component";
|
||||
|
||||
describe("MyAddonStatusColumnComponent", () => {
|
||||
@@ -7,13 +7,13 @@ import { Component, NgZone, OnDestroy } from "@angular/core";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { Addon } from "../../../common/entities/addon";
|
||||
import { AddonWarningType } from "../../../common/wowup/models";
|
||||
import { AddonViewModel } from "../../business-objects/addon-view-model";
|
||||
import { AddonInstallState } from "../../models/wowup/addon-install-state";
|
||||
import { AddonService } from "../../services/addons/addon.service";
|
||||
import * as AddonUtils from "../../utils/addon.utils";
|
||||
import { AlertDialogComponent } from "../alert-dialog/alert-dialog.component";
|
||||
import { Addon } from "../../../../common/entities/addon";
|
||||
import { AddonWarningType } from "../../../../common/wowup/models";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { AddonInstallState } from "../../../models/wowup/addon-install-state";
|
||||
import { AddonService } from "../../../services/addons/addon.service";
|
||||
import * as AddonUtils from "../../../utils/addon.utils";
|
||||
import { AlertDialogComponent } from "../../common/alert-dialog/alert-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-my-addon-status-column",
|
||||
@@ -0,0 +1,66 @@
|
||||
<div>
|
||||
<div class="addon-column row align-items-center">
|
||||
<div class="thumbnail-container">
|
||||
<app-addon-thumbnail [url]="thumbnailUrl$ | async" [name]="name$ | async"></app-addon-thumbnail>
|
||||
</div>
|
||||
<div class="version-container">
|
||||
<div class="title-container">
|
||||
<a class="addon-title hover-text-2" (click)="viewDetails()"
|
||||
[ngClass]="{ 'text-3': isIgnored$ | async, 'text-warning': hasWarning$ | async }">{{
|
||||
name$ | async
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<div class="addon-version text-2 row align-items-center" [ngClass]="{ 'ignored': isIgnored$ | async }">
|
||||
<div *ngIf="hasFundingLinks$ | async" class="addon-funding mr-2">
|
||||
<app-funding-button *ngFor="let link of fundingLinks$ | async" [funding]="link" size="small">
|
||||
</app-funding-button>
|
||||
</div>
|
||||
<div *ngIf="showChannel$ | async" class="channel bg-secondary-3 mr-2" [ngClass]="channelClass$ | async">
|
||||
{{ channelTranslationKey$ | async | translate }}
|
||||
</div>
|
||||
<div *ngIf="hasMultipleProviders$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon" svgIcon="fas:code-branch"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.MULTIPLE_PROVIDERS_TOOLTIP' | translate">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="autoUpdateEnabled$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon text-2"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate" svgIcon="far:clock">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="hasRequiredDependencies$ | async" class="mr-2"
|
||||
[matTooltip]="'COMMON.DEPENDENCY.TOOLTIP' | translate: (dependencyTooltip$ | async)">
|
||||
<mat-icon class="auto-update-icon" svgIcon="fas:link"></mat-icon>
|
||||
</div>
|
||||
<div *ngIf="isLoadOnDemand$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon text-warning"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.REQUIRED_DEPENDENCY_MISSING_TOOLTIP' | translate"
|
||||
svgIcon="fas:exclamation-triangle">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="hasWarning$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon text-warning" [matTooltip]="getWarningText(listItem)"
|
||||
svgIcon="fas:exclamation-triangle">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="hasIgnoreReason$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon" [matTooltip]="ignoreTooltipKey$ | async | translate"
|
||||
[style.color]="'#ff9800'" [svgIcon]="ignoreIcon$ | async">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<!-- If no warning and not ignored for some specific reason, default to this -->
|
||||
<div *ngIf="isUnknownAddon$ | async" class="mr-2">
|
||||
<mat-icon class="auto-update-icon" [matTooltip]="'PAGES.MY_ADDONS.UNKNOWN_ADDON_INFO_TOOLTIP' | translate"
|
||||
[matTooltipClass]="['text-center']" [style.color]="'#ff9800'" svgIcon="fas:exclamation-triangle">
|
||||
</mat-icon>
|
||||
</div>
|
||||
{{ installedVersion$ | async }}
|
||||
<div *ngIf="showUpdateVersion$ | async" class="text-1 row">
|
||||
<mat-icon class="upgrade-icon" svgIcon="fas:play"></mat-icon>
|
||||
<div class="bg-secondary-4 text-2 rounded px-1">{{ latestVersion$ | async }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +1,10 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.addon-column {
|
||||
padding-top: 0.6em;
|
||||
padding-bottom: 0.6em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
.thumbnail-container {
|
||||
margin-right: 11px;
|
||||
@@ -57,7 +59,7 @@
|
||||
}
|
||||
|
||||
.title-container {
|
||||
padding-bottom: 0.5em;
|
||||
// padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.addon-title {
|
||||
@@ -1,19 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { MyAddonsAddonCellComponent } from "./my-addons-addon-cell.component";
|
||||
import { AddonViewModel } from "../../business-objects/addon-view-model";
|
||||
import { Addon } from "../../../common/entities/addon";
|
||||
import { MatModule } from "../../mat-module";
|
||||
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { Addon } from "../../../../common/entities/addon";
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { MyAddonsAddonCellComponent } from "./my-addons-addon-cell.component";
|
||||
|
||||
describe("MyAddonsAddonCellComponent", () => {
|
||||
let component: MyAddonsAddonCellComponent;
|
||||
let fixture: ComponentFixture<MyAddonsAddonCellComponent>;
|
||||
let sessionService: SessionService;
|
||||
|
||||
beforeEach(async () => {
|
||||
sessionService = jasmine.createSpyObj("SessionService", [""], {});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MyAddonsAddonCellComponent],
|
||||
imports: [
|
||||
@@ -32,7 +38,13 @@ describe("MyAddonsAddonCellComponent", () => {
|
||||
},
|
||||
}),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(MyAddonsAddonCellComponent, {
|
||||
set: {
|
||||
providers: [{ provide: SessionService, useValue: sessionService }],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MyAddonsAddonCellComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { AgRendererComponent } from "ag-grid-angular";
|
||||
import { ICellRendererParams } from "ag-grid-community";
|
||||
import { BehaviorSubject, combineLatest } from "rxjs";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
|
||||
import { Component } from "@angular/core";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
import { ADDON_PROVIDER_UNKNOWN } from "../../../../common/constants";
|
||||
import { AddonChannelType, AddonDependencyType, AddonWarningType } from "../../../../common/wowup/models";
|
||||
import { AddonViewModel } from "../../../business-objects/addon-view-model";
|
||||
import { DialogFactory } from "../../../services/dialog/dialog.factory";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import * as AddonUtils from "../../../utils/addon.utils";
|
||||
|
||||
@Component({
|
||||
selector: "app-my-addons-addon-cell",
|
||||
templateUrl: "./my-addons-addon-cell.component.html",
|
||||
styleUrls: ["./my-addons-addon-cell.component.scss"],
|
||||
})
|
||||
export class MyAddonsAddonCellComponent implements AgRendererComponent {
|
||||
private readonly _listItemSrc = new BehaviorSubject<AddonViewModel | undefined>(undefined);
|
||||
|
||||
public readonly listItem$ = this._listItemSrc.asObservable().pipe(filter((item) => item !== undefined));
|
||||
|
||||
public readonly name$ = this.listItem$.pipe(map((item) => item.name));
|
||||
|
||||
public readonly isIgnored$ = this.listItem$.pipe(map((item) => item.isIgnored));
|
||||
|
||||
public readonly hasWarning$ = this.listItem$.pipe(map((item) => this.hasWarning(item)));
|
||||
|
||||
public readonly hasFundingLinks$ = this.listItem$.pipe(
|
||||
map((item) => Array.isArray(item.addon?.fundingLinks) && item.addon.fundingLinks.length > 0)
|
||||
);
|
||||
|
||||
public readonly fundingLinks$ = this.listItem$.pipe(map((item) => item.addon?.fundingLinks ?? []));
|
||||
|
||||
public readonly showChannel$ = this.listItem$.pipe(map((item) => item.isBetaChannel() || item.isAlphaChannel()));
|
||||
|
||||
public readonly channelClass$ = this.listItem$.pipe(
|
||||
map((item) => {
|
||||
if (item.isBetaChannel()) {
|
||||
return "beta";
|
||||
}
|
||||
if (item.isAlphaChannel()) {
|
||||
return "alpha";
|
||||
}
|
||||
return "";
|
||||
})
|
||||
);
|
||||
|
||||
public readonly channelTranslationKey$ = this.listItem$.pipe(
|
||||
map((item) => {
|
||||
const channelType = item.addon?.channelType ?? AddonChannelType.Stable;
|
||||
return channelType === AddonChannelType.Alpha
|
||||
? "COMMON.ENUM.ADDON_CHANNEL_TYPE.ALPHA"
|
||||
: "COMMON.ENUM.ADDON_CHANNEL_TYPE.BETA";
|
||||
})
|
||||
);
|
||||
|
||||
public readonly hasMultipleProviders$ = this.listItem$.pipe(
|
||||
map((item) => (item.addon === undefined ? false : AddonUtils.hasMultipleProviders(item.addon)))
|
||||
);
|
||||
|
||||
public readonly autoUpdateEnabled$ = this.listItem$.pipe(map((item) => item.addon?.autoUpdateEnabled ?? false));
|
||||
|
||||
public readonly hasIgnoreReason$ = this.listItem$.pipe(map((item) => this.hasIgnoreReason(item)));
|
||||
|
||||
public readonly hasRequiredDependencies$ = this.listItem$.pipe(
|
||||
map((item) => this.getRequireDependencyCount(item) > 0)
|
||||
);
|
||||
|
||||
public readonly dependencyTooltip$ = this.listItem$.pipe(
|
||||
map((item) => {
|
||||
return {
|
||||
dependencyCount: this.getRequireDependencyCount(item),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
public readonly isLoadOnDemand$ = this.listItem$.pipe(map((item) => item.isLoadOnDemand));
|
||||
|
||||
public readonly ignoreTooltipKey$ = this.listItem$.pipe(map((item) => this.getIgnoreTooltipKey(item)));
|
||||
|
||||
public readonly ignoreIcon$ = this.listItem$.pipe(map((item) => this.getIgnoreIcon(item)));
|
||||
|
||||
public readonly warningText$ = this.listItem$.pipe(
|
||||
filter((item) => this.hasWarning(item)),
|
||||
map((item) => this.getWarningText(item))
|
||||
);
|
||||
|
||||
public readonly isUnknownAddon$ = this.listItem$.pipe(
|
||||
map((item) => {
|
||||
return (
|
||||
!item.isLoadOnDemand &&
|
||||
!this.hasIgnoreReason(item) &&
|
||||
!this.hasWarning(item) &&
|
||||
item.addon.providerName === ADDON_PROVIDER_UNKNOWN
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
public readonly installedVersion$ = this.listItem$.pipe(map((item) => item.addon.installedVersion));
|
||||
|
||||
public readonly latestVersion$ = this.listItem$.pipe(map((item) => item.addon.latestVersion));
|
||||
|
||||
public readonly thumbnailUrl$ = this.listItem$.pipe(map((item) => item.addon.thumbnailUrl));
|
||||
|
||||
public readonly showUpdateVersion$ = combineLatest([
|
||||
this.listItem$,
|
||||
this.sessionService.myAddonsCompactVersion$,
|
||||
]).pipe(
|
||||
map(([item, compactVersion]) => {
|
||||
return compactVersion && item.needsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
public set listItem(item: AddonViewModel) {
|
||||
this._listItemSrc.next(item);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private _translateService: TranslateService,
|
||||
private _dialogFactory: DialogFactory,
|
||||
public sessionService: SessionService
|
||||
) {}
|
||||
|
||||
public agInit(params: ICellRendererParams): void {
|
||||
this._listItemSrc.next(params.data);
|
||||
}
|
||||
|
||||
public refresh(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public afterGuiAttached?(): void {}
|
||||
|
||||
public viewDetails(): void {
|
||||
this._dialogFactory.getAddonDetailsDialog(this._listItemSrc.value);
|
||||
}
|
||||
|
||||
public getRequireDependencyCount(item: AddonViewModel): number {
|
||||
return item.getDependencies(AddonDependencyType.Required).length;
|
||||
}
|
||||
|
||||
public hasIgnoreReason(item: AddonViewModel): boolean {
|
||||
return !!item?.addon?.ignoreReason;
|
||||
}
|
||||
|
||||
public getIgnoreTooltipKey(item: AddonViewModel): string {
|
||||
switch (item.addon?.ignoreReason) {
|
||||
case "git_repo":
|
||||
return "PAGES.MY_ADDONS.ADDON_IS_CODE_REPOSITORY";
|
||||
case "missing_dependency":
|
||||
case "unknown":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public getIgnoreIcon(item: AddonViewModel): string {
|
||||
switch (item.addon?.ignoreReason) {
|
||||
case "git_repo":
|
||||
return "fas:code";
|
||||
case "missing_dependency":
|
||||
case "unknown":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public hasWarning(item: AddonViewModel): boolean {
|
||||
return item?.addon?.warningType !== undefined;
|
||||
}
|
||||
|
||||
public getWarningText(item: AddonViewModel): string {
|
||||
if (!this.hasWarning(item)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const toolTipParams = {
|
||||
providerName: item.providerName,
|
||||
};
|
||||
|
||||
switch (item.addon.warningType) {
|
||||
case AddonWarningType.MissingOnProvider:
|
||||
return this._translateService.instant("COMMON.ADDON_WARNING.MISSING_ON_PROVIDER_TOOLTIP", toolTipParams);
|
||||
case AddonWarningType.NoProviderFiles:
|
||||
return this._translateService.instant("COMMON.ADDON_WARNING.NO_PROVIDER_FILES_TOOLTIP", toolTipParams);
|
||||
default:
|
||||
return this._translateService.instant("COMMON.ADDON_WARNING.GENERIC_TOOLTIP", toolTipParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.addon-column {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -87,6 +85,8 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dependency-icon {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { GetAddonListItemFilePropPipe } from "../../pipes/get-addon-list-item-file-prop.pipe";
|
||||
import { DialogFactory } from "../../services/dialog/dialog.factory";
|
||||
import { GetAddonListItemFilePropPipe } from "../../../pipes/get-addon-list-item-file-prop.pipe";
|
||||
import { DialogFactory } from "../../../services/dialog/dialog.factory";
|
||||
import { PotentialAddonTableColumnComponent } from "./potential-addon-table-column.component";
|
||||
|
||||
describe("PotentialAddonTableColumnComponent", () => {
|
||||
@@ -3,14 +3,14 @@ import { ICellRendererParams } from "ag-grid-community";
|
||||
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
|
||||
|
||||
import { WowClientType } from "../../../common/warcraft/wow-client-type";
|
||||
import { AddonChannelType, AddonDependencyType } from "../../../common/wowup/models";
|
||||
import { GetAddonListItem } from "../../business-objects/get-addon-list-item";
|
||||
import { AddonSearchResult } from "../../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultDependency } from "../../models/wowup/addon-search-result-dependency";
|
||||
import { GetAddonListItemFilePropPipe } from "../../pipes/get-addon-list-item-file-prop.pipe";
|
||||
import { DialogFactory } from "../../services/dialog/dialog.factory";
|
||||
import * as SearchResults from "../../utils/search-result.utils";
|
||||
import { WowClientType } from "../../../../common/warcraft/wow-client-type";
|
||||
import { AddonChannelType, AddonDependencyType } from "../../../../common/wowup/models";
|
||||
import { GetAddonListItem } from "../../../business-objects/get-addon-list-item";
|
||||
import { AddonSearchResult } from "../../../models/wowup/addon-search-result";
|
||||
import { AddonSearchResultDependency } from "../../../models/wowup/addon-search-result-dependency";
|
||||
import { GetAddonListItemFilePropPipe } from "../../../pipes/get-addon-list-item-file-prop.pipe";
|
||||
import { DialogFactory } from "../../../services/dialog/dialog.factory";
|
||||
import * as SearchResults from "../../../utils/search-result.utils";
|
||||
|
||||
export interface PotentialAddonViewDetailsEvent {
|
||||
searchResult: AddonSearchResult;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { TableContextHeaderCellComponent } from "./table-context-header-cell.component";
|
||||
|
||||
describe("TableContextHeaderCellComponent", () => {
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { IHeaderAngularComp } from "ag-grid-angular";
|
||||
import { IHeaderParams } from "ag-grid-community";
|
||||
import { SessionService } from "../../services/session/session.service";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
|
||||
interface HeaderParams extends IHeaderParams {
|
||||
menuIcon: string;
|
||||
onHeaderContext: (event: MouseEvent) => void;
|
||||
@@ -0,0 +1,66 @@
|
||||
<div mat-dialog-title class="">
|
||||
<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 *ngIf="busy$ | async" class="wtf-backup-dialog">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col align-items-center">
|
||||
<mat-spinner diameter="55"></mat-spinner>
|
||||
<div>{{ this.busyText$ | async | translate: (busyTextParams$ | async) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-content *ngIf="(busy$ | async) === false" class="wtf-backup-dialog">
|
||||
<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 | relativeDuration }}</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 [disabled]="busy$ | async"
|
||||
[matTooltip]="'WTF_BACKUP.TOOL_TIP.APPLY_BUTTON' | translate" (click)="onClickApplyBackup(backup)">
|
||||
<mat-icon svgIcon="fas:history"> </mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" [disabled]="busy$ | async"
|
||||
[matTooltip]="'WTF_BACKUP.TOOL_TIP.DELETE_BUTTON' | translate" (click)="onClickDeleteBackup(backup)">
|
||||
<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>
|
||||
@@ -0,0 +1,36 @@
|
||||
.wtf-backup-dialog {
|
||||
width: 600px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.backup-list {
|
||||
background-color: var(--background-secondary-2);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.backup-list-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--background-secondary-1);
|
||||
}
|
||||
|
||||
.backup-list-item {
|
||||
padding: 1em;
|
||||
list-style: none;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
.status-badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-left-color: var(--background-primary);
|
||||
background-color: var(--background-secondary-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { BehaviorSubject, from, of } from "rxjs";
|
||||
import { catchError, first, map, switchMap } from "rxjs/operators";
|
||||
import { WowInstallation } from "../../../../common/warcraft/wow-installation";
|
||||
import { ElectronService } from "../../../services";
|
||||
import { DialogFactory } from "../../../services/dialog/dialog.factory";
|
||||
import { SessionService } from "../../../services/session/session.service";
|
||||
import { SnackbarService } from "../../../services/snackbar/snackbar.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 busyText$ = new BehaviorSubject<string>("");
|
||||
public readonly busyTextParams$ = new BehaviorSubject<any>({ count: "" });
|
||||
|
||||
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;
|
||||
|
||||
public constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _sessionService: SessionService,
|
||||
private _wtfService: WtfService,
|
||||
private _snackbarService: SnackbarService,
|
||||
private _dialogFactory: DialogFactory,
|
||||
private _translateService: TranslateService
|
||||
) {
|
||||
this.selectedInstallation = this._sessionService.getSelectedWowInstallation();
|
||||
this.backupPath = this._wtfService.getBackupPath(this.selectedInstallation);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.loadBackups().catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
public async onShowFolder(): Promise<void> {
|
||||
const backupPath = this._wtfService.getBackupPath(this.selectedInstallation);
|
||||
await this._electronService.showItemInFolder(backupPath);
|
||||
}
|
||||
|
||||
public onClickApplyBackup(backup: WtfBackupViewModel): void {
|
||||
const title = this._translateService.instant("WTF_BACKUP.APPLY_CONFIRMATION.TITLE");
|
||||
const message = this._translateService.instant("WTF_BACKUP.APPLY_CONFIRMATION.MESSAGE", { name: backup.title });
|
||||
const dialogRef = this._dialogFactory.getConfirmDialog(title, message);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((result) => {
|
||||
if (!result) {
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
this.busy$.next(true);
|
||||
this.busyText$.next("WTF_BACKUP.BUSY_TEXT.APPLYING_BACKUP");
|
||||
|
||||
return from(this._wtfService.applyBackup(backup.title, this.selectedInstallation)).pipe(map(() => true));
|
||||
}),
|
||||
catchError((e) => {
|
||||
console.error(e);
|
||||
this._snackbarService.showErrorSnackbar("WTF_BACKUP.ERROR.BACKUP_APPLY_FAILED", {
|
||||
timeout: 2000,
|
||||
localeArgs: {
|
||||
name: backup.title,
|
||||
},
|
||||
});
|
||||
|
||||
return of(false);
|
||||
})
|
||||
)
|
||||
.subscribe((result) => {
|
||||
this.busy$.next(false);
|
||||
|
||||
if (result === true) {
|
||||
this._snackbarService.showSuccessSnackbar("WTF_BACKUP.BACKUP_APPLY_SUCCESS", {
|
||||
timeout: 2000,
|
||||
localeArgs: {
|
||||
name: backup.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onClickDeleteBackup(backup: WtfBackupViewModel): void {
|
||||
const title = this._translateService.instant("WTF_BACKUP.DELETE_CONFIRMATION.TITLE");
|
||||
const message = this._translateService.instant("WTF_BACKUP.DELETE_CONFIRMATION.MESSAGE", { name: backup.title });
|
||||
const dialogRef = this._dialogFactory.getConfirmDialog(title, message);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((result) => {
|
||||
if (!result) {
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
this.busyText$.next("WTF_BACKUP.BUSY_TEXT.REMOVING_BACKUP");
|
||||
this.busy$.next(true);
|
||||
|
||||
return from(this._wtfService.deleteBackup(backup.title, this.selectedInstallation)).pipe(
|
||||
switchMap(() => from(this.loadBackups()))
|
||||
);
|
||||
}),
|
||||
catchError((e) => {
|
||||
console.error("Failed to delete backup", e);
|
||||
this._snackbarService.showErrorSnackbar("WTF_BACKUP.ERROR.FAILED_TO_DELETE", {
|
||||
timeout: 2000,
|
||||
localeArgs: {
|
||||
name: backup.title,
|
||||
},
|
||||
});
|
||||
|
||||
return of(undefined);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.busy$.next(false);
|
||||
});
|
||||
}
|
||||
|
||||
public async onCreateBackup(): Promise<void> {
|
||||
this.busyText$.next("WTF_BACKUP.BUSY_TEXT.CREATING_BACKUP");
|
||||
this.busy$.next(true);
|
||||
|
||||
try {
|
||||
await this._wtfService.createBackup(this.selectedInstallation, (count) => {
|
||||
this.busyTextParams$.next({ count });
|
||||
});
|
||||
await this.loadBackups();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.busy$.next(false);
|
||||
this.busyTextParams$.next({ count: "" });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadBackups() {
|
||||
this.busyText$.next("WTF_BACKUP.BUSY_TEXT.LOADING_BACKUPS");
|
||||
this.busy$.next(true);
|
||||
|
||||
try {
|
||||
const backups = await this._wtfService.getBackupList(this.selectedInstallation);
|
||||
const viewModels = backups.map((b) => this.toViewModel(b));
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { AlertDialogComponent } from "./alert-dialog.component";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
import { httpLoaderFactory } from "../../app.module";
|
||||
import { TranslateMessageFormatCompiler } from "ngx-translate-messageformat-compiler";
|
||||
import { MatModule } from "../../mat-module";
|
||||
|
||||
import { HttpClient, HttpClientModule } from "@angular/common/http";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { TranslateCompiler, TranslateLoader, TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { httpLoaderFactory } from "../../../app.module";
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { AlertDialogComponent } from "./alert-dialog.component";
|
||||
|
||||
describe("AlertDialogComponent", () => {
|
||||
beforeEach(async () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ElectronService } from "../../services";
|
||||
import { ElectronService } from "../../../services";
|
||||
|
||||
import { AnimatedLogoComponent } from "./animated-logo.component";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ElectronService } from "../../services";
|
||||
import { ElectronService } from "../../../services";
|
||||
|
||||
@Component({
|
||||
selector: "app-animated-logo",
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MAT_SNACK_BAR_DATA } from "@angular/material/snack-bar";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatModule } from "../../mat-module";
|
||||
|
||||
import { MatModule } from "../../../modules/mat-module";
|
||||
import { CenteredSnackbarComponent, CenteredSnackbarComponentData } from "./centered-snackbar.component";
|
||||
|
||||
describe("CenteredSnackbarComponent", () => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user