App can now show unmatched addons

This commit is contained in:
jliddev
2020-10-29 13:01:46 -05:00
parent 3594c4c56e
commit 3544fc2e77
18 changed files with 285 additions and 340 deletions

View File

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

View File

@@ -622,6 +622,7 @@ export class CurseAddonProvider implements AddonProvider {
downloadCount: scanResult.searchResult.downloadCount,
summary: scanResult.searchResult.summary,
releasedAt: new Date(latestVersion.fileDate),
isLoadOnDemand: false
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,11 +10,11 @@
>
{{ getStatusText() | translate }}
</div>
<mat-icon
<!-- <mat-icon
*ngIf="this.listItem.isAutoUpdate === true"
class="auto-update-icon"
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate"
>
update
</mat-icon>
</mat-icon> -->
</div>

View File

@@ -1,64 +1,46 @@
<div class="addon-column row align-items-center">
<div class="addon-column row align-items-center" >
<div class="thumbnail-container">
<div
*ngIf="listItem.hasThumbnail === true"
class="addon-logo-container"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"
></div>
<div *ngIf="listItem.hasThumbnail === true" class="addon-logo-container"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"></div>
<div *ngIf="listItem.hasThumbnail === false" class="addon-logo-container">
<div class="addon-logo-letter">
{{ listItem.thumbnailLetter }}
</div>
</div>
<div
*ngIf="listItem.isBetaChannel || listItem.isAlphaChannel"
class="channel"
[ngClass]="{
<div *ngIf="listItem.isBetaChannel || listItem.isAlphaChannel" class="channel" [ngClass]="{
beta: listItem.isBetaChannel,
alpha: listItem.isAlphaChannel
}"
>
}">
{{ listItem.isAlphaChannel ? "Alpha" : "Beta" }}
</div>
</div>
<div>
<a
class="addon-title mat-subheading-2"
(click)="viewDetails()"
[ngClass]="{ ignored: listItem.isIgnored }"
>{{ listItem.addon.name }}</a
>
<a class="addon-title mat-subheading-2" (click)="viewDetails()"
[ngClass]="{ ignored: listItem.isIgnored }">{{ listItem.addon.name }}</a>
<div class="addon-funding">
<a
*ngIf="listItem.addon.patreonFundingLink"
appExternalLink
[href]="listItem.addon.patreonFundingLink"
matTooltip="Support the author on Patreon"
>
<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"
>
<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 *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" [ngClass]="{ ignored: listItem.isIgnored }">
{{ listItem.addon.installedVersion }}
<div class="addon-version row align-items-center" [ngClass]="{ ignored: listItem.isIgnored }">
<span class="mr-1">{{ listItem.addon.installedVersion }}</span>
<mat-icon *ngIf="listItem.isLoadOnDemand" [inline]="true" [matTooltip]="'Required dependency missing'"
[style.color]="'#ff9800'">
error_outline
</mat-icon>
<mat-icon *ngIf="listItem.isAutoUpdate" [inline]="true"
[matTooltip]="'PAGES.MY_ADDONS.TABLE.AUTO_UPDATE_ICON_TOOLTIP' | translate">
update
</mat-icon>
</div>
</div>
</div>
</div>

View File

@@ -18,6 +18,7 @@ export interface Addon {
author?: string;
installedFolders?: string;
isIgnored: boolean;
isLoadOnDemand: boolean;
autoUpdateEnabled: boolean;
clientType: WowClientType;
channelType: AddonChannelType;

View File

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

View File

@@ -12,4 +12,6 @@ export interface Toc {
wowInterfaceId?: string;
tukUiProjectId?: string;
tukUiProjectFolders?: string;
loadOnDemand?: string;
dependencyList: string[];
}

View File

@@ -1,97 +1,53 @@
<div
class="tab-container d-flex flex-col"
[ngClass]="{
<div class="tab-container d-flex flex-col" [ngClass]="{
mac: electronService.isMac,
windows: electronService.isWin,
linux: electronService.isLinux
}"
>
}">
<div class="control-container">
<div class="select-container">
<mat-form-field>
<mat-label>{{
"PAGES.MY_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate
}}</mat-label>
<mat-select
class="select pointer"
[(value)]="selectedClient"
(selectionChange)="onClientChange()"
[disabled]="enableControls === false"
>
<mat-option
[value]="clientType"
*ngFor="
<mat-select class="select pointer" [(value)]="selectedClient" (selectionChange)="onClientChange()"
[disabled]="enableControls === false">
<mat-option [value]="clientType" *ngFor="
let clientType of warcraftService.installedClientTypes$ | async
"
>
">
{{ warcraftService.getClientDisplayName(clientType) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="right-container">
<div
class="filter-container"
*ngIf="selectedClient !== wowClientType.None"
>
<div class="filter-container" *ngIf="selectedClient !== wowClientType.None">
<mat-form-field>
<mat-label>{{
"PAGES.MY_ADDONS.FILTER_LABEL" | translate
}}</mat-label>
<input matInput (keyup)="filterAddons()" [(ngModel)]="filter" />
<button
mat-button
color="accent"
*ngIf="filter"
matSuffix
mat-icon-button
aria-label="Clear"
(click)="onClearFilter()"
>
<button mat-button color="accent" *ngIf="filter" matSuffix mat-icon-button aria-label="Clear"
(click)="onClearFilter()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div class="button-container">
<button
mat-flat-button
color="primary"
[matTooltip]="'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false"
(click)="onUpdateAll()"
(contextmenu)="onUpdateAllContext($event)"
appUserActionTracker
category="MyAddons"
action="UpdateAll"
>
<button mat-flat-button color="primary" [matTooltip]="'PAGES.MY_ADDONS.UPDATE_ALL_BUTTON_TOOLTIP' | translate"
[disabled]="enableControls === false" (click)="onUpdateAll()" (contextmenu)="onUpdateAllContext($event)"
appUserActionTracker category="MyAddons" action="UpdateAll">
{{ "PAGES.MY_ADDONS.UPDATE_ALL_BUTTON" | translate }}
</button>
<button
mat-flat-button
color="primary"
[matTooltip]="
<button mat-flat-button color="primary" [matTooltip]="
'PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON_TOOLTIP' | translate
"
[disabled]="enableControls === false"
(click)="onRefresh()"
appUserActionTracker
category="MyAddons"
action="CheckUpdates"
>
" [disabled]="enableControls === false" (click)="onRefresh()" appUserActionTracker category="MyAddons"
action="CheckUpdates">
{{ "PAGES.MY_ADDONS.CHECK_UPDATES_BUTTON" | translate }}
</button>
<button
mat-flat-button
color="primary"
[matTooltip]="
<button mat-flat-button color="primary" [matTooltip]="
'PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON_TOOLTIP' | translate
"
[disabled]="enableControls === false"
(click)="onReScan()"
appUserActionTracker
category="MyAddons"
action="ReScanFolders"
>
" [disabled]="enableControls === false" (click)="onReScan()" appUserActionTracker category="MyAddons"
action="ReScanFolders">
{{ "PAGES.MY_ADDONS.RESCAN_FOLDERS_BUTTON" | translate }}
</button>
</div>
@@ -102,27 +58,18 @@
<app-progress-spinner [message]="spinnerMessage"> </app-progress-spinner>
</div>
<div
*ngIf="isBusy === false && sortedListItems.length === 0"
class="no-addons-container flex-grow-1"
>
<div *ngIf="isBusy === false && sortedListItems.length === 0" class="no-addons-container flex-grow-1">
<h1>No Addons found</h1>
</div>
<div
class="table-container flex-grow-1"
[hidden]="isBusy === true || sortedListItems.length === 0"
>
<div class="table-container flex-grow-1" [hidden]="isBusy === true || sortedListItems.length === 0">
<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>
<td mat-cell *matCellDef="let element" class="cell-padding">
<app-my-addons-addon-cell
[addon]="element"
(onViewDetails)="openDetailDialog($event)"
>
<app-my-addons-addon-cell [addon]="element" (onViewDetails)="openDetailDialog($event)">
</app-my-addons-addon-cell>
</td>
</ng-container>
@@ -131,37 +78,19 @@
<th mat-header-cell mat-sort-header *matHeaderCellDef>
{{ "PAGES.MY_ADDONS.TABLE.STATUS_COLUMN_HEADER" | translate }}
</th>
<td
mat-cell
*matCellDef="let element"
class="status-column cell-padding"
>
<app-my-addon-status-column
[listItem]="element"
></app-my-addon-status-column>
<td mat-cell *matCellDef="let element" class="status-column cell-padding">
<app-my-addon-status-column [listItem]="element"></app-my-addon-status-column>
</td>
</ng-container>
<ng-container matColumnDef="installedAt">
<th
class="installed-at-cell"
mat-header-cell
mat-sort-header
*matHeaderCellDef
>
<th class="installed-at-cell" mat-header-cell mat-sort-header *matHeaderCellDef>
{{ "PAGES.MY_ADDONS.TABLE.UPDATED_AT_COLUMN_HEADER" | translate }}
</th>
<td
mat-cell
*matCellDef="let element"
class="cell-padding"
[matTooltip]="
<td mat-cell *matCellDef="let element" class="cell-padding" [matTooltip]="
'COMMON.DATES.DATETIME_SHORT'
| translate: { d: element.installedAt }
"
matTooltipPosition="above"
matTooltipShowDelay="500"
>
" matTooltipPosition="above" matTooltipShowDelay="500">
{{ element.installedAt | relativeDuration }}
</td>
</ng-container>
@@ -176,36 +105,19 @@
</ng-container>
<ng-container matColumnDef="addon.releasedAt">
<th
class="released-at-cell"
mat-header-cell
mat-sort-header
*matHeaderCellDef
>
<th class="released-at-cell" mat-header-cell mat-sort-header *matHeaderCellDef>
{{ "PAGES.MY_ADDONS.TABLE.RELEASED_AT_COLUMN_HEADER" | translate }}
</th>
<td
mat-cell
*matCellDef="let element"
class="cell-padding"
[matTooltip]="
<td mat-cell *matCellDef="let element" class="cell-padding" [matTooltip]="
'COMMON.DATES.DATETIME_SHORT'
| translate: { d: element.addon.releasedAt }
"
matTooltipPosition="above"
matTooltipShowDelay="500"
>
" matTooltipPosition="above" matTooltipShowDelay="500">
{{ element.addon.releasedAt | relativeDuration }}
</td>
</ng-container>
<ng-container matColumnDef="addon.gameVersion">
<th
class="game-version-cell"
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 class="game-version-cell" mat-cell *matCellDef="let element">
@@ -214,30 +126,19 @@
</ng-container>
<ng-container matColumnDef="addon.providerName">
<th
mat-header-cell
mat-sort-header
*matHeaderCellDef
class="provider-column"
>
<th mat-header-cell mat-sort-header *matHeaderCellDef class="provider-column">
{{ "PAGES.MY_ADDONS.TABLE.PROVIDER_COLUMN_HEADER" | translate }}
</th>
<td mat-cell *matCellDef="let element" class="cell-padding">
<div *ngIf="element.addon.providerName !== 'WowUp'">
{{ element.addon.providerName }}
</div>
<div
*ngIf="element.addon.providerName === 'WowUp'"
class="addon-provider"
>
<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"
/>
<img class="provider-logo" [matTooltip]="'Sourced from ' + element.addon.providerName"
src="assets/icons/favicon.256x256.png" />
</div>
</td>
</ng-container>
@@ -255,42 +156,24 @@
</td>
</ng-container>
<tr
mat-header-row
*matHeaderRowDef="displayedColumns; sticky: true"
(contextmenu)="onHeaderContext($event); $event.preventDefault()"
></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"
(contextmenu)="onHeaderContext($event); $event.preventDefault()"></tr>
<tr
mat-row
*matRowDef="let row; let i = index; columns: displayedColumns"
tabindex="0"
[ngClass]="{ 'selected-row': row.selected }"
(click)="onRowClicked($event, row, i)"
(dblclick)="openDetailDialog(row)"
(contextmenu)="onCellContext($event, row)"
(keydown.control.a)="selectAllRows($event)"
(keydown.meta.a)="selectAllRows($event)"
></tr>
<tr mat-row *matRowDef="let row; let i = index; columns: displayedColumns" tabindex="0"
[ngClass]="{ 'selected-row': row.selected }" (click)="onRowClicked($event, row, i)"
(dblclick)="openDetailDialog(row)" (contextmenu)="onCellContext($event, row)"
(keydown.control.a)="selectAllRows($event)" (keydown.meta.a)="selectAllRows($event)"></tr>
</table>
</div>
</div>
<div
style="visibility: hidden; position: fixed"
#addonContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="contextMenu"
></div>
<div style="visibility: hidden; position: fixed" #addonContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x" [style.top]="contextMenuPosition.y" [matMenuTriggerFor]="contextMenu"></div>
<mat-menu #contextMenu="matMenu" class="addon-context-menu">
<ng-template matMenuContent let-listItem="listItem">
<div class="addon-context-menu-header">
<div
*ngIf="listItem.hasThumbnail === true"
class="thumbnail"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"
></div>
<div *ngIf="listItem.hasThumbnail === true" class="thumbnail"
[style.backgroundImage]="'url(' + listItem.addon.thumbnailUrl + ')'"></div>
<div *ngIf="listItem.hasThumbnail === false" class="thumbnail">
<div class="thumbnail-letter">
{{ listItem.thumbnailLetter }}
@@ -302,109 +185,59 @@
</div>
</div>
<mat-divider></mat-divider>
<mat-checkbox
class="mat-menu-item"
[checked]="listItem.addon.isIgnored"
(change)="onClickIgnoreAddon($event, listItem)"
appUserActionTracker
category="MyAddons"
action="IgnoreAddon"
[label]="listItem.addon.name"
>
<mat-checkbox *ngIf="addonService.isValidProviderName(listItem.addon.providerName)" class="mat-menu-item"
[checked]="listItem.addon.isIgnored" (change)="onClickIgnoreAddon($event, listItem)" appUserActionTracker
category="MyAddons" action="IgnoreAddon" [label]="listItem.addon.name">
{{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }}
</mat-checkbox>
<mat-checkbox
*ngIf="listItem.addon.isIgnored === false"
class="mat-menu-item"
[checked]="listItem.addon.autoUpdateEnabled"
(change)="onClickAutoUpdateAddon($event, listItem)"
appUserActionTracker
category="MyAddons"
action="AutoUpdateAddon"
[label]="listItem.addon.name"
>
<mat-checkbox *ngIf="listItem.addon.isIgnored === false" class="mat-menu-item"
[checked]="listItem.addon.autoUpdateEnabled" (change)="onClickAutoUpdateAddon($event, listItem)"
appUserActionTracker category="MyAddons" action="AutoUpdateAddon" [label]="listItem.addon.name">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.AUTO_UPDATE_ADDON_BUTTON"
| translate
}}
</mat-checkbox>
<button mat-menu-item [matMenuTriggerFor]="addonChannels">
<button *ngIf="addonService.isValidProviderName(listItem.addon.providerName)" mat-menu-item
[matMenuTriggerFor]="addonChannels">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.CHANNEL_SUBMENT_TITLE" | translate
}}
</button>
<button
mat-menu-item
(click)="onShowfolder(listItem.addon)"
appUserActionTracker
category="MyAddons"
action="ShowAddonFolder"
[label]="listItem.addon.name"
>
<button mat-menu-item (click)="onShowfolder(listItem.addon)" appUserActionTracker category="MyAddons"
action="ShowAddonFolder" [label]="listItem.addon.name">
{{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.SHOW_FOLDER" | translate }}
</button>
<button
mat-menu-item
(click)="onReInstallAddon(listItem)"
appUserActionTracker
category="MyAddons"
action="ReInstallAddon"
[label]="listItem.addon.name"
>
<button *ngIf="addonService.isValidProviderName(listItem.addon.providerName)" mat-menu-item
(click)="onReInstallAddon(listItem)" appUserActionTracker category="MyAddons" action="ReInstallAddon"
[label]="listItem.addon.name">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REINSTALL_ADDON_BUTTON" | translate
}}
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
(click)="onRemoveAddon(listItem.addon)"
appUserActionTracker
category="MyAddons"
action="RemoveAddon"
[label]="listItem.addon.name"
>
<button mat-menu-item (click)="onRemoveAddon(listItem.addon)" appUserActionTracker category="MyAddons"
action="RemoveAddon" [label]="listItem.addon.name">
{{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.REMOVE_ADDON_BUTTON" | translate }}
</button>
<mat-menu #addonChannels="matMenu" class="addon-context-menu">
<mat-radio-group
class="vertical-radio-group"
[ngModel]="listItem.addon.channelType"
(change)="onSelectedAddonChannelChange($event, listItem)"
>
<mat-radio-button
class="mat-menu-item"
[value]="0"
appUserActionTracker
category="MyAddons"
action="SetStableAddonChannel"
[label]="listItem.addon.name"
>
<mat-radio-group class="vertical-radio-group" [ngModel]="listItem.addon.channelType"
(change)="onSelectedAddonChannelChange($event, listItem)">
<mat-radio-button class="mat-menu-item" [value]="0" appUserActionTracker category="MyAddons"
action="SetStableAddonChannel" [label]="listItem.addon.name">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL"
| translate
}}
</mat-radio-button>
<mat-radio-button
class="mat-menu-item"
[value]="1"
appUserActionTracker
category="MyAddons"
action="SetBetaAddonChannel"
[label]="listItem.addon.name"
>
<mat-radio-button class="mat-menu-item" [value]="1" appUserActionTracker category="MyAddons"
action="SetBetaAddonChannel" [label]="listItem.addon.name">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.BETA_ADDON_CHANNEL" | translate
}}
</mat-radio-button>
<mat-radio-button
class="mat-menu-item"
[value]="2"
appUserActionTracker
category="MyAddons"
action="SetAlphaAddonChannel"
[label]="listItem.addon.name"
>
<mat-radio-button class="mat-menu-item" [value]="2" appUserActionTracker category="MyAddons"
action="SetAlphaAddonChannel" [label]="listItem.addon.name">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.ALPHA_ADDON_CHANNEL" | translate
}}
@@ -414,31 +247,20 @@
</ng-template>
</mat-menu>
<div
style="visibility: hidden; position: fixed"
#addonMultiContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="multiContextMenu"
></div>
<div style="visibility: hidden; position: fixed" #addonMultiContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x" [style.top]="contextMenuPosition.y" [matMenuTriggerFor]="multiContextMenu"></div>
<mat-menu #multiContextMenu="matMenu" class="addon-context-menu">
<ng-template matMenuContent let-listItems="listItems">
<div class="addon-context-menu-header">
{{ listItems.length + " addons selected" }}
</div>
<mat-divider></mat-divider>
<mat-checkbox
class="mat-menu-item"
[checked]="isSelectedItemsProp(listItems, 'addon.isIgnored')"
(change)="onClickIgnoreAddons($event, listItems)"
>
<mat-checkbox class="mat-menu-item" [checked]="isSelectedItemsProp(listItems, 'addon.isIgnored')"
(change)="onClickIgnoreAddons($event, listItems)">
{{ "PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.IGNORE_ADDON_BUTTON" | translate }}
</mat-checkbox>
<mat-checkbox
class="mat-menu-item"
[checked]="isSelectedItemsProp(listItems, 'addon.autoUpdateEnabled')"
(change)="onClickAutoUpdateAddons($event, listItems)"
>
<mat-checkbox class="mat-menu-item" [checked]="isSelectedItemsProp(listItems, 'addon.autoUpdateEnabled')"
(change)="onClickAutoUpdateAddons($event, listItems)">
{{
"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 }}
</button>
<mat-menu #addonChannels="matMenu" class="addon-context-menu">
<mat-radio-group
class="vertical-radio-group"
(change)="onSelectedAddonsChannelChange($event, listItems)"
>
<mat-radio-group class="vertical-radio-group" (change)="onSelectedAddonsChannelChange($event, listItems)">
<mat-radio-button class="mat-menu-item" [value]="0">
{{
"PAGES.MY_ADDONS.ADDON_CONTEXT_MENU.STABLE_ADDON_CHANNEL"
@@ -484,13 +303,9 @@
</ng-template>
</mat-menu>
<div
style="visibility: hidden; position: fixed"
#columnContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="columnContextMenu"
></div>
<div style="visibility: hidden; position: fixed" #columnContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x" [style.top]="contextMenuPosition.y" [matMenuTriggerFor]="columnContextMenu">
</div>
<mat-menu #columnContextMenu="matMenu" class="addon-context-menu">
<ng-template matMenuContent let-columns="columns">
<div class="addon-context-menu-header">
@@ -499,49 +314,31 @@
</div>
</div>
<mat-divider></mat-divider>
<mat-checkbox
*ngFor="let column of columns"
class="mat-menu-item"
[checked]="column.visible"
(change)="onColumnVisibleChange($event, column)"
>
<mat-checkbox *ngFor="let column of columns" class="mat-menu-item" [checked]="column.visible"
(change)="onColumnVisibleChange($event, column)">
{{ column.display }}
</mat-checkbox>
</ng-template>
</mat-menu>
<div
style="visibility: hidden; position: fixed"
#updateAllContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x"
[style.top]="contextMenuPosition.y"
[matMenuTriggerFor]="updateAllContextMenu"
></div>
<div style="visibility: hidden; position: fixed" #updateAllContextMenuTrigger="matMenuTrigger"
[style.left]="contextMenuPosition.x" [style.top]="contextMenuPosition.y" [matMenuTriggerFor]="updateAllContextMenu">
</div>
<mat-menu #updateAllContextMenu="matMenu" class="addon-context-menu">
<ng-template matMenuContent let-columns="columns">
<button
mat-menu-item
(click)="onUpdateAllRetailClassic()"
appUserActionTracker
category="MyAddons"
action="UpdateAllClassicRetail"
>
<button mat-menu-item (click)="onUpdateAllRetailClassic()" appUserActionTracker category="MyAddons"
action="UpdateAllClassicRetail">
{{
"PAGES.MY_ADDONS.UPDATE_ALL_CONTEXT_MENU.UPDATE_RETAIL_CLASSIC_BUTTON"
| translate
}}
</button>
<button
mat-menu-item
(click)="onUpdateAllClients()"
appUserActionTracker
category="MyAddons"
action="UpdateAllClients"
>
<button mat-menu-item (click)="onUpdateAllClients()" appUserActionTracker category="MyAddons"
action="UpdateAllClients">
{{
"PAGES.MY_ADDONS.UPDATE_ALL_CONTEXT_MENU.UPDATE_ALL_CLIENTS_BUTTON"
| translate
}}
</button>
</ng-template>
</mat-menu>
</mat-menu>

View File

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

View File

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

View File

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

View File

@@ -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<Toc> {
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<string[]> {
const tocText = await this._fileService.readFile(tocPath);

View File

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

View File

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