mirror of
https://github.com/WowUp/WowUp.git
synced 2026-05-24 22:46:45 -04:00
Merge branch 'electron' into feat/save-window-state
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
SHOW_DIRECTORY,
|
||||
PATH_EXISTS_CHANNEL,
|
||||
CURSE_GET_SCAN_RESULTS,
|
||||
WOWUP_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";
|
||||
@@ -23,6 +24,11 @@ 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";
|
||||
|
||||
import { WowUpGetScanResultsRequest } from "./src/common/wowup/wowup-get-scan-results-request";
|
||||
import { WowUpGetScanResultsResponse } from "./src/common/wowup/wowup-get-scan-results-response";
|
||||
import { WowUpFolderScanner } from "./src/common/wowup/wowup-folder-scanner";
|
||||
import { WowUpScanResult } from "./src/common/wowup/wowup-scan-result";
|
||||
|
||||
const nativeAddon = require("./build/Release/addon.node");
|
||||
|
||||
ipcMain.on(SHOW_DIRECTORY, async (evt, arg: ShowDirectoryRequest) => {
|
||||
@@ -142,3 +148,30 @@ ipcMain.on(
|
||||
evt.reply(arg.responseKey, response);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
WOWUP_GET_SCAN_RESULTS,
|
||||
async (evt, arg: WowUpGetScanResultsRequest) => {
|
||||
const response: WowUpGetScanResultsResponse = {
|
||||
scanResults: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const scanResults = await async.mapLimit<string, WowUpScanResult>(
|
||||
arg.filePaths,
|
||||
2,
|
||||
async (folder, callback) => {
|
||||
const scanResult = await new WowUpFolderScanner(folder).scanFolder();
|
||||
|
||||
callback(undefined, scanResult);
|
||||
}
|
||||
);
|
||||
|
||||
response.scanResults = scanResults;
|
||||
} catch (err) {
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
evt.reply(arg.responseKey, response);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wowup",
|
||||
"productName": "WowUp",
|
||||
"version": "2.0.0-alpha.7",
|
||||
"version": "2.0.0-alpha.8",
|
||||
"description": "Word of Warcraft addon updater",
|
||||
"homepage": "https://github.com/maximegris/angular-electron",
|
||||
"author": {
|
||||
|
||||
@@ -29,4 +29,4 @@ export interface AddonProvider {
|
||||
scan(clientType: WowClientType, addonChannelType: AddonChannelType, addonFolders: AddonFolder[]): Promise<void>;
|
||||
}
|
||||
|
||||
export type AddonProviderType = 'Curse' | 'GitHub' | 'TukUI' | 'WowInterface';
|
||||
export type AddonProviderType = 'Curse' | 'GitHub' | 'TukUI' | 'WowInterface' | 'WowUp';
|
||||
|
||||
261
wowup-electron/src/app/addon-providers/wowup-addon-provider.ts
Normal file
261
wowup-electron/src/app/addon-providers/wowup-addon-provider.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { AppConfig } from "environments/environment";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Addon } from "../entities/addon";
|
||||
import { WowClientType } from "../models/warcraft/wow-client-type";
|
||||
import { AddonSearchResult } from "../models/wowup/addon-search-result";
|
||||
import { PotentialAddon } from "../models/wowup/potential-addon";
|
||||
import { AddonProvider, AddonProviderType } from "./addon-provider";
|
||||
import { WowUpAddonRepresentation } from "../models/wowup-api/wowup-addon.representation";
|
||||
import { AddonFolder } from "app/models/wowup/addon-folder";
|
||||
import { WowUpGetScanResultsRequest } from "common/wowup/wowup-get-scan-results-request";
|
||||
import { WowUpGetScanResultsResponse } from "common/wowup/wowup-get-scan-results-response";
|
||||
import { ElectronService } from "app/services";
|
||||
import { WOWUP_GET_SCAN_RESULTS } from "common/constants";
|
||||
import { WowUpScanResult } from "common/wowup/wowup-scan-result";
|
||||
import { GetAddonsByFingerprintResponse } from "app/models/wowup-api/get-addons-by-fingerprint.response";
|
||||
import { WowUpAddonReleaseRepresentation } from "app/models/wowup-api/wowup-addon-release.representation";
|
||||
import { WowGameType } from "app/models/wowup-api/wow-game-type";
|
||||
import { AddonChannelType } from "app/models/wowup/addon-channel-type";
|
||||
import { AppWowUpScanResult } from "app/models/wowup/app-wowup-scan-result";
|
||||
|
||||
const API_URL = AppConfig.wowUpHubUrl;
|
||||
|
||||
export class WowUpAddonProvider implements AddonProvider {
|
||||
public readonly name = "WowUp";
|
||||
|
||||
constructor(
|
||||
private _httpClient: HttpClient,
|
||||
private _electronService: ElectronService
|
||||
) {}
|
||||
|
||||
async getAll(
|
||||
clientType: WowClientType,
|
||||
addonIds: string[]
|
||||
): Promise<AddonSearchResult[]> {
|
||||
const url = `${API_URL}/addons`;
|
||||
const addons = await this._httpClient
|
||||
.get<WowUpAddonRepresentation[]>(url.toString())
|
||||
.toPromise();
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getFeaturedAddons(clientType: WowClientType): Observable<PotentialAddon[]> {
|
||||
return of([]);
|
||||
}
|
||||
|
||||
async searchByQuery(
|
||||
query: string,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
searchByUrl(
|
||||
addonUri: URL,
|
||||
clientType: WowClientType
|
||||
): Promise<PotentialAddon> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
searchByName(
|
||||
addonName: string,
|
||||
folderName: string,
|
||||
clientType: WowClientType,
|
||||
nameOverride?: string
|
||||
): Promise<AddonSearchResult[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getById(
|
||||
addonId: string,
|
||||
clientType: WowClientType
|
||||
): Observable<AddonSearchResult> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
isValidAddonUri(addonUri: URL): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
onPostInstall(addon: Addon): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async scan(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: any,
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<void> {
|
||||
// const url = `${API_URL}/addons`;
|
||||
// const addons = await this._httpClient
|
||||
// .get<WuAddon[]>(url.toString())
|
||||
// .toPromise();
|
||||
|
||||
const scanResults = await this.getScanResults(addonFolders);
|
||||
|
||||
console.log("ScanResults", scanResults.length);
|
||||
|
||||
const fingerprintResponse = await this.getAddonsByFingerprints(
|
||||
scanResults.map((result) => result.fingerprint)
|
||||
).toPromise();
|
||||
|
||||
console.log("fingerprintResponse", fingerprintResponse);
|
||||
|
||||
for (let scanResult of scanResults) {
|
||||
// Curse can deliver the wrong result sometimes, ensure the result matches the client type
|
||||
scanResult.exactMatch = fingerprintResponse.exactMatches.find(
|
||||
(exactMatch) =>
|
||||
this.isGameType(exactMatch.matched_release, clientType) &&
|
||||
this.hasMatchingFingerprint(scanResult, exactMatch.matched_release)
|
||||
);
|
||||
|
||||
// If the addon does not have an exact match, check the partial matches.
|
||||
// if (!scanResult.exactMatch) {
|
||||
// scanResult.exactMatch = fingerprintResponse.partialMatches.find(
|
||||
// (partialMatch) =>
|
||||
// partialMatch.file?.modules?.some(
|
||||
// (module) => module.fingerprint === scanResult.fingerprint
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
const matchedScanResults = scanResults.filter((sr) => !!sr.exactMatch);
|
||||
const matchedScanResultIds = matchedScanResults.map(
|
||||
(sr) => sr.exactMatch.id
|
||||
);
|
||||
|
||||
for (let addonFolder of addonFolders) {
|
||||
var scanResult = scanResults.find((sr) => sr.path === addonFolder.path);
|
||||
if (!scanResult.exactMatch) {
|
||||
console.log("No search result match", scanResult.path);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasMatchingFingerprint(
|
||||
scanResult: WowUpScanResult,
|
||||
release: WowUpAddonReleaseRepresentation
|
||||
) {
|
||||
return release.addonFolders.some(
|
||||
(addonFolder) => addonFolder.fingerprint == scanResult.fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
private isGameType(
|
||||
release: WowUpAddonReleaseRepresentation,
|
||||
clientType: WowClientType
|
||||
) {
|
||||
return release.game_type === this.getWowGameType(clientType);
|
||||
}
|
||||
|
||||
private getWowGameType(clientType: WowClientType): string {
|
||||
switch (clientType) {
|
||||
case WowClientType.Classic:
|
||||
case WowClientType.ClassicPtr:
|
||||
return WowGameType.Classic;
|
||||
case WowClientType.Retail:
|
||||
case WowClientType.RetailPtr:
|
||||
case WowClientType.Beta:
|
||||
default:
|
||||
return WowGameType.Retail;
|
||||
}
|
||||
}
|
||||
|
||||
private getAddonsByFingerprints(
|
||||
fingerprints: string[]
|
||||
): Observable<GetAddonsByFingerprintResponse> {
|
||||
const url = `${API_URL}/addons/fingerprint`;
|
||||
|
||||
return this._httpClient.post<any>(url, {
|
||||
fingerprints,
|
||||
});
|
||||
}
|
||||
|
||||
private getScanResults = async (
|
||||
addonFolders: AddonFolder[]
|
||||
): Promise<AppWowUpScanResult[]> => {
|
||||
const t1 = Date.now();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const eventHandler = (_evt: any, arg: WowUpGetScanResultsResponse) => {
|
||||
if (arg.error) {
|
||||
return reject(arg.error);
|
||||
}
|
||||
|
||||
console.log("scan delta", Date.now() - t1);
|
||||
console.log("WowUpGetScanResultsResponse", arg);
|
||||
resolve(arg.scanResults);
|
||||
};
|
||||
|
||||
const request: WowUpGetScanResultsRequest = {
|
||||
filePaths: addonFolders.map((addonFolder) => addonFolder.path),
|
||||
responseKey: uuidv4(),
|
||||
};
|
||||
|
||||
this._electronService.ipcRenderer.once(request.responseKey, eventHandler);
|
||||
this._electronService.ipcRenderer.send(WOWUP_GET_SCAN_RESULTS, request);
|
||||
});
|
||||
};
|
||||
|
||||
private getAddon(
|
||||
clientType: WowClientType,
|
||||
addonChannelType: AddonChannelType,
|
||||
scanResult: AppWowUpScanResult
|
||||
): Addon {
|
||||
const primaryAddonFolder = scanResult.exactMatch.matched_release.addonFolders.find(
|
||||
(af) => af.load_on_demand === false
|
||||
);
|
||||
const authors = scanResult.exactMatch.owner_name;
|
||||
const folderList = scanResult.exactMatch.matched_release.addonFolders
|
||||
.map((af) => af.folder_name)
|
||||
.join(", ");
|
||||
|
||||
let channelType = addonChannelType;
|
||||
let latestVersion = primaryAddonFolder.version;
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
author: authors,
|
||||
name: scanResult.exactMatch.repository_name,
|
||||
channelType,
|
||||
autoUpdateEnabled: false,
|
||||
clientType,
|
||||
downloadUrl: scanResult.exactMatch.matched_release.download_url,
|
||||
externalUrl: scanResult.exactMatch.repository,
|
||||
externalId: scanResult.exactMatch.external_id,
|
||||
folderName: primaryAddonFolder.folder_name,
|
||||
gameVersion: scanResult.exactMatch.matched_release.game_version,
|
||||
installedAt: new Date(),
|
||||
installedFolders: folderList,
|
||||
installedVersion: scanResult.exactMatch.matched_release.tagName,
|
||||
isIgnored: false,
|
||||
latestVersion: scanResult.exactMatch.matched_release.tagName,
|
||||
providerName: this.name,
|
||||
providerSource: scanResult.exactMatch.source,
|
||||
thumbnailUrl: scanResult.exactMatch.image_url,
|
||||
patreonFundingLink: scanResult.exactMatch.patreon_funding_link,
|
||||
customFundingLink: scanResult.exactMatch.custom_funding_link,
|
||||
githubFundingLink: scanResult.exactMatch.github_funding_link
|
||||
};
|
||||
}
|
||||
}
|
||||
183
wowup-electron/src/app/business-objects/wowup-folder-scanner.ts
Normal file
183
wowup-electron/src/app/business-objects/wowup-folder-scanner.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
export interface ScanResult {
|
||||
path: string;
|
||||
fileFingerprints: { [path: string]: string };
|
||||
fingerprint: string;
|
||||
gameVersion: string;
|
||||
addonTitle: string;
|
||||
addonAuthors: string;
|
||||
loadOnDemand: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class WowUpFolderScanner {
|
||||
private _folderPath = "";
|
||||
|
||||
constructor(folderPath: string) {
|
||||
this._folderPath = folderPath;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async _scanFolder(folderPath: string): Promise<ScanResult> {
|
||||
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 tocFile = this.getTocFile(folderPath);
|
||||
const toc = await this.parseToc(tocFile);
|
||||
|
||||
let fileFingerprints: { [path: string]: string } = {};
|
||||
for (let file of matchingFiles) {
|
||||
const fileHash = await hashFile(file);
|
||||
fileFingerprints[file] = fileHash;
|
||||
}
|
||||
|
||||
const hashConcat = _.orderBy(Object.values(fileFingerprints)).join("");
|
||||
const fingerprint = hashString(hashConcat);
|
||||
|
||||
const result: ScanResult = {
|
||||
fileFingerprints,
|
||||
fingerprint,
|
||||
path: folderPath,
|
||||
addonAuthors: toc?.author ?? '',
|
||||
addonTitle: toc?.title ?? '',
|
||||
gameVersion: toc?.interface ?? '',
|
||||
loadOnDemand: toc?.loadOnDemand === '1',
|
||||
version: toc?.version ?? ''
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getTocFile(directory: string) {
|
||||
const baseFiles = fs.readdirSync(directory);
|
||||
const tocFile = baseFiles.find(file => path.extname(file) === '.toc');
|
||||
if (!tocFile) {
|
||||
console.warn('No toc file: ' + directory);
|
||||
return '';
|
||||
}
|
||||
return path.join(directory, tocFile);
|
||||
}
|
||||
|
||||
private async parseToc(filePath: string) {
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const tocText = await readFile(filePath);
|
||||
return parseToc(tocText);
|
||||
}
|
||||
|
||||
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 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 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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
<footer class="bg-dark-4 text-light-2">
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev">
|
||||
<a appExternalLink class="patreon-link" href="https://www.patreon.com/jliddev"
|
||||
matTooltip="Support WowUp on Patreon">
|
||||
<img class="patron-img" src="assets/Digital-Patreon-Wordmark_FieryCoral.png" />
|
||||
</a>
|
||||
<a appExternalLink class="discord-link" href="https://discord.gg/rk4F5aD" matTooltip="Chat with us on Discord">
|
||||
<img class="discord-img" src="assets/images/discord_logo_small.png" />
|
||||
</a>
|
||||
<p class="flex-grow-1">{{sessionService.statusText$ | async}}</p>
|
||||
<div class="row">
|
||||
<p class="mr-3">{{sessionService.pageContextText$ | async}}</p>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@import "../../../variables.scss";
|
||||
|
||||
.patron-img {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 25px;
|
||||
padding: 0.25em 0.5em;
|
||||
@@ -28,8 +26,23 @@ footer {
|
||||
}
|
||||
|
||||
.patreon-link{
|
||||
margin-right: 1em;
|
||||
&:hover {
|
||||
background-color: $dark-3;
|
||||
}
|
||||
|
||||
.patron-img {
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.discord-link {
|
||||
&:hover {
|
||||
background-color: $dark-3;
|
||||
}
|
||||
|
||||
.discord-img {
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,26 @@
|
||||
<div class="addon-logo-container" [style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'">
|
||||
</div>
|
||||
<div *ngIf="listItem.isBetaChannel || listItem.isAlphaChannel" class="channel"
|
||||
[ngClass]="{'beta': listItem.isBetaChannel, 'alpha': listItem.isAlphaChannel }">{{listItem.isAlphaChannel ? 'Alpha': 'Beta'}}</div>
|
||||
[ngClass]="{'beta': listItem.isBetaChannel, 'alpha': listItem.isAlphaChannel }">
|
||||
{{listItem.isAlphaChannel ? 'Alpha': 'Beta'}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<a appExternalLink class="addon-title mat-subheading-2" [href]="listItem.addon.externalUrl">{{listItem.addon.name}}</a>
|
||||
<a appExternalLink class="addon-title mat-subheading-2"
|
||||
[href]="listItem.addon.externalUrl">{{listItem.addon.name}}</a>
|
||||
<div class="addon-funding">
|
||||
<a *ngIf="listItem.addon.patreonFundingLink" appExternalLink [href]="listItem.addon.patreonFundingLink"
|
||||
matTooltip="Support the author on Patreon">
|
||||
<img class="funding-icon" src="assets/images/patreon_logo_small.png" />
|
||||
</a>
|
||||
<a *ngIf="listItem.addon.githubFundingLink" appExternalLink [href]="listItem.addon.githubFundingLink"
|
||||
matTooltip="Support the author on GitHub">
|
||||
<img class="funding-icon" src="assets/images/github_logo_small.png" />
|
||||
</a>
|
||||
<a *ngIf="listItem.addon.customFundingLink" appExternalLink [href]="listItem.addon.customFundingLink"
|
||||
matTooltip="Support this author">
|
||||
<img class="funding-icon" src="assets/images/custom_funding_logo_small.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="addon-version">{{listItem.addon.installedVersion}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,4 +56,19 @@
|
||||
.addon-version {
|
||||
color: $white-2;
|
||||
}
|
||||
|
||||
.addon-funding {
|
||||
a {
|
||||
margin-right: 1em;
|
||||
color: $white-1;
|
||||
}
|
||||
.funding-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Addon {
|
||||
installedAt?: Date;
|
||||
externalId?: string;
|
||||
providerName?: string;
|
||||
providerSource?: string;
|
||||
externalUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
gameVersion?: string;
|
||||
@@ -21,4 +22,7 @@ export interface Addon {
|
||||
clientType: WowClientType;
|
||||
channelType: AddonChannelType;
|
||||
updatedAt?: Date;
|
||||
patreonFundingLink?: string;
|
||||
githubFundingLink?: string;
|
||||
customFundingLink?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WowUpAddonRepresentation } from "./wowup-addon.representation";
|
||||
|
||||
export interface GetAddonsByFingerprintResponse {
|
||||
exactMatches: WowUpAddonRepresentation[];
|
||||
}
|
||||
4
wowup-electron/src/app/models/wowup-api/wow-game-type.ts
Normal file
4
wowup-electron/src/app/models/wowup-api/wow-game-type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum WowGameType {
|
||||
Retail = "retail",
|
||||
Classic = "classic",
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface WowUpAddonReleaseFolderRepresentation {
|
||||
id: number;
|
||||
folder_name: string;
|
||||
fingerprint: string;
|
||||
game_version: string;
|
||||
addon_title: string;
|
||||
addon_authors: string;
|
||||
load_on_demand: boolean;
|
||||
version: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { WowUpAddonReleaseFolderRepresentation } from "./wowup-addon-release-folder.representation";
|
||||
import { WowGameType } from "./wow-game-type";
|
||||
|
||||
export interface WowUpAddonReleaseRepresentation {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
tagName: string;
|
||||
external_id: string;
|
||||
prerelease: boolean;
|
||||
body: string;
|
||||
game_version: string;
|
||||
download_url: string;
|
||||
published_at: Date;
|
||||
addonFolders?: WowUpAddonReleaseFolderRepresentation[];
|
||||
game_type: WowGameType;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { WowUpAddonReleaseRepresentation } from "./wowup-addon-release.representation";
|
||||
|
||||
export interface WowUpAddonRepresentation {
|
||||
id: number;
|
||||
repository: string;
|
||||
repository_name: string;
|
||||
external_id: string;
|
||||
source: string;
|
||||
patreon_funding_link?: string;
|
||||
github_funding_link?: string;
|
||||
custom_funding_link?: string;
|
||||
owner_name?: string;
|
||||
owner_image_url?: string;
|
||||
image_url?: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
current_release?: WowUpAddonReleaseRepresentation;
|
||||
matched_release?: WowUpAddonReleaseRepresentation;
|
||||
releases?: WowUpAddonReleaseRepresentation[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WowUpAddonRepresentation } from "../wowup-api/wowup-addon.representation";
|
||||
import { WowUpScanResult } from "../../../common/wowup/wowup-scan-result";
|
||||
|
||||
export interface AppWowUpScanResult extends WowUpScanResult{
|
||||
exactMatch?: WowUpAddonRepresentation;
|
||||
}
|
||||
@@ -28,11 +28,13 @@
|
||||
[disabled]="enableControls === false" (click)="onUpdateAll()" (contextmenu)="onUpdateAllContext($event)">
|
||||
{{'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
|
||||
<button mat-flat-button color="primary"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate"
|
||||
[disabled]="enableControls === false" (click)="onRefresh()">
|
||||
{{'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON' | translate}}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
|
||||
<button mat-flat-button color="primary"
|
||||
[matTooltip]="'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate"
|
||||
[disabled]="enableControls === false" (click)="onReScan()">
|
||||
{{'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON' | translate}}
|
||||
</button>
|
||||
@@ -48,14 +50,16 @@
|
||||
<div class="table-container flex-grow-1" [hidden]="isBusy === true">
|
||||
<table mat-table matSort [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="addon.name">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>{{'PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}</th>
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER' | translate}}</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<app-my-addons-addon-cell [addon]="element"></app-my-addons-addon-cell>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="displayState">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>{{'PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}</th>
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
{{'PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER' | translate}}</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<div class="status-column">
|
||||
<button *ngIf="element.needsInstall === true" mat-flat-button color="primary" (click)="onInstall()">
|
||||
@@ -103,7 +107,14 @@
|
||||
{{'PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER' | translate}}
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{element.addon.providerName}}
|
||||
<div *ngIf="element.addon.providerName !== 'WowUp'">
|
||||
{{element.addon.providerName}}
|
||||
</div>
|
||||
<div *ngIf="element.addon.providerName === 'WowUp'" class="addon-provider">
|
||||
<div class="addon-provider-name">{{element.addon.providerSource}}</div>
|
||||
<img class="provider-logo" [matTooltip]="'Sourced from ' + element.addon.providerName"
|
||||
src="assets/icons/favicon.256x256.png">
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -100,6 +100,20 @@
|
||||
.selected-row {
|
||||
background: $dark-4;
|
||||
}
|
||||
|
||||
.addon-provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.addon-provider-name {
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author-column {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { WowUpAddonProvider } from "../../addon-providers/wowup-addon-provider";
|
||||
import { CachingService } from "../caching/caching-service";
|
||||
import { ElectronService } from "../electron/electron.service";
|
||||
import { FileService } from "../files/file.service";
|
||||
@@ -63,4 +64,8 @@ export class AddonProviderFactory {
|
||||
public createGitHubAddonProvider(): GitHubAddonProvider {
|
||||
return new GitHubAddonProvider(this._httpClient);
|
||||
}
|
||||
|
||||
public createWowUpAddonProvider(): WowUpAddonProvider {
|
||||
return new WowUpAddonProvider(this._httpClient, this._electronService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export class AddonService {
|
||||
private _addonProviderFactory: AddonProviderFactory
|
||||
) {
|
||||
this._addonProviders = [
|
||||
this._addonProviderFactory.createWowUpAddonProvider(),
|
||||
this._addonProviderFactory.createCurseAddonProvider(),
|
||||
this._addonProviderFactory.createTukUiAddonProvider(),
|
||||
this._addonProviderFactory.createWowInterfaceAddonProvider(),
|
||||
@@ -388,8 +389,9 @@ export class AddonService {
|
||||
rescan = false
|
||||
): Promise<Addon[]> {
|
||||
let addons = this._addonStorage.getAllForClientType(clientType);
|
||||
if (rescan || !addons.length) {
|
||||
if (rescan ) {
|
||||
const newAddons = await this.scanAddons(clientType);
|
||||
console.log(newAddons)
|
||||
this.updateAddons(addons, newAddons);
|
||||
}
|
||||
|
||||
@@ -434,6 +436,9 @@ export class AddonService {
|
||||
existingAddon.thumbnailUrl = matchingAddon.thumbnailUrl;
|
||||
existingAddon.gameVersion = matchingAddon.gameVersion;
|
||||
existingAddon.author = matchingAddon.author;
|
||||
existingAddon.patreonFundingLink = matchingAddon.patreonFundingLink;
|
||||
existingAddon.githubFundingLink = matchingAddon.githubFundingLink;
|
||||
existingAddon.customFundingLink = matchingAddon.customFundingLink;
|
||||
}
|
||||
|
||||
this._addonStorage.removeAll(...removedAddons);
|
||||
|
||||
BIN
wowup-electron/src/assets/images/custom_funding_logo_small.png
Normal file
BIN
wowup-electron/src/assets/images/custom_funding_logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
wowup-electron/src/assets/images/discord_logo_small.png
Normal file
BIN
wowup-electron/src/assets/images/discord_logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
wowup-electron/src/assets/images/github_logo_small.png
Normal file
BIN
wowup-electron/src/assets/images/github_logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
wowup-electron/src/assets/images/patreon_logo_small.png
Normal file
BIN
wowup-electron/src/assets/images/patreon_logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -12,3 +12,4 @@ 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";
|
||||
export const WOWUP_GET_SCAN_RESULTS = "wowup-get-scan-results";
|
||||
|
||||
196
wowup-electron/src/common/wowup/wowup-folder-scanner.ts
Normal file
196
wowup-electron/src/common/wowup/wowup-folder-scanner.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import * as _ from "lodash";
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { readDirRecursive, readFile } from "../../../file.utils";
|
||||
import { WowUpScanResult } from "./wowup-scan-result";
|
||||
|
||||
export class WowUpFolderScanner {
|
||||
private _folderPath = "";
|
||||
|
||||
constructor(folderPath: string) {
|
||||
this._folderPath = folderPath;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async scanFolder(): Promise<WowUpScanResult> {
|
||||
const folderPath = this._folderPath;
|
||||
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 tocFile = this.getTocFile(folderPath);
|
||||
|
||||
let fileFingerprints: string[] = [];
|
||||
for (let file of matchingFiles) {
|
||||
const fileHash = await this.hashFile(file);
|
||||
fileFingerprints.push(fileHash);
|
||||
}
|
||||
|
||||
const hashConcat = _.orderBy(fileFingerprints).join("");
|
||||
const fingerprint = this.hashString(hashConcat);
|
||||
|
||||
const result: WowUpScanResult = {
|
||||
fileFingerprints,
|
||||
fingerprint,
|
||||
path: folderPath,
|
||||
folderName: path.basename(folderPath),
|
||||
fileCount: matchingFiles.length,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getTocFile(directory: string) {
|
||||
const baseFiles = fs.readdirSync(directory);
|
||||
const tocFile = baseFiles.find((file) => path.extname(file) === ".toc");
|
||||
if (!tocFile) {
|
||||
console.warn("No toc file: " + directory);
|
||||
return "";
|
||||
}
|
||||
return path.join(directory, tocFile);
|
||||
}
|
||||
|
||||
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 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 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 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 hashString(str: string) {
|
||||
const md5 = crypto.createHash("md5");
|
||||
md5.update(str);
|
||||
return md5.digest("hex");
|
||||
}
|
||||
|
||||
private hashFile(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const hash = crypto.createHash("md5");
|
||||
hash.setEncoding("hex");
|
||||
|
||||
fileStream.on("end", function () {
|
||||
hash.end();
|
||||
const hashStr = hash.read();
|
||||
fileStream.destroy();
|
||||
resolve(hashStr);
|
||||
});
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
fileStream.pipe(hash);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IpcRequest } from "../models/ipc-request";
|
||||
|
||||
export interface WowUpGetScanResultsRequest extends IpcRequest {
|
||||
filePaths: string[];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WowUpScanResult } from "./wowup-scan-result";
|
||||
|
||||
export interface WowUpGetScanResultsResponse {
|
||||
error?: Error;
|
||||
scanResults: WowUpScanResult[];
|
||||
}
|
||||
7
wowup-electron/src/common/wowup/wowup-scan-result.ts
Normal file
7
wowup-electron/src/common/wowup/wowup-scan-result.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface WowUpScanResult {
|
||||
fileCount: number;
|
||||
fileFingerprints: string[];
|
||||
fingerprint: string;
|
||||
folderName: string;
|
||||
path: string;
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV',
|
||||
wowUpApiUrl: 'https://4g2nuwcupj.execute-api.us-east-1.amazonaws.com/production'
|
||||
wowUpApiUrl: 'https://api.dev.wowup.io',
|
||||
wowUpHubUrl: "https://hub.dev.wowup.io",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const AppConfig = {
|
||||
production: true,
|
||||
environment: 'PROD',
|
||||
wowUpApiUrl: 'https://api.wowup.io'
|
||||
environment: "PROD",
|
||||
wowUpApiUrl: "https://api.wowup.io",
|
||||
wowUpHubUrl: "https://hub.wowup.io",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'LOCAL',
|
||||
wowUpApiUrl: 'https://4g2nuwcupj.execute-api.us-east-1.amazonaws.com/production'
|
||||
wowUpApiUrl: 'https://api.dev.wowup.io',
|
||||
wowUpHubUrl: 'https://hub.dev.wowup.io'
|
||||
};
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV'
|
||||
environment: 'DEV',
|
||||
wowUpApiUrl: "https://api.dev.wowup.io",
|
||||
wowUpHubUrl: "https://hub.dev.wowup.io",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user