Create the most basic version of the ArtistBrowser app
This commit is contained in:
parent
1c32a71db9
commit
d5a114a44a
9 changed files with 348 additions and 1 deletions
|
|
@ -7,6 +7,14 @@
|
|||
"ArtBrowser": {
|
||||
"selected": "{current}/{required} Selected"
|
||||
},
|
||||
"ArtistBrowser": {
|
||||
"sort-options": {
|
||||
"name-asc": "Name (A -> Z)",
|
||||
"name-desc": "Name (Z -> A)",
|
||||
"image-count-asc": "Image Count (0 -> 9)",
|
||||
"image-count-desc": "Image Count (9 -> 0)"
|
||||
}
|
||||
},
|
||||
"ArtistApp": {
|
||||
"title": {
|
||||
"create": "Create New Artist",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Applications
|
||||
import { ArtBrowser } from "./apps/ArtBrowser.mjs";
|
||||
import { ArtistApp } from "./apps/ArtistApp.mjs";
|
||||
import { ArtistBrowser } from "./apps/ArtistBrowser.mjs";
|
||||
import { ImageApp } from "./apps/ImageApp.mjs";
|
||||
|
||||
// Utils
|
||||
|
|
@ -11,6 +12,7 @@ const { deepFreeze } = foundry.utils;
|
|||
export const api = deepFreeze({
|
||||
Apps: {
|
||||
ArtBrowser,
|
||||
ArtistBrowser,
|
||||
ArtistApp,
|
||||
ImageApp,
|
||||
},
|
||||
|
|
|
|||
235
module/apps/ArtistBrowser.mjs
Normal file
235
module/apps/ArtistBrowser.mjs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import { __ID__, filePath } from "../consts.mjs";
|
||||
import { getFile, lastModifiedAt } from "../utils/fs.mjs";
|
||||
import { paginate } from "../utils/pagination.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
const { FormDataExtended } = foundry.applications.ux;
|
||||
const { deepClone } = foundry.utils;
|
||||
|
||||
const PAGE_SIZE = 8;
|
||||
|
||||
export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
// #region Options
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: [
|
||||
__ID__,
|
||||
`ArtistBrowser`,
|
||||
`data-browser`,
|
||||
],
|
||||
position: {},
|
||||
window: {},
|
||||
actions: {
|
||||
nextPage: this.#nextPage,
|
||||
prevPage: this.#prevPage,
|
||||
},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
sidebar: {
|
||||
template: filePath(`templates/ArtistBrowser/sidebar.hbs`),
|
||||
},
|
||||
list: {
|
||||
template: filePath(`templates/ArtistBrowser/list.hbs`),
|
||||
templates: [
|
||||
filePath(`templates/ArtistBrowser/artist.hbs`),
|
||||
],
|
||||
},
|
||||
};
|
||||
// #endregion Options
|
||||
|
||||
// #region Instance Data
|
||||
#page = 1;
|
||||
filters = {
|
||||
name: ``,
|
||||
sortBy: `name-asc`,
|
||||
};
|
||||
|
||||
constructor({ selectCount = 0, onSubmit = null, ...opts } = {}) {
|
||||
super(opts);
|
||||
};
|
||||
|
||||
get page() {
|
||||
return this.#page;
|
||||
};
|
||||
|
||||
async setPage(page) {
|
||||
if (this.#page == page) { return };
|
||||
this.#page = page;
|
||||
if (this.rendered) {
|
||||
await this.render({ parts: [`images`] });
|
||||
return;
|
||||
};
|
||||
return;
|
||||
};
|
||||
// #endregion Instance Data
|
||||
|
||||
// #region Lifecycle
|
||||
#imagesDB = {
|
||||
lastModified: null,
|
||||
data: undefined,
|
||||
};
|
||||
async #getImages() {
|
||||
const newLastModified = await lastModifiedAt(`storage/db/images.json`);
|
||||
if (this.#imagesDB.lastModified && newLastModified === this.#imagesDB.lastModified) {
|
||||
return;
|
||||
};
|
||||
this.#imagesDB.lastModified = newLastModified;
|
||||
this.#imagesDB.data = await getFile(`storage/db/images.json`);
|
||||
};
|
||||
|
||||
#artistDB = {
|
||||
lastModified: null,
|
||||
data: undefined,
|
||||
};
|
||||
async #getArtists() {
|
||||
const newLastModified = await lastModifiedAt(`storage/db/artists.json`);
|
||||
if (this.#artistDB.lastModified && newLastModified === this.#artistDB.lastModified) {
|
||||
return;
|
||||
};
|
||||
this.#artistDB.lastModified = newLastModified;
|
||||
this.#artistDB.data = await getFile(`storage/db/artists.json`);
|
||||
};
|
||||
|
||||
async render(...args) {
|
||||
await this.#getArtists();
|
||||
await this.#getImages();
|
||||
return super.render(...args);
|
||||
};
|
||||
|
||||
async _onRender(ctx, options) {
|
||||
if (options.parts?.includes(`sidebar`)) {
|
||||
this.element.querySelector(`form.filters`)?.addEventListener(
|
||||
`change`,
|
||||
this.#onFilterSubmit.bind(this),
|
||||
);
|
||||
};
|
||||
|
||||
if (options.parts?.includes(`images`)) {
|
||||
this._updateSelectedCount();
|
||||
};
|
||||
};
|
||||
// #endregion Lifecycle
|
||||
|
||||
// #region Data Prep
|
||||
_prepareContext() {
|
||||
return {
|
||||
meta: {
|
||||
idp: this.id,
|
||||
},
|
||||
can: {
|
||||
upload: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_preparePartContext(partID, ctx) {
|
||||
switch (partID) {
|
||||
case `sidebar`: {
|
||||
this._prepareSidebarContext(ctx);
|
||||
break;
|
||||
};
|
||||
case `list`: {
|
||||
this._prepareListContext(ctx);
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
_prepareSidebarContext(ctx) {
|
||||
ctx.name = this.filters.name;
|
||||
ctx.sortBy = this.filters.sortBy;
|
||||
ctx.sortOptions = [
|
||||
{ value: `name-asc`, label: `IT.apps.ArtistBrowser.sort-options.name-asc` },
|
||||
{ value: `name-desc`, label: `IT.apps.ArtistBrowser.sort-options.name-desc` },
|
||||
{ value: `image-count-asc`, label: `IT.apps.ArtistBrowser.sort-options.image-count-asc` },
|
||||
{ value: `image-count-desc`, label: `IT.apps.ArtistBrowser.sort-options.image-count-desc` },
|
||||
];
|
||||
};
|
||||
|
||||
_prepareListContext(ctx) {
|
||||
const allArtists = Object.entries(deepClone(this.#artistDB.data));
|
||||
const allImages = Object.values(this.#imagesDB.data);
|
||||
|
||||
const artists = [];
|
||||
for (const [id, artist] of allArtists) {
|
||||
// Check if it matches the required filters
|
||||
if (this.filters.name && !artist.name.includes(this.filters.name)) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Populate ephemeral data for rendering
|
||||
artist.id = id;
|
||||
|
||||
let imageCount = 0;
|
||||
let tags = {};
|
||||
|
||||
for (const image of allImages) {
|
||||
if (!image.artists.includes(id)) continue;
|
||||
imageCount++;
|
||||
for (const tag of image.tags) {
|
||||
tags[tag] ??= { name: tag, count: 0 };
|
||||
tags[tag].count++;
|
||||
};
|
||||
};
|
||||
|
||||
artist.imageCount = imageCount;
|
||||
artist.commonTags = Object.values(tags)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 5);
|
||||
|
||||
artists.push(artist);
|
||||
};
|
||||
|
||||
// Paginate after filtering and sorting to give page continuity
|
||||
artists.sort(compareArtists.bind(undefined, this.filters.sortBy));
|
||||
const paginated = paginate(artists, this.#page, PAGE_SIZE);
|
||||
ctx.artists = paginated.page;
|
||||
ctx.page = this.#page;
|
||||
ctx.pages = paginated.total;
|
||||
ctx.has = {
|
||||
prev: paginated.prev,
|
||||
next: paginated.next
|
||||
};
|
||||
};
|
||||
// #endregion Data Prep
|
||||
|
||||
// #region Actions
|
||||
async #onFilterSubmit(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const data = (new FormDataExtended(event.currentTarget)).object;
|
||||
this.filters = data;
|
||||
this.render({ parts: [`list`] });
|
||||
};
|
||||
|
||||
/** @this {ArtistBrowser} */
|
||||
static async #nextPage() {
|
||||
this.setPage(this.#page + 1);
|
||||
};
|
||||
|
||||
/** @this {ArtistBrowser} */
|
||||
static async #prevPage() {
|
||||
this.setPage(this.#page - 1);
|
||||
};
|
||||
// #endregion Actions
|
||||
};
|
||||
|
||||
function compareArtists(mode, a, b) {
|
||||
switch (mode) {
|
||||
case `name-asc`: {
|
||||
return a.name.localeCompare(b.name);
|
||||
};
|
||||
case `name-desc`: {
|
||||
return b.name.localeCompare(a.name);
|
||||
};
|
||||
case `image-count-asc`: {
|
||||
return a.imageCount - b.imageCount;
|
||||
};
|
||||
case `image-count-desc`: {
|
||||
return b.imageCount - a.imageCount;
|
||||
};
|
||||
};
|
||||
return 0;
|
||||
};
|
||||
21
styles/apps/ArtistBrowser.css
Normal file
21
styles/apps/ArtistBrowser.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.image-tagger.ArtistBrowser {
|
||||
.artist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: var(--color-cool-4);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
font-family: "Signika", "Palatino Linotype", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
@import url("./apps/common.css") layer(apps);
|
||||
@import url("./apps/data-browser.css") layer(apps);
|
||||
@import url("./apps/ArtBrowser.css") layer(apps);
|
||||
@import url("./apps/ArtistBrowser.css") layer(apps);
|
||||
@import url("./apps/ArtistApp.css") layer(apps);
|
||||
@import url("./apps/ArtSidebar.css") layer(apps);
|
||||
@import url("./apps/ImageApp.css") layer(apps);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ArtistList"
|
||||
data-app="ArtistBrowser"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
|
|
|
|||
28
templates/ArtistBrowser/artist.hbs
Normal file
28
templates/ArtistBrowser/artist.hbs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<div class="entry artist">
|
||||
<div class="row">
|
||||
<h2>{{artist.name}}</h2>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
{{artist.imageCount}} Images
|
||||
</div>
|
||||
</div>
|
||||
{{#if artist.links}}
|
||||
<ul class="chip-list">
|
||||
{{#each artist.links as |link|}}
|
||||
<li class="chip">
|
||||
<a href="{{link.url}}">{{link.name}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{#if artist.commonTags}}
|
||||
<section>
|
||||
<h3>Common Image Tags</h3>
|
||||
<ul class="chip-list">
|
||||
{{#each artist.commonTags as |tag|}}
|
||||
<li class="chip">{{tag.name}} ({{tag.count}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</section>
|
||||
{{/if}}
|
||||
</div>
|
||||
34
templates/ArtistBrowser/list.hbs
Normal file
34
templates/ArtistBrowser/list.hbs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<div class="paginated">
|
||||
<div class="row">
|
||||
{{#if can.upload}}
|
||||
<button data-action="createArtist">Create New Artist</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if artists}}
|
||||
<ul class="entry-list list">
|
||||
{{#each artists as | artist |}}
|
||||
{{> (it-filePath "templates/ArtistBrowser/artist.hbs") artist=artist }}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<span class="placeholder">
|
||||
{{ localize "" }}
|
||||
</span>
|
||||
{{/if}}
|
||||
<div class="grow"></div>
|
||||
<div class="row page-nav">
|
||||
<button
|
||||
data-action="prevPage"
|
||||
{{disabled (not has.prev)}}
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
{{page}} / {{pages}}
|
||||
<button
|
||||
data-action="nextPage"
|
||||
{{disabled (not has.next)}}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
18
templates/ArtistBrowser/sidebar.hbs
Normal file
18
templates/ArtistBrowser/sidebar.hbs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<label for="{{meta.idp}}-name">Name</label>
|
||||
<input
|
||||
id="{{meta.idp}}-name"
|
||||
type="text"
|
||||
name="name"
|
||||
value="{{name}}"
|
||||
>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-sort">
|
||||
Sort By
|
||||
</label>
|
||||
<select name="sortBy" id="{{meta.idp}}-sort">
|
||||
{{it-options sortBy sortOptions localize=true}}
|
||||
</select>
|
||||
</form>
|
||||
Loading…
Add table
Add a link
Reference in a new issue