Make the wtf explorer more robust

This commit is contained in:
jliddev
2021-07-14 16:53:48 -05:00
parent 40d1a8a1ff
commit 74b272f3ad
18 changed files with 381 additions and 41 deletions

View File

@@ -1,6 +1,7 @@
import * as fs from "fs-extra";
import * as path from "path";
import { max } from "lodash";
import { max, sumBy } from "lodash";
import { TreeNode } from "../src/common/models/ipc-events";
export async function readDirRecursive(sourcePath: string): Promise<string[]> {
const dirFiles: string[] = [];
@@ -19,6 +20,39 @@ export async function readDirRecursive(sourcePath: string): Promise<string[]> {
return dirFiles;
}
export async function getDirTree(sourcePath: string): Promise<TreeNode> {
const files = await fs.readdir(sourcePath, { withFileTypes: true });
const node: TreeNode = {
name: path.basename(sourcePath),
path: sourcePath,
children: [],
isDirectory: true,
size: 0,
};
for (const file of files) {
const filePath = path.join(sourcePath, file.name);
if (file.isDirectory()) {
const nestedNode = await getDirTree(filePath);
node.children.push(nestedNode);
node.size = sumBy(node.children, (n) => n.size);
} else {
const stats = await fs.stat(filePath);
node.size += stats.size;
node.children.push({
name: file.name,
path: filePath,
children: [],
isDirectory: false,
size: stats.size,
});
}
}
return node;
}
export async function getLastModifiedFileDate(sourcePath: string): Promise<number> {
const dirFiles = await readDirRecursive(sourcePath);
const dates: number[] = [];

View File

@@ -66,6 +66,8 @@ import {
IPC_WINDOW_LEAVE_FULLSCREEN,
IPC_WOWUP_GET_SCAN_RESULTS,
IPC_WRITE_FILE_CHANNEL,
IPC_LIST_DIR_RECURSIVE,
IPC_GET_DIRECTORY_TREE,
} from "../src/common/constants";
import { CurseFolderScanResult } from "../src/common/curse/curse-folder-scan-result";
import { Addon } from "../src/common/entities/addon";
@@ -73,13 +75,13 @@ import { CopyFileRequest } from "../src/common/models/copy-file-request";
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 { FsDirent, FsStats } from "../src/common/models/ipc-events";
import { FsDirent, FsStats, TreeNode } from "../src/common/models/ipc-events";
import { UnzipRequest } from "../src/common/models/unzip-request";
import { RendererChannels } from "../src/common/wowup";
import { MenuConfig, SystemTrayConfig, WowUpScanResult } from "../src/common/wowup/models";
import { createAppMenu } from "./app-menu";
import { CurseFolderScanner } from "./curse-folder-scanner";
import { getLastModifiedFileDate } from "./file.utils";
import { getDirTree, getLastModifiedFileDate, readDirRecursive } from "./file.utils";
import { addonStore } from "./stores";
import { createTray, restoreWindow } from "./system-tray";
import { WowUpFolderScanner } from "./wowup-folder-scanner";
@@ -403,6 +405,14 @@ export function initializeIpcHandlers(window: BrowserWindow, userAgent: string):
return getLastModifiedFileDate(dirPath);
});
handle(IPC_LIST_DIR_RECURSIVE, (evt, dirPath: string): Promise<string[]> => {
return readDirRecursive(dirPath);
});
handle(IPC_GET_DIRECTORY_TREE, (evt, dirPath: string): Promise<TreeNode> => {
return getDirTree(dirPath);
});
handle(IPC_MINIMIZE_WINDOW, () => {
if (window?.minimizable) {
window.minimize();

View File

@@ -1,6 +1,6 @@
{
"name": "wowup",
"version": "2.5.0-beta.2",
"version": "2.5.0-beta.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3820,6 +3820,15 @@
"@bbob/preset": "^2.7.0"
}
},
"@circlon/angular-tree-component": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@circlon/angular-tree-component/-/angular-tree-component-11.0.4.tgz",
"integrity": "sha512-Ck86mG6Z9eWG03RiOACDzrCjuzEDXU8rcEDi0aw0+Ku62x6ZY2mx8G0VX3CLEkS1BAXM2ef6luOIcoSKAKtDaA==",
"requires": {
"mobx": "~4.14.1",
"tslib": "^2.0.0"
}
},
"@csstools/convert-colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
@@ -14357,6 +14366,11 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"mobx": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-4.14.1.tgz",
"integrity": "sha512-Oyg7Sr7r78b+QPYLufJyUmxTWcqeQ96S1nmtyur3QL8SeI6e0TqcKKcxbG+sVJLWANhHQkBW/mDmgG5DDC4fdw=="
},
"mocha": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",

View File

@@ -124,6 +124,7 @@
"@bbob/core": "2.7.0",
"@bbob/html": "2.7.0",
"@bbob/preset-html5": "2.7.0",
"@circlon/angular-tree-component": "11.0.4",
"@fortawesome/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-brands-svg-icons": "5.15.3",
"@fortawesome/free-regular-svg-icons": "5.15.3",

View File

@@ -1,7 +1,7 @@
<div class="container">
<div class="account-container">
<mat-form-field>
<div class="account-container row align-items-center">
<mat-form-field class="mr-3">
<mat-label>{{ "PAGES.GET_ADDONS.CLIENT_TYPE_SELECT_LABEL" | translate }}</mat-label>
<mat-select [(value)]="selectedInstallationId" (selectionChange)="onClientChange()">
<mat-option *ngFor="let installation of installations" [value]="installation.id">
@@ -9,6 +9,11 @@
</mat-option>
</mat-select>
</mat-form-field>
<div>
<button mat-raised-button color="primary" [disabled]="(loading$ | async) === true" (click)="onClickRefresh()">
Refresh
</button>
</div>
</div>
<div *ngIf="(loading$ | async) === true" class="account-container">
@@ -20,8 +25,30 @@
<p>{{error$ | async}}</p>
</div>
<div *ngIf="(loading$ | async) === false" class="">
<div *ngFor="let account of accountMap | async" class="account-container">
<div *ngIf="(loading$ | async) === false" class="tree-container">
<p>{{wtfPath}}</p>
<div class="tree">
<tree-root [nodes]="nodes$ | async">
<ng-template #treeNodeTemplate let-node let-index="index" let-templates="templates">
<div class="node-wrapper">
<div class="text-2" (click)="node.toggleExpanded()" *ngIf="node?.data?.children?.length > 0">
<mat-icon class="tree-icon" [ngClass]="{ 'expanded': node?.isExpanded }" svgIcon="fas:chevron-right">
</mat-icon>
</div>
<div class="node-content-wrapper">
<span
[ngClass]="{ 'text-warning': !node.data.ignore && node.data.isLua && !node.data.hasAddon, 'text-3': node.data.ignore}">{{
node.data.name }}</span>
</div>
</div>
</ng-template>
</tree-root>
</div>
<!-- <div *ngFor="let account of accountMap | async" class="account-container">
<h2>Account: {{account.name}}</h2>
<mat-accordion class="variable-accordion" multi>
<mat-expansion-panel>
@@ -72,6 +99,6 @@
</mat-expansion-panel>
</mat-accordion>
</div>
</div> -->
</div>
</div>

View File

@@ -18,3 +18,10 @@
align-items: center;
}
}
.tree-container {
padding: 1em;
position: relative;
}

View File

@@ -7,11 +7,14 @@ import { Component, Input, OnDestroy, OnInit } from "@angular/core";
import { ElectronService } from "../../services";
import { SessionService } from "../../services/session/session.service";
import { WarcraftService } from "../../services/warcraft/warcraft.service";
import { WtfService } from "../../services/wtf/wtf.service";
import { WtfNode, WtfService } from "../../services/wtf/wtf.service";
import { removeExtension } from "../../utils/string.utils";
import { AddonFolder } from "../../models/wowup/addon-folder";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { WarcraftInstallationService } from "../../services/warcraft/warcraft-installation.service";
import { TreeNode } from "../../../common/models/ipc-events";
import { formatSize } from "../../utils/number.utils";
import path from "path/posix";
interface SavedVariable {
name: string;
@@ -41,6 +44,14 @@ interface AccountItem {
sizeMb: string;
}
interface NodeModel {
name: string;
isLua: boolean;
ignore: boolean;
hasAddon: boolean;
children?: NodeModel[];
}
@Component({
selector: "app-extra",
templateUrl: "./extra.component.html",
@@ -64,8 +75,10 @@ export class ExtraComponent implements OnInit, OnDestroy {
public accountMap = new BehaviorSubject<AccountItem[]>([]);
public loading$ = new BehaviorSubject<boolean>(false);
public error$ = new BehaviorSubject<string>("");
public nodes$ = new BehaviorSubject<NodeModel[]>([]);
public installations: WowInstallation[] = [];
public selectedInstallationId = "";
public wtfPath = "";
public get selectedInstallationLabel(): string {
return this.installations.find((inst) => inst.id === this.selectedInstallationId)?.label ?? "";
@@ -84,25 +97,78 @@ export class ExtraComponent implements OnInit, OnDestroy {
public onClientChange(): void {
const installation = this.installations.find((inst) => inst.id === this.selectedInstallationId);
from(this.loadAccounts(installation))
.pipe(first())
.subscribe((accounts) => {
this.accountMap.next(accounts);
});
this.wtfPath = this._wtfService.getWtfPath(installation);
// from(this.loadAccounts(installation))
// .pipe(first())
// .subscribe((accounts) => {
// this.accountMap.next(accounts);
// });
from(this.loadWtfStructure(installation)).pipe(first()).subscribe();
}
public onClickRefresh(): void {
this.onClientChange();
}
private lazyLoad() {
console.debug("lazyLoad");
this.loading$.next(true);
this.error$.next("");
this.installations = this._warcraftInstallationService.getWowInstallations();
this.selectedInstallationId = this.installations[0]?.id ?? "";
from(this.loadAccounts(this.installations[0]))
.pipe(first())
.subscribe((accounts) => {
this.accountMap.next(accounts);
});
const installation = this.installations[0];
this.selectedInstallationId = installation?.id ?? "";
this.wtfPath = this._wtfService.getWtfPath(installation);
from(this.loadWtfStructure(installation)).pipe(first()).subscribe();
// from(this.loadAccounts(installation))
// .pipe(first())
// .subscribe((accounts) => {
// this.accountMap.next(accounts);
// });
}
private async loadWtfStructure(installation: WowInstallation) {
this.loading$.next(true);
try {
const addonFolders = await this._warcraftService.listAddons(installation);
const wtfTree = await this._wtfService.getWtfContents(installation);
this.nodes$.next(wtfTree.children.map((tn) => this.getNode(tn, addonFolders)));
} catch (e) {
console.error(e);
this.error$.next(e.message);
} finally {
this.loading$.next(false);
}
}
private getNode(treeNode: WtfNode, addonFolders: AddonFolder[]): NodeModel {
let name = `${treeNode.name} (${formatSize(treeNode.size)})`;
if (treeNode.isDirectory) {
name = `${treeNode.name} (${treeNode.children.length} files ${formatSize(treeNode.size)})`;
}
const nodeModel: NodeModel = {
name: name,
children: treeNode.children.map((tn) => this.getNode(tn, addonFolders)),
hasAddon: false,
isLua: treeNode.isLua,
ignore: treeNode.ignore,
};
if (treeNode.isLua) {
nodeModel.hasAddon = this.addonFolderExists(treeNode.name, addonFolders);
}
return nodeModel;
}
private addonFolderExists(fileName: string, addonFolders: AddonFolder[]): boolean {
return addonFolders.some((af) => af.name === removeExtension(fileName));
}
private async loadAccounts(installation: WowInstallation): Promise<AccountItem[]> {
@@ -113,7 +179,6 @@ export class ExtraComponent implements OnInit, OnDestroy {
const accounts = await this._wtfService.getAccounts(installation);
const addonFolders = await this._warcraftService.listAddons(installation);
console.log("addonFolders", addonFolders);
const accountMap: AccountItem[] = [];
for (const account of accounts) {
@@ -147,7 +212,6 @@ export class ExtraComponent implements OnInit, OnDestroy {
addonFolders: AddonFolder[]
): Promise<SavedVariable[]> {
const globalVariables = await this._wtfService.getGlobalVariables(installation, account);
console.debug("globalVariables", globalVariables);
const gVars: SavedVariable[] = globalVariables.map((gv) => {
return {
hasAddon: addonFolders.some((af) => af.name === removeExtension(gv.name)),

View File

@@ -1,8 +1,10 @@
import { AgGridModule } from "ag-grid-angular";
import { LIGHTBOX_CONFIG, LightboxModule } from "ng-gallery/lightbox";
import { CommonModule, DatePipe } from "@angular/common";
import { NgModule } from "@angular/core";
import { ReactiveFormsModule } from "@angular/forms";
import { TreeModule } from "@circlon/angular-tree-component";
import { AddonDetailComponent } from "../../components/addon-detail/addon-detail.component";
import { AddonInstallButtonComponent } from "../../components/addon-install-button/addon-install-button.component";
@@ -14,6 +16,7 @@ import { CenteredSnackbarComponent } from "../../components/centered-snackbar/ce
import { ConfirmDialogComponent } from "../../components/confirm-dialog/confirm-dialog.component";
import { DateTooltipCellComponent } from "../../components/date-tooltip-cell/date-tooltip-cell.component";
import { ExternalUrlConfirmationDialogComponent } from "../../components/external-url-confirmation-dialog/external-url-confirmation-dialog.component";
import { ExtraComponent } from "../../components/extra/extra.component";
import { FundingButtonComponent } from "../../components/funding-button/funding-button.component";
import { GetAddonStatusColumnComponent } from "../../components/get-addon-status-column/get-addon-status-column.component";
import { InstallFromProtocolDialogComponent } from "../../components/install-from-protocol-dialog/install-from-protocol-dialog.component";
@@ -39,6 +42,7 @@ import { GetAddonListItemFilePropPipe } from "../../pipes/get-addon-list-item-fi
import { InterfaceFormatPipe } from "../../pipes/interface-format.pipe";
import { NgxDatePipe } from "../../pipes/ngx-date.pipe";
import { RelativeDurationPipe } from "../../pipes/relative-duration-pipe";
import { SizeDisplayPipe } from "../../pipes/size-display.pipe";
import { TrustHtmlPipe } from "../../pipes/trust-html.pipe";
import { SharedModule } from "../../shared.module";
import { AboutComponent } from "../about/about.component";
@@ -47,9 +51,6 @@ import { MyAddonsComponent } from "../my-addons/my-addons.component";
import { OptionsComponent } from "../options/options.component";
import { HomeRoutingModule } from "./home-routing.module";
import { HomeComponent } from "./home.component";
import { LightboxModule, LIGHTBOX_CONFIG } from "ng-gallery/lightbox";
import { ExtraComponent } from "../../components/extra/extra.component";
import { SizeDisplayPipe } from "../../pipes/size-display.pipe";
@NgModule({
declarations: [
@@ -108,6 +109,7 @@ import { SizeDisplayPipe } from "../../pipes/size-display.pipe";
TableContextHeaderCellComponent,
]),
LightboxModule,
TreeModule,
],
providers: [
DatePipe,

View File

@@ -1,20 +1,11 @@
import { Pipe, PipeTransform } from "@angular/core";
import { formatSize } from "../utils/number.utils";
@Pipe({ name: "sizeDisplay" })
export class SizeDisplayPipe implements PipeTransform {
public constructor() {}
public transform(size: number): string {
if (size < 1024) {
return `${size} bytes`;
}
const sizeKb = Math.round(size / 1024);
if (sizeKb < 1024) {
return `${sizeKb} kb`;
}
const sizeMb = Math.round(size / 1024 / 1024);
return `${sizeMb} mb`;
return formatSize(size);
}
}

View File

@@ -17,10 +17,12 @@ import {
IPC_READDIR,
IPC_READ_FILE_BUFFER_CHANNEL,
IPC_GET_LATEST_DIR_UPDATE_TIME,
IPC_LIST_DIR_RECURSIVE,
IPC_GET_DIRECTORY_TREE,
} from "../../../common/constants";
import { CopyFileRequest } from "../../../common/models/copy-file-request";
import { UnzipRequest } from "../../../common/models/unzip-request";
import { FsDirent, FsStats } from "../../../common/models/ipc-events";
import { FsDirent, FsStats, TreeNode } from "../../../common/models/ipc-events";
import { ElectronService } from "../electron/electron.service";
@Injectable({
@@ -120,6 +122,14 @@ export class FileService {
return await this._electronService.invoke(IPC_GET_LATEST_DIR_UPDATE_TIME, dirPath);
}
public async listDirectoryRecursive(dirPath: string): Promise<string[]> {
return await this._electronService.invoke(IPC_LIST_DIR_RECURSIVE, dirPath);
}
public async getDirectoryTree(dirPath: string): Promise<TreeNode> {
return await this._electronService.invoke(IPC_GET_DIRECTORY_TREE, dirPath);
}
public async writeFile(sourcePath: string, contents: string): Promise<string> {
return await this._electronService.invoke(IPC_WRITE_FILE_CHANNEL, sourcePath, contents);
}

View File

@@ -27,6 +27,7 @@ import {
faCog,
faAngleUp,
faAngleDown,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle, faClock } from "@fortawesome/free-regular-svg-icons";
import { faDiscord, faGithub, faPatreon } from "@fortawesome/free-brands-svg-icons";
@@ -66,6 +67,7 @@ export class IconService {
this.addSvg(faCog);
this.addSvg(faAngleUp);
this.addSvg(faAngleDown);
this.addSvg(faChevronRight);
}
private addSvg(icon: IconDefinition): void {

View File

@@ -2,7 +2,7 @@ import { Injectable } from "@angular/core";
import * as path from "path";
import { Addon } from "../../../common/entities/addon";
import { FsStats } from "../../../common/models/ipc-events";
import { FsStats, TreeNode } from "../../../common/models/ipc-events";
import { WowInstallation } from "../../models/wowup/wow-installation";
import { FileService } from "../files/file.service";
@@ -16,12 +16,45 @@ export interface FileStats {
stats: FsStats;
}
export interface WtfEntry {
name: string;
children: WtfEntry[];
}
export interface WtfNode extends TreeNode {
isLua: boolean;
ignore: boolean;
children: WtfNode[];
}
@Injectable({
providedIn: "root",
})
export class WtfService {
public constructor(private _fileService: FileService) {}
public async getWtfContents(installation: WowInstallation): Promise<WtfNode> {
const wtfPath = this.getWtfPath(installation);
const tree = await this._fileService.getDirectoryTree(wtfPath);
return this.getWtfNode(tree);
}
public getWtfNode(treeNode: TreeNode): WtfNode {
const wtfNode: WtfNode = {
...treeNode,
isLua: path.extname(treeNode.name) === ".lua",
ignore: this.shouldIgnoreFile(treeNode.name),
children: treeNode.children.map((tn) => this.getWtfNode(tn)),
};
return wtfNode;
}
private shouldIgnoreFile(fileName: string): boolean {
const canonName = fileName.toLowerCase();
return canonName.endsWith(".lua.bak") || canonName.startsWith("blizzard_");
}
public getWtfPath(installation: WowInstallation): string {
return path.join(path.dirname(installation.location), WTF_FOLDER);
}

View File

@@ -8,6 +8,20 @@ export function shortenDownloadCount(value: number, nDigit: number): string {
return shortValue.toFixed(0);
}
export function formatSize(size: number): string {
if (size < 1024) {
return `${size} bytes`;
}
const sizeKb = Math.round(size / 1024);
if (sizeKb < 1024) {
return `${sizeKb} kb`;
}
const sizeMb = Math.round(size / 1024 / 1024);
return `${sizeMb} mb`;
}
// This is a horrifying way to round to the nearest tens place
export function roundDownloadCount(value: number): number {
if (value < 10) {

View File

@@ -78,6 +78,8 @@ export const IPC_CUSTOM_PROTOCOL_RECEIVED = "custom-protocol-received";
export const IPC_ADDONS_SAVE_ALL = "addons-save-all";
export const IPC_GET_PENDING_OPEN_URLS = "get-pending-open-urls";
export const IPC_GET_LATEST_DIR_UPDATE_TIME = "get-latest-dir-update-time";
export const IPC_LIST_DIR_RECURSIVE = "list-dir-recursive";
export const IPC_GET_DIRECTORY_TREE = "get-directory-tree";
export const IPC_SYSTEM_PREFERENCES_GET_USER_DEFAULT = "system-preferences-get-user-default";
export const IPC_SHOW_OPEN_DIALOG = "show-open-dialog";
export const IPC_APP_UPDATE_STATE = "app-update-state";

View File

@@ -37,3 +37,11 @@ export interface FsStats {
ctime: Date;
birthtime: Date;
}
export interface TreeNode {
name: string;
path: string;
isDirectory: boolean;
children: TreeNode[];
size: number;
}

View File

@@ -72,7 +72,9 @@ declare type RendererChannels =
| "system-preferences-get-user-default"
| "show-open-dialog"
| "app-install-update"
| "update-app-badge";
| "update-app-badge"
| "list-dir-recursive"
| "get-directory-tree";
declare global {
interface Window {

View File

@@ -601,6 +601,124 @@ $alliance-theme-light: mat.define-light-theme(
background-color: var(--background-secondary-2);
}
// TREE VIEW
div.tree div.tree-children::before {
content: "";
position: absolute;
border-left: 1px dotted var(--text-2);
height: 100%;
top: -14px;
left: 0;
}
div.tree {
padding-left: 0;
margin-left: -5px;
}
div.tree div.tree-children {
position: relative;
padding-left: 0;
margin-left: 16px;
}
div.tree div.tree-children::before {
left: 5px;
}
div.tree tree-node > div > .node-wrapper {
margin-left: 24px;
}
div.tree tree-node > div > .node-wrapper > .node-content-wrapper {
margin-left: 4px;
}
div.tree tree-node > div.tree-node-leaf > .node-wrapper {
margin-left: 0;
}
div.tree tree-node > div::before {
content: "";
position: absolute;
border-bottom: 1px dotted var(--text-2);
width: 7px;
margin-top: 12px;
left: 7px;
}
// div.tree tree-node > div .toggle-children-wrapper {
// width: 13px;
// height: 13px;
// border: 1px solid var(--text-2);
// position: absolute;
// left: 0;
// margin-top: 5px;
// margin-left: 0;
// display: inline-block;
// z-index: 1;
// }
// div.tree tree-node > div .toggle-children-wrapper::before {
// content: "";
// display: inline-block;
// width: 7px;
// border-top: 1px solid var(--text-2);
// position: absolute;
// top: 5px;
// left: 2px;
// }
// div.tree tree-node > div .toggle-children-wrapper.toggle-children-wrapper-collapsed::after {
// content: "";
// display: inline-block;
// height: 7px;
// border-left: 1px solid var(--text-2);
// position: absolute;
// top: 2px;
// left: 5px;
// }
div.tree tree-node > div .toggle-children-wrapper .toggle-children {
display: none;
}
div.tree tree-node > div .node-content-wrapper {
margin-left: 4px;
}
div.tree > tree-node > div::before {
left: 14px;
}
div.tree > tree-node > div > .node-wrapper > tree-node-expander > .toggle-children-wrapper {
left: 22px;
}
.node-content-wrapper:hover {
background-color: var(--background-secondary-4);
}
.node-content-wrapper-active,
.node-content-wrapper-focused,
.node-content-wrapper:hover {
box-shadow: none;
}
.node-content-wrapper-focused,
.node-content-wrapper-active,
.node-content-wrapper.node-content-wrapper-active:hover,
.node-content-wrapper-active.node-content-wrapper-focused {
background-color: var(--background-secondary-3);
}
.tree-icon {
transition: transform 0.3s;
}
.tree-icon.expanded {
transform: rotate(90deg);
}
// LIGHT BOX
.lb-outerContainer {

View File

@@ -1,3 +1,4 @@
@import "~@circlon/angular-tree-component/css/angular-tree-component.css";
@import "./markdown.scss";
@import "./custom-theme.scss";