diff --git a/package-lock.json b/package-lock.json index bd330c8..67b52c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@devvit/play", "version": "0.0.36", "license": "BSD-3-Clause", + "dependencies": { + "@lit/context": "^1.1.1" + }, "devDependencies": { "@ampproject/filesize": "4.3.0", "@codemirror/autocomplete": "6.16.0", @@ -1117,14 +1120,20 @@ "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", - "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==", - "dev": true + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" + }, + "node_modules/@lit/context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.1.tgz", + "integrity": "sha512-q/Rw7oWSJidUP43f/RUPwqZ6f5VlY8HzinTWxL/gW1Hvm2S5q2hZvV+qM8WFcC+oLNNknc3JKsd5TwxLk1hbdg==", + "dependencies": { + "@lit/reactive-element": "^1.6.2 || ^2.0.0" + } }, "node_modules/@lit/reactive-element": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", - "dev": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0" } diff --git a/package.json b/package.json index 6f63304..d18a715 100644 --- a/package.json +++ b/package.json @@ -114,5 +114,8 @@ }, "type": "module", "types": "dist/index.d.ts", - "version": "0.0.36" + "version": "0.0.36", + "dependencies": { + "@lit/context": "^1.1.1" + } } diff --git a/src/elements/play-assets-dialog.ts b/src/elements/play-assets-dialog.ts index 46e0ae6..2c75215 100644 --- a/src/elements/play-assets-dialog.ts +++ b/src/elements/play-assets-dialog.ts @@ -1,4 +1,4 @@ -import {customElement, property, query, state} from 'lit/decorators.js' +import {customElement, property, query} from 'lit/decorators.js' import { css, type CSSResultGroup, @@ -11,17 +11,20 @@ import {choose} from 'lit-html/directives/choose.js' import {when} from 'lit-html/directives/when.js' import { type AssetFilesystemType, + assetsContext, PlayAssets } from './play-assets/play-assets.js' +import {cssReset} from '../utils/css-reset.js' +import {consume} from '@lit/context' import './play-assets/play-assets.js' import './play-assets/play-assets-virtual-fs.js' import './play-assets/play-assets-local-directory.js' import './play-assets/play-assets-local-archive.js' import './play-dialog/play-dialog.js' +import {Bubble} from '../utils/bubble.js' declare global { - interface HTMLElementEventMap {} interface HTMLElementTagNameMap { 'play-assets-dialog': PlayAssetsDialog } @@ -30,7 +33,11 @@ declare global { @customElement('play-assets-dialog') export class PlayAssetsDialog extends LitElement { static override readonly styles: CSSResultGroup = css` - ${PlayDialog.styles} + ${cssReset} + + legend { + font-weight: bold; + } fieldset { margin-bottom: var(--space); @@ -46,11 +53,8 @@ export class PlayAssetsDialog extends LitElement { @property({attribute: 'enable-local-assets', type: Boolean}) enableLocalAssets: boolean = false - @state() - private _filesystemType: AssetFilesystemType = 'virtual' - - @query('play-assets') - private _assets: PlayAssets | undefined + @consume({context: assetsContext}) + private _assets!: PlayAssets @query('play-dialog', true) private _dialog!: PlayDialog @@ -63,18 +67,29 @@ export class PlayAssetsDialog extends LitElement { this._dialog.close() } + override connectedCallback() { + super.connectedCallback() + + this._assets.addEventListener('assets-updated', this.#assetsUpdated) + } + + override disconnectedCallback() { + super.disconnectedCallback() + + this._assets.removeEventListener('assets-updated', this.#assetsUpdated) + } + protected override render(): TemplateResult { return html` - ${when(this.enableLocalAssets, this.#renderFilesystemPicker)}
${this.#filesystemTitle}: - ${choose(this._filesystemType, [ + ${choose(this._assets.filesystemType, [ [ 'virtual', () => html`` @@ -93,7 +108,7 @@ export class PlayAssetsDialog extends LitElement { @@ -103,7 +118,7 @@ export class PlayAssetsDialog extends LitElement { @@ -112,15 +127,16 @@ export class PlayAssetsDialog extends LitElement {
` } - #setFilesystem = (ev: InputEvent & {currentTarget: HTMLInputElement}) => { - if (this._assets) { - this._assets.filesystemType = ev.currentTarget - .value as AssetFilesystemType - this.requestUpdate() - } + #setFilesystem = ( + ev: InputEvent & {currentTarget: HTMLInputElement} + ): void => { + const filesystemType = ev.currentTarget.value as AssetFilesystemType + this.dispatchEvent( + Bubble('assets-set-filesystem', filesystemType) + ) } - #renderLocalFs = () => { + #renderLocalFs = (): TemplateResult => { return html`
${when( @@ -133,13 +149,13 @@ export class PlayAssetsDialog extends LitElement { ` } - #assetsUpdated = () => { - this._filesystemType = this._assets?.filesystemType ?? 'virtual' - } - get #filesystemTitle(): string { - return this._filesystemType === 'virtual' + return this._assets.filesystemType === 'virtual' ? 'Manage files' : 'Filesystem source' } + + #assetsUpdated = (): void => { + this.requestUpdate() + } } diff --git a/src/elements/play-assets/file-upload-dropper.ts b/src/elements/play-assets/file-upload-dropper.ts index d90965b..59ad58c 100644 --- a/src/elements/play-assets/file-upload-dropper.ts +++ b/src/elements/play-assets/file-upload-dropper.ts @@ -21,7 +21,7 @@ import {PlayAssets} from './play-assets.js' declare global { interface HTMLElementEventMap { - 'files-selected': CustomEvent + 'files-selected': CustomEvent cancelled: CustomEvent } interface HTMLElementTagNameMap { @@ -29,7 +29,7 @@ declare global { } } -export type FilesSelectedEvent = { +export type FileSelection = { // Provided in fallback environments where File Access API is not available files?: File[] // Provided in environments where File Access API is available @@ -128,16 +128,14 @@ export class FileUploadDropper extends LitElement { /** * Uses File Access API to get FileSystemFileHandles */ - #pickFile = async () => { + #pickFile = async (): Promise => { const fileHandles = await fileAccessContext.showOpenFilePicker({ ...(this.id ? {id: this.id} : {}), types: this.acceptTypes, multiple: this.multiple }) if (fileHandles.length > 0) { - this.dispatchEvent( - Bubble('files-selected', {fileHandles}) - ) + this.dispatchEvent(Bubble('files-selected', {fileHandles})) } else { this.dispatchEvent(Bubble('cancelled', undefined)) } @@ -157,7 +155,9 @@ export class FileUploadDropper extends LitElement { /** * Handles the drag'n'drop result */ - #processDrop = async (ev: InputEvent & {currentTarget: HTMLInputElement}) => { + #processDrop = async ( + ev: InputEvent & {currentTarget: HTMLInputElement} + ): Promise => { ev.preventDefault() this.#dragEnd() @@ -181,9 +181,7 @@ export class FileUploadDropper extends LitElement { } } } - this.dispatchEvent( - Bubble('files-selected', {fileHandles}) - ) + this.dispatchEvent(Bubble('files-selected', {fileHandles})) } else { await this.#processFileList(ev.dataTransfer.files) } @@ -204,14 +202,14 @@ export class FileUploadDropper extends LitElement { } } - this.dispatchEvent(Bubble('files-selected', {files})) + this.dispatchEvent(Bubble('files-selected', {files})) } - #clearError = () => { + #clearError = (): void => { this._errorMessage = undefined } - #dragStart = (ev: Event) => { + #dragStart = (ev: Event): void => { if (ev.type === 'dragover') { ev.preventDefault() } @@ -219,7 +217,7 @@ export class FileUploadDropper extends LitElement { this._dragging = true } - #dragEnd = () => { + #dragEnd = (): void => { this._dragging = false } diff --git a/src/elements/play-assets/play-assets-local-archive.ts b/src/elements/play-assets/play-assets-local-archive.ts index 8a37497..0998498 100644 --- a/src/elements/play-assets/play-assets-local-archive.ts +++ b/src/elements/play-assets/play-assets-local-archive.ts @@ -1,18 +1,18 @@ -import {customElement, query, state} from 'lit/decorators.js' +import {customElement, state} from 'lit/decorators.js' import {css, html, LitElement, type TemplateResult} from 'lit' import {when} from 'lit-html/directives/when.js' import {cssReset} from '../../utils/css-reset.js' import {type FilePickerType} from '../../utils/file-access-api.js' -import type {FilesSelectedEvent} from './file-upload-dropper.js' -import {PlayAssets} from './play-assets.js' +import type {FileSelection} from './file-upload-dropper.js' +import {assetsContext, PlayAssets} from './play-assets.js' import {styleMap} from 'lit/directives/style-map.js' +import {consume} from '@lit/context' import '../play-button.js' import '../play-icon/play-icon.js' import './file-upload-dropper.js' declare global { - interface HTMLElementEventMap {} interface HTMLElementTagNameMap { 'play-assets-local-archive': PlayAssetsLocalArchive } @@ -55,8 +55,8 @@ export class PlayAssetsLocalArchive extends LitElement { } ` - @query('play-assets') - private _assets?: PlayAssets + @consume({context: assetsContext}) + private _assets!: PlayAssets @state() private _detailsStyle = STYLE_COLLAPSED @@ -64,14 +64,23 @@ export class PlayAssetsLocalArchive extends LitElement { @state() private _selectStyle = STYLE_VISIBLE + override connectedCallback() { + super.connectedCallback() + + this._assets.addEventListener('assets-updated', this.#assetsUpdated) + } + + override disconnectedCallback() { + super.disconnectedCallback() + + this._assets.removeEventListener('assets-updated', this.#assetsUpdated) + } + protected override render(): TemplateResult { - return html` - - ${this.#renderArchiveDetails()} ${this.#renderMountArchive()} - ` + return html` ${this.#renderArchiveDetails()} ${this.#renderMountArchive()} ` } - #renderMountArchive = () => { + #renderMountArchive = (): TemplateResult => { const types: FilePickerType[] = [ { description: 'ZIP Archive', @@ -98,13 +107,13 @@ export class PlayAssetsLocalArchive extends LitElement { ` } - #renderArchiveDetails = () => { + #renderArchiveDetails = (): TemplateResult => { return html`
Mounted archive: -
${this._assets?.archiveFilename}
- File count: ${this._assets?.assetCount} +
${this._assets.archiveFilename}
+ File count: ${this._assets.assetCount}
${when( PlayAssets.hasFileAccessAPI, @@ -114,7 +123,7 @@ export class PlayAssetsLocalArchive extends LitElement { size="small" icon="restart-outline" title="Refresh" - @click=${() => this._assets?.remountLocalArchive()} + @click=${() => this._assets.remountLocalArchive()} >` )} this._assets?.unmount()} + @click=${() => this._assets.unmount()} >
` } - #onFiles = async (ev: CustomEvent) => { + #onFiles = async (ev: CustomEvent): Promise => { const file = ev.detail.fileHandles?.[0] ?? ev.detail.files?.[0] if (file) { await this._assets?.mountLocalArchive(file) } } - #assetsUpdated = () => { - this._selectStyle = this._assets?.archiveHandle + #assetsUpdated = (): void => { + this._selectStyle = this._assets.isArchiveMounted ? STYLE_COLLAPSED : STYLE_VISIBLE - this._detailsStyle = this._assets?.archiveHandle + this._detailsStyle = this._assets.isArchiveMounted ? STYLE_VISIBLE : STYLE_COLLAPSED + this.requestUpdate() } } diff --git a/src/elements/play-assets/play-assets-local-directory.ts b/src/elements/play-assets/play-assets-local-directory.ts index 8e8560a..c371f5d 100644 --- a/src/elements/play-assets/play-assets-local-directory.ts +++ b/src/elements/play-assets/play-assets-local-directory.ts @@ -1,15 +1,14 @@ -import {customElement, query, state} from 'lit/decorators.js' +import {customElement} from 'lit/decorators.js' import {css, html, LitElement, type TemplateResult} from 'lit' import {cssReset} from '../../utils/css-reset.js' import {when} from 'lit-html/directives/when.js' import {fileAccessContext} from '../../utils/file-access-api.js' +import {assetsContext, type PlayAssets} from './play-assets.js' +import {consume} from '@lit/context' -import '../play-assets/play-assets.js' import '../play-button.js' -import type {PlayAssets} from './play-assets.js' declare global { - interface HTMLElementEventMap {} interface HTMLElementTagNameMap { 'play-assets-local-directory': PlayAssetsLocalDirectory } @@ -50,19 +49,27 @@ export class PlayAssetsLocalDirectory extends LitElement { } ` - @state() - private _directoryHandle: PlayAssets['directoryHandle'] - - @query('play-assets') + @consume({context: assetsContext}) private _assets!: PlayAssets + override connectedCallback(): void { + super.connectedCallback() + + this._assets.addEventListener('assets-updated', this.#assetsUpdated) + } + + override disconnectedCallback(): void { + super.disconnectedCallback() + + this._assets.removeEventListener('assets-updated', this.#assetsUpdated) + } + protected override render(): TemplateResult { return html` - Local directory:
${when( - this._directoryHandle, + this._assets.isDirectoryMounted, () => html`
.../${this._assets.directoryName}
{ + #pickDirectory = async (): Promise => { const directoryHandle = await fileAccessContext.showDirectoryPicker({ id: 'selectDirectory', mode: 'read' @@ -96,7 +103,7 @@ export class PlayAssetsLocalDirectory extends LitElement { } } - #assetsUpdated = () => { - this._directoryHandle = this._assets.directoryHandle + #assetsUpdated = (): void => { + this.requestUpdate() } } diff --git a/src/elements/play-assets/play-assets-virtual-fs.ts b/src/elements/play-assets/play-assets-virtual-fs.ts index 720601f..72d0553 100644 --- a/src/elements/play-assets/play-assets-virtual-fs.ts +++ b/src/elements/play-assets/play-assets-virtual-fs.ts @@ -7,18 +7,17 @@ import { type TemplateResult } from 'lit' import {cssReset} from '../../utils/css-reset.js' -import type {FilesSelectedEvent} from './file-upload-dropper.js' +import type {FileSelection} from './file-upload-dropper.js' import {when} from 'lit-html/directives/when.js' import {repeat} from 'lit/directives/repeat.js' -import type {PlayAssets} from './play-assets.js' +import {assetsContext, type PlayAssets} from './play-assets.js' +import {consume} from '@lit/context' -import './play-assets.js' import '../play-button.js' import '../play-icon/play-icon.js' import './file-upload-dropper.js' declare global { - interface HTMLElementEventMap {} interface HTMLElementTagNameMap { 'play-assets-virtual-fs': PlayAssetsVirtualFilesystem } @@ -98,19 +97,26 @@ export class PlayAssetsVirtualFilesystem extends LitElement { @state() private _clearAll: boolean = false - @query('play-assets') - private _assets?: PlayAssets + @consume({context: assetsContext}) + private _assets!: PlayAssets @query('#renameAsset', false) private _renameInput: HTMLInputElement | undefined - private get _assetCount(): number { - return this._assets?.assetCount ?? 0 + override connectedCallback() { + super.connectedCallback() + + this._assets.addEventListener('assets-updated', this.#assetsUpdated) + } + + override disconnectedCallback() { + super.disconnectedCallback() + + this._assets.removeEventListener('assets-updated', this.#assetsUpdated) } protected override render(): TemplateResult { return html` - - ${when(this._assetCount > 0, this.#renderFiles, this.#renderNoFiles)} + ${when( + this._assets.assetCount > 0, + this.#renderFiles, + this.#renderNoFiles + )} ` } - #renderFiles = () => { + #renderFiles = (): TemplateResult => { return html` ${repeat(this._assetNames, this.#renderAssetEntry)} ` } - #renderNoFiles = () => { + #renderNoFiles = (): TemplateResult => { return html`
No assets added!
` } - #renderAssetEntry = (name: string, index: number) => { + #renderAssetEntry = (name: string, index: number): TemplateResult => { if (index === this._renameIndex) { return this.#renderRenameAssetEntry(name, index) } else if (index === this._deleteIndex) { @@ -176,7 +186,7 @@ export class PlayAssetsVirtualFilesystem extends LitElement { ` } - #renderRenameAssetEntry = (name: string, index: number) => { + #renderRenameAssetEntry = (name: string, index: number): TemplateResult => { return html`
{ + #renderDeleteAssetEntry = (name: string): TemplateResult => { return html`
Remove ${name}? @@ -230,16 +240,16 @@ export class PlayAssetsVirtualFilesystem extends LitElement { ` } - #renderClearAllButton = () => + #renderClearAllButton = (): TemplateResult => html` (this._clearAll = true)} >` - #renderClearAllPrompt = () => html` + #renderClearAllPrompt = (): TemplateResult => html` Remove all assets? async () => { + #delete = (name: string) => async (): Promise => { await this._assets?.deleteVirtualAsset(name) this._deleteIndex = -1 } - #clearAllAssets = async () => { + #clearAllAssets = async (): Promise => { await this._assets?.clearVirtualAssets() } - #onFiles = async (ev: CustomEvent) => { + #onFiles = async (ev: CustomEvent): Promise => { const files = ev.detail.files ?? ev.detail.fileHandles if (files) { for (let index = 0; index < files.length; index++) { @@ -288,27 +298,28 @@ export class PlayAssetsVirtualFilesystem extends LitElement { } } - #renameKeyDown = (ev: KeyboardEvent) => { + #renameKeyDown = (ev: KeyboardEvent): void => { if (ev.key === '/' || ev.key === '\\') { ev.preventDefault() ev.stopImmediatePropagation() } } - #renameKeyUp = (ev: KeyboardEvent) => { + #renameKeyUp = (ev: KeyboardEvent): void => { if (ev.key === 'Enter') { this.#rename(this._renameIndex)() } } - #assetsUpdated = () => { - this._assets?.assetMap.then(assets => { - if (assets) { - this._assetNames = Object.keys(assets) - } - }) + #assetsUpdated = async (): Promise => { + const assets = await this._assets.getAssetMap() + if (assets) { + this._assetNames = Object.keys(assets) + } this._renameIndex = -1 this._deleteIndex = -1 this._clearAll = false + + this.requestUpdate() } } diff --git a/src/elements/play-assets/play-assets.test.ts b/src/elements/play-assets/play-assets.test.ts deleted file mode 100644 index 4a42844..0000000 --- a/src/elements/play-assets/play-assets.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {assert} from '@esm-bundle/chai' -import {PlayAssets} from './play-assets' - -test('tag is defined', () => { - const el = document.createElement('play-assets') - assert.instanceOf(el, PlayAssets) -}) diff --git a/src/elements/play-assets/play-assets.ts b/src/elements/play-assets/play-assets.ts index e2403c3..72243eb 100644 --- a/src/elements/play-assets/play-assets.ts +++ b/src/elements/play-assets/play-assets.ts @@ -1,5 +1,3 @@ -import {type PropertyValues, ReactiveElement} from 'lit' -import {customElement, property} from 'lit/decorators.js' import {WebAccess, WebStorage} from '@zenfs/dom' import {Bubble} from '../../utils/bubble.js' import {Zip} from '@zenfs/zip' @@ -12,20 +10,19 @@ import * as ZenFS from '@zenfs/core' import {FileSystem, fs, InMemory} from '@zenfs/core' import type {AssetMap} from '@devvit/shared-types/Assets.js' import * as IDB from 'idb-keyval' +import {createContext} from '@lit/context' declare global { - interface HTMLElementEventMap {} - interface HTMLElementTagNameMap { - 'play-assets': PlayAssets + interface HTMLElementEventMap { + 'assets-set-filesystem': CustomEvent + 'assets-updated': CustomEvent } } -export type AssetFilesystemType = 'virtual' | 'local' - const CACHE_LAST_DIR = 'lastMountedDirectory' const CACHE_LAST_ZIP = 'lastMountedArchive' -const MIME: {[ext: string]: string} = { +const MIME: {readonly [ext: string]: string} = { html: 'text/html', htm: 'text/html', jpg: 'image/jpeg', @@ -35,145 +32,94 @@ const MIME: {[ext: string]: string} = { } const DEFAULT_MIME = 'application/octet-stream' -declare global { - interface HTMLElementTagNameMap { - 'play-assets': PlayAssets - } - interface HTMLElementEventMap { - 'assets-updated': CustomEvent - } -} - -@customElement('play-assets') -export class PlayAssets extends ReactiveElement { - @property({attribute: 'allow-storage', type: Boolean}) - allowStorage: boolean = false - - @property({attribute: 'filesystem-type', reflect: true, type: String}) - filesystemType: AssetFilesystemType = 'virtual' - - @property({attribute: false}) - directoryHandle: FileSystemDirectoryHandle | undefined - - @property({attribute: false}) - directoryName: string | undefined - - @property({attribute: false}) - archiveHandle: FileSystemFileHandle | File | undefined - - @property({attribute: false}) - archiveFilename: string | undefined +export type AssetFilesystemType = 'virtual' | 'local' - @property({attribute: false}) - assetCount: number = 0 +export const assetsContext = createContext('play-assets') - _rootAssets: PlayAssets | undefined +export class PlayAssets extends EventTarget { + #filesystemType: AssetFilesystemType = 'virtual' + #allowStorage: boolean = false #assetMap: AssetMap = {} + #assetCount: number = 0 + #archiveHandle: FileSystemFileHandle | File | undefined + #directoryHandle: FileSystemDirectoryHandle | undefined - private _waitForInit: Promise = new Promise( - resolve => (this._initComplete = resolve) + #waitForMount: Promise = new Promise( + resolve => (this._mountReady = resolve) ) - private _initComplete!: () => void + private _mountReady!: () => void static get hasFileAccessAPI(): boolean { return hasFileAccessAPI } - get assetMap(): Promise { - if (this._rootAssets) { - return this._rootAssets.assetMap + async getAssetMap(): Promise { + if (this.#directoryHandle) { + await this.#waitForMount + await this.#updateMap(false) } - const resolvedMap = () => this._waitForInit.then(() => this.#assetMap) - if (this.directoryHandle) { - return this.#updateMap(false).then(resolvedMap) + return this.#assetMap + } + + get filesystemType(): AssetFilesystemType { + return this.#filesystemType + } + + set allowStorage(value: boolean) { + const oldValue = this.#allowStorage + this.#allowStorage = value + if (oldValue !== value && this.filesystemType === 'virtual') { + void this.initialize(this.filesystemType) } - return resolvedMap() } - override connectedCallback() { - super.connectedCallback() + get allowStorage(): boolean { + return this.#allowStorage + } - // try and find a play-assets element higher up the DOM to attach to + get assetCount(): number { + return this.#assetCount + } - this._rootAssets = this.#findRootAssets() - this._rootAssets?.addEventListener('assets-updated', this.#syncRootAssets) - this.#syncRootAssets() + get isArchiveMounted(): boolean { + return !!this.#archiveHandle } - override disconnectedCallback() { - super.disconnectedCallback() + get archiveFilename(): string | undefined { + return this.#archiveHandle?.name + } - this._rootAssets?.removeEventListener( - 'assets-updated', - this.#syncRootAssets - ) + get isDirectoryMounted(): boolean { + return !!this.#directoryHandle } - protected override willUpdate(changedProperties: PropertyValues) { - if (changedProperties.has('filesystemType')) { - if (this._rootAssets) { - if ( - changedProperties.get('filesystemType') && - this._rootAssets.filesystemType !== this.filesystemType - ) { - this._rootAssets.filesystemType = this.filesystemType - } - } else { - void this.#initialize(this.filesystemType) - this.#emitAssetsUpdated() - } - } - if (changedProperties.has('archiveHandle')) { - if (this.archiveHandle) { - this.archiveFilename = this.archiveHandle.name - this.directoryHandle = undefined - this.directoryName = undefined - } else { - this.archiveFilename = undefined - } - } - if (changedProperties.has('directoryHandle')) { - if (this.directoryHandle) { - this.archiveHandle = undefined - this.archiveFilename = undefined - this.directoryName = this.directoryHandle.name - } else { - this.directoryName = undefined - } - } + get directoryName(): string | undefined { + return this.#directoryHandle?.name } //region Mount APIs - mountVirtualFs = async () => this.#callRootAssets(this.#mountVirtualFs) - - async #mountVirtualFs(): Promise { + async mountVirtualFs(): Promise { if (this.allowStorage) { await this.#mountRoot(WebStorage.create({})) - console.log('Assets will be persisted') } else { await this.#mountRoot(InMemory.create({})) - console.log('Assets will not be persisted') } this.#emitAssetsUpdated() } - mountLocalDirectory = async (directoryHandle: FileSystemDirectoryHandle) => - this.#callRootAssets(this.#mountLocalDirectory, directoryHandle) - - async #mountLocalDirectory( + async mountLocalDirectory( directoryHandle: FileSystemDirectoryHandle ): Promise { await this.#mountRoot(WebAccess.create({handle: directoryHandle})) await this.#updateCache({directory: directoryHandle}) - this.directoryHandle = directoryHandle + this.#directoryHandle = directoryHandle + this.#archiveHandle = undefined this.#emitAssetsUpdated() + this._mountReady() } - mountLocalArchive = async (fileHandle: FileSystemFileHandle | File) => - this.#callRootAssets(this.#mountLocalArchive, fileHandle) - - async #mountLocalArchive( + async mountLocalArchive( fileHandle: FileSystemFileHandle | File ): Promise { let file @@ -185,22 +131,19 @@ export class PlayAssets extends ReactiveElement { await this.#clearCache() } await this.#mountRoot(Zip.create({zipData: await file.arrayBuffer()})) - this.archiveHandle = fileHandle + this.#archiveHandle = fileHandle + this.#directoryHandle = undefined this.#emitAssetsUpdated() + this._mountReady() } - remountLocalArchive = async () => - this.#callRootAssets(this.#remountLocalArchive) - - async #remountLocalArchive(): Promise { - if (this.archiveHandle) { - await this.mountLocalArchive(this.archiveHandle) + async remountLocalArchive(): Promise { + if (this.#archiveHandle) { + await this.mountLocalArchive(this.#archiveHandle) } } - unmount = () => this.#callRootAssets(this.#unmount) - - async #unmount(): Promise { + async unmount(): Promise { this.#unmountRoot() await this.#updateMap() await this.#updateCache({}) @@ -209,10 +152,7 @@ export class PlayAssets extends ReactiveElement { //endregion //region Virtual FS - addVirtualAsset = async (handle: FileSystemFileHandle | File) => - this.#callRootAssets(this.#addVirtualAsset, handle) - - async #addVirtualAsset(handle: FileSystemFileHandle | File): Promise { + async addVirtualAsset(handle: FileSystemFileHandle | File): Promise { const file = await tryGetFile(handle) await fs.promises.writeFile( file.name, @@ -223,11 +163,8 @@ export class PlayAssets extends ReactiveElement { await this.#updateMap() } - renameVirtualAsset = async (oldName: string, newName: string) => - this.#callRootAssets(this.#renameVirtualAsset, oldName, newName) - - async #renameVirtualAsset(oldName: string, newName: string): Promise { - if (this.filesystemType !== 'virtual') { + async renameVirtualAsset(oldName: string, newName: string): Promise { + if (this.#filesystemType !== 'virtual') { return } @@ -236,11 +173,8 @@ export class PlayAssets extends ReactiveElement { await this.#updateMap() } - deleteVirtualAsset = async (name: string) => - this.#callRootAssets(this.#deleteVirtualAsset, name) - - async #deleteVirtualAsset(name: string): Promise { - if (this.filesystemType !== 'virtual') { + async deleteVirtualAsset(name: string): Promise { + if (this.#filesystemType !== 'virtual') { return } @@ -249,11 +183,8 @@ export class PlayAssets extends ReactiveElement { await this.#updateMap() } - clearVirtualAssets = async () => - this.#callRootAssets(this.#clearVirtualAssets) - - async #clearVirtualAssets(): Promise { - if (this.filesystemType !== 'virtual' || !this.#assetMap) { + async clearVirtualAssets(): Promise { + if (this.#filesystemType !== 'virtual' || !this.#assetMap) { return } @@ -267,34 +198,10 @@ export class PlayAssets extends ReactiveElement { } //endregion - #findRootAssets(): PlayAssets | undefined { - let currentElement = this.#traverseToParent(this as Element) - let root: PlayAssets | undefined - - while (currentElement) { - const assets = - currentElement.querySelector('play-assets') ?? - currentElement.shadowRoot?.querySelector('play-assets') - if (assets && assets !== this) { - root = assets as PlayAssets - } - currentElement = this.#traverseToParent(currentElement) - } - - return root - } - - #traverseToParent(el: Element): Element | undefined { - if (el.parentElement) { - return el.parentElement - } - if (el.parentNode && 'host' in el.parentNode) { - return (el.parentNode as ShadowRoot).host - } - } - - async #initialize(filesystemType: AssetFilesystemType): Promise { - this.filesystemType = filesystemType || 'virtual' + //region Internal FS management + async initialize(filesystemType: AssetFilesystemType): Promise { + this.#filesystemType = filesystemType || 'virtual' + this.#unmountRoot() if (filesystemType === 'virtual') { await this.mountVirtualFs() } else { @@ -314,25 +221,6 @@ export class PlayAssets extends ReactiveElement { } } } - this._initComplete() - } - - #callRootAssets R>( - method: T, - ...args: unknown[] - ): R { - return method.bind(this._rootAssets ?? this)(...args) - } - - #syncRootAssets = () => { - if (this._rootAssets) { - this.filesystemType = this._rootAssets.filesystemType - this.archiveHandle = this._rootAssets.archiveHandle - this.directoryHandle = this._rootAssets.directoryHandle - this.assetCount = this._rootAssets.assetCount - // relay the event to local listeners - this.#emitAssetsUpdated() - } } async #mountRoot(fs: FileSystem): Promise { @@ -344,15 +232,18 @@ export class PlayAssets extends ReactiveElement { #unmountRoot(): void { if (ZenFS.mounts.has('/')) { ZenFS.umount('/') - this.archiveHandle = undefined - this.directoryHandle = undefined + this.#archiveHandle = undefined + this.#directoryHandle = undefined + this.#assetMap = {} + this.#assetCount = 0 + this.#waitForMount = new Promise( + resolve => (this._mountReady = resolve) + ) } } + //endregion - #emitAssetsUpdated() { - this.dispatchEvent(Bubble('assets-updated', undefined)) - } - + //region Internal FileSystemHandle caching async #loadFromCache(): Promise< [FileSystemDirectoryHandle | undefined, FileSystemFileHandle | undefined] > { @@ -370,10 +261,16 @@ export class PlayAssets extends ReactiveElement { ]) } - async #clearCache() { + async #clearCache(): Promise { await IDB.delMany([CACHE_LAST_DIR, CACHE_LAST_ZIP]) } + async #verifyPermissions(handle: FileSystemHandle): Promise { + return (await tryQueryPermission(handle, {mode: 'read'})) === 'granted' + } + //endregion + + //region Internal AssetMap caching async #updateMap(emitEvent: boolean = true): Promise { this.#clearAssetMap() if (!ZenFS.mounts.has('/')) { @@ -403,7 +300,7 @@ export class PlayAssets extends ReactiveElement { this.#assetMap![name] = window.URL.createObjectURL( new Blob([buffer], {type: mimetype}) ) - this.assetCount++ + this.#assetCount++ } } if (emitEvent) { @@ -411,7 +308,7 @@ export class PlayAssets extends ReactiveElement { } } - #clearAssetMap() { + #clearAssetMap(): void { const assetMap = this.#assetMap! for (const entry of Object.keys(assetMap)) { const url = assetMap[entry] ?? undefined @@ -420,10 +317,11 @@ export class PlayAssets extends ReactiveElement { } } this.#assetMap = {} - this.assetCount = 0 + this.#assetCount = 0 } + //endregion - async #verifyPermissions(handle: FileSystemHandle): Promise { - return (await tryQueryPermission(handle, {mode: 'read'})) === 'granted' + #emitAssetsUpdated(): void { + this.dispatchEvent(Bubble('assets-updated', undefined)) } } diff --git a/src/elements/play-dialog/play-dialog.ts b/src/elements/play-dialog/play-dialog.ts index 482ea32..ccc13c0 100644 --- a/src/elements/play-dialog/play-dialog.ts +++ b/src/elements/play-dialog/play-dialog.ts @@ -82,16 +82,12 @@ export class PlayDialog extends LitElement implements PlayDialogLike { line-height: 28px; letter-spacing: 0.2px; } - - legend { - font-weight: bold; - } ` - @property({reflect: true}) - override title: string = '' + @property({attribute: 'dialog-title', type: String, reflect: true}) + dialogTitle: string = '' - @property({reflect: true}) + @property({attribute: 'description', type: String, reflect: true}) description: string = '' @query('dialog') @@ -109,7 +105,7 @@ export class PlayDialog extends LitElement implements PlayDialogLike { return html`
-

${this.title}

+

${this.dialogTitle}

    diff --git a/src/elements/play-pen/play-pen.ts b/src/elements/play-pen/play-pen.ts index 3e643c9..13f3c08 100644 --- a/src/elements/play-pen/play-pen.ts +++ b/src/elements/play-pen/play-pen.ts @@ -41,12 +41,13 @@ import type {OpenLine} from '../play-console.js' import type {PlayEditor} from '../play-editor/play-editor.js' import type {PlayToast} from '../play-toast.js' import penVars from './pen-vars.css' -import type { - AssetFilesystemType, +import { + type AssetFilesystemType, + assetsContext, PlayAssets } from '../play-assets/play-assets.js' +import {provide} from '@lit/context' -import '../play-assets/play-assets.js' import '../play-editor/play-editor.js' import '../play-pen-footer.js' import '../play-pen-header.js' @@ -163,7 +164,8 @@ export class PlayPen extends LitElement { #bundleStore?: BundleStore | undefined readonly #env: VirtualTypeScriptEnvironment = newTSEnv() @state() _uploaded: Promise = Promise.resolve({}) - @query('play-assets') private _assets!: PlayAssets + @provide({context: assetsContext}) private _assets: PlayAssets = + new PlayAssets() /** Try to ensure the bundle hostname is unique. See compute-util. */ #version: number = Date.now() @@ -198,6 +200,9 @@ export class PlayPen extends LitElement { // bundle is loaded. } + this._assets.addEventListener('assets-updated', this.#assetsUpdated) + void this._assets.initialize(this._assetFilesystem) + let pen if (this.allowURL) pen = loadPen(location) if (this.allowStorage) pen ??= loadPen(localStorage) @@ -211,16 +216,8 @@ export class PlayPen extends LitElement { this.#setName(pen.name, false) } - protected override firstUpdated(_changedProperties: PropertyValues) { - this._assets.filesystemType = this._assetFilesystem - } - protected override render(): TemplateResult { return html` - Copied the URL!) => { this._enableLocalAssets = ev.detail - if (!this._enableLocalAssets) { - this._assets.filesystemType = 'virtual' + if ( + !this._enableLocalAssets && + this._assets.filesystemType !== 'virtual' + ) { + void this._assets.initialize('virtual') } }} + @assets-set-filesystem=${(ev: CustomEvent) => + void this._assets.initialize(ev.detail)} @share=${this.#onShare} >
    @@ -373,8 +375,7 @@ export class PlayPen extends LitElement { if (props.has('_useRemoteRuntime') || props.has('_remoteRuntimeOrigin')) this.#upload() - if (props.has('_assetFilesystem') && this._assets) - this._assets.filesystemType = this._assetFilesystem + if (props.has('allowStorage')) this._assets.allowStorage = this.allowStorage } #appendPreviewError(err: DevvitUIError): void { @@ -427,7 +428,7 @@ export class PlayPen extends LitElement { this._bundle = link( compile(this.#env), newHostname(this._name, this.#version), - await this._assets.assetMap + await this._assets.getAssetMap() ) if (save) this.#save() this.#upload() diff --git a/src/elements/play-settings-dialog.ts b/src/elements/play-settings-dialog.ts index 8f1cf5d..e70452d 100644 --- a/src/elements/play-settings-dialog.ts +++ b/src/elements/play-settings-dialog.ts @@ -9,6 +9,7 @@ import {customElement, property, query} from 'lit/decorators.js' import {defaultSettings} from '../storage/settings-save.js' import {Bubble} from '../utils/bubble.js' import {PlayDialog, type PlayDialogLike} from './play-dialog/play-dialog.js' +import {cssReset} from '../utils/css-reset.js' import './play-button.js' import './play-dialog/play-dialog.js' @@ -32,7 +33,11 @@ declare global { @customElement('play-settings-dialog') export class PlaySettingsDialog extends LitElement implements PlayDialogLike { static override readonly styles: CSSResultGroup = css` - ${PlayDialog.styles} + ${cssReset} + + legend { + font-weight: bold; + } input[type='checkbox'] { float: left; @@ -84,7 +89,7 @@ export class PlaySettingsDialog extends LitElement implements PlayDialogLike { protected override render(): TemplateResult { const description = `Settings are ${this.allowStorage ? 'saved and ' : ''}not shareable.` return html` - +
    Reddit Internal

    Runtime settings take effect on subsequent execution.