diff --git a/wowup-electron/ipc-events.ts b/wowup-electron/ipc-events.ts index 12476e39..8885b917 100644 --- a/wowup-electron/ipc-events.ts +++ b/wowup-electron/ipc-events.ts @@ -3,7 +3,7 @@ import * as fs from "fs-extra"; import * as async from "async"; import * as path from "path"; import * as admZip from "adm-zip"; -import { readdir } from "fs"; +import { readdir, stat } from "fs"; import axios from "axios"; import { @@ -19,6 +19,7 @@ import { GET_ASSET_FILE_PATH, DOWNLOAD_FILE_CHANNEL, CREATE_DIRECTORY_CHANNEL, + STAT_FILES_CHANNEL, } from "./src/common/constants"; import { CurseScanResult } from "./src/common/curse/curse-scan-result"; import { CurseFolderScanner } from "./src/common/curse/curse-folder-scanner"; @@ -70,6 +71,21 @@ export function initializeIpcHanders(window: BrowserWindow) { }); }); + ipcMain.handle(STAT_FILES_CHANNEL, async (evt, filePaths: string[]) => { + const results: { [path: string]: fs.Stats } = {}; + await async.eachLimit(filePaths, 3, (path, cb) => { + fs.stat(path, (err, stats) => { + if (err) { + return cb(err); + } + + results[path] = stats; + cb(); + }); + }); + return results; + }); + ipcMain.handle(PATH_EXISTS_CHANNEL, async (evt, filePath: string) => { console.log(PATH_EXISTS_CHANNEL, filePath); diff --git a/wowup-electron/src/app/addon-providers/curse-addon-provider.ts b/wowup-electron/src/app/addon-providers/curse-addon-provider.ts index 689da7a3..a411cd42 100644 --- a/wowup-electron/src/app/addon-providers/curse-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/curse-addon-provider.ts @@ -622,6 +622,7 @@ export class CurseAddonProvider implements AddonProvider { downloadCount: scanResult.searchResult.downloadCount, summary: scanResult.searchResult.summary, releasedAt: new Date(latestVersion.fileDate), + isLoadOnDemand: false }; } } diff --git a/wowup-electron/src/app/addon-providers/tukui-addon-provider.ts b/wowup-electron/src/app/addon-providers/tukui-addon-provider.ts index 315f6005..e5bf9df1 100644 --- a/wowup-electron/src/app/addon-providers/tukui-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/tukui-addon-provider.ts @@ -177,6 +177,7 @@ export class TukUiAddonProvider implements AddonProvider { downloadCount: Number.parseFloat(tukUiAddon.downloads), screenshotUrls: [tukUiAddon.screenshot_url], releasedAt: new Date(`${tukUiAddon.lastupdate} UTC`), + isLoadOnDemand: false, }; } } diff --git a/wowup-electron/src/app/addon-providers/wow-interface-addon-provider.ts b/wowup-electron/src/app/addon-providers/wow-interface-addon-provider.ts index 7ec19101..41505abb 100644 --- a/wowup-electron/src/app/addon-providers/wow-interface-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/wow-interface-addon-provider.ts @@ -210,6 +210,7 @@ export class WowInterfaceAddonProvider implements AddonProvider { screenshotUrls: response.images?.map((img) => img.imageUrl), downloadCount: response.downloads, releasedAt: new Date(response.lastUpdate), + isLoadOnDemand: false, }; } diff --git a/wowup-electron/src/app/addon-providers/wowup-addon-provider.ts b/wowup-electron/src/app/addon-providers/wowup-addon-provider.ts index 70c0d980..bf92bcfa 100644 --- a/wowup-electron/src/app/addon-providers/wowup-addon-provider.ts +++ b/wowup-electron/src/app/addon-providers/wowup-addon-provider.ts @@ -254,6 +254,7 @@ export class WowUpAddonProvider implements AddonProvider { patreonFundingLink: scanResult.exactMatch.patreon_funding_link, customFundingLink: scanResult.exactMatch.custom_funding_link, githubFundingLink: scanResult.exactMatch.github_funding_link, + isLoadOnDemand: false, }; } } diff --git a/wowup-electron/src/app/business-objects/my-addon-list-item.ts b/wowup-electron/src/app/business-objects/my-addon-list-item.ts index 59a390a2..763e2dfc 100644 --- a/wowup-electron/src/app/business-objects/my-addon-list-item.ts +++ b/wowup-electron/src/app/business-objects/my-addon-list-item.ts @@ -12,6 +12,10 @@ export class AddonViewModel { public stateTextTranslationKey: string = ""; public selected: boolean = false; + get isLoadOnDemand() { + return this.addon?.isLoadOnDemand; + } + get installedAt() { return new Date(this.addon?.installedAt); } diff --git a/wowup-electron/src/app/components/my-addon-status-column/my-addon-status-column.component.html b/wowup-electron/src/app/components/my-addon-status-column/my-addon-status-column.component.html index a926e2b8..e2947a90 100644 --- a/wowup-electron/src/app/components/my-addon-status-column/my-addon-status-column.component.html +++ b/wowup-electron/src/app/components/my-addon-status-column/my-addon-status-column.component.html @@ -10,11 +10,11 @@ > {{ getStatusText() | translate }} - update - + --> diff --git a/wowup-electron/src/app/components/my-addons-addon-cell/my-addons-addon-cell.component.html b/wowup-electron/src/app/components/my-addons-addon-cell/my-addons-addon-cell.component.html index ba1b5e21..50da945d 100644 --- a/wowup-electron/src/app/components/my-addons-addon-cell/my-addons-addon-cell.component.html +++ b/wowup-electron/src/app/components/my-addons-addon-cell/my-addons-addon-cell.component.html @@ -1,64 +1,46 @@ -
+
-
+
{{ listItem.thumbnailLetter }}
-
+ }"> {{ listItem.isAlphaChannel ? "Alpha" : "Beta" }}
- {{ listItem.addon.name }} + {{ listItem.addon.name }} -
- {{ listItem.addon.installedVersion }} +
+ {{ listItem.addon.installedVersion }} + + error_outline + + + update +
-
+
\ No newline at end of file diff --git a/wowup-electron/src/app/entities/addon.ts b/wowup-electron/src/app/entities/addon.ts index 41a8a7b8..179372b7 100644 --- a/wowup-electron/src/app/entities/addon.ts +++ b/wowup-electron/src/app/entities/addon.ts @@ -18,6 +18,7 @@ export interface Addon { author?: string; installedFolders?: string; isIgnored: boolean; + isLoadOnDemand: boolean; autoUpdateEnabled: boolean; clientType: WowClientType; channelType: AddonChannelType; diff --git a/wowup-electron/src/app/models/wowup/addon-folder.ts b/wowup-electron/src/app/models/wowup/addon-folder.ts index 125a9c27..dbe767e4 100644 --- a/wowup-electron/src/app/models/wowup/addon-folder.ts +++ b/wowup-electron/src/app/models/wowup/addon-folder.ts @@ -1,5 +1,6 @@ import { Addon } from "../../entities/addon"; import { Toc } from "./toc"; +import { Stats } from "fs"; export interface AddonFolder { name: string; @@ -10,4 +11,5 @@ export interface AddonFolder { toc: Toc; tocMetaData: string[]; matchingAddon?: Addon; + fileStats?: Stats; } diff --git a/wowup-electron/src/app/models/wowup/toc.ts b/wowup-electron/src/app/models/wowup/toc.ts index 17642464..0ca69591 100644 --- a/wowup-electron/src/app/models/wowup/toc.ts +++ b/wowup-electron/src/app/models/wowup/toc.ts @@ -12,4 +12,6 @@ export interface Toc { wowInterfaceId?: string; tukUiProjectId?: string; tukUiProjectFolders?: string; + loadOnDemand?: string; + dependencyList: string[]; } diff --git a/wowup-electron/src/app/pages/my-addons/my-addons.component.html b/wowup-electron/src/app/pages/my-addons/my-addons.component.html index 1abd453f..0b054892 100644 --- a/wowup-electron/src/app/pages/my-addons/my-addons.component.html +++ b/wowup-electron/src/app/pages/my-addons/my-addons.component.html @@ -1,97 +1,53 @@ -
+ }">
{{ "PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate }} - - + + "> {{ warcraftService.getClientDisplayName(clientType) }}
-
+
{{ "PAGES.MY_ADDONS.FILTER_LABEL" | translate }} -
- - -
@@ -102,27 +58,18 @@
-
+

No Addons found

-
+
@@ -131,37 +78,19 @@ - - - @@ -176,36 +105,19 @@ - - - @@ -255,42 +156,24 @@ - + - +
{{ "PAGES.MY_ADDONS.TABLE.ADDON_COLUMN_HEADER" | translate }} - + {{ "PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER" | translate }} - + + + {{ "PAGES.MY_ADDONS.TABLE.UPDATED_AT_COLUMN_HEADER" | translate }} + " matTooltipPosition="above" matTooltipShowDelay="500"> {{ element.installedAt | relativeDuration }} + {{ "PAGES.MY_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER" | translate }} + " matTooltipPosition="above" matTooltipShowDelay="500"> {{ element.addon.releasedAt | relativeDuration }} + {{ "PAGES.MY_ADDONS.TABLE.GAME_VERSION_COLUMN_HEADER" | translate }} @@ -214,30 +126,19 @@ - + {{ "PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER" | translate }}
{{ element.addon.providerName }}
-
+
{{ element.addon.providerSource }}
- +
-
+
-
+
{{ listItem.thumbnailLetter }} @@ -302,109 +185,59 @@
- + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }} - + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON" | translate }} - - - - - - + + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL" | translate }} - + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.BETA_ADDON_CHANNEL" | translate }} - + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ALPHA_ADDON_CHANNEL" | translate }} @@ -414,31 +247,20 @@ -
+
{{ listItems.length + " addons selected" }}
- + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }} - + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON" | translate @@ -459,10 +281,7 @@ {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REMOVE_ADDON_BUTTON" | translate }} - + {{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL" @@ -484,13 +303,9 @@
-
+
+
@@ -499,49 +314,31 @@
- + {{ column.display }}
-
+
+
- - - + \ No newline at end of file diff --git a/wowup-electron/src/app/pages/my-addons/my-addons.component.ts b/wowup-electron/src/app/pages/my-addons/my-addons.component.ts index 2b0b25d7..232a3d4b 100644 --- a/wowup-electron/src/app/pages/my-addons/my-addons.component.ts +++ b/wowup-electron/src/app/pages/my-addons/my-addons.component.ts @@ -516,6 +516,11 @@ export class MyAddonsComponent implements OnInit, OnDestroy { listItems: AddonViewModel[] ) { listItems.forEach((listItem) => { + // if provider is not valid (Unknown) then ignore this + if (!this.addonService.isValidProviderName(listItem.addon.providerName)) { + return; + } + listItem.addon.isIgnored = evt.checked; if (evt.checked) { listItem.addon.autoUpdateEnabled = false; diff --git a/wowup-electron/src/app/services/addons/addon.service.ts b/wowup-electron/src/app/services/addons/addon.service.ts index e9bb5ef6..2dd6aa2f 100644 --- a/wowup-electron/src/app/services/addons/addon.service.ts +++ b/wowup-electron/src/app/services/addons/addon.service.ts @@ -84,7 +84,7 @@ export class AddonService { ); var searchResults = await Promise.all(searchTasks); - await this._analyticsService.trackUserAction( + this._analyticsService.trackUserAction( "addons", "search", `${clientType}|${query}` @@ -308,6 +308,11 @@ export class AddonService { return addon.name; }; + public isValidProviderName(providerName: string) { + const providerNames = this._addonProviders.map((provider) => provider.name); + return _.includes(providerNames, providerName); + } + public async logDebugData() { const curseProvider = this._addonProviders.find( (p) => p.name === "Curse" @@ -616,6 +621,8 @@ export class AddonService { const matchedAddonFolders = addonFolders.filter( (addonFolder) => !!addonFolder.matchingAddon ); + const matchedAddonFolderNames = matchedAddonFolders.map((mf) => mf.name); + const matchedGroups = _.groupBy( matchedAddonFolders, (addonFolder) => @@ -624,7 +631,55 @@ export class AddonService { console.log(Object.keys(matchedGroups)); - return Object.values(matchedGroups).map((value) => value[0].matchingAddon); + const addonList = Object.values(matchedGroups).map( + (value) => value[0].matchingAddon + ); + + const unmatchedFolders = addonFolders.filter((af) => + this.isAddonFolderUnmatched(matchedAddonFolderNames, af) + ); + console.debug("unmatchedFolders", unmatchedFolders); + + const unmatchedAddons = unmatchedFolders.map((uf) => + this.createUnmatchedAddon(uf, clientType) + ); + + console.debug("unmatchedAddons", unmatchedAddons); + + addonList.push(...unmatchedAddons); + + return addonList; + } + + private reconcileUnmatchedAddons(addon: Addon) {} + + /** + * This should verify that a folder that did not have a match, is actually unmatched + * This will happen for any sub folders of TukUI or WowInterface addons + */ + private isAddonFolderUnmatched( + matchedFolderNames: string[], + addonFolder: AddonFolder + ) { + if (addonFolder.matchingAddon) { + return false; + } + + // if the folder is load on demand, it 'should' be a sub folder + const isLoadOnDemand = addonFolder.toc?.loadOnDemand === "1"; + if ( + isLoadOnDemand && + this.allItemsMatch(addonFolder.toc.dependencyList, matchedFolderNames) + ) { + return false; + } + + return true; + } + + /** Check if all primitives in subset are in the superset (strings, ints) */ + private allItemsMatch(subset: any[], superset: any[]) { + return _.difference(subset, superset).length === 0; } public getFeaturedAddons( @@ -757,6 +812,37 @@ export class AddonService { releasedAt: latestFile.releaseDate, summary: searchResult.summary, screenshotUrls: searchResult.screenshotUrls, + isLoadOnDemand: false, + }; + } + + private createUnmatchedAddon( + addonFolder: AddonFolder, + clientType: WowClientType + ): Addon { + return { + id: uuidv4(), + name: addonFolder.toc?.title || addonFolder.name, + thumbnailUrl: "", + latestVersion: addonFolder.toc?.version || "", + installedVersion: addonFolder.toc?.version || "", + clientType: clientType, + externalId: "", + folderName: addonFolder.name, + gameVersion: addonFolder.toc?.interface || "", + author: addonFolder.toc?.author || "", + downloadUrl: "", + externalUrl: "", + providerName: "Unknown", + channelType: this._wowUpService.getDefaultAddonChannel(clientType), + isIgnored: true, + autoUpdateEnabled: false, + releasedAt: new Date(), + installedAt: addonFolder.fileStats?.mtime || new Date(), + installedFolders: addonFolder.name, + summary: "", + screenshotUrls: [], + isLoadOnDemand: addonFolder.toc?.loadOnDemand === "1", }; } } diff --git a/wowup-electron/src/app/services/files/file.service.ts b/wowup-electron/src/app/services/files/file.service.ts index fd72314a..4690b24b 100644 --- a/wowup-electron/src/app/services/files/file.service.ts +++ b/wowup-electron/src/app/services/files/file.service.ts @@ -11,6 +11,7 @@ import { PATH_EXISTS_CHANNEL, READ_FILE_CHANNEL, SHOW_DIRECTORY, + STAT_FILES_CHANNEL, UNZIP_FILE_CHANNEL, } from "../../../common/constants"; import { CopyFileRequest } from "../../../common/models/copy-file-request"; @@ -108,6 +109,15 @@ export class FileService { ); } + public async statFiles( + filePaths: string[] + ): Promise<{ [path: string]: fs.Stats }> { + return await this._electronService.ipcRenderer.invoke( + STAT_FILES_CHANNEL, + filePaths + ); + } + public listEntries(sourcePath: string, filter: string) { const globFilter = globrex(filter); diff --git a/wowup-electron/src/app/services/toc/toc.service.ts b/wowup-electron/src/app/services/toc/toc.service.ts index d10aa92f..f8193239 100644 --- a/wowup-electron/src/app/services/toc/toc.service.ts +++ b/wowup-electron/src/app/services/toc/toc.service.ts @@ -2,6 +2,9 @@ import { Injectable } from "@angular/core"; import { Toc } from "../../models/wowup/toc"; import { FileService } from "../files/file.service"; +const TOC_DEPENDENCIES = "Dependencies"; +const TOC_REQUIRED_DEPS = "RequiredDeps"; + @Injectable({ providedIn: "root", }) @@ -9,7 +12,14 @@ export class TocService { constructor(private _fileService: FileService) {} public async parse(tocPath: string): Promise { - const tocText = await this._fileService.readFile(tocPath); + let tocText = await this._fileService.readFile(tocPath); + tocText = tocText.trim(); + + const dependencies = + this.getValue(TOC_DEPENDENCIES, tocText) || + this.getValue(TOC_REQUIRED_DEPS, tocText); + + const dependencyList: string[] = this.getDependencyList(tocText); return { author: this.getValue("Author", tocText), @@ -22,12 +32,27 @@ export class TocService { category: this.getValue("X-Category", tocText), localizations: this.getValue("X-Localizations", tocText), wowInterfaceId: this.getValue("X-WoWI-ID", tocText), - dependencies: this.getValue("Dependencies", tocText), + dependencies, + dependencyList, tukUiProjectId: this.getValue("X-Tukui-ProjectID", tocText), tukUiProjectFolders: this.getValue("X-Tukui-ProjectFolders", tocText), + loadOnDemand: this.getValue("LoadOnDemand", tocText), }; } + private getDependencyList(tocText: string) { + const dependencies = this.getValue(TOC_DEPENDENCIES, tocText); + const requiredDeps = this.getValue(TOC_REQUIRED_DEPS, tocText); + + const deps = [] + .concat(...dependencies.split(","), ...requiredDeps.split(",")) + .filter((dep) => !!dep); + + console.debug("deps", deps); + + return deps; + } + public async parseMetaData(tocPath: string): Promise { const tocText = await this._fileService.readFile(tocPath); diff --git a/wowup-electron/src/app/services/warcraft/warcraft.service.ts b/wowup-electron/src/app/services/warcraft/warcraft.service.ts index bfc6c164..416ab551 100644 --- a/wowup-electron/src/app/services/warcraft/warcraft.service.ts +++ b/wowup-electron/src/app/services/warcraft/warcraft.service.ts @@ -181,10 +181,18 @@ export class WarcraftService { const directories = await this._fileService.listDirectories( addonFolderPath ); + + const dirPaths = directories.map((dir) => path.join(addonFolderPath, dir)); + const dirStats = await this._fileService.statFiles(dirPaths); + + console.debug("directories", directories); + console.debug("dirStats", dirStats); + // const directories = files.filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); for (let i = 0; i < directories.length; i += 1) { const dir = directories[i]; const addonFolder = await this.getAddonFolder(addonFolderPath, dir); + addonFolder.fileStats = dirStats[path.join(addonFolderPath, dir)]; if (addonFolder) { addonFolders.push(addonFolder); } diff --git a/wowup-electron/src/common/constants.ts b/wowup-electron/src/common/constants.ts index 845df5ff..bbcc6830 100644 --- a/wowup-electron/src/common/constants.ts +++ b/wowup-electron/src/common/constants.ts @@ -4,6 +4,7 @@ export const CREATE_DIRECTORY_CHANNEL = "create-directory"; export const DELETE_DIRECTORY_CHANNEL = "delete-directory"; export const STAT_DIRECTORY_CHANNEL = "stat-directory"; export const LIST_DIRECTORIES_CHANNEL = "list-directories"; +export const STAT_FILES_CHANNEL = "stat_files"; export const PATH_EXISTS_CHANNEL = "path-exists"; export const LIST_FILES_CHANNEL = "list-files"; export const READ_FILE_CHANNEL = "read-file"; @@ -27,6 +28,8 @@ export const USE_HARDWARE_ACCELERATION_PREFERENCE_KEY = "use_hardware_acceleration"; export const START_WITH_SYSTEM_PREFERENCE_KEY = "start_with_system"; export const START_MINIMIZED_PREFERENCE_KEY = "start_minimized"; + +// ERRORS export const NO_SEARCH_RESULTS_ERROR = "NO_SEARCH_RESULTS"; export const NO_LATEST_SEARCH_RESULT_FILES_ERROR = "NO_LATEST_SEARCH_RESULT_FILES";