mirror of
https://github.com/WowUp/WowUp.git
synced 2026-05-24 22:46:45 -04:00
Merge remote-tracking branch 'upstream/electron' into feat/save-window-state
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
],
|
||||
"win": {
|
||||
"icon": "electron-build/icon.ico",
|
||||
"target": ["portable"]
|
||||
"target": ["nsis", "portable"]
|
||||
},
|
||||
"mac": {
|
||||
"icon": "dist/assets/icons",
|
||||
|
||||
50
wowup-electron/file.utils.js
Normal file
50
wowup-electron/file.utils.js
Normal file
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.readFile = exports.readDirRecursive = void 0;
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
function readDirRecursive(sourcePath) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dirFiles = [];
|
||||
fs.readdir(sourcePath, { withFileTypes: true }, (err, files) => __awaiter(this, void 0, void 0, function* () {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
for (let file of files) {
|
||||
const filePath = path.join(sourcePath, file.name);
|
||||
if (file.isDirectory()) {
|
||||
const nestedFiles = yield readDirRecursive(filePath);
|
||||
dirFiles.push(...nestedFiles);
|
||||
}
|
||||
else {
|
||||
dirFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
resolve(dirFiles);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.readDirRecursive = readDirRecursive;
|
||||
function readFile(sourcePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(sourcePath, { encoding: "utf-8" }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.readFile = readFile;
|
||||
//# sourceMappingURL=file.utils.js.map
|
||||
36
wowup-electron/file.utils.ts
Normal file
36
wowup-electron/file.utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
export async function readDirRecursive(sourcePath: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dirFiles: string[] = [];
|
||||
fs.readdir(sourcePath, { withFileTypes: true }, async (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
for (let 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);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(dirFiles);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function readFile(sourcePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(sourcePath, { encoding: "utf-8" }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,32 +1,48 @@
|
||||
import { ipcMain, shell } from "electron";
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CURSE_HASH_FILE_CHANNEL, LIST_DIRECTORIES_CHANNEL, LIST_FILES_CHANNEL, SHOW_DIRECTORY, PATH_EXISTS_CHANNEL } from './src/common/constants';
|
||||
import { CurseHashFileRequest } from './src/common/models/curse-hash-file-request';
|
||||
import { CurseHashFileResponse } from './src/common/models/curse-hash-file-response';
|
||||
import * as fs from "fs";
|
||||
import * as async from "async";
|
||||
|
||||
import { readDirRecursive } from "./file.utils";
|
||||
import {
|
||||
CURSE_HASH_FILE_CHANNEL,
|
||||
LIST_DIRECTORIES_CHANNEL,
|
||||
LIST_FILES_CHANNEL,
|
||||
SHOW_DIRECTORY,
|
||||
PATH_EXISTS_CHANNEL,
|
||||
CURSE_GET_SCAN_RESULTS,
|
||||
} from "./src/common/constants";
|
||||
import { CurseGetScanResultsRequest } from "./src/common/curse/curse-get-scan-results-request";
|
||||
import { CurseGetScanResultsResponse } from "./src/common/curse/curse-get-scan-results-response";
|
||||
import { CurseHashFileRequest } from "./src/common/models/curse-hash-file-request";
|
||||
import { CurseHashFileResponse } from "./src/common/models/curse-hash-file-response";
|
||||
import { ListFilesRequest } from "./src/common/models/list-files-request";
|
||||
import { ListFilesResponse } from "./src/common/models/list-files-response";
|
||||
import { ShowDirectoryRequest } from "./src/common/models/show-directory-request";
|
||||
import { ValueRequest } from "./src/common/models/value-request";
|
||||
import { ValueResponse } from "./src/common/models/value-response";
|
||||
import { CurseScanResult } from "./src/common/curse/curse-scan-result";
|
||||
import { CurseFolderScanner } from "./src/common/curse/curse-folder-scanner";
|
||||
|
||||
const nativeAddon = require('./build/Release/addon.node');
|
||||
const nativeAddon = require("./build/Release/addon.node");
|
||||
|
||||
ipcMain.on(SHOW_DIRECTORY, async (evt, arg: ShowDirectoryRequest) => {
|
||||
const result = await shell.openPath(arg.sourceDir);
|
||||
evt.reply(arg.responseKey, true);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => {
|
||||
// console.log(CURSE_HASH_FILE_CHANNEL, arg);
|
||||
|
||||
const response: CurseHashFileResponse = {
|
||||
fingerprint: 0
|
||||
fingerprint: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
if (arg.targetString !== undefined) {
|
||||
const strBuffer = Buffer.from(arg.targetString, arg.targetStringEncoding || 'ascii');
|
||||
const strBuffer = Buffer.from(
|
||||
arg.targetString,
|
||||
arg.targetStringEncoding || "ascii"
|
||||
);
|
||||
const hash = nativeAddon.computeHash(strBuffer, strBuffer.length);
|
||||
response.fingerprint = hash;
|
||||
evt.reply(arg.responseKey, response);
|
||||
@@ -52,9 +68,8 @@ ipcMain.on(CURSE_HASH_FILE_CHANNEL, async (evt, arg: CurseHashFileRequest) => {
|
||||
});
|
||||
|
||||
ipcMain.on(LIST_FILES_CHANNEL, async (evt, arg: ListFilesRequest) => {
|
||||
console.log('list files', arg);
|
||||
const response: ListFilesResponse = {
|
||||
files: []
|
||||
files: [],
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -73,19 +88,21 @@ ipcMain.on(LIST_DIRECTORIES_CHANNEL, (evt, arg: ValueRequest<string>) => {
|
||||
if (err) {
|
||||
response.error = err;
|
||||
} else {
|
||||
response.value = files.filter(file => file.isDirectory()).map(file => file.name);
|
||||
response.value = files
|
||||
.filter((file) => file.isDirectory())
|
||||
.map((file) => file.name);
|
||||
}
|
||||
|
||||
evt.reply(arg.responseKey, response);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on(PATH_EXISTS_CHANNEL, (evt, arg: ValueRequest<string>) => {
|
||||
const response: ValueResponse<boolean> = { value: false };
|
||||
|
||||
fs.open(arg.value, 'r', (err, fid) => {
|
||||
fs.open(arg.value, "r", (err, fid) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (err.code === "ENOENT") {
|
||||
response.value = false;
|
||||
} else {
|
||||
response.error = err;
|
||||
@@ -96,27 +113,32 @@ ipcMain.on(PATH_EXISTS_CHANNEL, (evt, arg: ValueRequest<string>) => {
|
||||
|
||||
evt.reply(arg.responseKey, response);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
async function readDirRecursive(sourcePath: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dirFiles: string[] = [];
|
||||
fs.readdir(sourcePath, { withFileTypes: true }, async (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
ipcMain.on(
|
||||
CURSE_GET_SCAN_RESULTS,
|
||||
async (evt, arg: CurseGetScanResultsRequest) => {
|
||||
const response: CurseGetScanResultsResponse = {
|
||||
scanResults: [],
|
||||
};
|
||||
|
||||
for (let 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)
|
||||
try {
|
||||
// Scan addon folders in parallel for speed!?
|
||||
const scanResults = await async.mapLimit<string, CurseScanResult>(
|
||||
arg.filePaths,
|
||||
2,
|
||||
async (folder, callback) => {
|
||||
const scanResult = await new CurseFolderScanner().scanFolder(folder);
|
||||
|
||||
callback(undefined, scanResult);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
resolve(dirFiles);
|
||||
});
|
||||
});
|
||||
}
|
||||
response.scanResults = scanResults;
|
||||
} catch (err) {
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
evt.reply(arg.responseKey, response);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,136 +1,160 @@
|
||||
import { app, BrowserWindow, screen, BrowserWindowConstructorOptions, Tray, Menu, nativeImage, ipcMain, MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import * as fs from 'fs';
|
||||
import { release, arch } from 'os';
|
||||
import * as electronDl from 'electron-dl';
|
||||
import * as admZip from 'adm-zip';
|
||||
import { DownloadRequest } from './src/common/models/download-request';
|
||||
import { DownloadStatus } from './src/common/models/download-status';
|
||||
import { DownloadStatusType } from './src/common/models/download-status-type';
|
||||
import { UnzipStatus } from './src/common/models/unzip-status';
|
||||
import { DOWNLOAD_FILE_CHANNEL, UNZIP_FILE_CHANNEL, COPY_FILE_CHANNEL, COPY_DIRECTORY_CHANNEL, DELETE_DIRECTORY_CHANNEL, RENAME_DIRECTORY_CHANNEL, READ_FILE_CHANNEL } from './src/common/constants';
|
||||
import { UnzipStatusType } from './src/common/models/unzip-status-type';
|
||||
import { UnzipRequest } from './src/common/models/unzip-request';
|
||||
import { CopyFileRequest } from './src/common/models/copy-file-request';
|
||||
import { CopyDirectoryRequest } from './src/common/models/copy-directory-request';
|
||||
import { DeleteDirectoryRequest } from './src/common/models/delete-directory-request';
|
||||
import { ReadFileRequest } from './src/common/models/read-file-request';
|
||||
import { ReadFileResponse } from './src/common/models/read-file-response';
|
||||
import './ipc-events';
|
||||
import { ncp } from 'ncp';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as log from 'electron-log';
|
||||
import { autoUpdater } from "electron-updater"
|
||||
import * as Store from 'electron-store'
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
screen,
|
||||
BrowserWindowConstructorOptions,
|
||||
Tray,
|
||||
Menu,
|
||||
nativeImage,
|
||||
ipcMain,
|
||||
MenuItem,
|
||||
MenuItemConstructorOptions,
|
||||
} from "electron";
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
import * as fs from "fs";
|
||||
import { release, arch } from "os";
|
||||
import * as electronDl from "electron-dl";
|
||||
import * as admZip from "adm-zip";
|
||||
import { DownloadRequest } from "./src/common/models/download-request";
|
||||
import { DownloadStatus } from "./src/common/models/download-status";
|
||||
import { DownloadStatusType } from "./src/common/models/download-status-type";
|
||||
import { UnzipStatus } from "./src/common/models/unzip-status";
|
||||
import {
|
||||
DOWNLOAD_FILE_CHANNEL,
|
||||
UNZIP_FILE_CHANNEL,
|
||||
COPY_FILE_CHANNEL,
|
||||
COPY_DIRECTORY_CHANNEL,
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
RENAME_DIRECTORY_CHANNEL,
|
||||
READ_FILE_CHANNEL,
|
||||
} from "./src/common/constants";
|
||||
import { UnzipStatusType } from "./src/common/models/unzip-status-type";
|
||||
import { UnzipRequest } from "./src/common/models/unzip-request";
|
||||
import { CopyFileRequest } from "./src/common/models/copy-file-request";
|
||||
import { CopyDirectoryRequest } from "./src/common/models/copy-directory-request";
|
||||
import { DeleteDirectoryRequest } from "./src/common/models/delete-directory-request";
|
||||
import { ReadFileRequest } from "./src/common/models/read-file-request";
|
||||
import { ReadFileResponse } from "./src/common/models/read-file-response";
|
||||
import "./ipc-events";
|
||||
import { ncp } from "ncp";
|
||||
import * as rimraf from "rimraf";
|
||||
import * as log from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import * as Store from "electron-store";
|
||||
import { readFile } from "./file.utils";
|
||||
import { WindowState } from './src/app/models/wowup/window-state';
|
||||
import { isBetween } from './src/app/utils/number.utils';
|
||||
import { Subject } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isWin = process.platform === 'win32';
|
||||
const preferenceStore = new Store({ name: 'preferences' });
|
||||
const isMac = process.platform === "darwin";
|
||||
const isWin = process.platform === "win32";
|
||||
const preferenceStore = new Store({ name: "preferences" });
|
||||
|
||||
let appIsQuitting = false;
|
||||
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.on('update-available', () => {
|
||||
log.info('AVAILABLE')
|
||||
win.webContents.send('update_available');
|
||||
autoUpdater.on("update-available", () => {
|
||||
log.info("AVAILABLE");
|
||||
win.webContents.send("update_available");
|
||||
});
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
log.info('DOWNLOADED')
|
||||
win.webContents.send('update_downloaded');
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
log.info("DOWNLOADED");
|
||||
win.webContents.send("update_downloaded");
|
||||
});
|
||||
|
||||
const appMenuTemplate: Array<MenuItemConstructorOptions | MenuItem> = isMac ? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: 'quit' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: "separator" },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
}
|
||||
] : [];
|
||||
const appMenuTemplate: Array<MenuItemConstructorOptions | MenuItem> = isMac
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [{ role: "quit" }],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "selectAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const appMenu = Menu.buildFromTemplate(appMenuTemplate);
|
||||
Menu.setApplicationMenu(appMenu);
|
||||
|
||||
const LOG_PATH = path.join(app.getPath('userData'), 'logs');
|
||||
const LOG_PATH = path.join(app.getPath("userData"), "logs");
|
||||
app.setAppLogsPath(LOG_PATH);
|
||||
log.transports.file.resolvePath = (variables: log.PathVariables, message?: log.LogMessage) => {
|
||||
console.log('RES', path.join(LOG_PATH, variables.fileName))
|
||||
log.transports.file.resolvePath = (
|
||||
variables: log.PathVariables,
|
||||
message?: log.LogMessage
|
||||
) => {
|
||||
console.log("RES", path.join(LOG_PATH, variables.fileName));
|
||||
return path.join(LOG_PATH, variables.fileName);
|
||||
}
|
||||
log.info('Main starting');
|
||||
};
|
||||
log.info("Main starting");
|
||||
|
||||
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
|
||||
app.commandLine.appendSwitch("disable-features", "OutOfBlinkCors");
|
||||
electronDl();
|
||||
|
||||
const USER_AGENT = `WowUp-Client/${app.getVersion()} (${release()}; ${arch()}; +https://wowup.io)`;
|
||||
log.info('USER_AGENT', USER_AGENT);
|
||||
log.info("USER_AGENT", USER_AGENT);
|
||||
|
||||
let win: BrowserWindow = null;
|
||||
let tray: Tray = null;
|
||||
|
||||
const args = process.argv.slice(1),
|
||||
serve = args.some(val => val === '--serve');
|
||||
serve = args.some((val) => val === "--serve");
|
||||
|
||||
function createTray() {
|
||||
console.log('TRAY')
|
||||
const trayIconPath = path.join(__dirname, 'assets', 'wowup_logo_512np.png');
|
||||
console.log("TRAY");
|
||||
const trayIconPath = path.join(__dirname, "assets", "wowup_logo_512np.png");
|
||||
const icon = nativeImage.createFromPath(trayIconPath).resize({ width: 16 });
|
||||
|
||||
tray = new Tray(icon)
|
||||
tray = new Tray(icon);
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: app.name, type: 'normal', icon: icon, enabled: false },
|
||||
{ label: app.name, type: "normal", icon: icon, enabled: false },
|
||||
{
|
||||
label: 'Show', click: () => {
|
||||
label: "Show",
|
||||
click: () => {
|
||||
win.show();
|
||||
|
||||
if (isMac) {
|
||||
app.dock.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{ role: 'quit' },
|
||||
{ role: "quit" },
|
||||
]);
|
||||
|
||||
if (isWin) {
|
||||
tray.on('click', function (event) {
|
||||
console.log('SHOW')
|
||||
tray.on("click", function (event) {
|
||||
console.log("SHOW");
|
||||
win.show();
|
||||
});
|
||||
}
|
||||
|
||||
tray.setToolTip('WowUp')
|
||||
tray.setContextMenu(contextMenu)
|
||||
tray.setToolTip("WowUp");
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
function windowStateManager(windowName: string, { width, height }: { width: number, height: number }) {
|
||||
@@ -191,7 +215,7 @@ function windowStateManager(windowName: string, { width, height }: { width: numb
|
||||
|
||||
setState();
|
||||
|
||||
return({
|
||||
return ({
|
||||
...windowState,
|
||||
monitorState,
|
||||
});
|
||||
@@ -211,11 +235,11 @@ function createWindow(): BrowserWindow {
|
||||
title: 'WowUp',
|
||||
titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegration: true,
|
||||
allowRunningInsecureContent: (serve) ? true : false,
|
||||
allowRunningInsecureContent: serve ? true : false,
|
||||
webSecurity: false,
|
||||
enableRemoteModule: true
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
minWidth: 900,
|
||||
minHeight: 550,
|
||||
@@ -251,7 +275,7 @@ function createWindow(): BrowserWindow {
|
||||
})
|
||||
|
||||
if (isMac) {
|
||||
win.on('close', (e) => {
|
||||
win.on("close", (e) => {
|
||||
if (appIsQuitting) {
|
||||
return;
|
||||
}
|
||||
@@ -259,28 +283,29 @@ function createWindow(): BrowserWindow {
|
||||
e.preventDefault();
|
||||
win.hide();
|
||||
|
||||
if (preferenceStore.get('collapse_to_tray') === true) {
|
||||
if (preferenceStore.get("collapse_to_tray") === true) {
|
||||
app.dock.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
win.once('closed', () => {
|
||||
win.once("closed", () => {
|
||||
win = null;
|
||||
})
|
||||
});
|
||||
|
||||
if (serve) {
|
||||
require('electron-reload')(__dirname, {
|
||||
electron: require(`${__dirname}/node_modules/electron`)
|
||||
require("electron-reload")(__dirname, {
|
||||
electron: require(`${__dirname}/node_modules/electron`),
|
||||
});
|
||||
win.loadURL('http://localhost:4200');
|
||||
|
||||
win.loadURL("http://localhost:4200");
|
||||
} else {
|
||||
win.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'dist/index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
}));
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, "dist/index.html"),
|
||||
protocol: "file:",
|
||||
slashes: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Emitted when the window is closed.
|
||||
@@ -291,7 +316,6 @@ function createWindow(): BrowserWindow {
|
||||
// win = null;
|
||||
// });
|
||||
|
||||
|
||||
// win.on('minimize', function (event) {
|
||||
// event.preventDefault();
|
||||
// win.hide();
|
||||
@@ -311,27 +335,27 @@ try {
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
// Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
|
||||
app.on('ready', () => {
|
||||
app.on("ready", () => {
|
||||
setTimeout(() => {
|
||||
createWindow();
|
||||
createTray();
|
||||
}, 400)
|
||||
}, 400);
|
||||
});
|
||||
|
||||
app.on('before-quit', (e) => {
|
||||
app.on("before-quit", (e) => {
|
||||
appIsQuitting = true;
|
||||
})
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
app.on("window-all-closed", () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (isMac) {
|
||||
@@ -343,7 +367,6 @@ try {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
// Catch Error
|
||||
// throw e;
|
||||
@@ -351,27 +374,26 @@ try {
|
||||
|
||||
ipcMain.on(DOWNLOAD_FILE_CHANNEL, async (evt, arg: DownloadRequest) => {
|
||||
try {
|
||||
const download = await electronDl.download(
|
||||
win,
|
||||
arg.url,
|
||||
{
|
||||
directory: arg.outputFolder,
|
||||
onProgress: (progress) => {
|
||||
win.webContents.send(arg.url, {
|
||||
type: DownloadStatusType.Progress,
|
||||
progress: parseFloat((progress.percent * 100.0).toFixed(2))
|
||||
} as DownloadStatus);
|
||||
}
|
||||
}
|
||||
);
|
||||
const download = await electronDl.download(win, arg.url, {
|
||||
directory: arg.outputFolder,
|
||||
onProgress: (progress) => {
|
||||
win.webContents.send(arg.url, {
|
||||
type: DownloadStatusType.Progress,
|
||||
progress: parseFloat((progress.percent * 100.0).toFixed(2)),
|
||||
} as DownloadStatus);
|
||||
},
|
||||
});
|
||||
|
||||
win.webContents.send(arg.url, {
|
||||
type: DownloadStatusType.Complete,
|
||||
savePath: download.getSavePath()
|
||||
savePath: download.getSavePath(),
|
||||
} as DownloadStatus);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
win.webContents.send(arg.url, { type: DownloadStatusType.Error, error: err } as DownloadStatus)
|
||||
win.webContents.send(arg.url, {
|
||||
type: DownloadStatusType.Error,
|
||||
error: err,
|
||||
} as DownloadStatus);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -383,7 +405,7 @@ ipcMain.on(UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => {
|
||||
zip.extractAllToAsync(outputFolder, true, (err) => {
|
||||
const status: UnzipStatus = {
|
||||
type: UnzipStatusType.Complete,
|
||||
outputFolder
|
||||
outputFolder,
|
||||
};
|
||||
|
||||
if (err) {
|
||||
@@ -391,47 +413,48 @@ ipcMain.on(UNZIP_FILE_CHANNEL, async (evt, arg: UnzipRequest) => {
|
||||
status.error = err;
|
||||
}
|
||||
|
||||
win.webContents.send(zipFilePath, status)
|
||||
win.webContents.send(zipFilePath, status);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_FILE_CHANNEL, async (evt, arg: CopyFileRequest) => {
|
||||
console.log('Copy File', arg);
|
||||
console.log("Copy File", arg);
|
||||
fs.copyFile(arg.sourceFilePath, arg.destinationFilePath, (err) => {
|
||||
win.webContents.send(arg.destinationFilePath, { error: err });
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(COPY_DIRECTORY_CHANNEL, async (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log('Copy Dir', arg);
|
||||
console.log("Copy Dir", arg);
|
||||
ncp(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
win.webContents.send(arg.destinationPath, err);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(DELETE_DIRECTORY_CHANNEL, async (evt, arg: DeleteDirectoryRequest) => {
|
||||
console.log('Delete Dir', arg);
|
||||
rimraf(arg.sourcePath, (err) => {
|
||||
win.webContents.send(arg.sourcePath, err);
|
||||
});
|
||||
});
|
||||
ipcMain.on(
|
||||
DELETE_DIRECTORY_CHANNEL,
|
||||
async (evt, arg: DeleteDirectoryRequest) => {
|
||||
console.log("Delete Dir", arg);
|
||||
rimraf(arg.sourcePath, (err) => {
|
||||
win.webContents.send(arg.sourcePath, err);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(RENAME_DIRECTORY_CHANNEL, async (evt, arg: CopyDirectoryRequest) => {
|
||||
console.log('Rename Dir', arg);
|
||||
console.log("Rename Dir", arg);
|
||||
fs.rename(arg.sourcePath, arg.destinationPath, (err) => {
|
||||
win.webContents.send(arg.destinationPath, err);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(READ_FILE_CHANNEL, async (evt, arg: ReadFileRequest) => {
|
||||
// console.log('Read File', arg);
|
||||
fs.readFile(arg.sourcePath, { encoding: 'utf-8' }, (err, data) => {
|
||||
const response: ReadFileResponse = {
|
||||
data: data,
|
||||
error: err
|
||||
}
|
||||
win.webContents.send(arg.sourcePath, response);
|
||||
});
|
||||
const response: ReadFileResponse = { data: "" };
|
||||
try {
|
||||
response.data = await readFile(arg.sourcePath);
|
||||
} catch (err) {
|
||||
response.error = err;
|
||||
}
|
||||
win.webContents.send(arg.sourcePath, response);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@types/adm-zip": "0.4.33",
|
||||
"@types/async": "3.2.3",
|
||||
"@types/globrex": "0.1.0",
|
||||
"@types/jasmine": "3.5.14",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
@@ -109,6 +110,7 @@
|
||||
"@angular/material": "10.2.3",
|
||||
"@types/lodash": "4.14.161",
|
||||
"adm-zip": "0.4.16",
|
||||
"async": "3.2.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"conf": "7.1.2",
|
||||
"electron-dl": "3.0.2",
|
||||
|
||||
@@ -2,27 +2,27 @@ import { AddonProvider } from "./addon-provider";
|
||||
import { WowClientType } from "../models/warcraft/wow-client-type";
|
||||
import { Addon } from "../entities/addon";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { CurseSearchResult } from "../models/curse/curse-search-result";
|
||||
import { map, mergeMap } from "rxjs/operators";
|
||||
import { CurseFile } from "../models/curse/curse-file";
|
||||
import { map } from "rxjs/operators";
|
||||
import * as _ from "lodash";
|
||||
import * as fp from "lodash/fp";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { from, Observable, of } from "rxjs";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { AddonSearchResultFile } from "../models/wowup/addon-search-result-file";
|
||||
import { CurseReleaseType } from "../models/curse/curse-release-type";
|
||||
import { AddonChannelType } from "../models/wowup/addon-channel-type";
|
||||
import { PotentialAddon } from "../models/wowup/potential-addon";
|
||||
import { CurseGetFeaturedResponse } from "../models/curse/curse-get-featured-response";
|
||||
import { CachingService } from "app/services/caching/caching-service";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
import { CurseFolderScanner } from "./curse/curse-folder-scanner";
|
||||
import { ElectronService } from "app/services";
|
||||
import { CurseScanResult } from "../models/curse/curse-scan-result";
|
||||
import { CurseFingerprintsResponse } from "app/models/curse/curse-fingerprint-response";
|
||||
import { CurseMatch } from "app/models/curse/curse-match";
|
||||
import { AppCurseScanResult } from "../models/curse/app-curse-scan-result";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { CURSE_GET_SCAN_RESULTS } from "common/constants";
|
||||
import { CurseGetScanResultsRequest } from "common/curse/curse-get-scan-results-request";
|
||||
import { CurseGetScanResultsResponse } from "common/curse/curse-get-scan-results-response";
|
||||
import { CurseMatch } from "common/curse/curse-match";
|
||||
import { CurseFingerprintsResponse } from "../models/curse/curse-fingerprint-response";
|
||||
import { CurseSearchResult } from "../../common/curse/curse-search-result";
|
||||
import { CurseFile } from "common/curse/curse-file";
|
||||
import { CurseReleaseType } from "common/curse/curse-release-type";
|
||||
import { CurseGetFeaturedResponse } from "app/models/curse/curse-get-featured-response";
|
||||
|
||||
const API_URL = "https://addons-ecs.forgesvc.net/api/v2";
|
||||
|
||||
@@ -32,9 +32,8 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) { }
|
||||
private _electronService: ElectronService
|
||||
) {}
|
||||
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
@@ -53,13 +52,11 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
|
||||
console.log("mapAddonFolders");
|
||||
|
||||
const addonIds = fp.flow(
|
||||
fp.filter((sr: CurseScanResult) => !!sr.exactMatch),
|
||||
fp.map((sr: CurseScanResult) => sr.exactMatch.id),
|
||||
fp.uniq
|
||||
)(scanResults);
|
||||
|
||||
// console.log(_.sortBy(addonIds).join('\n'));
|
||||
const matchedScanResults = scanResults.filter((sr) => !!sr.exactMatch);
|
||||
const matchedScanResultIds = matchedScanResults.map(
|
||||
(sr) => sr.exactMatch.id
|
||||
);
|
||||
const addonIds = _.uniq(matchedScanResultIds);
|
||||
|
||||
var addonResults = await this.getAllIds(addonIds).toPromise();
|
||||
|
||||
@@ -68,6 +65,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
(sr) => sr.addonFolder.name === addonFolder.name
|
||||
);
|
||||
if (!scanResult.exactMatch) {
|
||||
console.log("No search result match", scanResult.directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -75,16 +73,21 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
(addonResult) => addonResult.id === scanResult.exactMatch.id
|
||||
);
|
||||
if (!scanResult.searchResult) {
|
||||
console.log("No search result match", scanResult.directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
addonFolder.matchingAddon = this.getAddon(
|
||||
const newAddon = this.getAddon(
|
||||
clientType,
|
||||
addonChannelType,
|
||||
scanResult
|
||||
);
|
||||
|
||||
addonFolder.matchingAddon = newAddon;
|
||||
} catch (err) {
|
||||
console.error(scanResult);
|
||||
console.error(err);
|
||||
// TODO
|
||||
// _analyticsService.Track(ex, $"Failed to create addon for result {scanResult.FolderScanner.Fingerprint}");
|
||||
}
|
||||
@@ -92,9 +95,13 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
}
|
||||
|
||||
private async mapAddonFolders(
|
||||
scanResults: CurseScanResult[],
|
||||
scanResults: AppCurseScanResult[],
|
||||
clientType: WowClientType
|
||||
) {
|
||||
if (clientType === WowClientType.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(
|
||||
scanResults.map((result) => result.fingerprint)
|
||||
).toPromise();
|
||||
@@ -115,7 +122,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
if (!scanResult.exactMatch) {
|
||||
scanResult.exactMatch = fingerprintResponse.partialMatches.find(
|
||||
(partialMatch) =>
|
||||
partialMatch.file.modules.some(
|
||||
partialMatch.file?.modules?.some(
|
||||
(module) => module.fingerprint === scanResult.fingerprint
|
||||
)
|
||||
);
|
||||
@@ -124,7 +131,7 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
}
|
||||
|
||||
private hasMatchingFingerprint(
|
||||
scanResult: CurseScanResult,
|
||||
scanResult: AppCurseScanResult,
|
||||
exactMatch: CurseMatch
|
||||
) {
|
||||
return exactMatch.file.modules.some(
|
||||
@@ -157,30 +164,40 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
return this._httpClient.post<CurseSearchResult[]>(url, addonIds);
|
||||
}
|
||||
|
||||
private async getScanResults(
|
||||
private getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<CurseScanResult[]> {
|
||||
const scanResults: CurseScanResult[] = [];
|
||||
|
||||
): Promise<AppCurseScanResult[]> => {
|
||||
const t1 = Date.now();
|
||||
|
||||
// Scan addon folders in parallel for speed!?
|
||||
for (let folder of addonFolders) {
|
||||
const scanResult = await new CurseFolderScanner(
|
||||
this._electronService,
|
||||
this._fileService
|
||||
).scanFolder(folder);
|
||||
scanResults.push(scanResult);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseGetScanResultsResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
const appScanResults: AppCurseScanResult[] = arg.scanResults.map(
|
||||
(scanResult) => {
|
||||
const addonFolder = addonFolders.find(
|
||||
(af) => af.path === scanResult.directory
|
||||
);
|
||||
|
||||
// const str = _.orderBy(scanResults, sr => sr.folderName.toLowerCase())
|
||||
// .map(sr => `${sr.fingerprint} ${sr.folderName}`).join('\n');
|
||||
// console.log(str);
|
||||
return Object.assign({}, scanResult, { addonFolder });
|
||||
}
|
||||
);
|
||||
|
||||
return scanResults;
|
||||
}
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
resolve(appScanResults);
|
||||
};
|
||||
|
||||
const request: CurseGetScanResultsRequest = {
|
||||
filePaths: addonFolders.map((addonFolder) => addonFolder.path),
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_GET_SCAN_RESULTS, request);
|
||||
});
|
||||
};
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
@@ -460,10 +477,22 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private getWowUpChannel(releaseType: CurseReleaseType): AddonChannelType {
|
||||
switch (releaseType) {
|
||||
case CurseReleaseType.Alpha:
|
||||
return AddonChannelType.Alpha;
|
||||
case CurseReleaseType.Beta:
|
||||
return AddonChannelType.Beta;
|
||||
case CurseReleaseType.Release:
|
||||
default:
|
||||
return AddonChannelType.Stable;
|
||||
}
|
||||
}
|
||||
|
||||
private getAddon(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
scanResult: CurseScanResult
|
||||
scanResult: AppCurseScanResult
|
||||
): Addon {
|
||||
const currentVersion = scanResult.exactMatch.file;
|
||||
const authors = scanResult.searchResult.authors
|
||||
@@ -476,17 +505,25 @@ export class CurseAddonProvider implements AddonProvider {
|
||||
scanResult.searchResult,
|
||||
clientType
|
||||
);
|
||||
const latestVersion = latestFiles.find(
|
||||
|
||||
let channelType = addonChannelType;
|
||||
let latestVersion = latestFiles.find(
|
||||
(lf) => this.getChannelType(lf.releaseType) <= addonChannelType
|
||||
);
|
||||
|
||||
// If there were no releases that met the channel type restrictions
|
||||
if (!latestVersion) {
|
||||
latestVersion = _.first(latestFiles);
|
||||
channelType = this.getWowUpChannel(latestVersion.releaseType);
|
||||
}
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
author: authors,
|
||||
name: scanResult.searchResult.name,
|
||||
channelType: addonChannelType,
|
||||
channelType,
|
||||
autoUpdateEnabled: false,
|
||||
clientType: clientType,
|
||||
clientType,
|
||||
downloadUrl: latestVersion.downloadUrl,
|
||||
externalUrl: scanResult.searchResult.websiteUrl,
|
||||
externalId: scanResult.searchResult.id.toString(),
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import { FileService } from "app/services/files/file.service";
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { ElectronService } from "app/services";
|
||||
import { CurseHashFileResponse } from "common/models/curse-hash-file-response";
|
||||
import { CurseHashFileRequest } from "common/models/curse-hash-file-request";
|
||||
import { CURSE_HASH_FILE_CHANNEL } from "common/constants";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { CurseScanResult } from "../../models/curse/curse-scan-result";
|
||||
import { from } from "rxjs";
|
||||
import { mergeMap } from "rxjs/operators";
|
||||
|
||||
export class CurseFolderScanner {
|
||||
|
||||
constructor(
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService
|
||||
) { }
|
||||
|
||||
private get tocFileCommentsRegex() {
|
||||
return /\s*#.*$/mg;
|
||||
}
|
||||
|
||||
private get tocFileIncludesRegex() {
|
||||
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/mig;
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^\/]+)[\\\/]\1\.toc$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlIncludesRegex() {
|
||||
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/ig;
|
||||
}
|
||||
|
||||
private get bindingsXmlCommentsRegex() {
|
||||
return /<!--.*?-->/gs;
|
||||
}
|
||||
|
||||
async scanFolder(addonFolder: AddonFolder): Promise<CurseScanResult> {
|
||||
const folderPath = addonFolder.path;
|
||||
const files = await this._fileService.listAllFiles(folderPath);
|
||||
console.log('listAllFiles', folderPath, files.length);
|
||||
|
||||
let matchingFiles = await this.getMatchingFiles(folderPath, files);
|
||||
matchingFiles = _.sortBy(matchingFiles, f => f.toLowerCase());
|
||||
|
||||
// console.log('matching files', matchingFiles.length)
|
||||
// const fst = matchingFiles.map(f => f.toLowerCase()).join('\n');
|
||||
|
||||
const individualFingerprints: number[] = [];
|
||||
for (let path of matchingFiles) {
|
||||
const normalizedFileHash = await this.computeNormalizedFileHash(path);
|
||||
individualFingerprints.push(normalizedFileHash);
|
||||
}
|
||||
|
||||
const hashConcat = _.orderBy(individualFingerprints).join('');
|
||||
const fingerprint = await this.computeStringHash(hashConcat);
|
||||
console.log('fingerprint', fingerprint);
|
||||
|
||||
return {
|
||||
directory: folderPath,
|
||||
fileCount: matchingFiles.length,
|
||||
fingerprint,
|
||||
folderName: path.basename(folderPath),
|
||||
individualFingerprints,
|
||||
addonFolder
|
||||
};
|
||||
}
|
||||
|
||||
private async getMatchingFiles(folderPath: string, filePaths: string[]): Promise<string[]> {
|
||||
const parentDir = path.dirname(folderPath) + path.sep;
|
||||
const matchingFileList: string[] = [];
|
||||
const fileInfoList: string[] = [];
|
||||
for (let filePath of filePaths) {
|
||||
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), '');
|
||||
|
||||
if (this.tocFileRegex.test(input)) {
|
||||
fileInfoList.push(filePath);
|
||||
} else if (this.bindingsXmlRegex.test(input)) {
|
||||
matchingFileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('fileInfoList', fileInfoList.length)
|
||||
for (let fileInfo of fileInfoList) {
|
||||
await this.processIncludeFile(matchingFileList, fileInfo);
|
||||
}
|
||||
|
||||
return matchingFileList;
|
||||
}
|
||||
|
||||
private async processIncludeFile(matchingFileList: string[], fileInfo: string) {
|
||||
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchingFileList.push(fileInfo);
|
||||
|
||||
let input = await this._fileService.readFile(fileInfo);
|
||||
input = this.removeComments(fileInfo, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(fileInfo, input);
|
||||
if (!inclusions || !inclusions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dirname = path.dirname(fileInfo);
|
||||
for (let include of inclusions) {
|
||||
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
|
||||
await this.processIncludeFile(matchingFileList, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private getFileInclusionMatches(fileInfo: string, fileContent: string): string[] | null {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case '.xml':
|
||||
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
|
||||
case '.toc':
|
||||
return this.matchAll(fileContent, this.tocFileIncludesRegex);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private removeComments(fileInfo: string, fileContent: string): string {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case '.xml':
|
||||
return fileContent.replace(this.bindingsXmlCommentsRegex, '');
|
||||
case '.toc':
|
||||
return fileContent.replace(this.tocFileCommentsRegex, '');
|
||||
default:
|
||||
return fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
private matchAll(str: string, regex: RegExp): string[] {
|
||||
const matches: string[] = [];
|
||||
let currentMatch: RegExpExecArray;
|
||||
do {
|
||||
currentMatch = regex.exec(str);
|
||||
if (currentMatch) {
|
||||
matches.push(currentMatch[1]);
|
||||
}
|
||||
} while (currentMatch);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private computeNormalizedFileHash(filePath: string) {
|
||||
return this.computeFileHash(filePath, true);
|
||||
}
|
||||
|
||||
private computeFileHash(filePath: string, normalizeWhitespace: boolean) {
|
||||
return this.computeHash(filePath, 0, normalizeWhitespace);
|
||||
}
|
||||
|
||||
private computeStringHash(str: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
const request: CurseHashFileRequest = {
|
||||
targetString: str,
|
||||
targetStringEncoding: 'ascii',
|
||||
responseKey: uuidv4(),
|
||||
normalizeWhitespace: false,
|
||||
precomputedLength: 0
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
private computeHash(
|
||||
filePath: string,
|
||||
precomputedLength: number = 0,
|
||||
normalizeWhitespace: boolean = false
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
resolve(arg.fingerprint);
|
||||
};
|
||||
|
||||
const request: CurseHashFileRequest = {
|
||||
responseKey: uuidv4(),
|
||||
filePath,
|
||||
normalizeWhitespace,
|
||||
precomputedLength
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,36 @@
|
||||
import { AfterViewInit, Component } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AppConfig } from '../environments/environment';
|
||||
import { TelemetryDialogComponent } from './components/telemetry-dialog/telemetry-dialog.component';
|
||||
import { ElectronService } from './services';
|
||||
import { AnalyticsService } from './services/analytics/analytics.service';
|
||||
import { WarcraftService } from './services/warcraft/warcraft.service';
|
||||
import { WowUpService } from './services/wowup/wowup.service';
|
||||
import { AfterViewInit, Component } from "@angular/core";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { AppConfig } from "../environments/environment";
|
||||
import { TelemetryDialogComponent } from "./components/telemetry-dialog/telemetry-dialog.component";
|
||||
import { ElectronService } from "./services";
|
||||
import { AddonService } from "./services/addons/addon.service";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { WarcraftService } from "./services/warcraft/warcraft.service";
|
||||
import { WowUpService } from "./services/wowup/wowup.service";
|
||||
|
||||
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
selector: "app-root",
|
||||
templateUrl: "./app.component.html",
|
||||
styleUrls: ["./app.component.scss"],
|
||||
})
|
||||
export class AppComponent implements AfterViewInit {
|
||||
private _autoUpdateInterval?: number;
|
||||
|
||||
constructor(
|
||||
private _analyticsService: AnalyticsService,
|
||||
private electronService: ElectronService,
|
||||
private translate: TranslateService,
|
||||
private warcraft: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _dialog: MatDialog
|
||||
private _dialog: MatDialog,
|
||||
private _addonService: AddonService
|
||||
) {
|
||||
this.translate.setDefaultLang('en');
|
||||
this.translate.setDefaultLang("en");
|
||||
|
||||
this.translate.use(this.electronService.locale);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
@@ -31,15 +39,26 @@ export class AppComponent implements AfterViewInit {
|
||||
} else {
|
||||
// TODO track startup
|
||||
}
|
||||
|
||||
this.onAutoUpdateInterval();
|
||||
this._autoUpdateInterval = window.setInterval(
|
||||
this.onAutoUpdateInterval,
|
||||
AUTO_UPDATE_PERIOD_MS
|
||||
);
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this._dialog.open(TelemetryDialogComponent, {
|
||||
disableClose: true
|
||||
disableClose: true,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this._wowUpService.telemetryEnabled = result;
|
||||
});
|
||||
}
|
||||
|
||||
private onAutoUpdateInterval = async () => {
|
||||
console.log("Auto update");
|
||||
const updateCount = await this._addonService.processAutoUpdates();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import 'reflect-metadata';
|
||||
import '../polyfills';
|
||||
import "reflect-metadata";
|
||||
import "../polyfills";
|
||||
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ErrorHandler, InjectionToken, NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { ErrorHandler, NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import {
|
||||
HttpClientModule,
|
||||
HttpClient,
|
||||
HTTP_INTERCEPTORS,
|
||||
} from "@angular/common/http";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
// NG Translate
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
|
||||
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
|
||||
|
||||
import { HomeModule } from './pages/home/home.module';
|
||||
import { HomeModule } from "./pages/home/home.module";
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { TitlebarComponent } from './components/titlebar/titlebar.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { DefaultHeadersInterceptor } from './interceptors/default-headers.interceptor';
|
||||
import { AnalyticsService } from './services/analytics/analytics.service';
|
||||
import { DirectiveModule } from './directive.module';
|
||||
import { MatModule } from './mat-module';
|
||||
import { MatProgressButtonsModule } from 'mat-progress-buttons';
|
||||
import { AppComponent } from "./app.component";
|
||||
import { TitlebarComponent } from "./components/titlebar/titlebar.component";
|
||||
import { FooterComponent } from "./components/footer/footer.component";
|
||||
import { DefaultHeadersInterceptor } from "./interceptors/default-headers.interceptor";
|
||||
import { AnalyticsService } from "./services/analytics/analytics.service";
|
||||
import { DirectiveModule } from "./directive.module";
|
||||
import { MatModule } from "./mat-module";
|
||||
import { MatProgressButtonsModule } from "mat-progress-buttons";
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
return new TranslateHttpLoader(http, "./assets/i18n/", ".json");
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
TitlebarComponent,
|
||||
FooterComponent,
|
||||
],
|
||||
declarations: [AppComponent, TitlebarComponent, FooterComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
@@ -50,16 +50,19 @@ export function httpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: httpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
deps: [HttpClient],
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
|
||||
],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true },
|
||||
{ provide: ErrorHandler, useClass: AnalyticsService }
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: DefaultHeadersInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: AnalyticsService },
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class MyAddonsListItem {
|
||||
return AddonDisplayState.Install;
|
||||
}
|
||||
|
||||
if (this.addon.installedVersion != this.addon.latestVersion) {
|
||||
if (this.addon.installedVersion !== this.addon.latestVersion) {
|
||||
return AddonDisplayState.Update;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ export class MyAddonsListItem {
|
||||
}
|
||||
|
||||
public onClicked() {
|
||||
console.log(this.addon.name);
|
||||
this.selected = !this.selected;
|
||||
}
|
||||
|
||||
@@ -79,6 +78,7 @@ export class MyAddonsListItem {
|
||||
case AddonDisplayState.Install:
|
||||
case AddonDisplayState.Unknown:
|
||||
default:
|
||||
console.log('Unhandled display state', this.displayState)
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev">
|
||||
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
|
||||
</a>
|
||||
<div>{{sessionService.statusText$ | async}}</div>
|
||||
<div>v{{wowUpService.applicationVersion}}</div>
|
||||
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
|
||||
<div class="row">
|
||||
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>
|
||||
<p>v{{wowUpService.applicationVersion}}</p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -7,9 +7,13 @@ footer {
|
||||
height: 25px;
|
||||
padding: 0.25em 0.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a, img {
|
||||
height: 25px;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Component, NgZone, OnInit } from '@angular/core';
|
||||
import { SessionService } from 'app/services/session/session.service';
|
||||
import { WowUpService } from 'app/services/wowup/wowup.service';
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { WowUpService } from "app/services/wowup/wowup.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
selector: "app-footer",
|
||||
templateUrl: "./footer.component.html",
|
||||
styleUrls: ["./footer.component.scss"],
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private _zone: NgZone,
|
||||
public wowUpService: WowUpService,
|
||||
public sessionService: SessionService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
// Force the angular zone to pump for every progress update since its outside the zone
|
||||
this.sessionService.statusText$
|
||||
.subscribe(text => {
|
||||
this._zone.run(() => { });
|
||||
})
|
||||
}
|
||||
this.sessionService.statusText$.subscribe((text) => {
|
||||
this._zone.run(() => {});
|
||||
});
|
||||
|
||||
this.sessionService.pageContextText$.subscribe((text) => {
|
||||
this._zone.run(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
<div class="title-container">
|
||||
<div>WowUp.io</div>
|
||||
</div>
|
||||
<div class="window-control-container">
|
||||
<div *ngIf="isMac" class="window-control-container">
|
||||
<mat-icon class="debug-button" (click)="onClickDebug()">bug_report</mat-icon>
|
||||
</div>
|
||||
<div *ngIf="isWindows" class="window-control-container">
|
||||
<mat-icon (click)="onClickDebug()">bug_report</mat-icon>
|
||||
<div class="window-control" (click)="electronService.minimizeWindow()">
|
||||
<img src="assets/chrome-minimize.svg" />
|
||||
@@ -21,4 +24,4 @@
|
||||
<img src="assets/chrome-close.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.debug-button {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
.titlebar-mac {
|
||||
height: 22px;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { CurseScanResult } from "common/curse/curse-scan-result";
|
||||
|
||||
export interface AppCurseScanResult extends CurseScanResult {
|
||||
addonFolder?: AddonFolder;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CurseMatch } from "./curse-match";
|
||||
import { CurseMatch } from "../../../common/curse/curse-match";
|
||||
|
||||
export interface CurseFingerprintsResponse {
|
||||
isCacheBuild: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CurseSearchResult } from './curse-search-result';
|
||||
import { CurseSearchResult } from "../../../common/curse/curse-search-result";
|
||||
|
||||
export interface CurseGetFeaturedResponse {
|
||||
Featured: CurseSearchResult[];
|
||||
Popular: CurseSearchResult[];
|
||||
RecentlyUpdated: CurseSearchResult[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { remote } from 'electron'
|
||||
import { ChangeLog } from '../../models/wowup/change-log';
|
||||
|
||||
@@ -12,6 +12,8 @@ import { ElectronService } from 'app/services';
|
||||
styleUrls: ['./about.component.scss']
|
||||
})
|
||||
export class AboutComponent implements OnInit {
|
||||
|
||||
@Input('tabIndex') tabIndex: number;
|
||||
|
||||
public version = '';
|
||||
public changeLogs: ChangeLog[] = ChangeLogJson.ChangeLogs;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { Component, Input, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { AddonDetailComponent } from "app/components/addon-detail/addon-detail.component";
|
||||
import { InstallFromUrlDialogComponent } from "app/components/install-from-url-dialog/install-from-url-dialog.component";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { AddonDetailModel } from "app/models/wowup/addon-detail.model";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { ColumnState } from "app/models/wowup/column-state";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
import { ElectronService } from "app/services";
|
||||
@@ -13,9 +12,9 @@ import { SessionService } from "app/services/session/session.service";
|
||||
import { WarcraftService } from "app/services/warcraft/warcraft.service";
|
||||
import { BehaviorSubject, Subject, Subscription } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import * as _ from 'lodash';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { MatSort } from "@angular/material/sort";
|
||||
import * as _ from "lodash";
|
||||
|
||||
@Component({
|
||||
selector: "app-get-addons",
|
||||
@@ -23,12 +22,16 @@ import * as _ from 'lodash';
|
||||
styleUrls: ["./get-addons.component.scss"],
|
||||
})
|
||||
export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
@Input("tabIndex") tabIndex: number;
|
||||
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<PotentialAddon[]>([]);
|
||||
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<PotentialAddon[]>(
|
||||
[]
|
||||
);
|
||||
private readonly _destroyed$ = new Subject<void>();
|
||||
private subscriptions: Subscription[] = [];
|
||||
private isSelectedTab: boolean = false;
|
||||
|
||||
public dataSource = new MatTableDataSource<PotentialAddon>([]);
|
||||
|
||||
@@ -53,33 +56,45 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
private _dialog: MatDialog,
|
||||
public electronService: ElectronService,
|
||||
public warcraftService: WarcraftService
|
||||
) { }
|
||||
) {
|
||||
_sessionService.selectedHomeTab$.subscribe((tabIndex) => {
|
||||
this.isSelectedTab = tabIndex === this.tabIndex;
|
||||
if (this.isSelectedTab) {
|
||||
this.setPageContextText();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const selectedClientSubscription = this._sessionService.selectedClientType$.pipe(
|
||||
map((clientType) => {
|
||||
this.selectedClient = clientType;
|
||||
this.loadPopularAddons(this.selectedClient);
|
||||
})
|
||||
).subscribe();
|
||||
const selectedClientSubscription = this._sessionService.selectedClientType$
|
||||
.pipe(
|
||||
map((clientType) => {
|
||||
this.selectedClient = clientType;
|
||||
this.loadPopularAddons(this.selectedClient);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const addonRemovedSubscription = this._addonService.addonRemoved$.pipe(
|
||||
map((event: string) => {
|
||||
this.onRefresh();
|
||||
})
|
||||
).subscribe();
|
||||
const addonRemovedSubscription = this._addonService.addonRemoved$
|
||||
.pipe(
|
||||
map((event: string) => {
|
||||
this.onRefresh();
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const displayAddonSubscription = this._displayAddonsSrc
|
||||
.subscribe((items: PotentialAddon[]) => {
|
||||
const displayAddonSubscription = this._displayAddonsSrc.subscribe(
|
||||
(items: PotentialAddon[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.dataSource.sortingDataAccessor = _.get;
|
||||
this.dataSource.sort = this.sort;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.subscriptions = [
|
||||
selectedClientSubscription,
|
||||
addonRemovedSubscription,
|
||||
displayAddonSubscription
|
||||
displayAddonSubscription,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -110,6 +125,7 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
async onSearch() {
|
||||
if (!this.query) {
|
||||
this.loadPopularAddons(this.selectedClient);
|
||||
this.setPageContextText();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,6 +140,7 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
this.formatAddons(searchResults);
|
||||
this._displayAddonsSrc.next(searchResults);
|
||||
this.isBusy = false;
|
||||
this.setPageContextText();
|
||||
}
|
||||
|
||||
openDetailDialog(addon: PotentialAddon) {
|
||||
@@ -171,4 +188,12 @@ export class GetAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setPageContextText() {
|
||||
const contextStr = this._displayAddonsSrc.value?.length
|
||||
? `${this._displayAddonsSrc.value.length} results`
|
||||
: "";
|
||||
|
||||
this._sessionService.setContextText(this.tabIndex, contextStr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
<div class="page-container">
|
||||
<div class="tabs">
|
||||
<mat-tab-group mat-align-tabs="center" [backgroundColor]="'primary'"
|
||||
[disablePagination]="true" [(selectedIndex)]="selectedIndex"
|
||||
(selectedIndexChange)="onSelectedIndexChange($event)">
|
||||
<mat-tab-group mat-align-tabs="center" [backgroundColor]="'primary'" [disablePagination]="true"
|
||||
[(selectedIndex)]="selectedIndex" (selectedIndexChange)="onSelectedIndexChange($event)">
|
||||
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.MY_ADDONS_TAB_TITLE' | translate">
|
||||
<app-my-addons></app-my-addons>
|
||||
<app-my-addons [tabIndex]="0"></app-my-addons>
|
||||
</mat-tab>
|
||||
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.GET_ADDONS_TAB_TITLE' | translate">
|
||||
<app-get-addons></app-get-addons>
|
||||
<app-get-addons [tabIndex]="1"></app-get-addons>
|
||||
</mat-tab>
|
||||
<mat-tab [disabled]="hasWowClient !== true" [label]="'PAGES.HOME.ABOUT_TAB_TITLE' | translate">
|
||||
<app-about></app-about>
|
||||
<app-about [tabIndex]="2"></app-about>
|
||||
</mat-tab>
|
||||
<mat-tab [label]="'PAGES.HOME.OPTIONS_TAB_TITLE' | translate">
|
||||
<app-options></app-options>
|
||||
<app-options [tabIndex]="3"></app-options>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@@ -15,21 +15,18 @@ export class HomeComponent implements OnInit {
|
||||
private _sessionService: SessionService,
|
||||
private _warcraftService: WarcraftService
|
||||
) {
|
||||
this._warcraftService.installedClientTypes$
|
||||
.subscribe((clientTypes) => {
|
||||
if(clientTypes === undefined){
|
||||
this.hasWowClient = false;
|
||||
this.selectedIndex = 3;
|
||||
} else {
|
||||
this.hasWowClient = clientTypes.length > 0;
|
||||
this.selectedIndex = this.hasWowClient ? 0 : 3;
|
||||
}
|
||||
});
|
||||
this._warcraftService.installedClientTypes$.subscribe((clientTypes) => {
|
||||
if (clientTypes === undefined) {
|
||||
this.hasWowClient = false;
|
||||
this.selectedIndex = 3;
|
||||
} else {
|
||||
this.hasWowClient = clientTypes.length > 0;
|
||||
this.selectedIndex = this.hasWowClient ? 0 : 3;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._sessionService.appLoaded();
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
onSelectedIndexChange(index: number) {
|
||||
this._sessionService.selectedHomeTab = index;
|
||||
|
||||
@@ -43,7 +43,7 @@ import { AddonInstallButtonComponent } from "app/components/addon-install-button
|
||||
InstallFromUrlDialogComponent,
|
||||
AddonDetailComponent,
|
||||
AddonProviderBadgeComponent,
|
||||
AddonInstallButtonComponent
|
||||
AddonInstallButtonComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -90,15 +90,15 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="addon.gameVersion">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
<th class="game-version-cell" mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.GAME_VERSION_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<td class="game-version-cell" mat-cell *matCellDef="let element">
|
||||
{{element.addon.gameVersion}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="addon.provider">
|
||||
<ng-container matColumnDef="addon.providerName">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef class="provider-column">
|
||||
{{'PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
@@ -151,6 +151,9 @@
|
||||
<button mat-menu-item [matMenuTriggerFor]="addonChannels">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.CHANNEL_SUBMENT_TITLE' | translate}}
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowfolder(listItem.addon)">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.SHOW_FOLDER' | translate}}
|
||||
</button>
|
||||
<button mat-menu-item (click)="onReInstallAddon(listItem.addon)">
|
||||
{{'PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REINSTALL_ADDON_BUTTON' | translate}}
|
||||
</button>
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.game-version-cell {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.status-column {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
|
||||
@@ -1,63 +1,96 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { WowClientType } from '../../models/warcraft/wow-client-type';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { from, BehaviorSubject, Subscription, Subject } from 'rxjs';
|
||||
import { Addon } from 'app/entities/addon';
|
||||
import { WarcraftService } from 'app/services/warcraft/warcraft.service';
|
||||
import { AddonService } from 'app/services/addons/addon.service';
|
||||
import { SessionService } from 'app/services/session/session.service';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ColumnState } from 'app/models/wowup/column-state';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MyAddonsListItem } from 'app/business-objects/my-addons-list-item';
|
||||
import * as _ from 'lodash';
|
||||
import { ElectronService } from 'app/services';
|
||||
import { AddonDisplayState } from 'app/models/wowup/addon-display-state';
|
||||
import { AddonInstallState } from 'app/models/wowup/addon-install-state';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { MatRadioChange } from '@angular/material/radio';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ConfirmDialogComponent } from 'app/components/confirm-dialog/confirm-dialog.component';
|
||||
import { getEnumName } from 'app/utils/enum.utils';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { stringIncludes } from 'app/utils/string.utils';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { WowClientType } from "../../models/warcraft/wow-client-type";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { from, BehaviorSubject, Subscription, Subject } from "rxjs";
|
||||
import { Addon } from "app/entities/addon";
|
||||
import { WarcraftService } from "app/services/warcraft/warcraft.service";
|
||||
import { AddonService } from "app/services/addons/addon.service";
|
||||
import { SessionService } from "app/services/session/session.service";
|
||||
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { ColumnState } from "app/models/wowup/column-state";
|
||||
import { MatCheckboxChange } from "@angular/material/checkbox";
|
||||
import { MyAddonsListItem } from "app/business-objects/my-addons-list-item";
|
||||
import * as _ from "lodash";
|
||||
import { ElectronService } from "app/services";
|
||||
import { AddonDisplayState } from "app/models/wowup/addon-display-state";
|
||||
import { AddonInstallState } from "app/models/wowup/addon-install-state";
|
||||
import { MatMenuTrigger } from "@angular/material/menu";
|
||||
import { MatRadioChange } from "@angular/material/radio";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { ConfirmDialogComponent } from "app/components/confirm-dialog/confirm-dialog.component";
|
||||
import { getEnumName } from "app/utils/enum.utils";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { MatSort } from "@angular/material/sort";
|
||||
import { stringIncludes } from "app/utils/string.utils";
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-addons',
|
||||
templateUrl: './my-addons.component.html',
|
||||
styleUrls: ['./my-addons.component.scss']
|
||||
selector: "app-my-addons",
|
||||
templateUrl: "./my-addons.component.html",
|
||||
styleUrls: ["./my-addons.component.scss"],
|
||||
})
|
||||
export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
@Input("tabIndex") tabIndex: number;
|
||||
|
||||
@ViewChild('addonContextMenuTrigger') contextMenu: MatMenuTrigger;
|
||||
@ViewChild('columnContextMenuTrigger') columnContextMenu: MatMenuTrigger;
|
||||
@ViewChild('updateAllContextMenuTrigger') updateAllContextMenu: MatMenuTrigger;
|
||||
@ViewChild("addonContextMenuTrigger") contextMenu: MatMenuTrigger;
|
||||
@ViewChild("columnContextMenuTrigger") columnContextMenu: MatMenuTrigger;
|
||||
@ViewChild("updateAllContextMenuTrigger")
|
||||
updateAllContextMenu: MatMenuTrigger;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<MyAddonsListItem[]>([]);
|
||||
private readonly _displayAddonsSrc = new BehaviorSubject<MyAddonsListItem[]>(
|
||||
[]
|
||||
);
|
||||
private readonly _destroyed$ = new Subject<void>();
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
private isSelectedTab: boolean = false;
|
||||
|
||||
public spinnerMessage = 'Loading...';
|
||||
public spinnerMessage = "Loading...";
|
||||
|
||||
contextMenuPosition = { x: '0px', y: '0px' };
|
||||
contextMenuPosition = { x: "0px", y: "0px" };
|
||||
|
||||
public dataSource = new MatTableDataSource<MyAddonsListItem>([]);
|
||||
public filter = '';
|
||||
public filter = "";
|
||||
|
||||
columns: ColumnState[] = [
|
||||
{ name: 'addon.name', display: 'Addon', visible: true },
|
||||
{ name: 'displayState', display: 'Status', visible: true },
|
||||
{ name: 'addon.latestVersion', display: 'Latest Version', visible: true, allowToggle: true },
|
||||
{ name: 'addon.gameVersion', display: 'Game Version', visible: true, allowToggle: true },
|
||||
{ name: 'addon.provider', display: 'Provider', visible: true, allowToggle: true },
|
||||
{ name: 'addon.author', display: 'Author', visible: true, allowToggle: true },
|
||||
]
|
||||
{ name: "addon.name", display: "Addon", visible: true },
|
||||
{ name: "displayState", display: "Status", visible: true },
|
||||
{
|
||||
name: "addon.latestVersion",
|
||||
display: "Latest Version",
|
||||
visible: true,
|
||||
allowToggle: true,
|
||||
},
|
||||
{
|
||||
name: "addon.gameVersion",
|
||||
display: "Game Version",
|
||||
visible: true,
|
||||
allowToggle: true,
|
||||
},
|
||||
{
|
||||
name: "addon.providerName",
|
||||
display: "Provider",
|
||||
visible: true,
|
||||
allowToggle: true,
|
||||
},
|
||||
{
|
||||
name: "addon.author",
|
||||
display: "Author",
|
||||
visible: true,
|
||||
allowToggle: true,
|
||||
},
|
||||
];
|
||||
|
||||
public get displayedColumns(): string[] {
|
||||
return this.columns.filter(col => col.visible).map(col => col.name);
|
||||
return this.columns.filter((col) => col.visible).map((col) => col.name);
|
||||
}
|
||||
|
||||
public selectedClient = WowClientType.None;
|
||||
@@ -69,67 +102,98 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private addonService: AddonService,
|
||||
private _sessionService: SessionService,
|
||||
private _ngZone: NgZone,
|
||||
private _dialog: MatDialog,
|
||||
public electronService: ElectronService,
|
||||
public overlay: Overlay,
|
||||
public viewContainerRef: ViewContainerRef,
|
||||
public warcraftService: WarcraftService,
|
||||
private _ngZone: NgZone,
|
||||
private _dialog: MatDialog
|
||||
public warcraftService: WarcraftService
|
||||
) {
|
||||
|
||||
const addonInstalledSubscription = this.addonService.addonInstalled$.subscribe((evt) => {
|
||||
console.log('UPDATE')
|
||||
let listItems: MyAddonsListItem[] = [].concat(this._displayAddonsSrc.value);
|
||||
const listItemIdx = listItems.findIndex(li => li.addon.id === evt.addon.id);
|
||||
const listItem = this.createAddonListItem(evt.addon);
|
||||
listItem.isInstalling = evt.installState === AddonInstallState.Installing || evt.installState === AddonInstallState.Downloading;
|
||||
listItem.statusText = this.getInstallStateText(evt.installState);
|
||||
listItem.installProgress = evt.progress;
|
||||
|
||||
if (listItemIdx === -1) {
|
||||
listItems.push(listItem);
|
||||
} else {
|
||||
listItems[listItemIdx] = listItem;
|
||||
_sessionService.selectedHomeTab$.subscribe((tabIndex) => {
|
||||
this.isSelectedTab = tabIndex === this.tabIndex;
|
||||
console.log("TAB CHANGE", tabIndex, this.tabIndex);
|
||||
if (this.isSelectedTab) {
|
||||
this.setPageContextText();
|
||||
}
|
||||
|
||||
listItems = this.sortListItems(listItems);
|
||||
|
||||
this._ngZone.run(() => {
|
||||
this._displayAddonsSrc.next(listItems);
|
||||
});
|
||||
});
|
||||
|
||||
const addonRemovedSubscription = this.addonService.addonRemoved$
|
||||
.subscribe((addonId) => {
|
||||
const addons: MyAddonsListItem[] = [].concat(this._displayAddonsSrc.value);
|
||||
const listItemIdx = addons.findIndex(li => li.addon.id === addonId);
|
||||
const addonInstalledSubscription = this.addonService.addonInstalled$.subscribe(
|
||||
(evt) => {
|
||||
let listItems: MyAddonsListItem[] = [].concat(
|
||||
this._displayAddonsSrc.value
|
||||
);
|
||||
|
||||
const listItemIdx = listItems.findIndex(
|
||||
(li) => li.addon.id === evt.addon.id
|
||||
);
|
||||
|
||||
const listItem = this.createAddonListItem(evt.addon);
|
||||
listItem.isInstalling =
|
||||
evt.installState === AddonInstallState.Installing ||
|
||||
evt.installState === AddonInstallState.Downloading;
|
||||
listItem.statusText = this.getInstallStateText(evt.installState);
|
||||
listItem.installProgress = evt.progress;
|
||||
|
||||
if (listItemIdx === -1) {
|
||||
listItems.push(listItem);
|
||||
} else {
|
||||
listItems[listItemIdx] = listItem;
|
||||
}
|
||||
|
||||
listItems = this.sortListItems(listItems);
|
||||
|
||||
this._ngZone.run(() => {
|
||||
this._displayAddonsSrc.next(listItems);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const addonRemovedSubscription = this.addonService.addonRemoved$.subscribe(
|
||||
(addonId) => {
|
||||
const addons: MyAddonsListItem[] = [].concat(
|
||||
this._displayAddonsSrc.value
|
||||
);
|
||||
const listItemIdx = addons.findIndex((li) => li.addon.id === addonId);
|
||||
addons.splice(listItemIdx, 1);
|
||||
|
||||
this._ngZone.run(() => {
|
||||
this._displayAddonsSrc.next(addons);
|
||||
});
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const displayAddonSubscription = this._displayAddonsSrc
|
||||
.subscribe((items: MyAddonsListItem[]) => {
|
||||
const displayAddonSubscription = this._displayAddonsSrc.subscribe(
|
||||
(items: MyAddonsListItem[]) => {
|
||||
this.dataSource.data = items;
|
||||
this.dataSource.sortingDataAccessor = _.get;
|
||||
this.dataSource.filterPredicate = (item: MyAddonsListItem, filter: string) => {
|
||||
if (stringIncludes(item.addon.name, filter) || stringIncludes(item.addon.latestVersion, filter) || stringIncludes(item.addon.author, filter)) {
|
||||
this.dataSource.filterPredicate = (
|
||||
item: MyAddonsListItem,
|
||||
filter: string
|
||||
) => {
|
||||
if (
|
||||
stringIncludes(item.addon.name, filter) ||
|
||||
stringIncludes(item.addon.latestVersion, filter) ||
|
||||
stringIncludes(item.addon.author, filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
this.dataSource.sort = this.sort;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.subscriptions.concat(...[addonInstalledSubscription, addonRemovedSubscription, displayAddonSubscription]);
|
||||
this.subscriptions.push(
|
||||
addonInstalledSubscription,
|
||||
addonRemovedSubscription,
|
||||
displayAddonSubscription
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const selectedClientSubscription = this._sessionService.selectedClientType$
|
||||
.pipe(
|
||||
map(clientType => {
|
||||
map((clientType) => {
|
||||
this.selectedClient = clientType;
|
||||
this.loadAddons(this.selectedClient);
|
||||
})
|
||||
@@ -140,7 +204,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
||||
this._destroyed$.next();
|
||||
this._destroyed$.complete();
|
||||
}
|
||||
@@ -151,7 +215,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
onRowClicked(event: MouseEvent, row: MyAddonsListItem, index: number) {
|
||||
console.log(row.displayState);
|
||||
console.log('index clicked: ' + index);
|
||||
console.log("index clicked: " + index);
|
||||
|
||||
if (event.ctrlKey) {
|
||||
row.selected = !row.selected;
|
||||
@@ -161,7 +225,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
let listItems: MyAddonsListItem[] = [].concat(this._displayAddonsSrc.value);
|
||||
|
||||
if (event.shiftKey) {
|
||||
const startIdx = listItems.findIndex(item => item.selected);
|
||||
const startIdx = listItems.findIndex((item) => item.selected);
|
||||
listItems.forEach((item, i) => {
|
||||
if (i >= startIdx && i <= index) {
|
||||
item.selected = true;
|
||||
@@ -189,7 +253,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onClearFilter(): void {
|
||||
this.filter = '';
|
||||
this.filter = "";
|
||||
this.filterAddons();
|
||||
}
|
||||
|
||||
@@ -197,11 +261,15 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
this.enableControls = false;
|
||||
|
||||
try {
|
||||
const listItems = _.filter(this._displayAddonsSrc.value,
|
||||
listItem => listItem.displayState === AddonDisplayState.Install || listItem.displayState === AddonDisplayState.Update);
|
||||
const listItems = _.filter(
|
||||
this._displayAddonsSrc.value,
|
||||
(listItem) =>
|
||||
listItem.displayState === AddonDisplayState.Install ||
|
||||
listItem.displayState === AddonDisplayState.Update
|
||||
);
|
||||
|
||||
for (let listItem of listItems) {
|
||||
await this.addonService.installAddon(listItem.addon.id,)
|
||||
await this.addonService.installAddon(listItem.addon.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -211,26 +279,37 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async onUpdateAllRetailClassic() {
|
||||
await this.updateAllWithSpinner(WowClientType.Retail, WowClientType.Classic);
|
||||
await this.updateAllWithSpinner(
|
||||
WowClientType.Retail,
|
||||
WowClientType.Classic
|
||||
);
|
||||
}
|
||||
|
||||
async onUpdateAllClients() {
|
||||
await this.updateAllWithSpinner(WowClientType.Retail, WowClientType.RetailPtr, WowClientType.Beta, WowClientType.ClassicPtr, WowClientType.Classic);
|
||||
await this.updateAllWithSpinner(
|
||||
WowClientType.Retail,
|
||||
WowClientType.RetailPtr,
|
||||
WowClientType.Beta,
|
||||
WowClientType.ClassicPtr,
|
||||
WowClientType.Classic
|
||||
);
|
||||
}
|
||||
|
||||
onHeaderContext(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.updateContextMenuPosition(event);
|
||||
this.columnContextMenu.menuData = { 'columns': this.columns.filter(col => col.allowToggle) };
|
||||
this.columnContextMenu.menu.focusFirstItem('mouse');
|
||||
this.columnContextMenu.menuData = {
|
||||
columns: this.columns.filter((col) => col.allowToggle),
|
||||
};
|
||||
this.columnContextMenu.menu.focusFirstItem("mouse");
|
||||
this.columnContextMenu.openMenu();
|
||||
}
|
||||
|
||||
onCellContext(event: MouseEvent, listItem: MyAddonsListItem) {
|
||||
event.preventDefault();
|
||||
this.updateContextMenuPosition(event);
|
||||
this.contextMenu.menuData = { 'listItem': listItem };
|
||||
this.contextMenu.menu.focusFirstItem('mouse');
|
||||
this.contextMenu.menuData = { listItem: listItem };
|
||||
this.contextMenu.menu.focusFirstItem("mouse");
|
||||
this.contextMenu.openMenu();
|
||||
}
|
||||
|
||||
@@ -248,6 +327,15 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
onShowfolder(addon: Addon) {
|
||||
try {
|
||||
const addonPath = this.addonService.getFullInstallPath(addon);
|
||||
this.electronService.shell.openExternal(addonPath);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateAddon(listItem: MyAddonsListItem) {
|
||||
listItem.isInstalling = true;
|
||||
|
||||
@@ -257,7 +345,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
public onColumnVisibleChange(event: MatCheckboxChange, column: ColumnState) {
|
||||
console.log(event, column);
|
||||
|
||||
const col = this.columns.find(col => col.name === column.name);
|
||||
const col = this.columns.find((col) => col.name === column.name);
|
||||
col.visible = event.checked;
|
||||
}
|
||||
|
||||
@@ -265,15 +353,15 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: `Start re-scan?`,
|
||||
message: `Doing a re-scan may reset the addon information and attempt to re-guess what you have installed. This operation can take a moment.`
|
||||
}
|
||||
message: `Doing a re-scan may reset the addon information and attempt to re-guess what you have installed. This operation can take a moment.`,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.loadAddons(this.selectedClient, true)
|
||||
this.loadAddons(this.selectedClient, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,12 +373,12 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
const dialogRef = this._dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: `Uninstall Addon?`,
|
||||
message: `Are you sure you want to remove ${addon.name}?\nThis will remove all related folders from your World of Warcraft folder.`
|
||||
}
|
||||
message: `Are you sure you want to remove ${addon.name}?\nThis will remove all related folders from your World of Warcraft folder.`,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log('The dialog was closed', result);
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
console.log("The dialog was closed", result);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
@@ -299,9 +387,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
onInstall() {
|
||||
|
||||
}
|
||||
onInstall() {}
|
||||
|
||||
onClickIgnoreAddon(evt: MatCheckboxChange, listItem: MyAddonsListItem) {
|
||||
listItem.addon.isIgnored = evt.checked;
|
||||
@@ -322,7 +408,7 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async updateAllWithSpinner(...clientTypes: WowClientType[]) {
|
||||
this.isBusy = true;
|
||||
this.spinnerMessage = 'Gathering addons...';
|
||||
this.spinnerMessage = "Gathering addons...";
|
||||
|
||||
try {
|
||||
let updatedCt = 0;
|
||||
@@ -333,76 +419,90 @@ export class MyAddonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Only care about the ones that need to be updated/installed
|
||||
addons = addons
|
||||
.map(addon => new MyAddonsListItem(addon))
|
||||
.filter(listItem => listItem.needsUpdate || listItem.needsInstall)
|
||||
.map(listItem => listItem.addon);
|
||||
.map((addon) => new MyAddonsListItem(addon))
|
||||
.filter((listItem) => listItem.needsUpdate || listItem.needsInstall)
|
||||
.map((listItem) => listItem.addon);
|
||||
|
||||
this.spinnerMessage = `Updating ${updatedCt}/${addons.length}`;
|
||||
|
||||
for (let addon of addons) {
|
||||
updatedCt += 1;
|
||||
this.spinnerMessage = `Updating ${updatedCt}/${addons.length}\n${getEnumName(WowClientType, addon.clientType)}: ${addon.name}`;
|
||||
this.spinnerMessage = `Updating ${updatedCt}/${
|
||||
addons.length
|
||||
}\n${getEnumName(WowClientType, addon.clientType)}: ${addon.name}`;
|
||||
|
||||
await this.addonService.installAddon(addon.id);
|
||||
}
|
||||
|
||||
this.loadAddons(this.selectedClient);
|
||||
} catch (err) {
|
||||
console.error('Failed to update classic/retail', err);
|
||||
console.error("Failed to update classic/retail", err);
|
||||
this.isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private updateContextMenuPosition(event: MouseEvent) {
|
||||
this.contextMenuPosition.x = event.clientX + 'px';
|
||||
this.contextMenuPosition.y = event.clientY + 'px';
|
||||
this.contextMenuPosition.x = event.clientX + "px";
|
||||
this.contextMenuPosition.y = event.clientY + "px";
|
||||
}
|
||||
|
||||
private loadAddons(clientType: WowClientType, rescan = false) {
|
||||
this.isBusy = true;
|
||||
this.enableControls = false;
|
||||
|
||||
console.log('Load-addons', clientType);
|
||||
console.log("Load-addons", clientType);
|
||||
|
||||
from(this.addonService.getAddons(clientType, rescan))
|
||||
.subscribe({
|
||||
next: (addons) => {
|
||||
this.isBusy = false;
|
||||
this.enableControls = true;
|
||||
this._ngZone.run(() => {
|
||||
this._displayAddonsSrc.next(this.formatAddons(addons));
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
this.isBusy = false;
|
||||
this.enableControls = true;
|
||||
}
|
||||
});
|
||||
from(this.addonService.getAddons(clientType, rescan)).subscribe({
|
||||
next: (addons) => {
|
||||
this.isBusy = false;
|
||||
this.enableControls = true;
|
||||
this._ngZone.run(() => {
|
||||
this._displayAddonsSrc.next(this.formatAddons(addons));
|
||||
this.setPageContextText();
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.isBusy = false;
|
||||
this.enableControls = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private formatAddons(addons: Addon[]): MyAddonsListItem[] {
|
||||
const listItems = addons.map(addon => this.createAddonListItem(addon));
|
||||
const listItems = addons.map((addon) => this.createAddonListItem(addon));
|
||||
|
||||
return this.sortListItems(listItems);
|
||||
}
|
||||
|
||||
private sortListItems(listItems: MyAddonsListItem[]) {
|
||||
return _.orderBy(listItems, ['displayState', 'addon.name']);
|
||||
return _.orderBy(listItems, ["displayState", "addon.name"]);
|
||||
}
|
||||
|
||||
private createAddonListItem(addon: Addon) {
|
||||
const listItem = new MyAddonsListItem(addon);
|
||||
|
||||
if (!listItem.addon.thumbnailUrl) {
|
||||
listItem.addon.thumbnailUrl = 'assets/wowup_logo_512np.png';
|
||||
listItem.addon.thumbnailUrl = "assets/wowup_logo_512np.png";
|
||||
}
|
||||
if (!listItem.addon.installedVersion) {
|
||||
listItem.addon.installedVersion = 'None';
|
||||
listItem.addon.installedVersion = "None";
|
||||
}
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
private setPageContextText() {
|
||||
if (!this._displayAddonsSrc.value?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sessionService.setContextText(
|
||||
this.tabIndex,
|
||||
`${this._displayAddonsSrc.value.length} addons`
|
||||
);
|
||||
}
|
||||
|
||||
private getInstallStateText(installState: AddonInstallState) {
|
||||
switch (installState) {
|
||||
case AddonInstallState.Pending:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, NgZone, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, OnInit, NgZone, OnChanges, SimpleChanges, Input } from '@angular/core';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { WowClientType } from 'app/models/warcraft/wow-client-type';
|
||||
import { ElectronService } from 'app/services';
|
||||
@@ -21,6 +21,8 @@ import { MatSelectChange } from '@angular/material/select';
|
||||
})
|
||||
export class OptionsComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input('tabIndex') tabIndex: number;
|
||||
|
||||
public retailLocation = '';
|
||||
public classicLocation = '';
|
||||
public retailPtrLocation = '';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AddonProvider } from "app/addon-providers/addon-provider";
|
||||
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
|
||||
import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
|
||||
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
|
||||
|
||||
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { FileService } from "../files/file.service";
|
||||
import { SessionService } from "../session/session.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AddonProviderFactory {
|
||||
constructor(
|
||||
private _cachingService: CachingService,
|
||||
private _electronService: ElectronService,
|
||||
private _httpClient: HttpClient,
|
||||
private _sessionService: SessionService,
|
||||
private _fileService: FileService
|
||||
) {}
|
||||
|
||||
public getAddonProvider<T extends object>(providerType: T & AddonProvider) {
|
||||
switch (providerType.name) {
|
||||
case CurseAddonProvider.name:
|
||||
return this.createCurseAddonProvider();
|
||||
case TukUiAddonProvider.name:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public createCurseAddonProvider(): CurseAddonProvider {
|
||||
return new CurseAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService
|
||||
);
|
||||
}
|
||||
|
||||
public createTukUiAddonProvider(): TukUiAddonProvider {
|
||||
return new TukUiAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._fileService
|
||||
);
|
||||
}
|
||||
|
||||
public createWowInterfaceAddonProvider(): WowInterfaceAddonProvider {
|
||||
return new WowInterfaceAddonProvider(
|
||||
this._httpClient,
|
||||
this._cachingService,
|
||||
this._electronService,
|
||||
this._fileService
|
||||
);
|
||||
}
|
||||
|
||||
public createGitHubAddonProvider(): GitHubAddonProvider {
|
||||
return new GitHubAddonProvider(this._httpClient);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Injectable, Injector } from "@angular/core";
|
||||
import { AddonStorageService } from "../storage/addon-storage.service";
|
||||
import { Addon } from "../../entities/addon";
|
||||
import { WarcraftService } from "../warcraft/warcraft.service";
|
||||
import { AddonProvider } from "../../addon-providers/addon-provider";
|
||||
import { CurseAddonProvider } from "../../addon-providers/curse-addon-provider";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import * as _ from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { WowUpApiService } from "../wowup-api/wowup-api.service";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { PotentialAddon } from "app/models/wowup/potential-addon";
|
||||
@@ -29,12 +29,12 @@ import { TukUiAddonProvider } from "app/addon-providers/tukui-addon-provider";
|
||||
import { AddonUpdateEvent } from "app/models/wowup/addon-update-event";
|
||||
import { WowInterfaceAddonProvider } from "app/addon-providers/wow-interface-addon-provider";
|
||||
import { GitHubAddonProvider } from "app/addon-providers/github-addon-provider";
|
||||
import { AddonProviderFactory } from "./addon.provider.factory";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AddonService {
|
||||
|
||||
private readonly _addonProviders: AddonProvider[];
|
||||
private readonly _addonInstalledSrc = new Subject<AddonUpdateEvent>();
|
||||
private readonly _addonRemovedSrc = new Subject<string>();
|
||||
@@ -44,21 +44,18 @@ export class AddonService {
|
||||
|
||||
constructor(
|
||||
private _addonStorage: AddonStorageService,
|
||||
private _cachingService: CachingService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService,
|
||||
private _wowupApiService: WowUpApiService,
|
||||
private _downloadService: DownloadSevice,
|
||||
private _electronService: ElectronService,
|
||||
private _fileService: FileService,
|
||||
private _tocService: TocService,
|
||||
httpClient: HttpClient
|
||||
private _addonProviderFactory: AddonProviderFactory
|
||||
) {
|
||||
this._addonProviders = [
|
||||
new CurseAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new TukUiAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new WowInterfaceAddonProvider(httpClient, this._cachingService, this._electronService, this._fileService),
|
||||
new GitHubAddonProvider(httpClient),
|
||||
this._addonProviderFactory.createCurseAddonProvider(),
|
||||
this._addonProviderFactory.createTukUiAddonProvider(),
|
||||
this._addonProviderFactory.createWowInterfaceAddonProvider(),
|
||||
this._addonProviderFactory.createGitHubAddonProvider(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,41 +63,62 @@ export class AddonService {
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
}
|
||||
|
||||
public async search(query: string, clientType: WowClientType): Promise<PotentialAddon[]> {
|
||||
var searchTasks = this._addonProviders.map(p => p.searchByQuery(query, clientType));
|
||||
public async search(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
var searchTasks = this._addonProviders.map((p) =>
|
||||
p.searchByQuery(query, clientType)
|
||||
);
|
||||
var searchResults = await Promise.all(searchTasks);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "Search", $"{clientType}|{query}");
|
||||
const flatResults = searchResults.flat(1);
|
||||
|
||||
return _.orderBy(flatResults, 'downloadCount').reverse();
|
||||
return _.orderBy(flatResults, "downloadCount").reverse();
|
||||
}
|
||||
|
||||
public async installPotentialAddon(
|
||||
potentialAddon: PotentialAddon,
|
||||
clientType: WowClientType,
|
||||
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
|
||||
onUpdate: (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => void = undefined
|
||||
) {
|
||||
var existingAddon = this._addonStorage.getByExternalId(potentialAddon.externalId, clientType);
|
||||
var existingAddon = this._addonStorage.getByExternalId(
|
||||
potentialAddon.externalId,
|
||||
clientType
|
||||
);
|
||||
if (existingAddon) {
|
||||
throw new Error('Addon already installed');
|
||||
throw new Error("Addon already installed");
|
||||
}
|
||||
|
||||
const addon = await this.getAddon(potentialAddon.externalId, potentialAddon.providerName, clientType).toPromise();
|
||||
const addon = await this.getAddon(
|
||||
potentialAddon.externalId,
|
||||
potentialAddon.providerName,
|
||||
clientType
|
||||
).toPromise();
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
await this.installAddon(addon.id, onUpdate);
|
||||
}
|
||||
|
||||
public async processAutoUpdates(): Promise<number> {
|
||||
const autoUpdateAddons = this.getAutoUpdateEnabledAddons();
|
||||
const clientTypeGroups = _.groupBy(autoUpdateAddons, addon => addon.clientType);
|
||||
const clientTypeGroups = _.groupBy(
|
||||
autoUpdateAddons,
|
||||
(addon) => addon.clientType
|
||||
);
|
||||
let updateCt = 0;
|
||||
|
||||
for (let clientTypeStr in clientTypeGroups) {
|
||||
const clientType: WowClientType = parseInt(clientTypeStr, 10);
|
||||
// console.log('clientType', clientType, clientTypeGroups[clientType]);
|
||||
|
||||
const synced = await this.syncAddons(clientType, clientTypeGroups[clientType]);
|
||||
const synced = await this.syncAddons(
|
||||
clientType,
|
||||
clientTypeGroups[clientType]
|
||||
);
|
||||
if (!synced) {
|
||||
continue;
|
||||
}
|
||||
@@ -113,30 +131,33 @@ export class AddonService {
|
||||
try {
|
||||
await this.installAddon(addon.id);
|
||||
updateCt += 1;
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
} catch (err) {
|
||||
// _analyticsService.Track(ex, "Failed to install addon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return updateCt;
|
||||
}
|
||||
|
||||
public canUpdateAddon(addon: Addon) {
|
||||
return addon.installedVersion && addon.installedVersion !== addon.latestVersion;
|
||||
return (
|
||||
addon.installedVersion && addon.installedVersion !== addon.latestVersion
|
||||
);
|
||||
}
|
||||
|
||||
public getAutoUpdateEnabledAddons() {
|
||||
return this._addonStorage.queryAll(addon => {
|
||||
return this._addonStorage.queryAll((addon) => {
|
||||
return addon.isIgnored !== true && addon.autoUpdateEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
public async installAddon(
|
||||
addonId: string,
|
||||
onUpdate: (installState: AddonInstallState, progress: number) => void = undefined
|
||||
onUpdate: (
|
||||
installState: AddonInstallState,
|
||||
progress: number
|
||||
) => void = undefined
|
||||
) {
|
||||
const addon = this.getAddonById(addonId);
|
||||
if (addon == null || !addon.downloadUrl) {
|
||||
@@ -144,35 +165,56 @@ export class AddonService {
|
||||
}
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Downloading, 25);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Downloading, progress: 25 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Downloading,
|
||||
progress: 25,
|
||||
});
|
||||
|
||||
let downloadedFilePath = '';
|
||||
let unzippedDirectory = '';
|
||||
let downloadedThumbnail = '';
|
||||
let downloadedFilePath = "";
|
||||
let unzippedDirectory = "";
|
||||
let downloadedThumbnail = "";
|
||||
try {
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(addon.downloadUrl, this._wowUpService.applicationDownloadsFolderPath);
|
||||
downloadedFilePath = await this._downloadService.downloadZipFile(
|
||||
addon.downloadUrl,
|
||||
this._wowUpService.applicationDownloadsFolderPath
|
||||
);
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Installing, 75);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Installing, progress: 75 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Installing,
|
||||
progress: 75,
|
||||
});
|
||||
|
||||
const unzipPath = path.join(this._wowUpService.applicationDownloadsFolderPath, uuidv4());
|
||||
unzippedDirectory = await this._downloadService.unzipFile(downloadedFilePath, unzipPath);
|
||||
const unzipPath = path.join(
|
||||
this._wowUpService.applicationDownloadsFolderPath,
|
||||
uuidv4()
|
||||
);
|
||||
unzippedDirectory = await this._downloadService.unzipFile(
|
||||
downloadedFilePath,
|
||||
unzipPath
|
||||
);
|
||||
|
||||
await this.installUnzippedDirectory(unzippedDirectory, addon.clientType);
|
||||
const unzippedDirectoryNames = await this._fileService.listDirectories(unzippedDirectory);
|
||||
const unzippedDirectoryNames = await this._fileService.listDirectories(
|
||||
unzippedDirectory
|
||||
);
|
||||
|
||||
addon.installedVersion = addon.latestVersion;
|
||||
addon.installedAt = new Date();
|
||||
addon.installedFolders = unzippedDirectoryNames.join(',');
|
||||
addon.installedFolders = unzippedDirectoryNames.join(",");
|
||||
|
||||
if (!!addon.gameVersion) {
|
||||
addon.gameVersion = await this.getLatestGameVersion(unzippedDirectory, unzippedDirectoryNames);
|
||||
addon.gameVersion = await this.getLatestGameVersion(
|
||||
unzippedDirectory,
|
||||
unzippedDirectoryNames
|
||||
);
|
||||
}
|
||||
|
||||
this._addonStorage.set(addon.id, addon);
|
||||
|
||||
// await _analyticsService.TrackUserAction("Addons", "InstallById", $"{addon.ClientType}|{addon.Name}");
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -188,10 +230,17 @@ export class AddonService {
|
||||
}
|
||||
|
||||
onUpdate?.call(this, AddonInstallState.Complete, 100);
|
||||
this._addonInstalledSrc.next({ addon, installState: AddonInstallState.Complete, progress: 100 });
|
||||
this._addonInstalledSrc.next({
|
||||
addon,
|
||||
installState: AddonInstallState.Complete,
|
||||
progress: 100,
|
||||
});
|
||||
}
|
||||
|
||||
private async getLatestGameVersion(baseDir: string, installedFolders: string[]) {
|
||||
private async getLatestGameVersion(
|
||||
baseDir: string,
|
||||
installedFolders: string[]
|
||||
) {
|
||||
const versions = [];
|
||||
|
||||
for (let dir of installedFolders) {
|
||||
@@ -211,31 +260,44 @@ export class AddonService {
|
||||
versions.push(toc.interface);
|
||||
}
|
||||
|
||||
return _.orderBy(versions)[0] || '';
|
||||
return _.orderBy(versions)[0] || "";
|
||||
}
|
||||
|
||||
private async installUnzippedDirectory(unzippedDirectory: string, clientType: WowClientType) {
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(clientType);
|
||||
const unzippedFolders = await this._fileService.listDirectories(unzippedDirectory);
|
||||
private async installUnzippedDirectory(
|
||||
unzippedDirectory: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
clientType
|
||||
);
|
||||
const unzippedFolders = await this._fileService.listDirectories(
|
||||
unzippedDirectory
|
||||
);
|
||||
for (let unzippedFolder of unzippedFolders) {
|
||||
const unzippedFilePath = path.join(unzippedDirectory, unzippedFolder);
|
||||
const unzipLocation = path.join(addonFolderPath, unzippedFolder);
|
||||
const unzipBackupLocation = path.join(addonFolderPath, `${unzippedFolder}-bak`);
|
||||
const unzipBackupLocation = path.join(
|
||||
addonFolderPath,
|
||||
`${unzippedFolder}-bak`
|
||||
);
|
||||
|
||||
try {
|
||||
// If the user already has the addon installed, create a temporary backup
|
||||
if (fs.existsSync(unzipLocation)) {
|
||||
console.log('BACKING UP', unzipLocation);
|
||||
await this._fileService.renameDirectory(unzipLocation, unzipBackupLocation);
|
||||
console.log("BACKING UP", unzipLocation);
|
||||
await this._fileService.renameDirectory(
|
||||
unzipLocation,
|
||||
unzipBackupLocation
|
||||
);
|
||||
}
|
||||
|
||||
// Copy contents from unzipped new directory to existing addon folder location
|
||||
console.log('COPY', unzipLocation);
|
||||
console.log("COPY", unzipLocation);
|
||||
await this._fileService.copyDirectory(unzippedFilePath, unzipLocation);
|
||||
|
||||
// If the copy succeeds, delete the backup
|
||||
if (fs.existsSync(unzipBackupLocation)) {
|
||||
console.log('DELETE BKUP', unzipLocation);
|
||||
console.log("DELETE BKUP", unzipLocation);
|
||||
await this._fileService.deleteDirectory(unzipBackupLocation);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -251,7 +313,10 @@ export class AddonService {
|
||||
|
||||
// Move the backup folder into the original location
|
||||
console.log(`Attempting to roll back ${unzipBackupLocation}`);
|
||||
await this._fileService.copyDirectory(unzipBackupLocation, unzipLocation);
|
||||
await this._fileService.copyDirectory(
|
||||
unzipBackupLocation,
|
||||
unzipLocation
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -269,27 +334,46 @@ export class AddonService {
|
||||
return await provider.searchByUrl(url, clientType);
|
||||
}
|
||||
|
||||
public getAddon(externalId: string, providerName: string, clientType: WowClientType) {
|
||||
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(clientType);
|
||||
public getAddon(
|
||||
externalId: string,
|
||||
providerName: string,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const targetAddonChannel = this._wowUpService.getDefaultAddonChannel(
|
||||
clientType
|
||||
);
|
||||
const provider = this.getProvider(providerName);
|
||||
return provider.getById(externalId, clientType)
|
||||
.pipe(
|
||||
map(searchResult => {
|
||||
console.log('SEARCH RES', searchResult);
|
||||
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
|
||||
if (!latestFile) {
|
||||
latestFile = searchResult.files[0];
|
||||
}
|
||||
return provider.getById(externalId, clientType).pipe(
|
||||
map((searchResult) => {
|
||||
console.log("SEARCH RES", searchResult);
|
||||
let latestFile = this.getLatestFile(searchResult, targetAddonChannel);
|
||||
if (!latestFile) {
|
||||
latestFile = searchResult.files[0];
|
||||
}
|
||||
|
||||
return this.createAddon(latestFile.folders[0], searchResult, latestFile, clientType);
|
||||
})
|
||||
)
|
||||
return this.createAddon(
|
||||
latestFile.folders[0],
|
||||
searchResult,
|
||||
latestFile,
|
||||
clientType
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getFullInstallPath(addon: Addon) {
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
addon.clientType
|
||||
);
|
||||
return path.join(addonFolderPath, addon.folderName);
|
||||
}
|
||||
|
||||
public async removeAddon(addon: Addon) {
|
||||
const installedDirectories = addon.installedFolders.split(',');
|
||||
const installedDirectories = addon.installedFolders.split(",");
|
||||
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(addon.clientType);
|
||||
const addonFolderPath = this._warcraftService.getAddonFolderPath(
|
||||
addon.clientType
|
||||
);
|
||||
for (let directory of installedDirectories) {
|
||||
const addonDirectory = path.join(addonFolderPath, directory);
|
||||
await this._fileService.deleteDirectory(addonDirectory);
|
||||
@@ -299,31 +383,44 @@ export class AddonService {
|
||||
this._addonRemovedSrc.next(addon.id);
|
||||
}
|
||||
|
||||
public async getAddons(clientType: WowClientType, rescan = false): Promise<Addon[]> {
|
||||
public async getAddons(
|
||||
clientType: WowClientType,
|
||||
rescan = false
|
||||
): Promise<Addon[]> {
|
||||
let addons = this._addonStorage.getAllForClientType(clientType);
|
||||
if (rescan || !addons.length) {
|
||||
const newAddons = await this.scanAddons(clientType);
|
||||
this.updateAddons(addons, newAddons);
|
||||
}
|
||||
|
||||
this.syncAddons(clientType, addons);
|
||||
await this.syncAddons(clientType, addons);
|
||||
|
||||
return addons;
|
||||
}
|
||||
|
||||
private updateAddons(existingAddons: Addon[], newAddons: Addon[]): Addon[] {
|
||||
const removedAddons = existingAddons
|
||||
.filter(existingAddon => !newAddons.some(newAddon => this.addonsMatch(existingAddon, newAddon)));
|
||||
const removedAddons = existingAddons.filter(
|
||||
(existingAddon) =>
|
||||
!newAddons.some((newAddon) => this.addonsMatch(existingAddon, newAddon))
|
||||
);
|
||||
|
||||
const addedAddons = newAddons
|
||||
.filter(newAddon => !existingAddons.some(existingAddon => this.addonsMatch(existingAddon, newAddon)));
|
||||
const addedAddons = newAddons.filter(
|
||||
(newAddon) =>
|
||||
!existingAddons.some((existingAddon) =>
|
||||
this.addonsMatch(existingAddon, newAddon)
|
||||
)
|
||||
);
|
||||
|
||||
_.remove(existingAddons, addon => removedAddons.some(removedAddon => removedAddon.id === addon.id));
|
||||
_.remove(existingAddons, (addon) =>
|
||||
removedAddons.some((removedAddon) => removedAddon.id === addon.id)
|
||||
);
|
||||
|
||||
existingAddons.push(...addedAddons);
|
||||
|
||||
for (let existingAddon of existingAddons) {
|
||||
var matchingAddon = newAddons.find(newAddon => this.addonsMatch(newAddon, existingAddon));
|
||||
var matchingAddon = newAddons.find((newAddon) =>
|
||||
this.addonsMatch(newAddon, existingAddon)
|
||||
);
|
||||
if (!matchingAddon) {
|
||||
continue;
|
||||
}
|
||||
@@ -346,9 +443,11 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private addonsMatch(addon1: Addon, addon2: Addon): boolean {
|
||||
return addon1.externalId == addon2.externalId &&
|
||||
return (
|
||||
addon1.externalId == addon2.externalId &&
|
||||
addon1.providerName == addon2.providerName &&
|
||||
addon1.clientType == addon2.clientType;
|
||||
addon1.clientType == addon2.clientType
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAddons(clientType: WowClientType, addons: Addon[]) {
|
||||
@@ -358,25 +457,40 @@ export class AddonService {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async syncProviderAddons(clientType: WowClientType, addons: Addon[], addonProvider: AddonProvider) {
|
||||
const providerAddonIds = this.getExternalIdsForProvider(addonProvider, addons);
|
||||
private async syncProviderAddons(
|
||||
clientType: WowClientType,
|
||||
addons: Addon[],
|
||||
addonProvider: AddonProvider
|
||||
) {
|
||||
const providerAddonIds = this.getExternalIdsForProvider(
|
||||
addonProvider,
|
||||
addons
|
||||
);
|
||||
if (!providerAddonIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResults = await addonProvider.getAll(clientType, providerAddonIds);
|
||||
const searchResults = await addonProvider.getAll(
|
||||
clientType,
|
||||
providerAddonIds
|
||||
);
|
||||
for (let result of searchResults) {
|
||||
const addon = addons.find(addon => addon.externalId === result?.externalId);
|
||||
const addon = addons.find(
|
||||
(addon) => addon.externalId === result?.externalId
|
||||
);
|
||||
const latestFile = this.getLatestFile(result, addon?.channelType);
|
||||
|
||||
if (!result || !latestFile || latestFile.version === addon.latestVersion) {
|
||||
if (
|
||||
!result ||
|
||||
!latestFile ||
|
||||
latestFile.version === addon.latestVersion
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -396,37 +510,60 @@ export class AddonService {
|
||||
}
|
||||
}
|
||||
|
||||
private getExternalIdsForProvider(addonProvider: AddonProvider, addons: Addon[]): string[] {
|
||||
return addons.filter(addon => addon.providerName === addonProvider.name)
|
||||
.map(addon => addon.externalId);
|
||||
private getExternalIdsForProvider(
|
||||
addonProvider: AddonProvider,
|
||||
addons: Addon[]
|
||||
): string[] {
|
||||
return addons
|
||||
.filter((addon) => addon.providerName === addonProvider.name)
|
||||
.map((addon) => addon.externalId);
|
||||
}
|
||||
|
||||
private async scanAddons(clientType: WowClientType): Promise<Addon[]> {
|
||||
if (clientType === WowClientType.None) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const addonFolders = await this._warcraftService.listAddons(clientType);
|
||||
for (let provider of this._addonProviders) {
|
||||
try {
|
||||
const validFolders = addonFolders.filter(af => !af.matchingAddon && af.toc)
|
||||
await provider.scan(clientType, this._wowUpService.getDefaultAddonChannel(clientType), validFolders);
|
||||
const validFolders = addonFolders.filter(
|
||||
(af) => !af.matchingAddon && af.toc
|
||||
);
|
||||
await provider.scan(
|
||||
clientType,
|
||||
this._wowUpService.getDefaultAddonChannel(clientType),
|
||||
validFolders
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
const matchedAddonFolders = addonFolders.filter(addonFolder => !!addonFolder.matchingAddon);
|
||||
const matchedGroups = _.groupBy(matchedAddonFolders, addonFolder => `${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`);
|
||||
const matchedAddonFolders = addonFolders.filter(
|
||||
(addonFolder) => !!addonFolder.matchingAddon
|
||||
);
|
||||
const matchedGroups = _.groupBy(
|
||||
matchedAddonFolders,
|
||||
(addonFolder) =>
|
||||
`${addonFolder.matchingAddon.providerName}${addonFolder.matchingAddon.externalId}`
|
||||
);
|
||||
|
||||
console.log(Object.keys(matchedGroups));
|
||||
console.log(matchedGroups['Curse2382'])
|
||||
return Object.values(matchedGroups).map(value => value[0].matchingAddon);
|
||||
|
||||
return Object.values(matchedGroups).map((value) => value[0].matchingAddon);
|
||||
}
|
||||
|
||||
public getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return forkJoin(this._addonProviders.map(p => p.getFeaturedAddons(clientType)))
|
||||
.pipe(
|
||||
map(results => {
|
||||
return _.orderBy(results.flat(1), ['downloadCount']).reverse();
|
||||
})
|
||||
);
|
||||
public getFeaturedAddons(
|
||||
clientType: WowClientType
|
||||
): Observable<PotentialAddon[]> {
|
||||
return forkJoin(
|
||||
this._addonProviders.map((p) => p.getFeaturedAddons(clientType))
|
||||
).pipe(
|
||||
map((results) => {
|
||||
return _.orderBy(results.flat(1), ["downloadCount"]).reverse();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public isInstalled(externalId: string, clientType: WowClientType) {
|
||||
@@ -434,17 +571,19 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private getProvider(providerName: string) {
|
||||
return this._addonProviders.find(provider => provider.name === providerName);
|
||||
return this._addonProviders.find(
|
||||
(provider) => provider.name === providerName
|
||||
);
|
||||
}
|
||||
|
||||
private getAllStoredAddons(clientType: WowClientType) {
|
||||
const addons: Addon[] = [];
|
||||
|
||||
this._addonStorage.query(store => {
|
||||
this._addonStorage.query((store) => {
|
||||
for (const result of store) {
|
||||
addons.push(result[1] as Addon);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return addons;
|
||||
}
|
||||
@@ -452,7 +591,7 @@ export class AddonService {
|
||||
private async getLocalAddons(clientType: WowClientType): Promise<any> {
|
||||
const addonFolders = await this._warcraftService.listAddons(clientType);
|
||||
const addons: Addon[] = [];
|
||||
console.log('addonFolders', addonFolders);
|
||||
console.log("addonFolders", addonFolders);
|
||||
|
||||
for (const folder of addonFolders) {
|
||||
try {
|
||||
@@ -461,7 +600,6 @@ export class AddonService {
|
||||
if (folder.toc.curseProjectId) {
|
||||
addon = await this.getCurseAddonById(folder, clientType);
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
if (!addon) {
|
||||
@@ -478,22 +616,42 @@ export class AddonService {
|
||||
}
|
||||
|
||||
private getAddonProvider(addonUri: URL): AddonProvider {
|
||||
return this._addonProviders.find(provider => provider.isValidAddonUri(addonUri));
|
||||
return this._addonProviders.find((provider) =>
|
||||
provider.isValidAddonUri(addonUri)
|
||||
);
|
||||
}
|
||||
|
||||
private async getCurseAddonById(
|
||||
addonFolder: AddonFolder,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
const curseProvider = this._addonProviders.find(p => p instanceof CurseAddonProvider);
|
||||
const searchResult = await curseProvider.getById(addonFolder.toc.curseProjectId, clientType).toPromise();
|
||||
const latestFile = this.getLatestFile(searchResult, AddonChannelType.Stable);
|
||||
return this.createAddon(addonFolder.name, searchResult, latestFile, clientType);
|
||||
const curseProvider = this._addonProviders.find(
|
||||
(p) => p instanceof CurseAddonProvider
|
||||
);
|
||||
const searchResult = await curseProvider
|
||||
.getById(addonFolder.toc.curseProjectId, clientType)
|
||||
.toPromise();
|
||||
const latestFile = this.getLatestFile(
|
||||
searchResult,
|
||||
AddonChannelType.Stable
|
||||
);
|
||||
return this.createAddon(
|
||||
addonFolder.name,
|
||||
searchResult,
|
||||
latestFile,
|
||||
clientType
|
||||
);
|
||||
}
|
||||
|
||||
private getLatestFile(searchResult: AddonSearchResult, channelType: AddonChannelType): AddonSearchResultFile {
|
||||
let files = _.filter(searchResult.files, (f: AddonSearchResultFile) => f.channelType <= channelType);
|
||||
files = _.orderBy(files, ['releaseDate']).reverse();
|
||||
private getLatestFile(
|
||||
searchResult: AddonSearchResult,
|
||||
channelType: AddonChannelType
|
||||
): AddonSearchResultFile {
|
||||
let files = _.filter(
|
||||
searchResult.files,
|
||||
(f: AddonSearchResultFile) => f.channelType <= channelType
|
||||
);
|
||||
files = _.orderBy(files, ["releaseDate"]).reverse();
|
||||
return _.first(files);
|
||||
}
|
||||
|
||||
@@ -525,4 +683,4 @@ export class AddonService {
|
||||
autoUpdateEnabled: this._wowUpService.getDefaultAutoUpdate(clientType),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ export class ElectronService {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
get locale(): string {
|
||||
return this.remote.app.getLocale().split('-')[0];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Conditional imports
|
||||
if (!this.isElectron) {
|
||||
@@ -58,6 +62,7 @@ export class ElectronService {
|
||||
this.remote.getCurrentWindow().on('unmaximize', () => {
|
||||
this._windowMaximizedSrc.next(false);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
minimizeWindow() {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Injectable, InjectionToken } from "@angular/core";
|
||||
import { WowClientType } from "app/models/warcraft/wow-client-type";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { filter, first, map } from "rxjs/operators";
|
||||
import { AddonService } from "../addons/addon.service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { WarcraftService } from "../warcraft/warcraft.service";
|
||||
import { WowUpService } from "../wowup/wowup.service";
|
||||
|
||||
const AUTO_UPDATE_PERIOD_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
@@ -16,26 +12,37 @@ export class SessionService {
|
||||
private readonly _selectedClientTypeSrc = new BehaviorSubject(
|
||||
WowClientType.None
|
||||
);
|
||||
private readonly _statusTextSrc = new BehaviorSubject("");
|
||||
private readonly _selectedHomeTab = new BehaviorSubject(0);
|
||||
|
||||
private _autoUpdateInterval?: number;
|
||||
private readonly _pageContextTextSrc = new BehaviorSubject(""); // right side bar text, context to the screen
|
||||
private readonly _statusTextSrc = new BehaviorSubject(""); // left side bar text, context to the app
|
||||
private readonly _selectedHomeTabSrc = new BehaviorSubject(0);
|
||||
|
||||
public readonly selectedClientType$ = this._selectedClientTypeSrc.asObservable();
|
||||
public readonly statusText$ = this._statusTextSrc.asObservable();
|
||||
public readonly selectedHomeTab$ = this._selectedHomeTab.asObservable();
|
||||
public readonly selectedHomeTab$ = this._selectedHomeTabSrc.asObservable();
|
||||
public readonly pageContextText$ = this._pageContextTextSrc.asObservable();
|
||||
|
||||
constructor(
|
||||
private _addonService: AddonService,
|
||||
private _electronService: ElectronService,
|
||||
private _warcraftService: WarcraftService,
|
||||
private _wowUpService: WowUpService
|
||||
) {
|
||||
this.loadInitialClientType().pipe(first()).subscribe();
|
||||
}
|
||||
|
||||
public setContextText(tabIndex: number, text: string) {
|
||||
if (tabIndex !== this._selectedHomeTabSrc.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pageContextTextSrc.next(text);
|
||||
}
|
||||
|
||||
public set statusText(text: string) {
|
||||
this._statusTextSrc.next(text);
|
||||
}
|
||||
|
||||
public set selectedHomeTab(tabIndex: number) {
|
||||
this._selectedHomeTab.next(tabIndex);
|
||||
this._pageContextTextSrc.next("");
|
||||
this._selectedHomeTabSrc.next(tabIndex);
|
||||
}
|
||||
|
||||
public set selectedClientType(clientType: WowClientType) {
|
||||
@@ -47,25 +54,10 @@ export class SessionService {
|
||||
return this._selectedClientTypeSrc.value;
|
||||
}
|
||||
|
||||
public appLoaded() {
|
||||
if (!this._autoUpdateInterval) {
|
||||
this.onAutoUpdateInterval();
|
||||
this._autoUpdateInterval = window.setInterval(
|
||||
this.onAutoUpdateInterval,
|
||||
AUTO_UPDATE_PERIOD_MS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public startUpdaterCheck() {
|
||||
this.checkUpdaterApp();
|
||||
}
|
||||
|
||||
private onAutoUpdateInterval = async () => {
|
||||
console.log("Auto update");
|
||||
const updateCount = await this._addonService.processAutoUpdates();
|
||||
};
|
||||
|
||||
private loadInitialClientType() {
|
||||
return this._warcraftService.installedClientTypes$.pipe(
|
||||
filter((clientTypes) => clientTypes !== undefined),
|
||||
|
||||
@@ -167,6 +167,10 @@ export class WarcraftService {
|
||||
|
||||
public async listAddons(clientType: WowClientType) {
|
||||
const addonFolders: AddonFolder[] = [];
|
||||
if (clientType === WowClientType.None) {
|
||||
return addonFolders;
|
||||
}
|
||||
|
||||
const addonFolderPath = this.getAddonFolderPath(clientType);
|
||||
|
||||
// Folder may not exist if no addons have been installed
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import { remote } from 'electron'
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { remote } from "electron";
|
||||
import { ListFilesResponse } from "common/models/list-files-response";
|
||||
import { ListFilesRequest } from "common/models/list-files-request";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { LIST_FILES_CHANNEL, READ_FILE_CHANNEL } from "common/constants";
|
||||
import { ReadFileResponse } from "common/models/read-file-response";
|
||||
import { ReadFileRequest } from "common/models/read-file-request";
|
||||
|
||||
const fsAccess = util.promisify(fs.access)
|
||||
const fsReadFile = util.promisify(fs.readFile)
|
||||
const userDataPath = remote.app.getPath('userData');
|
||||
const fsAccess = util.promisify(fs.access);
|
||||
const fsReadFile = util.promisify(fs.readFile);
|
||||
const userDataPath = remote.app.getPath("userData");
|
||||
|
||||
export class FileUtils {
|
||||
static async exists(path: string) {
|
||||
try {
|
||||
await fsAccess(path, fs.constants.F_OK)
|
||||
return true
|
||||
await fsAccess(path, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static readFile(path: string) {
|
||||
return fsReadFile(path)
|
||||
return fsReadFile(path);
|
||||
}
|
||||
|
||||
static readFileSync(path: string) {
|
||||
@@ -27,4 +33,4 @@ export class FileUtils {
|
||||
static getUserDataPath() {
|
||||
return userDataPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
wowup-electron/src/assets/i18n/de.json
Normal file
123
wowup-electron/src/assets/i18n/de.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Log ändern",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Schau dir die Webseite an!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Aktualisieren",
|
||||
"INSTALL_FROM_URL_BUTTON": "Von URL installieren",
|
||||
"SEARCH_LABEL": "Suchen",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Anbieter",
|
||||
"STATUS_COLUMN_HEADER": "Status"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "App funktioniert !",
|
||||
"GO_TO_DETAIL": "Zum Detail gehen",
|
||||
"MY_ADDONS_TAB_TITLE": "Meine Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Addons abrufen",
|
||||
"ABOUT_TAB_TITLE": "Über",
|
||||
"OPTIONS_TAB_TITLE": "Optionen"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Updates prüfen",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Nach neuesten Addon-Updates suchen",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Ordner erneut scannen",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scannen Sie Ihren Client-Ordner nach installierten Addons",
|
||||
"UPDATE_ALL_BUTTON": "Alle aktualisieren",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Alle Addons für diesen Client aktualisieren",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Installieren",
|
||||
"ADDON_UPDATE_BUTTON": "Aktualisieren",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Auto-Update aktiviert",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Spielversion",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Neueste Version",
|
||||
"PROVIDER_COLUMN_HEADER": "Anbieter",
|
||||
"STATUS_COLUMN_HEADER": "Status"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorieren",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Automatisches Aktualisieren",
|
||||
"CHANNEL_SUBMENT_TITLE": "Kanal",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Neu installieren",
|
||||
"REMOVE_ADDON_BUTTON": "Entfernen",
|
||||
"STABLE_ADDON_CHANNEL": "Stall",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alpha"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Spalten anzeigen"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Retail/Classic aktualisieren",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Alle Clients aktualisieren"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimieren bei Schliessen",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Beim Schließen des WowUp-Fensters auf das Systemabschnitt minimieren.",
|
||||
"TELEMETRY_DESCRIPTION": "Helfen Sie WowUp zu verbessern, indem Sie anonyme Installationsdaten und/oder Fehler senden.",
|
||||
"TELEMETRY_LABEL": "Telemetrie",
|
||||
"TITLE": "Applikation"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Debug-Daten dumpen",
|
||||
"DEBUG_DATA_DESCRIPTION": "Protokollieren Sie Debug-Daten, um mögliche Probleme zu diagnostizieren. Dies finden Sie in Ihrer aktuellen Protokolldatei für Neugierde.",
|
||||
"DEBUG_DATA_LABEL": "Debug-Daten",
|
||||
"LOG_FILES_BUTTON": "Log-Dateien anzeigen",
|
||||
"LOG_FILES_DESCRIPTION": "Öffnen Sie den Ordner, der Ihre letzten Logdateien enthält.",
|
||||
"LOG_FILES_LABEL": "Log-Dateien",
|
||||
"TITLE": "Debuggen"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Neu installierte Addons werden standardmäßig auf Auto-Update gesetzt",
|
||||
"AUTO_UPDATE_LABEL": "Auto-Update",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Standard-Addon-Kanal",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Addon-Kanal",
|
||||
"RESCAN_CLIENTS_BUTTON": "Neu scannen",
|
||||
"RESCAN_CLIENTS_LABEL": "Installierte World of Warcraft Produkte erneut durchsuchen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Im Browser anzeigen"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Okay"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Nein",
|
||||
"POSITIVE_BUTTON": "Ja"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub or WowInterface URL",
|
||||
"CLOSE_BUTTON": "Schließen",
|
||||
"IMPORT_BUTTON": "Importieren",
|
||||
"INSTALL_BUTTON": "Installieren",
|
||||
"INSTALL_SUCCESS_LABEL": "Installiert!",
|
||||
"TITLE": "Installieren Sie die Addon-URL",
|
||||
"DESCRIPTION": "Wenn Sie ein Addon direkt von einer URL installieren möchten, fügen Sie es unten ein, um loszulegen.",
|
||||
"SUPPORTED_SOURCES": "Unterstützt WowInterface und GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Hilf mir, WowUp zu verbessern, indem du anonyme Installationsdateien und/oder Fehler schickst?",
|
||||
"NEGATIVE_BUTTON": "Nein Danke",
|
||||
"POSITIVE_BUTTON": "Sicher!",
|
||||
"TITLE": "WowUp Telemetrie"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@
|
||||
"IGNORE_ADDON_BUTTON": "Ignore",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Auto Update",
|
||||
"CHANNEL_SUBMENT_TITLE": "Channel",
|
||||
"SHOW_FOLDER": "Show Folder",
|
||||
"REINSTALL_ADDON_BUTTON": "Re-Install",
|
||||
"REMOVE_ADDON_BUTTON": "Remove",
|
||||
"STABLE_ADDON_CHANNEL": "Stable",
|
||||
|
||||
123
wowup-electron/src/assets/i18n/es.json
Normal file
123
wowup-electron/src/assets/i18n/es.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Registro de Cambios",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Echa un vistazo a la página web!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Actualizar",
|
||||
"INSTALL_FROM_URL_BUTTON": "Instalar desde URL",
|
||||
"SEARCH_LABEL": "Buscar",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Proveedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "¡La aplicación funciona!",
|
||||
"GO_TO_DETAIL": "Ir a Detalle",
|
||||
"MY_ADDONS_TAB_TITLE": "Mis Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Obtener Addons",
|
||||
"ABOUT_TAB_TITLE": "Acerca de",
|
||||
"OPTIONS_TAB_TITLE": "Opciones"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Verificar Actualizaciones",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Buscar últimas actualizaciones de addon",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Volver a escanear carpetas",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Busca por addons instalados",
|
||||
"UPDATE_ALL_BUTTON": "Actualizar todo",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Actualizar todos los addons para este cliente",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Instalar",
|
||||
"ADDON_UPDATE_BUTTON": "Actualizar",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Actualización automática habilitada",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Versión del juego",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Última Versión",
|
||||
"PROVIDER_COLUMN_HEADER": "Proveedor",
|
||||
"STATUS_COLUMN_HEADER": "Situación"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorar",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Actualización automática",
|
||||
"CHANNEL_SUBMENT_TITLE": "Canal",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Reinstalar",
|
||||
"REMOVE_ADDON_BUTTON": "Eliminar",
|
||||
"STABLE_ADDON_CHANNEL": "Estable",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alfa"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Mostrar Columna"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Actualizar Retail/Clásico",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Actualizar Todos los Clientes"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimizar al Cerrar",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Al cerrar la ventana de WowUp, minimízala a la bandeja del sistema.",
|
||||
"TELEMETRY_DESCRIPTION": "Ayude a mejorar WowUp enviando datos y / o errores de instalación de forma anónima.",
|
||||
"TELEMETRY_LABEL": "Telemetría",
|
||||
"TITLE": "Aplicación"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Eliminar registro de depuración de datos",
|
||||
"DEBUG_DATA_DESCRIPTION": "Registra datos de depuración y ayuda a diagnosticar problemas potenciales. Solo por curiosidad, esto se puede encontrar en su último archivo de registro.",
|
||||
"DEBUG_DATA_LABEL": "Datos de depuración",
|
||||
"LOG_FILES_BUTTON": "Mostrar Archivos de Registro",
|
||||
"LOG_FILES_DESCRIPTION": "Abra la carpeta que contiene sus últimos archivos de registro.",
|
||||
"LOG_FILES_LABEL": "Archivos de Registro",
|
||||
"TITLE": "Depuración"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Los addons recién instalados se configurarán para actualizarse automáticamente de forma predeterminada",
|
||||
"AUTO_UPDATE_LABEL": "Actualización automática",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canal de Addon Estándar",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Volver a escanear",
|
||||
"RESCAN_CLIENTS_LABEL": "Volver a escanear los productos de World of Warcraft instalados"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Ver en el navegador"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Aceptar"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "No",
|
||||
"POSITIVE_BUTTON": "Si"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Por ejemplo, URL de GitHub o WowInterface",
|
||||
"CLOSE_BUTTON": "Cerrar",
|
||||
"IMPORT_BUTTON": "Importar",
|
||||
"INSTALL_BUTTON": "Instalar",
|
||||
"INSTALL_SUCCESS_LABEL": "Instalado!",
|
||||
"TITLE": "Instalar desde URL",
|
||||
"DESCRIPTION": "Si desea instalar un addon directamente desde una URL, péguelo a continuación para comenzar.",
|
||||
"SUPPORTED_SOURCES": "Soporta WowInterface y GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "¿Ayudarme a mejorar WowUp enviando datos y / o errores de instalación de forma anónima?",
|
||||
"NEGATIVE_BUTTON": "No, gracias",
|
||||
"POSITIVE_BUTTON": "¡Seguro!",
|
||||
"TITLE": "Telemetría WowUp"
|
||||
}
|
||||
}
|
||||
}
|
||||
123
wowup-electron/src/assets/i18n/fr.json
Normal file
123
wowup-electron/src/assets/i18n/fr.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Journal des modifications",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Découvrez le site web!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Rafraîchir",
|
||||
"INSTALL_FROM_URL_BUTTON": "Installer depuis l'URL",
|
||||
"SEARCH_LABEL": "Chercher",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Auteur",
|
||||
"PROVIDER_COLUMN_HEADER": "Fournisseur",
|
||||
"STATUS_COLUMN_HEADER": "Statut"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "L'application fonctionne !",
|
||||
"GO_TO_DETAIL": "Aller au détail",
|
||||
"MY_ADDONS_TAB_TITLE": "Mes Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Obtenir des Addons",
|
||||
"ABOUT_TAB_TITLE": "À propos de",
|
||||
"OPTIONS_TAB_TITLE": "Options"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Vérifier les mises à jour",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Vérifier les dernières mises à jour des modules complémentaires",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Re-scanner les dossiers",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scannez votre dossier client pour trouver des extensions installées",
|
||||
"UPDATE_ALL_BUTTON": "Tout mettre à jour",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Mettre à jour tous les addons pour ce client",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Installer",
|
||||
"ADDON_UPDATE_BUTTON": "Mise à jour",
|
||||
"AUTHOR_COLUMN_HEADER": "Auteur",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Mise à jour automatique activée",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Version du jeu",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Dernière version",
|
||||
"PROVIDER_COLUMN_HEADER": "Fournisseur",
|
||||
"STATUS_COLUMN_HEADER": "Statut"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorer",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Mise à jour automatique",
|
||||
"CHANNEL_SUBMENT_TITLE": "Chaîne",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Réinstaller",
|
||||
"REMOVE_ADDON_BUTTON": "Retirer",
|
||||
"STABLE_ADDON_CHANNEL": "Écurie",
|
||||
"BETA_ADDON_CHANNEL": "Bêta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alphabétisation"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Afficher les colonnes"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Mise à jour Retail/Classique",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Mettre à jour tous les clients"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimiser à la fermeture",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Lorsque vous fermez la fenêtre WowUp, minimisez dans la barre d'état système.",
|
||||
"TELEMETRY_DESCRIPTION": "Aidez à améliorer WowUp en envoyant des données d'installation et/ou des erreurs anonymes.",
|
||||
"TELEMETRY_LABEL": "Télémétrie",
|
||||
"TITLE": "Application"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Dump des données de débogage",
|
||||
"DEBUG_DATA_DESCRIPTION": "Log les données de débogage pour aider à diagnostiquer les problèmes potentiels. Cela peut être trouvé dans votre dernier fichier journal pour les curieux.",
|
||||
"DEBUG_DATA_LABEL": "Déboguer les données",
|
||||
"LOG_FILES_BUTTON": "Afficher les fichiers de log",
|
||||
"LOG_FILES_DESCRIPTION": "Ouvrez le dossier qui contient vos derniers fichiers journaux.",
|
||||
"LOG_FILES_LABEL": "Fichiers de log",
|
||||
"TITLE": "Debug"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Les extensions nouvellement installées seront mises à jour automatiquement par défaut",
|
||||
"AUTO_UPDATE_LABEL": "Mise à jour automatique",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canal d'extension par défaut",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal d'Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Re-scanner",
|
||||
"RESCAN_CLIENTS_LABEL": "Rescanner les produits de World of Warcraft installés"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Voir dans le navigateur"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Ok"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Non",
|
||||
"POSITIVE_BUTTON": "Oui"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub or WowInterface URL",
|
||||
"CLOSE_BUTTON": "Clôturer",
|
||||
"IMPORT_BUTTON": "Importation",
|
||||
"INSTALL_BUTTON": "Installer",
|
||||
"INSTALL_SUCCESS_LABEL": "Installé !",
|
||||
"TITLE": "Install Addon URL",
|
||||
"DESCRIPTION": "Si vous voulez installer un addon directement à partir d'une URL collez le ci-dessous pour commencer.",
|
||||
"SUPPORTED_SOURCES": "Supporte WowInterface et GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Aidez-moi à améliorer WowUp en envoyant des applications anonymes installant des données et/ou des erreurs?",
|
||||
"NEGATIVE_BUTTON": "Non Merci",
|
||||
"POSITIVE_BUTTON": "Bien sûr!",
|
||||
"TITLE": "Télémétrie WowUp"
|
||||
}
|
||||
}
|
||||
}
|
||||
123
wowup-electron/src/assets/i18n/it.json
Normal file
123
wowup-electron/src/assets/i18n/it.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Registro Delle Modifiche",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Dai un'occhiata al sito!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Aggiorna",
|
||||
"INSTALL_FROM_URL_BUTTON": "Installa da URL",
|
||||
"SEARCH_LABEL": "Cerca",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autore",
|
||||
"PROVIDER_COLUMN_HEADER": "Provveditore",
|
||||
"STATUS_COLUMN_HEADER": "Stato"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "L'app funziona !",
|
||||
"GO_TO_DETAIL": "Vai ai dettagli",
|
||||
"MY_ADDONS_TAB_TITLE": "I Miei Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Ottieni Addons",
|
||||
"ABOUT_TAB_TITLE": "Informazioni",
|
||||
"OPTIONS_TAB_TITLE": "Opzioni"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Controlla Aggiornamenti",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Controlla gli ultimi aggiornamenti dei componenti aggiuntivi",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Ri-Scansiona Cartelle",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Scansiona la cartella client per gli addons installati",
|
||||
"UPDATE_ALL_BUTTON": "Aggiorna Tutto",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Aggiorna tutti gli addons per questo client",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Installa",
|
||||
"ADDON_UPDATE_BUTTON": "Aggiorna",
|
||||
"AUTHOR_COLUMN_HEADER": "Autore",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Aggiornamento automatico abilitato",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Versione Del Gioco",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Ultima Versione",
|
||||
"PROVIDER_COLUMN_HEADER": "Provveditore",
|
||||
"STATUS_COLUMN_HEADER": "Stato"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignora",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Aggiornamento Automatico",
|
||||
"CHANNEL_SUBMENT_TITLE": "Canale",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Reinstalla",
|
||||
"REMOVE_ADDON_BUTTON": "Rimuovi",
|
||||
"STABLE_ADDON_CHANNEL": "Stabile",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alfa"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Mostra Colonne"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Aggiorna Retail/Classic",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Aggiorna Tutti I Clienti"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimizza alla chiusura",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Quando si chiude la finestra WowUp minimizzare nel vassoio di sistema.",
|
||||
"TELEMETRY_DESCRIPTION": "Aiuta a migliorare WowUp inviando dati di installazione e/o errori anonimi.",
|
||||
"TELEMETRY_LABEL": "Telemetria",
|
||||
"TITLE": "Applicazione"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Dump Dati Di Debug",
|
||||
"DEBUG_DATA_DESCRIPTION": "Registra i dati di debug per aiutare a diagnosticare potenziali problemi. Questo può essere trovato nel tuo ultimo file di log per i curiosi.",
|
||||
"DEBUG_DATA_LABEL": "Dati Di Debug",
|
||||
"LOG_FILES_BUTTON": "Mostra File Di Log",
|
||||
"LOG_FILES_DESCRIPTION": "Aprire la cartella che contiene gli ultimi due file di registro.",
|
||||
"LOG_FILES_LABEL": "File Di Log",
|
||||
"TITLE": "Debug"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Gli addons di nuova installazione saranno impostati per l'aggiornamento automatico di default",
|
||||
"AUTO_UPDATE_LABEL": "Aggiornamento Automatico",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canale Addon Predefinito",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canale Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Riscansiona",
|
||||
"RESCAN_CLIENTS_LABEL": "Rescan i prodotti World of Warcraft installati"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Visualizza nel browser"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Ok"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "No",
|
||||
"POSITIVE_BUTTON": "Sì"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "URL di esempio GitHub o WowInterface",
|
||||
"CLOSE_BUTTON": "Chiudi",
|
||||
"IMPORT_BUTTON": "Importa",
|
||||
"INSTALL_BUTTON": "Installa",
|
||||
"INSTALL_SUCCESS_LABEL": "Installato!",
|
||||
"TITLE": "Installa l'URL del componente aggiuntivo",
|
||||
"DESCRIPTION": "Se si desidera installare un addon direttamente da un URL incollarlo qui sotto per iniziare.",
|
||||
"SUPPORTED_SOURCES": "Supporta WowInterface e GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Aiutami a migliorare WowUp inviando dati e/o errori di installazione anonimi dell'app?",
|
||||
"NEGATIVE_BUTTON": "No Grazie",
|
||||
"POSITIVE_BUTTON": "Certo!",
|
||||
"TITLE": "Telemetria WowUp"
|
||||
}
|
||||
}
|
||||
}
|
||||
124
wowup-electron/src/assets/i18n/pt.json
Normal file
124
wowup-electron/src/assets/i18n/pt.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Registro de Alterações",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Conheça o nosso site!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Atualizar",
|
||||
"INSTALL_FROM_URL_BUTTON": "Instalar pela URL",
|
||||
"SEARCH_LABEL": "Procurar",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "App funciona!",
|
||||
"GO_TO_DETAIL": "Ir para Detalhes",
|
||||
"MY_ADDONS_TAB_TITLE": "Meus Addons",
|
||||
"GET_ADDONS_TAB_TITLE": "Obtenha Addons",
|
||||
"ABOUT_TAB_TITLE": "Sobre",
|
||||
"OPTIONS_TAB_TITLE": "Opções"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Verificar Atualizações",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Verificar atualizações recentes",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Re-escanear pastas",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Procura por Addons instalados",
|
||||
"UPDATE_ALL_BUTTON": "Actualizar todos",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Atualizar todos os Addons",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Instalar",
|
||||
"ADDON_UPDATE_BUTTON": "Atualizar",
|
||||
"AUTHOR_COLUMN_HEADER": "Autor",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Atualização automática habilitada",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Versão do Jogo",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Ultima versão",
|
||||
"PROVIDER_COLUMN_HEADER": "Provedor",
|
||||
"STATUS_COLUMN_HEADER": "Estado"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Ignorar",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Atualização Automática",
|
||||
"CHANNEL_SUBMENT_TITLE": "Canal",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Reinstalar",
|
||||
"REMOVE_ADDON_BUTTON": "Remover",
|
||||
"STABLE_ADDON_CHANNEL": "Estável",
|
||||
"BETA_ADDON_CHANNEL": "Beta",
|
||||
"ALPHA_ADDON_CHANNEL": "Alfa"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Exibir Colunas"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Atualizar Retail/Clássico",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Atualizar todos os clientes"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Minimizar ao Fechar",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "Ao fechar a janela do WowUp, minimize para a bandeja do sistema.",
|
||||
"TELEMETRY_DESCRIPTION": "Ajude a melhorar o WowUp enviando dados e/ou erros de instalação anônimamente.",
|
||||
"TELEMETRY_LABEL": "Telemetria",
|
||||
"TITLE": "Aplicativo"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Esvaziar log de depuração de dados",
|
||||
"DEBUG_DATA_DESCRIPTION": "Registra os dados de depuração e ajuda a diagnosticar problemas potenciais. Apenas por o curiosidade, isso pode ser encontrado em seu último arquivo de registro.",
|
||||
"DEBUG_DATA_LABEL": "Depurar Dados",
|
||||
"LOG_FILES_BUTTON": "Mostrar Arquivos de Registro",
|
||||
"LOG_FILES_DESCRIPTION": "Abre a pasta que contém seus últimos arquivos de registro.",
|
||||
"LOG_FILES_LABEL": "Arquivos de Registro",
|
||||
"TITLE": "Depurar"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Addons recém-instalados serão definidos para atualizar automáticamente por padrão",
|
||||
"AUTO_UPDATE_LABEL": "Atualização Automática",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Canal de Addon Padrão",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Canal de Addon",
|
||||
"RESCAN_CLIENTS_BUTTON": "Re-escanear",
|
||||
"RESCAN_CLIENTS_LABEL": "Reescanear World of Warcraft instalados"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Visualizar no navegador"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Ok"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Não",
|
||||
"POSITIVE_BUTTON": "Sim"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Ex. GitHub ou WowInterface URL",
|
||||
"CLOSE_BUTTON": "Fechar",
|
||||
"IMPORT_BUTTON": "Importar",
|
||||
"INSTALL_BUTTON": "Instalar",
|
||||
"INSTALL_SUCCESS_LABEL": "Instalado!",
|
||||
"TITLE": "Instalar Addon pela URL",
|
||||
"DESCRIPTION": "Se você deseja instalar um addon diretamente de uma URL, cole-a abaixo para iniciar.",
|
||||
"SUPPORTED_SOURCES": "Suporta WowInterface e GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Ajude-nos a melhorar o WowUp enviando dados e/ou erros de instalação do aplicativo anônimamente?",
|
||||
"NEGATIVE_BUTTON": "Não obrigado",
|
||||
"POSITIVE_BUTTON": "Claro!",
|
||||
"TITLE": "Telemetria do WowUp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
wowup-electron/src/assets/i18n/ru.json
Normal file
123
wowup-electron/src/assets/i18n/ru.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "Журнал изменений",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "Посмотрите на сайт!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "Обновить",
|
||||
"INSTALL_FROM_URL_BUTTON": "Установить из URL",
|
||||
"SEARCH_LABEL": "Искать",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "Автор",
|
||||
"PROVIDER_COLUMN_HEADER": "Поставщик",
|
||||
"STATUS_COLUMN_HEADER": "Статус"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "Приложение работает !",
|
||||
"GO_TO_DETAIL": "Детали",
|
||||
"MY_ADDONS_TAB_TITLE": "Мои аддоны",
|
||||
"GET_ADDONS_TAB_TITLE": "Получить аддоны",
|
||||
"ABOUT_TAB_TITLE": "О программе",
|
||||
"OPTIONS_TAB_TITLE": "Варианты"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "Проверить обновления",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "Проверить наличие последних обновлений аддона",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"RESCAN_FOLDERS_BUTTON": "Пересканировать папки",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "Сканирование клиентской папки для установленных аддонов",
|
||||
"UPDATE_ALL_BUTTON": "Обновить все",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "Обновить все аддоны для этого клиента",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "Установить",
|
||||
"ADDON_UPDATE_BUTTON": "Обновить",
|
||||
"AUTHOR_COLUMN_HEADER": "Автор",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "Автообновление включено",
|
||||
"GAME_VERSION_COLUMN_HEADER": "Версия игры",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "Последняя версия",
|
||||
"PROVIDER_COLUMN_HEADER": "Поставщик",
|
||||
"STATUS_COLUMN_HEADER": "Статус"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "Пропустить",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "Автообновление",
|
||||
"CHANNEL_SUBMENT_TITLE": "Канал",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "Переустановить",
|
||||
"REMOVE_ADDON_BUTTON": "Удалить",
|
||||
"STABLE_ADDON_CHANNEL": "Конюшня",
|
||||
"BETA_ADDON_CHANNEL": "Бета",
|
||||
"ALPHA_ADDON_CHANNEL": "Альфа"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "Показать колонки"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "Обновить Retail/Classic",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "Обновить всех клиентов"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "Свернуть при закрытии",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "При закрытии окна WowUp сворачиваем в системный трей.",
|
||||
"TELEMETRY_DESCRIPTION": "Помогите улучшить WowUp, отправив анонимные данные об установке и/или ошибках.",
|
||||
"TELEMETRY_LABEL": "Телеметрия",
|
||||
"TITLE": "Приложение"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "Дамп отладочных данных",
|
||||
"DEBUG_DATA_DESCRIPTION": "Записывать отладочные данные, чтобы помочь в диагностике потенциальных проблем. Это можно найти в последнем файле журнала для любопытства.",
|
||||
"DEBUG_DATA_LABEL": "Отладка данных",
|
||||
"LOG_FILES_BUTTON": "Показать лог-файлы",
|
||||
"LOG_FILES_DESCRIPTION": "Откройте папку, содержащую последние несколько лог файлов.",
|
||||
"LOG_FILES_LABEL": "Файлы логов",
|
||||
"TITLE": "Debug"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "Новые установленные дополнения будут автоматически обновляться по умолчанию",
|
||||
"AUTO_UPDATE_LABEL": "Автообновление",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "Канал аддона по умолчанию",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "Канал дополнения",
|
||||
"RESCAN_CLIENTS_BUTTON": "Пересканировать",
|
||||
"RESCAN_CLIENTS_LABEL": "Пересканируйте установленные продукты World of Warcraft"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "Просмотр в браузере"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "Окей"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "Нет",
|
||||
"POSITIVE_BUTTON": "Да"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "Пример URL-адреса GitHub или WowInterface",
|
||||
"CLOSE_BUTTON": "Закрыть",
|
||||
"IMPORT_BUTTON": "Импорт",
|
||||
"INSTALL_BUTTON": "Установить",
|
||||
"INSTALL_SUCCESS_LABEL": "Установлено!",
|
||||
"TITLE": "Install Addon URL",
|
||||
"DESCRIPTION": "Если вы хотите установить аддон непосредственно с URL, вставьте его ниже, чтобы начать.",
|
||||
"SUPPORTED_SOURCES": "Поддерживает WowInterface и GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "Помогите мне улучшить WowUp, отправив анонимные данные и/или ошибки в приложении?",
|
||||
"NEGATIVE_BUTTON": "Нет, спасибо",
|
||||
"POSITIVE_BUTTON": "Конечно!",
|
||||
"TITLE": "WowUp Телеметрия"
|
||||
}
|
||||
}
|
||||
}
|
||||
123
wowup-electron/src/assets/i18n/zh.json
Normal file
123
wowup-electron/src/assets/i18n/zh.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"PAGES": {
|
||||
"ABOUT": {
|
||||
"CHANGE_LOG_SECTION_LABEL": "更改日志",
|
||||
"TITLE": "WowUp.io",
|
||||
"WEBSITE_LINK_LABEL": "查看网站!"
|
||||
},
|
||||
"GET_ADDONS": {
|
||||
"CLIENT_TYPE_SELECT_LABEL": "World of Warcraft",
|
||||
"REFRESH_BUTTON": "刷新",
|
||||
"INSTALL_FROM_URL_BUTTON": "从 URL 安装",
|
||||
"SEARCH_LABEL": "搜索",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"AUTHOR_COLUMN_HEADER": "作者",
|
||||
"PROVIDER_COLUMN_HEADER": "提供商",
|
||||
"STATUS_COLUMN_HEADER": "状态"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"TITLE": "应用程序正常工作!",
|
||||
"GO_TO_DETAIL": "转到详细信息",
|
||||
"MY_ADDONS_TAB_TITLE": "我的附加组件",
|
||||
"GET_ADDONS_TAB_TITLE": "获取 Addons",
|
||||
"ABOUT_TAB_TITLE": "关于的",
|
||||
"OPTIONS_TAB_TITLE": "备选方案"
|
||||
},
|
||||
"MY_ADDONS": {
|
||||
"CHECK_UPDATES_BUTTON": "检查更新",
|
||||
"CHECK_UPDATES_BUTTON_TOOLTIP": "检查最新的插件更新",
|
||||
"CLIENT_TYPE_SELECT_LABEL": "战术世界",
|
||||
"RESCAN_FOLDERS_BUTTON": "重新扫描文件夹",
|
||||
"RESCAN_FOLDERS_BUTTON_TOOLTIP": "扫描已安装附加组件的客户端文件夹",
|
||||
"UPDATE_ALL_BUTTON": "全部更新",
|
||||
"UPDATE_ALL_BUTTON_TOOLTIP": "更新此客户端的所有插件",
|
||||
"TABLE": {
|
||||
"ADDON_COLUMN_HEADER": "Addon",
|
||||
"ADDON_INSTALL_BUTTON": "安装",
|
||||
"ADDON_UPDATE_BUTTON": "更新",
|
||||
"AUTHOR_COLUMN_HEADER": "作者",
|
||||
"AUTO_UPDATE_ICON_TOOLTIP": "自动更新已启用",
|
||||
"GAME_VERSION_COLUMN_HEADER": "游戏版本",
|
||||
"LATEST_VERSION_COLUMN_HEADER": "最新版本",
|
||||
"PROVIDER_COLUMN_HEADER": "提供商",
|
||||
"STATUS_COLUMN_HEADER": "状态"
|
||||
},
|
||||
"ADDON_CONTEXT_MENU": {
|
||||
"IGNORE_ADDON_BUTTON": "忽略",
|
||||
"AUTO_UPDATE_ADDON_BUTTON": "自动更新",
|
||||
"CHANNEL_SUBMENT_TITLE": "频道",
|
||||
"SHOW_FOLDER": "SHOW_FOLDER",
|
||||
"REINSTALL_ADDON_BUTTON": "重新安装",
|
||||
"REMOVE_ADDON_BUTTON": "删除",
|
||||
"STABLE_ADDON_CHANNEL": "稳定",
|
||||
"BETA_ADDON_CHANNEL": "测试版",
|
||||
"ALPHA_ADDON_CHANNEL": "阿尔法"
|
||||
},
|
||||
"COLUMNS_CONTEXT_MENU": {
|
||||
"TITLE": "显示列"
|
||||
},
|
||||
"UPDATE_ALL_CONTEXT_MENU": {
|
||||
"UPDATE_RETAIL_CLASSIC_BUTTON": "更新零售/经典",
|
||||
"UPDATE_ALL_CLIENTS_BUTTON": "更新所有客户端"
|
||||
}
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPLICATION": {
|
||||
"MINIMIZE_ON_CLOSE_LABEL": "关闭时最小化",
|
||||
"MINIMIZE_ON_CLOSE_DESCRIPTION": "关闭WowUp窗口时,最小化到系统托盘。",
|
||||
"TELEMETRY_DESCRIPTION": "通过匿名发送数据和/或安装错误来帮助改进WowUp。",
|
||||
"TELEMETRY_LABEL": "遥测",
|
||||
"TITLE": "程序"
|
||||
},
|
||||
"DEBUG": {
|
||||
"DEBUG_DATA_BUTTON": "转储调试数据",
|
||||
"DEBUG_DATA_DESCRIPTION": "记录调试数据以帮助诊断潜在的问题。这可以在您最新的日志文件中找到。",
|
||||
"DEBUG_DATA_LABEL": "调试数据",
|
||||
"LOG_FILES_BUTTON": "显示日志文件",
|
||||
"LOG_FILES_DESCRIPTION": "打开包含您最后几个日志文件的文件夹。",
|
||||
"LOG_FILES_LABEL": "日志文件",
|
||||
"TITLE": "除错"
|
||||
},
|
||||
"WOW": {
|
||||
"AUTO_UPDATE_DESCRIPTION": "新安装的插件将默认设置为自动更新",
|
||||
"AUTO_UPDATE_LABEL": "自动更新",
|
||||
"TITLE": "World of Warcraft",
|
||||
"DEFAULT_ADDON_CHANNEL_LABEL": "默认附加组件频道",
|
||||
"DEFAULT_ADDON_CHANNEL_SELECT_LABEL": "附加组件频道",
|
||||
"RESCAN_CLIENTS_BUTTON": "重新扫描",
|
||||
"RESCAN_CLIENTS_LABEL": "重新扫描已安装的Warcraft产品世界"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DIALOGS": {
|
||||
"ADDON_DETAILS": {
|
||||
"VIEW_IN_BROWSER_BUTTON": "在浏览器中查看"
|
||||
},
|
||||
"ALERT": {
|
||||
"POSITIVE_BUTTON": "好的"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"NEGATIVE_BUTTON": "否",
|
||||
"POSITIVE_BUTTON": "是"
|
||||
},
|
||||
"INSTALL_FROM_URL": {
|
||||
"ADDON_URL_INPUT_LABEL": "Addon URL",
|
||||
"ADDON_URL_INPUT_PLACEHOLDER": "例。 GitHub或WowInterface URL",
|
||||
"CLOSE_BUTTON": "关闭",
|
||||
"IMPORT_BUTTON": "导入",
|
||||
"INSTALL_BUTTON": "安装",
|
||||
"INSTALL_SUCCESS_LABEL": "已安装!",
|
||||
"TITLE": "通过网址远程安装",
|
||||
"DESCRIPTION": "如果您想直接从下面的 URL 粘贴中安装一个插件,就开始了。",
|
||||
"SUPPORTED_SOURCES": "支持Wow界面和 GitHub*"
|
||||
},
|
||||
"TELEMETRY": {
|
||||
"DESCRIPTION": "通过发送匿名应用安装数据和/或错误来帮助我改进WowUp?",
|
||||
"NEGATIVE_BUTTON": "不,谢谢!",
|
||||
"POSITIVE_BUTTON": "当然!",
|
||||
"TITLE": "WowUp遥测"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
export const DOWNLOAD_FILE_CHANNEL = 'download-file';
|
||||
export const COPY_DIRECTORY_CHANNEL = 'copy-directory';
|
||||
export const DELETE_DIRECTORY_CHANNEL = 'delete-directory';
|
||||
export const RENAME_DIRECTORY_CHANNEL = 'rename-directory';
|
||||
export const STAT_DIRECTORY_CHANNEL = 'stat-directory';
|
||||
export const LIST_DIRECTORIES_CHANNEL = 'list-directories';
|
||||
export const PATH_EXISTS_CHANNEL = 'path-exists';
|
||||
export const LIST_FILES_CHANNEL = 'list-files';
|
||||
export const READ_FILE_CHANNEL = 'read-file';
|
||||
export const UNZIP_FILE_CHANNEL = 'unzip-file';
|
||||
export const COPY_FILE_CHANNEL = 'copy-file';
|
||||
export const CURSE_HASH_FILE_CHANNEL = 'curse-hash-file';
|
||||
export const SHOW_DIRECTORY = 'show-directory';
|
||||
export const DOWNLOAD_FILE_CHANNEL = "download-file";
|
||||
export const COPY_DIRECTORY_CHANNEL = "copy-directory";
|
||||
export const DELETE_DIRECTORY_CHANNEL = "delete-directory";
|
||||
export const RENAME_DIRECTORY_CHANNEL = "rename-directory";
|
||||
export const STAT_DIRECTORY_CHANNEL = "stat-directory";
|
||||
export const LIST_DIRECTORIES_CHANNEL = "list-directories";
|
||||
export const PATH_EXISTS_CHANNEL = "path-exists";
|
||||
export const LIST_FILES_CHANNEL = "list-files";
|
||||
export const READ_FILE_CHANNEL = "read-file";
|
||||
export const UNZIP_FILE_CHANNEL = "unzip-file";
|
||||
export const COPY_FILE_CHANNEL = "copy-file";
|
||||
export const CURSE_HASH_FILE_CHANNEL = "curse-hash-file";
|
||||
export const SHOW_DIRECTORY = "show-directory";
|
||||
export const CURSE_GET_SCAN_RESULTS = "curse-get-scan-results";
|
||||
|
||||
253
wowup-electron/src/common/curse/curse-folder-scanner.ts
Normal file
253
wowup-electron/src/common/curse/curse-folder-scanner.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as _ from "lodash";
|
||||
import * as async from "async";
|
||||
import { CurseScanResult } from "./curse-scan-result";
|
||||
import { readDirRecursive, readFile } from "../../../file.utils";
|
||||
|
||||
const nativeAddon = require("../../../build/Release/addon.node");
|
||||
|
||||
export class CurseFolderScanner {
|
||||
private get tocFileCommentsRegex() {
|
||||
return /\s*#.*$/gm;
|
||||
}
|
||||
|
||||
private get tocFileIncludesRegex() {
|
||||
return /^\s*((?:(?<!\.\.).)+\.(?:xml|lua))\s*$/gim;
|
||||
}
|
||||
|
||||
private get tocFileRegex() {
|
||||
return /^([^\/]+)[\\\/]\1\.toc$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlRegex() {
|
||||
return /^[^\/\\]+[\/\\]Bindings\.xml$/i;
|
||||
}
|
||||
|
||||
private get bindingsXmlIncludesRegex() {
|
||||
return /<(?:Include|Script)\s+file=[\""\""']((?:(?<!\.\.).)+)[\""\""']\s*\/>/gi;
|
||||
}
|
||||
|
||||
private get bindingsXmlCommentsRegex() {
|
||||
return /<!--.*?-->/gs;
|
||||
}
|
||||
|
||||
async scanFolder(folderPath: string): Promise<CurseScanResult> {
|
||||
const files = await readDirRecursive(folderPath);
|
||||
console.log("listAllFiles", folderPath, files.length);
|
||||
|
||||
let matchingFiles = await this.getMatchingFiles(folderPath, files);
|
||||
matchingFiles = _.sortBy(matchingFiles, (f) => f.toLowerCase());
|
||||
|
||||
const individualFingerprints = await async.mapLimit<string, number>(
|
||||
matchingFiles,
|
||||
2,
|
||||
async (path, callback) => {
|
||||
const normalizedFileHash = await this.getFileHash(path);
|
||||
callback(undefined, normalizedFileHash);
|
||||
}
|
||||
);
|
||||
|
||||
const hashConcat = _.orderBy(individualFingerprints).join("");
|
||||
const fingerprint = this.getStringHash(hashConcat);
|
||||
console.log("fingerprint", fingerprint);
|
||||
|
||||
return {
|
||||
directory: folderPath,
|
||||
fileCount: matchingFiles.length,
|
||||
fingerprint,
|
||||
folderName: path.basename(folderPath),
|
||||
individualFingerprints,
|
||||
};
|
||||
}
|
||||
|
||||
private async getMatchingFiles(
|
||||
folderPath: string,
|
||||
filePaths: string[]
|
||||
): Promise<string[]> {
|
||||
const parentDir = path.dirname(folderPath) + path.sep;
|
||||
const matchingFileList: string[] = [];
|
||||
const fileInfoList: string[] = [];
|
||||
for (let filePath of filePaths) {
|
||||
const input = filePath.toLowerCase().replace(parentDir.toLowerCase(), "");
|
||||
|
||||
if (this.tocFileRegex.test(input)) {
|
||||
fileInfoList.push(filePath);
|
||||
} else if (this.bindingsXmlRegex.test(input)) {
|
||||
matchingFileList.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('fileInfoList', fileInfoList.length)
|
||||
for (let fileInfo of fileInfoList) {
|
||||
await this.processIncludeFile(matchingFileList, fileInfo);
|
||||
}
|
||||
|
||||
return matchingFileList;
|
||||
}
|
||||
|
||||
private async processIncludeFile(
|
||||
matchingFileList: string[],
|
||||
fileInfo: string
|
||||
) {
|
||||
if (!fs.existsSync(fileInfo) || matchingFileList.indexOf(fileInfo) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchingFileList.push(fileInfo);
|
||||
|
||||
let input = await readFile(fileInfo);
|
||||
input = this.removeComments(fileInfo, input);
|
||||
|
||||
const inclusions = this.getFileInclusionMatches(fileInfo, input);
|
||||
if (!inclusions || !inclusions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dirname = path.dirname(fileInfo);
|
||||
for (let include of inclusions) {
|
||||
const fileName = path.join(dirname, include.replace(/\\/g, path.sep));
|
||||
await this.processIncludeFile(matchingFileList, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private getFileInclusionMatches(
|
||||
fileInfo: string,
|
||||
fileContent: string
|
||||
): string[] | null {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case ".xml":
|
||||
return this.matchAll(fileContent, this.bindingsXmlIncludesRegex);
|
||||
case ".toc":
|
||||
return this.matchAll(fileContent, this.tocFileIncludesRegex);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private removeComments(fileInfo: string, fileContent: string): string {
|
||||
const ext = path.extname(fileInfo);
|
||||
switch (ext) {
|
||||
case ".xml":
|
||||
return fileContent.replace(this.bindingsXmlCommentsRegex, "");
|
||||
case ".toc":
|
||||
return fileContent.replace(this.tocFileCommentsRegex, "");
|
||||
default:
|
||||
return fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
private matchAll(str: string, regex: RegExp): string[] {
|
||||
const matches: string[] = [];
|
||||
let currentMatch: RegExpExecArray;
|
||||
do {
|
||||
currentMatch = regex.exec(str);
|
||||
if (currentMatch) {
|
||||
matches.push(currentMatch[1]);
|
||||
}
|
||||
} while (currentMatch);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// private computeNormalizedFileHash = (filePath: string) => {
|
||||
// return this.computeFileHash(filePath, true);
|
||||
// };
|
||||
|
||||
// private computeFileHash = (
|
||||
// filePath: string,
|
||||
// normalizeWhitespace: boolean
|
||||
// ) => {
|
||||
// return this.getFileHash(filePath);
|
||||
// };
|
||||
|
||||
// private computeStringHash = (str: string): Promise<number> => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
// if (arg.error) {
|
||||
// return reject(arg.error);
|
||||
// }
|
||||
|
||||
// resolve(arg.fingerprint);
|
||||
// };
|
||||
|
||||
// const request: CurseHashFileRequest = {
|
||||
// targetString: str,
|
||||
// targetStringEncoding: "ascii",
|
||||
// responseKey: uuidv4(),
|
||||
// normalizeWhitespace: false,
|
||||
// precomputedLength: 0,
|
||||
// };
|
||||
|
||||
// this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
// this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
// });
|
||||
// };
|
||||
|
||||
// private computeHash = (
|
||||
// filePath: string,
|
||||
// precomputedLength: number = 0,
|
||||
// normalizeWhitespace: boolean = false
|
||||
// ): Promise<number> => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// const eventHandler = (_evt: any, arg: CurseHashFileResponse) => {
|
||||
// if (arg.error) {
|
||||
// return reject(arg.error);
|
||||
// }
|
||||
|
||||
// resolve(arg.fingerprint);
|
||||
// };
|
||||
|
||||
// const request: CurseHashFileRequest = {
|
||||
// responseKey: uuidv4(),
|
||||
// filePath,
|
||||
// normalizeWhitespace,
|
||||
// precomputedLength,
|
||||
// };
|
||||
|
||||
// this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
// this._electronService.ipcRenderer.send(CURSE_HASH_FILE_CHANNEL, request);
|
||||
// });
|
||||
// };
|
||||
|
||||
private getStringHash(
|
||||
targetString: string,
|
||||
targetStringEncoding?: BufferEncoding
|
||||
): number {
|
||||
try {
|
||||
const strBuffer = Buffer.from(
|
||||
targetString,
|
||||
targetStringEncoding || "ascii"
|
||||
);
|
||||
|
||||
const hash = nativeAddon.computeHash(strBuffer, strBuffer.length);
|
||||
|
||||
return hash;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.log(targetString, targetStringEncoding);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error(err);
|
||||
console.log(filePath);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IpcRequest } from "../models/ipc-request";
|
||||
|
||||
export interface CurseGetScanResultsRequest extends IpcRequest {
|
||||
filePaths: string[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { CurseScanResult } from "./curse-scan-result";
|
||||
|
||||
export interface CurseGetScanResultsResponse {
|
||||
error?: Error;
|
||||
scanResults: CurseScanResult[];
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { CurseMatch } from "./curse-match";
|
||||
import { CurseSearchResult } from "./curse-search-result";
|
||||
|
||||
@@ -9,7 +8,6 @@ export interface CurseScanResult {
|
||||
folderName: string;
|
||||
individualFingerprints: number[];
|
||||
directory: string;
|
||||
addonFolder?: AddonFolder;
|
||||
exactMatch?: CurseMatch;
|
||||
searchResult?: CurseSearchResult;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface CurseSearchResult {
|
||||
name: string;
|
||||
authors: CurseAuthor[];
|
||||
attachments: CurseAttachment[];
|
||||
websiteUrl: string
|
||||
websiteUrl: string;
|
||||
gameId: number;
|
||||
defaultFileId: number;
|
||||
downloadCount: number;
|
||||
@@ -19,7 +19,7 @@ export interface CurseSearchResult {
|
||||
status: number;
|
||||
primaryCategoryId: number;
|
||||
categorySection: CurseCategorySection;
|
||||
slug: string
|
||||
slug: string;
|
||||
gameVersionLatestFiles: CurseGameVersionLatestFile[];
|
||||
isFeatured: boolean;
|
||||
popularityScore: number;
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ReadFileResponse {
|
||||
error: Error;
|
||||
error?: Error;
|
||||
data: string;
|
||||
}
|
||||
@@ -56,6 +56,16 @@ img {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: .25em !important;
|
||||
}
|
||||
.mr-2 {
|
||||
margin-right: .5em !important;
|
||||
}
|
||||
.mr-3 {
|
||||
margin-right: 1em !important;
|
||||
}
|
||||
|
||||
.no-reg-drag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user