Merge remote-tracking branch 'upstream/electron' into feat/save-window-state

This commit is contained in:
Dean Campbell
2020-10-08 00:15:25 -07:00
61 changed files with 2370 additions and 874 deletions

View File

@@ -41,7 +41,7 @@
],
"win": {
"icon": "electron-build/icon.ico",
"target": ["portable"]
"target": ["nsis", "portable"]
},
"mac": {
"icon": "dist/assets/icons",

View 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

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,10 @@
flex-direction: row;
align-items: center;
position: relative;
.debug-button {
height: 22px;
}
}
.titlebar-mac {
height: 22px;

View File

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

View File

@@ -1,4 +1,4 @@
import { CurseMatch } from "./curse-match";
import { CurseMatch } from "../../../common/curse/curse-match";
export interface CurseFingerprintsResponse {
isCacheBuild: boolean;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ import { AddonInstallButtonComponent } from "app/components/addon-install-button
InstallFromUrlDialogComponent,
AddonDetailComponent,
AddonProviderBadgeComponent,
AddonInstallButtonComponent
AddonInstallButtonComponent,
],
imports: [
CommonModule,

View File

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

View File

@@ -75,6 +75,10 @@
white-space: pre-wrap;
}
.game-version-cell {
min-width: 110px;
}
.status-column {
display: flex;
width: 130px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"
}
}
}

View File

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

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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 Телеметрия"
}
}
}

View 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遥测"
}
}
}

View File

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

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

View File

@@ -0,0 +1,5 @@
import { IpcRequest } from "../models/ipc-request";
export interface CurseGetScanResultsRequest extends IpcRequest {
filePaths: string[];
}

View File

@@ -0,0 +1,6 @@
import { CurseScanResult } from "./curse-scan-result";
export interface CurseGetScanResultsResponse {
error?: Error;
scanResults: CurseScanResult[];
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
export interface ReadFileResponse {
error: Error;
error?: Error;
data: string;
}

View File

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