Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
35 changed files with 175 additions and 636 deletions
|
|
@ -28,11 +28,8 @@ jobs:
|
|||
DOWNLOAD_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/v${{steps.version.outputs.version}}/release.zip"
|
||||
LATEST_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/latest/${{env.MANIFEST}}"
|
||||
|
||||
- name: Set up default storage
|
||||
run: mkdir -p storage/db && mv importable/artists.json storage/db && mv importable/images.json storage/db
|
||||
|
||||
- name: Compress files
|
||||
run: zip -r release.zip langs module styles storage templates README.md assets LICENSE ${{env.MANIFEST}}
|
||||
run: zip -r release.zip langs module styles templates README.md assets LICENSE ${{env.MANIFEST}}
|
||||
|
||||
- name: Create forgejo release
|
||||
run: node scripts/src/createForgejoRelease.mjs
|
||||
|
|
|
|||
27
README.md
27
README.md
|
|
@ -1,27 +1,2 @@
|
|||
# Image Tagger
|
||||
A Foundry VTT module to help you find images that fit what you're wanting faster!
|
||||
# tokens
|
||||
|
||||
## How It Works
|
||||
When you upload a new image using the module, you can give it:
|
||||
- a name
|
||||
- any number of tags
|
||||
- any number of artists
|
||||
|
||||
Then, when you are looking for an image for something, you can use the integrated
|
||||
Art Browser in order to search based on name, tags, and/or artists!
|
||||
|
||||
## Image Optimization
|
||||
Included in the module is an automatic conversion of any static images (png, jpeg,
|
||||
etc.) into an optimized webp format! If you upload a webp or gif image, they will
|
||||
not be converted at all, they will stay how you have them.
|
||||
|
||||
This optimization is done so that the storage on your server or computer is reduced,
|
||||
as well as making it so all of the images load faster on your player's computers.
|
||||
|
||||
# Disclaimer:
|
||||
This module is currently in beta testing, as such there may be bugs and missing
|
||||
features. To report bugs or request features you can:
|
||||
- open an issue on the
|
||||
[primary issue tracker](https://git.varify.ca/Foundry/image-tagger/issues)
|
||||
- send an email to [hello@varify.ca](mailto:hello@varify.ca)
|
||||
- or, open an issue on [Github](https://github.com/Eldritch-Oliver/Foundry-Image-Tagger/issues/new)
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import { __ID__ } from "../module/consts.mjs";
|
||||
|
||||
Hooks.on(`ready`, () => {
|
||||
globalThis.IT = game.modules.get(__ID__).api;
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"FVTT": {
|
||||
"name": "Foundry Defaults",
|
||||
"links": [
|
||||
{ "name": "FoundryVTT", "url": "https://foundryvtt.com" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
{
|
||||
"min_tag_frequency": 3,
|
||||
"ignorePatterns": [
|
||||
"**/svg/**",
|
||||
"**/dice/**"
|
||||
],
|
||||
"skipFiles": [
|
||||
"vtt.png",
|
||||
"vtt-512.png",
|
||||
"anvil.png",
|
||||
"fvtt.icns",
|
||||
"fvtt.ico",
|
||||
"logo-scifi.png",
|
||||
"logo-scifi-blank.png",
|
||||
"LICENSE"
|
||||
],
|
||||
"replacements": {
|
||||
"abilities": "ability",
|
||||
"antlers": "antler",
|
||||
"arrows": "arrow",
|
||||
"barrels": "barrel",
|
||||
"beams": "beam",
|
||||
"birds": "bird",
|
||||
"bolts": "bolt",
|
||||
"books": "book",
|
||||
"bows": "bow",
|
||||
"boxes": "box",
|
||||
"bubbles": "bubble",
|
||||
"bullets": "bullet",
|
||||
"cards": "card",
|
||||
"chains": "chain",
|
||||
"claws": "claw",
|
||||
"clouds": "cloud",
|
||||
"clubs": "club",
|
||||
"coins": "coin",
|
||||
"documents": "document",
|
||||
"eggs": "egg",
|
||||
"eyes": "eye",
|
||||
"fangs": "fang",
|
||||
"flags": "flag",
|
||||
"flowers": "flower",
|
||||
"grains": "grain",
|
||||
"guns": "gun",
|
||||
"hammers": "hammer",
|
||||
"hands": "hand",
|
||||
"herbs": "herb",
|
||||
"horns": "horn",
|
||||
"lights": "light",
|
||||
"maces": "mace",
|
||||
"magical": "magic",
|
||||
"mushrooms": "mushroom",
|
||||
"orbs": "orb",
|
||||
"projectiles": "projectile",
|
||||
"ribs": "rib",
|
||||
"roots": "root",
|
||||
"ropes": "rope",
|
||||
"runes": "rune",
|
||||
"scales": "scale",
|
||||
"sickles": "sickle",
|
||||
"slimes": "slime",
|
||||
"spirits": "spirit",
|
||||
"stems": "stem",
|
||||
"stick": "stick",
|
||||
"symbols": "symbol",
|
||||
"tentacles": "tentacle",
|
||||
"tips": "tip",
|
||||
"traps": "trap",
|
||||
"vines": "vine",
|
||||
"waves": "wave",
|
||||
"webs": "web",
|
||||
"weapons": "weapon",
|
||||
"wands": "wand",
|
||||
"swords": "sword",
|
||||
"cutlasses": "cutlass",
|
||||
"tools": "tool",
|
||||
"containers": "container",
|
||||
"skills": "skill",
|
||||
"creatures": "creature",
|
||||
"sundries": "sundry",
|
||||
"consumables": "consumable",
|
||||
"gems": "gem",
|
||||
"daggers": "dagger",
|
||||
"commodities": "commodity",
|
||||
"feathers": "feather",
|
||||
"dynamite": "explosive",
|
||||
"bomb": "explosive",
|
||||
"bones": "bone",
|
||||
"potions": "potion",
|
||||
"bags": "bag",
|
||||
"axes": "axe",
|
||||
"scrolls": "scroll",
|
||||
"glow": "glowing",
|
||||
"materials": "material",
|
||||
"plants": "plant",
|
||||
"polearms": "polearm",
|
||||
"staves": "stave",
|
||||
"academics": "academic",
|
||||
"aliens": "alien",
|
||||
"amphibians": "amphibian",
|
||||
"wings": "wing"
|
||||
},
|
||||
"removals": [
|
||||
"",
|
||||
"white",
|
||||
"yellow",
|
||||
"blue",
|
||||
"red",
|
||||
"green",
|
||||
"black",
|
||||
"brown",
|
||||
"teal",
|
||||
"silver",
|
||||
"purple",
|
||||
"gray",
|
||||
"grey",
|
||||
"orange",
|
||||
"lime",
|
||||
"crimson",
|
||||
"magenta",
|
||||
"gold",
|
||||
"cyan",
|
||||
"pink",
|
||||
"tan",
|
||||
"violet",
|
||||
"beige",
|
||||
"simple",
|
||||
"ringed",
|
||||
"hollow",
|
||||
"guard",
|
||||
"pressure",
|
||||
"gas",
|
||||
"fuse",
|
||||
"cloth",
|
||||
"sharp",
|
||||
"worn",
|
||||
"svg",
|
||||
"rough",
|
||||
"control",
|
||||
"3d",
|
||||
"admission",
|
||||
"alient",
|
||||
"alpha",
|
||||
"and",
|
||||
"token",
|
||||
"assorted",
|
||||
"ember",
|
||||
"misc",
|
||||
"mix"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import { globSync } from "glob";
|
||||
import crypto from "crypto";
|
||||
import { createReadStream } from "fs";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
const config = JSON.parse(await readFile(`importable/config.json`, `utf8`));
|
||||
import { writeFile } from "fs/promises";
|
||||
|
||||
async function hashFile(path) {
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -20,64 +18,82 @@ async function hashFile(path) {
|
|||
});
|
||||
};
|
||||
|
||||
const skipFiles = [
|
||||
`vtt.png`,
|
||||
`vtt-512.png`,
|
||||
`anvil.png`,
|
||||
`fvtt.icns`,
|
||||
`fvtt.ico`,
|
||||
`logo-scifi.png`,
|
||||
`logo-scifi-blank.png`,
|
||||
`LICENSE`,
|
||||
];
|
||||
|
||||
const tagRenames = {
|
||||
weapons: `weapon`,
|
||||
wands: `wand`,
|
||||
swords: `sword`,
|
||||
cutlasses: `cutlass`,
|
||||
daggers: `dagger`,
|
||||
commodities: `commodity`,
|
||||
feathers: `feather`,
|
||||
dynamite: `explosive`,
|
||||
bomb: `explosive`,
|
||||
}
|
||||
|
||||
const purgeTags = new Set([
|
||||
// All of the colours
|
||||
`white`, `yellow`, `blue`, `red`, `green`, `black`, `brown`, `teal`, `silver`, `purple`, `gray`, `grey`, `orange`, `lime`, `crimson`, `magenta`, `gold`, `cyan`, `pink`, `tan`, `violet`,
|
||||
|
||||
// Terms that just suck as tags
|
||||
`simple`, `ringed`, `hollow`, `guard`, `pressure`, `gas`, `fuse`,
|
||||
`cloth`, `sharp`, `worn`,
|
||||
]);
|
||||
|
||||
async function main() {
|
||||
const imageList = globSync(
|
||||
`foundry/public/icons/**/*`,
|
||||
{
|
||||
nodir: true,
|
||||
ignore: config.ignorePatterns,
|
||||
});
|
||||
const imageList = globSync(`foundry/public/icons/**/*`, { nodir: true });
|
||||
const images = {};
|
||||
const allTags = {};
|
||||
|
||||
for (const path of imageList) {
|
||||
if (config.skipFiles.some(file => path.endsWith(file))) continue;
|
||||
if (skipFiles.some(file => path.endsWith(file))) continue;
|
||||
|
||||
const hash = await hashFile(path);
|
||||
const filename = path.split(`/`).at(-1);
|
||||
|
||||
const tags = Array.from(new Set(
|
||||
const tags = new Set(
|
||||
path
|
||||
.replace(`foundry/public/icons/`, ``)
|
||||
.replace(/\.[a-z]+$/i, ``)
|
||||
.replaceAll(`-`, `/`)
|
||||
.split(`/`)
|
||||
.map(tag => config.replacements[tag] ?? tag)
|
||||
.filter(tag => !config.removals.includes(tag))
|
||||
));
|
||||
|
||||
tags.forEach(t => {
|
||||
allTags[t] ??= 0;
|
||||
allTags[t]++;
|
||||
});
|
||||
.map(tag => tagRenames[tag] ?? tag)
|
||||
.filter(tag => !purgeTags.has(tag))
|
||||
);
|
||||
|
||||
images[hash] = {
|
||||
name: filename,
|
||||
tags,
|
||||
tags: Array.from(tags),
|
||||
artists: [`FVTT`],
|
||||
path: path.replace(`foundry/public/`, ``),
|
||||
external: true
|
||||
};
|
||||
};
|
||||
|
||||
const sortedTags = Object.entries(allTags)
|
||||
.sort((a,b) => a[0].localeCompare(b[0]))
|
||||
|
||||
// Remove all of the tags that have too low of frequency
|
||||
const removeTags = new Set();
|
||||
for (const [tag, count] of sortedTags) {
|
||||
if (count <= config.min_tag_frequency) {
|
||||
removeTags.add(tag);
|
||||
};
|
||||
const data = {
|
||||
images,
|
||||
artists: {
|
||||
FVTT: { // Using a custom artist ID since this is an import anyway
|
||||
name: `Foundry Defaults`,
|
||||
links: [
|
||||
{ name: `FoundryVTT`, url: `https://foundryvtt.com` }
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
for (const image of Object.values(images)) {
|
||||
image.tags = image.tags.filter(t => !removeTags.has(t));
|
||||
};
|
||||
console.log(`Removed ${removeTags.size} tags from images due to frequency requirements.`);
|
||||
|
||||
await writeFile(
|
||||
`importable/images.json`,
|
||||
JSON.stringify(images),
|
||||
`importable/foundry.json`,
|
||||
JSON.stringify(data),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
1
importable/v13.351.json
Normal file
1
importable/v13.351.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,26 +1,11 @@
|
|||
{
|
||||
"IT": {
|
||||
"common": {
|
||||
"artists": "Artists",
|
||||
"images": "Images",
|
||||
"unsaved": "This change hasn't been saved, if you close without saving it will be undone.",
|
||||
"page": {
|
||||
"previous": "Prev",
|
||||
"next": "Next"
|
||||
},
|
||||
"name": "Name",
|
||||
"tags": "Tags",
|
||||
"select": "Select",
|
||||
"links": "Links",
|
||||
"sort-by": "Sort By",
|
||||
"page-reset-warning":"Changing any of these will reset your page to 1."
|
||||
"unsaved": "This change hasn't been saved, if you close without saving it will be undone."
|
||||
},
|
||||
"apps": {
|
||||
"ArtBrowser": {
|
||||
"selected": "{current}/{required} Selected",
|
||||
"upload-image": "Upload Image",
|
||||
"no-results": "No results were found for that search",
|
||||
"select-image": "Select image"
|
||||
"selected": "{current}/{required} Selected"
|
||||
},
|
||||
"ArtistBrowser": {
|
||||
"sort-options": {
|
||||
|
|
@ -28,10 +13,7 @@
|
|||
"name-desc": "Name (Z -> A)",
|
||||
"image-count-asc": "Image Count (0 -> 9)",
|
||||
"image-count-desc": "Image Count (9 -> 0)"
|
||||
},
|
||||
"image-count": "{count} Images",
|
||||
"common-tags": "Common Image Tags",
|
||||
"create-artist": "Create New Artist"
|
||||
}
|
||||
},
|
||||
"ArtistApp": {
|
||||
"title": {
|
||||
|
|
@ -40,22 +22,12 @@
|
|||
},
|
||||
"add-link": "Add New Link"
|
||||
},
|
||||
"ArtSidebar": {
|
||||
"view": "View All",
|
||||
"add-new": "Add New",
|
||||
"db-size": "Database Size: {size}"
|
||||
},
|
||||
"ImageApp": {
|
||||
"title": {
|
||||
"upload": "Upload New Image",
|
||||
"register": "Register External Image",
|
||||
"edit-named": "Editing Image: {name}",
|
||||
"edit-generic": "Editing Image"
|
||||
},
|
||||
"toggle-upload-mode": "Toggle Upload Mode",
|
||||
"image-label": "Image",
|
||||
"clear": "Clear",
|
||||
"preview-placeholder": "Select an image to see a preview"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
|
@ -75,8 +47,7 @@
|
|||
"error": {
|
||||
"db-out-of-date": "Database out of date, please try again.",
|
||||
"document-ID-404": "Cannot find {dbType} with ID: {id}",
|
||||
"no-upload-permission": "Cannot save due to missing the \"Upload Files\" permission.",
|
||||
"cant-find-image": "Cannot find image at location: {url}"
|
||||
"no-upload-permission": "Cannot save due to missing the \"Upload Files\" permission."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,17 @@
|
|||
"id": "image-tagger",
|
||||
"title": "Image Tagger",
|
||||
"description": "A module that helps you tag tokens to find what you're looking for faster!",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"compatibility": {
|
||||
"minimum": 13,
|
||||
"verified": 13,
|
||||
"maximum": 13
|
||||
},
|
||||
"authors": [
|
||||
{ "name": "Oliver", "email": "hello@varify.ca" }
|
||||
],
|
||||
"url": "https://git.varify.ca/Foundry/image-tagger",
|
||||
"bugs": "https://git.varify.ca/Foundry/image-tagger/issues",
|
||||
"manifest": "",
|
||||
"download": "",
|
||||
"esmodules": [
|
||||
"module/image-tagger.mjs",
|
||||
"dev/main.mjs"
|
||||
"module/image-tagger.mjs"
|
||||
],
|
||||
"styles": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { ArtistBrowser } from "./apps/ArtistBrowser.mjs";
|
|||
import { ImageApp } from "./apps/ImageApp.mjs";
|
||||
|
||||
// Utils
|
||||
import { convertToWebp, getFileSize, hashFile, lastModifiedAt } from "./utils/fs.mjs";
|
||||
import { getFile, hashFile, lastModifiedAt, uploadJson } from "./utils/fs.mjs";
|
||||
|
||||
export const api = foundry.utils.deepFreeze({
|
||||
const { deepFreeze } = foundry.utils;
|
||||
|
||||
export const api = deepFreeze({
|
||||
Apps: {
|
||||
ArtBrowser,
|
||||
ArtistBrowser,
|
||||
|
|
@ -16,10 +18,10 @@ export const api = foundry.utils.deepFreeze({
|
|||
},
|
||||
utils: {
|
||||
fs: {
|
||||
convertToWebp,
|
||||
hashFile,
|
||||
lastModifiedAt,
|
||||
getFileSize,
|
||||
getFile,
|
||||
uploadJson,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
single: this.selectMode === `single`,
|
||||
},
|
||||
can: {
|
||||
upload: game.user.can(`FILES_UPLOAD`),
|
||||
upload: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -272,7 +272,6 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
event.stopPropagation();
|
||||
const data = (new FormDataExtended(event.currentTarget)).object;
|
||||
this.filters = data;
|
||||
this.#page = 1;
|
||||
this.render({ parts: [`images`] });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { api } from "../api.mjs";
|
||||
import { __ID__, filePath } from "../consts.mjs";
|
||||
import { getFileSize } from "../utils/fs.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
const { AbstractSidebarTab } = foundry.applications.sidebar;
|
||||
const { deepClone } = foundry.utils;
|
||||
|
||||
export class ArtSidebar extends HandlebarsApplicationMixin(AbstractSidebarTab) {
|
||||
// #region Options
|
||||
|
|
@ -32,43 +30,16 @@ export class ArtSidebar extends HandlebarsApplicationMixin(AbstractSidebarTab) {
|
|||
// #endregion Options
|
||||
|
||||
// #region Data Prep
|
||||
_prepareContext() {
|
||||
return {
|
||||
meta: {
|
||||
idp: this.id,
|
||||
},
|
||||
can: {
|
||||
upload: game.user.can(`FILES_UPLOAD`),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
async _preparePartContext(partID, ctx) {
|
||||
ctx = deepClone(ctx);
|
||||
|
||||
_preparePartContext(partID, ctx) {
|
||||
switch (partID) {
|
||||
case `tokens`: {
|
||||
await this._prepareTokensContext(ctx);
|
||||
break;
|
||||
};
|
||||
case `artists`: {
|
||||
await this._prepareArtistsContext(ctx);
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
async _prepareTokensContext(ctx) {
|
||||
const size = await getFileSize(filePath(`storage/db/images.json`));
|
||||
ctx.size = size.friendly;
|
||||
};
|
||||
_prepareTokensContext(ctx) {};
|
||||
|
||||
async _prepareArtistsContext(ctx) {
|
||||
const size = await getFileSize(filePath(`storage/db/artists.json`));
|
||||
ctx.size = size.friendly;
|
||||
};
|
||||
_prepareArtistsContext(ctx) {};
|
||||
// #endregion Data Prep
|
||||
|
||||
// #region Actions
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
if (this.#page == page) { return };
|
||||
this.#page = page;
|
||||
if (this.rendered) {
|
||||
await this.render({ parts: [`list`] });
|
||||
await this.render({ parts: [`images`] });
|
||||
return;
|
||||
};
|
||||
return;
|
||||
|
|
@ -117,7 +117,7 @@ export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
idp: this.id,
|
||||
},
|
||||
can: {
|
||||
upload: game.user.can(`FILES_UPLOAD`),
|
||||
upload: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -152,24 +152,6 @@ export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
const allArtists = Object.entries(deepClone(this.#artistDB.data));
|
||||
const allImages = Object.values(this.#imagesDB.data);
|
||||
|
||||
/*
|
||||
This collates all of the image data into the required summary data for the
|
||||
display of the artists. Because this does the collation all in one iteration
|
||||
it is a more performant way of collecting all of the information once the
|
||||
databases get larger
|
||||
*/
|
||||
const summary = {};
|
||||
for (const image of allImages) {
|
||||
for (const artistID of image.artists) {
|
||||
summary[artistID] ??= { count: 0, tags: {} };
|
||||
summary[artistID].count++;
|
||||
for (const tag of image.tags) {
|
||||
summary[artistID].tags[tag] ??= { name: tag, count: 0 };
|
||||
summary[artistID].tags[tag].count++;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const artists = [];
|
||||
for (const [id, artist] of allArtists) {
|
||||
// Check if it matches the required filters
|
||||
|
|
@ -180,8 +162,20 @@ export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
// Populate ephemeral data for rendering
|
||||
artist.id = id;
|
||||
|
||||
artist.imageCount = summary[id].count;
|
||||
artist.commonTags = Object.values(summary[id].tags)
|
||||
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);
|
||||
|
||||
|
|
@ -206,7 +200,6 @@ export class ArtistBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const data = (new FormDataExtended(event.currentTarget)).object;
|
||||
this.#page = 1;
|
||||
this.filters = data;
|
||||
this.render({ parts: [`list`] });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { __ID__, filePath } from "../consts.mjs";
|
||||
import { convertToWebp, determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs";
|
||||
import { imagePath } from "../utils/imagePath.mjs";
|
||||
import { determineFileExtension, getFile, hashFile, lastModifiedAt, uploadFile, uploadJson } from "../utils/fs.mjs";
|
||||
import { DBConnectorMixin } from "./mixins/DBConnector.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||
const { fetchWithTimeout } = foundry.utils;
|
||||
|
||||
export class ImageApp extends
|
||||
DBConnectorMixin(
|
||||
|
|
@ -29,13 +27,6 @@ export class ImageApp extends
|
|||
contentClasses: [
|
||||
`standard-form`,
|
||||
],
|
||||
controls: [
|
||||
{
|
||||
icon: `fa-solid fa-arrow-up-right-from-square`,
|
||||
label: `IT.apps.ImageApp.toggle-upload-mode`,
|
||||
action: `toggleUploadMode`,
|
||||
}
|
||||
],
|
||||
},
|
||||
form: {
|
||||
handler: this.#onSubmit,
|
||||
|
|
@ -43,7 +34,6 @@ export class ImageApp extends
|
|||
closeOnSubmit: true,
|
||||
},
|
||||
actions: {
|
||||
toggleUploadMode: this.#toggleUploadMode,
|
||||
removeEditingImage: this.#removeEditingImage,
|
||||
},
|
||||
};
|
||||
|
|
@ -57,10 +47,8 @@ export class ImageApp extends
|
|||
// #endregion Options
|
||||
|
||||
// #region Instance Data
|
||||
#isExternal = false;
|
||||
|
||||
/** The artist that is being edited, or the default artist values */
|
||||
_doc = { name: ``, path: ``, tags: [], artists: [] };
|
||||
_doc = { path: ``, tags: [], artists: [] };
|
||||
|
||||
get title() {
|
||||
if (this._docID) {
|
||||
|
|
@ -73,9 +61,6 @@ export class ImageApp extends
|
|||
return _loc(`IT.apps.ImageApp.title.edit-generic`);
|
||||
}
|
||||
};
|
||||
if (this.#isExternal) {
|
||||
return _loc(`IT.apps.ImageApp.title.register`);
|
||||
}
|
||||
return _loc(`IT.apps.ImageApp.title.upload`);
|
||||
};
|
||||
// #endregion Instance Data
|
||||
|
|
@ -91,11 +76,7 @@ export class ImageApp extends
|
|||
if (options.parts?.includes(`header`)) {
|
||||
this.element.querySelector(`input[type="file"]`)?.addEventListener(
|
||||
`change`,
|
||||
this.#changeImageInput.bind(this),
|
||||
);
|
||||
this.element.querySelector(`file-picker`)?.addEventListener(
|
||||
`change`,
|
||||
this.#changeFilePickerInput.bind(this),
|
||||
this.#changeImage.bind(this),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -107,38 +88,12 @@ export class ImageApp extends
|
|||
// Validate the DB hasn't been updated since opening
|
||||
if (!(await this.isAbleToSave())) { return };
|
||||
|
||||
// Add the external image into the DB
|
||||
if (this.#isExternal) {
|
||||
|
||||
const path = data.path;
|
||||
const response = await fetchWithTimeout(path);
|
||||
|
||||
const type = response.headers.get(`Content-Type`)
|
||||
if (!Object.values(CONST.IMAGE_FILE_EXTENSIONS).includes(type)) {
|
||||
// TODO: error
|
||||
return;
|
||||
};
|
||||
|
||||
const blob = await response.blob();
|
||||
const hash = await hashFile(blob);
|
||||
|
||||
const images = await getFile(`storage/db/images.json`);
|
||||
images[hash] = {
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
artists: data.artists,
|
||||
external: true,
|
||||
path,
|
||||
};
|
||||
await uploadJson(`db`, `images.json`, images);
|
||||
|
||||
Hooks.callAll(`${__ID__}.imageUploaded`, hash, images[hash]);
|
||||
}
|
||||
|
||||
// Upload new image to server
|
||||
else if (!this._docID) {
|
||||
if (!this._docID) {
|
||||
const hash = this._doc.hash;
|
||||
const extension = determineFileExtension(this.#file);
|
||||
const path = `storage/tokens/${hash}.${extension}`;
|
||||
|
||||
if (!this.#file) {
|
||||
// TODO: ERROR
|
||||
|
|
@ -151,12 +106,12 @@ export class ImageApp extends
|
|||
);
|
||||
await uploadFile(`tokens`, file);
|
||||
|
||||
const images = await getFile(`storage/db/images.json`);
|
||||
const images = await getFile(this.constructor.dbPath);
|
||||
images[hash] = {
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
artists: data.artists,
|
||||
path: `storage/tokens/${hash}.${extension}`,
|
||||
path: this.#file ? path : ``,
|
||||
};
|
||||
await uploadJson(`db`, `images.json`, images);
|
||||
|
||||
|
|
@ -194,21 +149,17 @@ export class ImageApp extends
|
|||
};
|
||||
};
|
||||
|
||||
let path = imagePath(this._doc);
|
||||
if (this.#isExternal || this._doc.path.startsWith(`blob`)) {
|
||||
path = this._doc.path;
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
meta: {
|
||||
idp: this.id,
|
||||
},
|
||||
imageTypes: Object.values(CONST.IMAGE_FILE_EXTENSIONS).join(`,`),
|
||||
imageURL: path,
|
||||
imageURL: this._doc.path.startsWith(`blob`)
|
||||
? this._doc.path
|
||||
: filePath(this._doc.path),
|
||||
docID: this._docID,
|
||||
image: this._doc,
|
||||
artists: this.#artistCache,
|
||||
external: this.#isExternal,
|
||||
};
|
||||
|
||||
return ctx;
|
||||
|
|
@ -218,9 +169,9 @@ export class ImageApp extends
|
|||
// #region Actions
|
||||
/** @type {File | null} */
|
||||
#file = null;
|
||||
async #changeImageInput(event) {
|
||||
async #changeImage(event) {
|
||||
/** @type {File} */
|
||||
let file = this.#file = event.target.files[0];
|
||||
const file = this.#file = event.target.files[0];
|
||||
|
||||
// Prevent memory leaks
|
||||
URL.revokeObjectURL(this._doc.path);
|
||||
|
|
@ -228,22 +179,14 @@ export class ImageApp extends
|
|||
if (!file) {
|
||||
this.#file = null;
|
||||
this._docID = null;
|
||||
this._doc = { path: ``, name: ``, artists: [], tags: [] };
|
||||
this._doc = { path: ``, artists: [], tags: [] };
|
||||
this._updateFrame({ window: { title: this.title } });
|
||||
await this.render({ parts: [`header`, `preview`, `form`] });
|
||||
return;
|
||||
};
|
||||
|
||||
// Ensure we don't already have the file uploaded
|
||||
const webp = await convertToWebp(file);
|
||||
let extension;
|
||||
if (webp) {
|
||||
file = this.#file = webp;
|
||||
extension = `webp`;
|
||||
} else {
|
||||
extension = determineFileExtension(file);
|
||||
};
|
||||
|
||||
const extension = determineFileExtension(file);
|
||||
const hash = await hashFile(file);
|
||||
const path = `storage/tokens/${hash}.${extension}`;
|
||||
const lastModified = await lastModifiedAt(path);
|
||||
|
|
@ -258,42 +201,9 @@ export class ImageApp extends
|
|||
|
||||
// Temporarily blob the image so it can be displayed in-app
|
||||
const url = URL.createObjectURL(file);
|
||||
this._doc.name = file.name.replace(`.${extension}`, ``);
|
||||
this._doc.path = url;
|
||||
this._doc.hash = hash;
|
||||
await this.render({ parts: [`preview`, `form`] });
|
||||
};
|
||||
|
||||
async #changeFilePickerInput(event) {
|
||||
const picker = event.currentTarget;
|
||||
const path = picker.value;
|
||||
|
||||
if (!path) {
|
||||
this._docID = null;
|
||||
this._doc = { path, name: ``, artists: [], tags: [] };
|
||||
this.render({ parts: [ `preview`, `form` ] });
|
||||
return;
|
||||
};
|
||||
|
||||
let hash;
|
||||
try {
|
||||
const response = await fetchWithTimeout(path);
|
||||
const blob = await response.blob();
|
||||
hash = await hashFile(blob);
|
||||
} catch {
|
||||
ui.notifications.error(_loc(`IT.notifs.error.cant-find-image`, { url: path }));
|
||||
picker.value = ``;
|
||||
return;
|
||||
};
|
||||
|
||||
this._docID = hash;
|
||||
await this._fetchDocument(true);
|
||||
if (this._doc.path !== path) {
|
||||
this._docID = null;
|
||||
this._doc = { path, name: ``, artists: [], tags: [] };
|
||||
};
|
||||
|
||||
this.render({ parts: [ `preview`, `form` ] });
|
||||
await this.render({ parts: [`preview`] });
|
||||
};
|
||||
|
||||
/** @this {ImageApp} */
|
||||
|
|
@ -304,11 +214,5 @@ export class ImageApp extends
|
|||
this._updateFrame({ window: { title: this.title } });
|
||||
await this.render({ parts: [`header`, `preview`, `form`] });
|
||||
};
|
||||
|
||||
/** @this {ImageApp} */
|
||||
static async #toggleUploadMode() {
|
||||
this.#isExternal = !this.#isExternal;
|
||||
ImageApp.#removeEditingImage.call(this);
|
||||
};
|
||||
// #endregion Actions
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,17 +43,15 @@ export function DBConnectorMixin(HandlebarsApp) {
|
|||
return super.render(options, ...args);
|
||||
};
|
||||
|
||||
async _fetchDocument(silent = false) {
|
||||
async _fetchDocument() {
|
||||
if (!this._docID) { return }
|
||||
const documents = await getFile(this.dbPath);
|
||||
|
||||
if (documents[this._docID] == null) {
|
||||
if (!silent) {
|
||||
ui.notifications.error(_loc(
|
||||
`IT.notifs.error.document-ID-404`,
|
||||
{ id: this._docID, dbType: this.dbType },
|
||||
));
|
||||
}
|
||||
ui.notifications.error(_loc(
|
||||
`IT.notifs.error.document-ID-404`,
|
||||
{ id: this._docID, dbType: this.dbType },
|
||||
));
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
export const config = CONFIG.ImageTagger = foundry.utils.deepSeal({
|
||||
WEBP_QUALITY: 0.5,
|
||||
WEBP_IGNORE: [`image/gif`],
|
||||
});
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { api } from "../api.mjs";
|
||||
import { ArtSidebar } from "../apps/ArtSidebar.mjs";
|
||||
import { registerCustomComponents } from "../apps/elements/_index.mjs";
|
||||
import { __ID__ } from "../consts.mjs";
|
||||
import helpers from "../handlebarsHelpers/_index.mjs";
|
||||
import { registerUserSettings } from "../settings/user.mjs";
|
||||
|
||||
|
|
@ -22,6 +20,4 @@ Hooks.on(`init`, () => {
|
|||
const temp = CONFIG.ui.sidebar.TABS.settings;
|
||||
delete CONFIG.ui.sidebar.TABS.settings;
|
||||
CONFIG.ui.sidebar.TABS.settings = temp;
|
||||
|
||||
game.modules.get(__ID__).api = api;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { config } from "../config.mjs";
|
||||
import { __ID__, devMode, filePath } from "../consts.mjs";
|
||||
|
||||
const { fetchJsonWithTimeout } = foundry.utils;
|
||||
|
|
@ -29,42 +28,6 @@ export async function hashFile(file) {
|
|||
.join(``);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an image file into a webp file unless it is already a webp file or
|
||||
* cannot be converted to a webp, in which case it doesn't modify the file at all.
|
||||
*
|
||||
* @param {File} file The file to convert
|
||||
* @returns {Promise<File>}
|
||||
*/
|
||||
export async function convertToWebp(file) {
|
||||
if (file.type === `image/webp`) { return null };
|
||||
if (config.WEBP_IGNORE.includes(file.type)) { return null };
|
||||
|
||||
/** @type {HTMLImageElement} */
|
||||
const image = document.createElement(`img`);
|
||||
const url = URL.createObjectURL(file);
|
||||
image.src = url;
|
||||
await image.decode();
|
||||
|
||||
/** @type {HTMLCanvasElement} */
|
||||
const canvas = document.createElement(`canvas`);
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
canvas.getContext(`2d`).drawImage(image, 0, 0);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
const name = file.name.split(`.`).slice(0, -1).join(`.`);
|
||||
const webp = new File([blob], `${name}.webp`, { type: blob.type });
|
||||
resolve(webp);
|
||||
},
|
||||
`image/webp`,
|
||||
config.WEBP_QUALITY,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export async function lastModifiedAt(path) {
|
||||
try {
|
||||
const response = await fetch(filePath(path), { method: `HEAD` });
|
||||
|
|
@ -74,30 +37,6 @@ export async function lastModifiedAt(path) {
|
|||
};
|
||||
};
|
||||
|
||||
export async function getFileSize(path) {
|
||||
try {
|
||||
const response = await fetch(filePath(path), { method: `HEAD` });
|
||||
const bytes = response.headers.get(`Content-Length`);
|
||||
if (!bytes) { return null }
|
||||
|
||||
// round the non-bytes to 2 decimal places
|
||||
const kilobytes = Math.floor(bytes / 10) / 100;
|
||||
const megabytes = Math.floor(bytes / 10_000) / 100;
|
||||
|
||||
// determine the most appropriate unit to use
|
||||
let friendly = `${bytes} bytes`;
|
||||
if (megabytes > 0.25) {
|
||||
friendly = `${megabytes}MB`;
|
||||
} else if (kilobytes > 0) {
|
||||
friendly = `${kilobytes}KB`;
|
||||
};
|
||||
|
||||
return { bytes, kilobytes, megabytes, friendly };
|
||||
} catch {
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getFile(path) {
|
||||
try {
|
||||
return fetchJsonWithTimeout(filePath(path));
|
||||
|
|
@ -147,7 +86,6 @@ export async function uploadFile(path, file) {
|
|||
};
|
||||
|
||||
export function determineFileExtension(file) {
|
||||
if (!file) return null;
|
||||
for (const [short, long] of Object.entries(CONST.IMAGE_FILE_EXTENSIONS)) {
|
||||
if (long === file.type) {
|
||||
return short;
|
||||
|
|
|
|||
2
scripts
2
scripts
|
|
@ -1 +1 @@
|
|||
Subproject commit 0065967ad7f086c63c07d28d834bd7bdf31ccbd5
|
||||
Subproject commit 06fb33b35ff446dee613afe271b6fe2ff976735a
|
||||
|
|
@ -16,11 +16,4 @@
|
|||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@
|
|||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
file-picker {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<button
|
||||
data-action="select"
|
||||
>
|
||||
{{localize "IT.common.select"}}
|
||||
Select
|
||||
</button>
|
||||
{{else if is.multi}}
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="IT.apps.ArtBrowser.select-image"
|
||||
aria-label="Select image"
|
||||
data-action="select"
|
||||
{{checked image.selected}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<div class="paginated">
|
||||
<div class="row">
|
||||
{{#if can.upload}}
|
||||
<button data-action="uploadImage">
|
||||
{{localize "IT.apps.ArtBrowser.upload-image"}}
|
||||
</button>
|
||||
<button data-action="uploadImage">Upload Image</button>
|
||||
{{/if}}
|
||||
{{#if is.multi}}
|
||||
<div class="grow"></div>
|
||||
|
|
@ -22,7 +20,7 @@
|
|||
</ul>
|
||||
{{else}}
|
||||
<span class="placeholder">
|
||||
{{ localize "IT.apps.ArtBrowser.no-results" }}
|
||||
{{ localize "" }}
|
||||
</span>
|
||||
{{/if}}
|
||||
<div class="grow"></div>
|
||||
|
|
@ -31,14 +29,14 @@
|
|||
data-action="prevPage"
|
||||
{{disabled (not has.prev)}}
|
||||
>
|
||||
{{localize "IT.common.page.previous"}}
|
||||
Prev
|
||||
</button>
|
||||
{{page}} / {{pages}}
|
||||
<button
|
||||
data-action="nextPage"
|
||||
{{disabled (not has.next)}}
|
||||
>
|
||||
{{localize "IT.common.page.next"}}
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<p>
|
||||
{{localize "IT.common.page-reset-warning"}}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="{{meta.idp}}-name"
|
||||
|
|
@ -18,7 +12,7 @@
|
|||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-tags">
|
||||
{{localize "IT.common.tags"}}
|
||||
Tags
|
||||
</label>
|
||||
<string-tags
|
||||
id="{{meta.idp}}-tags"
|
||||
|
|
@ -29,7 +23,7 @@
|
|||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-artists">
|
||||
{{localize "IT.common.artists"}}
|
||||
Artists
|
||||
</label>
|
||||
<multi-select
|
||||
id="{{meta.idp}}-artists"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
<section>
|
||||
<div>
|
||||
<h4 class="divider">{{ localize "IT.common.artists" }}</h4>
|
||||
<p class="subtitle">
|
||||
{{ localize "IT.apps.ArtSidebar.db-size" size=size }}
|
||||
</p>
|
||||
</div>
|
||||
<h4 class="divider">Artists</h4>
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ArtistBrowser"
|
||||
>
|
||||
{{ localize "IT.apps.ArtSidebar.view" }}
|
||||
View All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ArtistApp"
|
||||
>
|
||||
Add New
|
||||
</button>
|
||||
{{#if can.upload}}
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ArtistApp"
|
||||
>
|
||||
{{ localize "IT.apps.ArtSidebar.add-new" }}
|
||||
</button>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
<section>
|
||||
<div>
|
||||
<h4 class="divider">{{ localize "IT.common.images" }}</h4>
|
||||
<p class="subtitle">
|
||||
{{ localize "IT.apps.ArtSidebar.db-size" size=size }}
|
||||
</p>
|
||||
</div>
|
||||
<h4 class="divider">Tokens</h4>
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ArtBrowser"
|
||||
>
|
||||
{{ localize "IT.apps.ArtSidebar.view" }}
|
||||
View All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ImageApp"
|
||||
>
|
||||
Add New
|
||||
</button>
|
||||
{{#if can.upload}}
|
||||
<button
|
||||
type="button"
|
||||
data-action="openApp"
|
||||
data-app="ImageApp"
|
||||
>
|
||||
{{ localize "IT.apps.ArtSidebar.add-new" }}
|
||||
</button>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div>
|
||||
<div class="row">
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
Name
|
||||
</label>
|
||||
<div class="grow"></div>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<div>
|
||||
<div class="row">
|
||||
<span class="label">
|
||||
{{localize "IT.common.links"}}
|
||||
</span>
|
||||
<span class="label">Links</span>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<h2>{{artist.name}}</h2>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
{{localize "IT.apps.ArtistBrowser.image-count" count=artist.imageCount}}
|
||||
{{artist.imageCount}} Images
|
||||
</div>
|
||||
</div>
|
||||
{{#if artist.links}}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
{{/if}}
|
||||
{{#if artist.commonTags}}
|
||||
<section>
|
||||
<h3>{{localize "IT.apps.ArtistBrowser.common-tags"}}</h3>
|
||||
<h3>Common Image Tags</h3>
|
||||
<ul class="chip-list">
|
||||
{{#each artist.commonTags as |tag|}}
|
||||
<li class="chip">{{tag.name}} ({{tag.count}})</li>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
<div class="paginated">
|
||||
<div class="row">
|
||||
{{#if can.upload}}
|
||||
<button data-action="createArtist">
|
||||
{{localize "IT.apps.ArtistBrowser.create-artist"}}
|
||||
</button>
|
||||
<button data-action="createArtist">Create New Artist</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if artists}}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<p>
|
||||
{{localize "IT.common.page-reset-warning"}}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
</label>
|
||||
<label for="{{meta.idp}}-name">Name</label>
|
||||
<input
|
||||
id="{{meta.idp}}-name"
|
||||
type="text"
|
||||
|
|
@ -18,7 +10,7 @@
|
|||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-sort">
|
||||
{{localize "IT.common.sort-by"}}
|
||||
Sort By
|
||||
</label>
|
||||
<select name="sortBy" id="{{meta.idp}}-sort">
|
||||
{{it-options sortBy sortOptions localize=true}}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="inputs">
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
Name (Optional)
|
||||
</label>
|
||||
<input
|
||||
id="{{meta.idp}}-name"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
|
||||
<label for="{{meta.idp}}-tags">
|
||||
{{localize "IT.common.tags"}}
|
||||
Tags (Optional)
|
||||
</label>
|
||||
<string-tags
|
||||
id="{{meta.idp}}-tags"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
></string-tags>
|
||||
|
||||
<label for="{{meta.idp}}-artists">
|
||||
{{localize "IT.common.artists"}}
|
||||
Artists (Optional)
|
||||
</label>
|
||||
<multi-select
|
||||
id="{{meta.idp}}-artists"
|
||||
|
|
|
|||
|
|
@ -1,33 +1,29 @@
|
|||
<header class="row">
|
||||
<label for="{{meta.idp}}-image">
|
||||
{{ localize "IT.apps.ImageApp.image-label" }}
|
||||
</label>
|
||||
{{#if external}}
|
||||
<file-picker
|
||||
id="{{meta.idp}}-image"
|
||||
type="image"
|
||||
name="path"
|
||||
></file-picker>
|
||||
{{else if docID}}
|
||||
<input
|
||||
type="text"
|
||||
id="{{meta.idp}}-image"
|
||||
value="{{imageURL}}"
|
||||
disabled
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="removeEditingImage"
|
||||
>
|
||||
{{ localize "IT.apps.ImageApp.clear" }}
|
||||
</button>
|
||||
{{else}}
|
||||
<input
|
||||
type="file"
|
||||
id="{{meta.idp}}-image"
|
||||
accept="{{imageTypes}}"
|
||||
name="file"
|
||||
>
|
||||
{{/if}}
|
||||
<header>
|
||||
<div class="row">
|
||||
<label for="{{meta.idp}}-image">
|
||||
Image
|
||||
</label>
|
||||
{{#if docID}}
|
||||
<input
|
||||
type="text"
|
||||
id="{{meta.idp}}-image"
|
||||
value="{{imageURL}}"
|
||||
disabled
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="removeEditingImage"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
{{else}}
|
||||
<input
|
||||
type="file"
|
||||
id="{{meta.idp}}-image"
|
||||
accept="{{imageTypes}}"
|
||||
name="file"
|
||||
>
|
||||
{{/if}}
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
{{else}}
|
||||
<span class="placeholder">
|
||||
{{ localize "IT.apps.ImageApp.preview-placeholder" }}
|
||||
Select an image to see the preview
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue