Compare commits
1 commit
main
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fb2bd796d |
63 changed files with 292 additions and 1346 deletions
|
|
@ -1,40 +0,0 @@
|
|||
on: [ workflow_dispatch ]
|
||||
env:
|
||||
MANIFEST: "module.json"
|
||||
jobs:
|
||||
create-draft-release:
|
||||
name: "Create Draft Release"
|
||||
runs-on: act
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm clean-install
|
||||
|
||||
- id: version
|
||||
run: cat ${{env.MANIFEST}} | echo version=`jq -r ".version"` >> "$FORGEJO_OUTPUT"
|
||||
|
||||
- name: Assert that the tag doesn't exist
|
||||
run: node scripts/src/tagExists.mjs
|
||||
env:
|
||||
TAG_NAME: "v${{steps.version.outputs.version}}"
|
||||
|
||||
- name: Update manifest
|
||||
run: node scripts/src/prepareManifest.mjs
|
||||
env:
|
||||
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}}
|
||||
|
||||
- name: Create forgejo release
|
||||
run: node scripts/src/createForgejoRelease.mjs
|
||||
env:
|
||||
TAG: "v${{steps.version.outputs.version}}"
|
||||
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)
|
||||
|
|
|
|||
4
assets/LICENSES
Normal file
4
assets/LICENSES
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Disclaimer: All icons have been optimized and scaled for the web unless otherwise noted.
|
||||
|
||||
icons/layout/grid.svg (https://thenounproject.com/icon/grid-2442978/)
|
||||
icons/layout/list.svg (https://thenounproject.com/icon/list-5060696/)
|
||||
1
assets/icons/layout/grid.svg
Normal file
1
assets/icons/layout/grid.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M77.641 100a8.6 8.6 0 0 1-4.332-2.36 7.42 7.42 0 0 1-1.86-5.109V78.88a6.87 6.87 0 0 1 4-6.648A9 9 0 0 1 79 71.469c4.41-.09 8.829 0 13.25 0 3.95 0 6.84 2.211 7.61 6q.042.111.11.211v16.09c-.052.18-.11.352-.161.531a6.6 6.6 0 0 1-2.758 4.23 26.5 26.5 0 0 1-3.281 1.47zM22.359 0c1.094.375 2.156.84 3.172 1.39a6.76 6.76 0 0 1 3 6q.059 6.902 0 13.81a6.92 6.92 0 0 1-4.121 6.652 8.4 8.4 0 0 1-3.32.687c-4.551.07-9.102.051-13.648 0a7.15 7.15 0 0 1-7.301-5.8 3 3 0 0 0-.14-.38V6.232c0-.149.108-.301.148-.45A6.6 6.6 0 0 1 2.922 1.47 26 26 0 0 1 6.23 0zM100 22.359a21.3 21.3 0 0 1-1.398 3.18 6.74 6.74 0 0 1-6 3q-6.901.058-13.81 0h-.003a6.8 6.8 0 0 1-6.52-3.918 8.7 8.7 0 0 1-.808-3.52c-.09-4.55-.059-9.101 0-13.648V7.45A7.134 7.134 0 0 1 77.35.121a2 2 0 0 0 .29-.12h16.128q.183.075.371.128a6.5 6.5 0 0 1 4.34 2.723A26 26 0 0 1 100 6.23zM0 77.641a19.7 19.7 0 0 1 1.34-3.121 6.83 6.83 0 0 1 6.058-3.07h13.813a6.8 6.8 0 0 1 6.52 3.922c.512 1.101.789 2.3.808 3.519.09 4.55.059 9.102 0 13.648a7.14 7.14 0 0 1-5.91 7.352 1.7 1.7 0 0 0-.289.12H6.231a3 3 0 0 0-.371-.132 6.56 6.56 0 0 1-4.39-2.79A25 25 0 0 1 0 93.77zm0-35.789a21.5 21.5 0 0 1 1.531-3.281 6.2 6.2 0 0 1 5.29-2.79h14.69a7.077 7.077 0 0 1 7 7.25c0 4.66.059 9.321 0 14a7.006 7.006 0 0 1-7 7.23c-4.82.11-9.64.103-14.449.001-3.558-.07-6-2.16-6.91-5.64 0-.149-.101-.29-.16-.442q.007-8.179.008-16.328m100 16.296a20.4 20.4 0 0 1-1.48 3.223 6.29 6.29 0 0 1-5.34 2.84c-4.922 0-9.852.12-14.77 0a6.944 6.944 0 0 1-7-7.223c-.059-4.66 0-9.32 0-14a7.07 7.07 0 0 1 1.984-5.086 7.1 7.1 0 0 1 5.016-2.164c4.82 0 9.64-.078 14.449 0a7.006 7.006 0 0 1 6.922 5.723q.053.182.14.359v16.328zM41.852 100a22.7 22.7 0 0 1-3.402-1.629 6 6 0 0 1-2.64-5c-.09-3.371-.06-6.871-.071-10.37v-4.552a7.1 7.1 0 0 1 2.168-5.011 7.08 7.08 0 0 1 5.082-1.989h14a7.1 7.1 0 0 1 5.086 1.985 7.1 7.1 0 0 1 2.156 5.015c0 4.871.09 9.739 0 14.61-.078 3.55-2.238 6-5.711 6.84q-.182.057-.36.14H41.853zM58 0a9.2 9.2 0 0 1 3.61 1.602A6.48 6.48 0 0 1 64.2 6.52c.089 4 .058 7.93.07 11.89v3a7.12 7.12 0 0 1-7.348 7.121h-13.81c-4.19 0-7.19-2.84-7.288-7-.13-4.87-.11-9.738 0-14.608C35.87 3.43 38 1 41.5.142A3 3 0 0 0 41.85 0zm6.211 50c0 2.55.09 5.11 0 7.66a6.75 6.75 0 0 1-6.512 6.531q-7.699.181-15.398 0a6.706 6.706 0 0 1-6.472-6.48 349 349 0 0 1 0-15.398v-.004a6.695 6.695 0 0 1 6.46-6.48 330 330 0 0 1 15.48 0 6.75 6.75 0 0 1 6.43 6.433c.11 2.578 0 5.16 0 7.738z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
assets/icons/layout/list.svg
Normal file
1
assets/icons/layout/list.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M41.668 9.375H87.5c4.027 0 7.293 4.027 7.293 7.293V25a7.294 7.294 0 0 1-7.293 7.293H41.668c-4.027 0-7.293-4.027-7.293-7.293v-8.332a7.294 7.294 0 0 1 7.293-7.293m-29.168 0h8.332c4.027 0 7.293 4.027 7.293 7.293V25a7.294 7.294 0 0 1-7.293 7.293H12.5c-4.027 0-7.293-4.027-7.293-7.293v-8.332A7.294 7.294 0 0 1 12.5 9.375m29.168 58.332H87.5c4.027 0 7.293 4.027 7.293 7.293v8.332a7.294 7.294 0 0 1-7.293 7.293H41.668c-4.027 0-7.293-4.027-7.293-7.293V75a7.294 7.294 0 0 1 7.293-7.293m-29.168 0h8.332c4.027 0 7.293 4.027 7.293 7.293v8.332a7.294 7.294 0 0 1-7.293 7.293H12.5c-4.027 0-7.293-4.027-7.293-7.293V75a7.294 7.294 0 0 1 7.293-7.293m29.168-29.164h29.168c4.027 0 7.293 4.027 7.293 7.293v8.332a7.294 7.294 0 0 1-7.293 7.293H41.668c-4.027 0-7.293-4.027-7.293-7.293v-8.332a7.294 7.294 0 0 1 7.293-7.293m-29.168 0h8.332c4.027 0 7.293 4.027 7.293 7.293v8.332a7.294 7.294 0 0 1-7.293 7.293H12.5c-4.027 0-7.293-4.027-7.293-7.293v-8.332a7.294 7.294 0 0 1 7.293-7.293"/></svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -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,84 +0,0 @@
|
|||
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`));
|
||||
|
||||
async function hashFile(path) {
|
||||
return new Promise((resolve) => {
|
||||
const stream = createReadStream(path);
|
||||
const hash = crypto.createHash(`sha1`);
|
||||
hash.setEncoding(`hex`);
|
||||
|
||||
stream.on(`end`, () => {
|
||||
hash.end();
|
||||
resolve(hash.read());
|
||||
});
|
||||
|
||||
stream.pipe(hash);
|
||||
});
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const imageList = globSync(
|
||||
`foundry/public/icons/**/*`,
|
||||
{
|
||||
nodir: true,
|
||||
ignore: config.ignorePatterns,
|
||||
});
|
||||
const images = {};
|
||||
const allTags = {};
|
||||
|
||||
for (const path of imageList) {
|
||||
if (config.skipFiles.some(file => path.endsWith(file))) continue;
|
||||
|
||||
const hash = await hashFile(path);
|
||||
const filename = path.split(`/`).at(-1);
|
||||
|
||||
const tags = Array.from(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]++;
|
||||
});
|
||||
|
||||
images[hash] = {
|
||||
name: filename,
|
||||
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);
|
||||
};
|
||||
};
|
||||
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),
|
||||
);
|
||||
};
|
||||
|
||||
main();
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,37 +1,11 @@
|
|||
{
|
||||
"IT": {
|
||||
"TB": {
|
||||
"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"
|
||||
},
|
||||
"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)"
|
||||
},
|
||||
"image-count": "{count} Images",
|
||||
"common-tags": "Common Image Tags",
|
||||
"create-artist": "Create New Artist"
|
||||
"selected": "{current}/{required} Selected"
|
||||
},
|
||||
"ArtistApp": {
|
||||
"title": {
|
||||
|
|
@ -40,28 +14,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": {
|
||||
"openForEditImage": {
|
||||
"name": "Open Art Browser for Documents",
|
||||
"hint": "Whether or not to open the custom Art Browser when picking an image using the edit image action within Foundry for all documents. Systems and Modules that use a different edit image action will not work with this setting."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
|
|
@ -75,8 +33,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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
module.json
13
module.json
|
|
@ -2,27 +2,22 @@
|
|||
"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.0.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",
|
||||
"url": "https://git.varify.ca/Foundry/token-browser",
|
||||
"manifest": "",
|
||||
"download": "",
|
||||
"esmodules": [
|
||||
"module/image-tagger.mjs",
|
||||
"dev/main.mjs"
|
||||
"module/token-browser.mjs"
|
||||
],
|
||||
"styles": [
|
||||
{
|
||||
"src": "styles/main.css",
|
||||
"layer": "modules.image-tagger"
|
||||
"layer": "modules.token-browser"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
// 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
|
||||
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,
|
||||
ArtistApp,
|
||||
ImageApp,
|
||||
},
|
||||
utils: {
|
||||
fs: {
|
||||
convertToWebp,
|
||||
hashFile,
|
||||
lastModifiedAt,
|
||||
getFileSize,
|
||||
getFile,
|
||||
uploadJson,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
|||
const { FormDataExtended } = foundry.applications.ux;
|
||||
const { deepClone } = foundry.utils;
|
||||
|
||||
const PAGE_SIZE = 48;
|
||||
const PAGE_SIZE = 8;
|
||||
|
||||
export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
// #region Options
|
||||
|
|
@ -16,7 +16,6 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
classes: [
|
||||
__ID__,
|
||||
`ArtBrowser`,
|
||||
`data-browser`,
|
||||
],
|
||||
position: {},
|
||||
window: {},
|
||||
|
|
@ -36,6 +35,7 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
template: filePath(`templates/ArtBrowser/images.hbs`),
|
||||
templates: [
|
||||
filePath(`templates/ArtBrowser/image/grid.hbs`),
|
||||
filePath(`templates/ArtBrowser/image/list.hbs`),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -45,20 +45,23 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
#page = 1;
|
||||
#selectCount = 0;
|
||||
#onSubmit = null;
|
||||
#layout = `grid`;
|
||||
filters = {
|
||||
name: ``,
|
||||
tags: [],
|
||||
artists: [],
|
||||
};
|
||||
|
||||
constructor({ selectCount = 0, onSubmit = null, ...opts } = {}) {
|
||||
constructor({
|
||||
selectCount = 0,
|
||||
onSubmit = null,
|
||||
layout = `grid`,
|
||||
...opts
|
||||
} = {}) {
|
||||
super(opts);
|
||||
this.#selectCount = selectCount;
|
||||
this.#onSubmit = onSubmit;
|
||||
};
|
||||
|
||||
get page() {
|
||||
return this.#page;
|
||||
this.#layout = layout;
|
||||
};
|
||||
|
||||
async setPage(page) {
|
||||
|
|
@ -70,6 +73,15 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
return;
|
||||
};
|
||||
|
||||
async changeLayout(layout) {
|
||||
console.log(`changing layout to:`, layout)
|
||||
if (![`grid`, `list`].includes(layout)) {
|
||||
throw `Cannot set layout to: ${layout}`;
|
||||
};
|
||||
this.#layout = layout;
|
||||
await this.render({ parts: [`images`] });
|
||||
};
|
||||
|
||||
get selectMode() {
|
||||
if (this.#selectCount === 0) {
|
||||
return `view`;
|
||||
|
|
@ -137,6 +149,11 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
|
||||
if (options.parts?.includes(`images`)) {
|
||||
this._updateSelectedCount();
|
||||
|
||||
const layoutTypeInputs = this.element.querySelectorAll(`input[name="layoutType"]`);
|
||||
for (const element of layoutTypeInputs) {
|
||||
element.addEventListener(`change`, this.#onLayoutChange.bind(this));
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -147,7 +164,7 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
if (element) {
|
||||
element.innerText = `${this.#selected.size} Selected`;
|
||||
element.innerText = _loc(
|
||||
`IT.apps.ArtBrowser.selected`,
|
||||
`TB.apps.ArtBrowser.selected`,
|
||||
{
|
||||
current: this.#selected.size,
|
||||
required: this.#selectCount,
|
||||
|
|
@ -183,11 +200,12 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
idp: this.id,
|
||||
},
|
||||
is: {
|
||||
multi: this.selectMode === `multi`,
|
||||
single: this.selectMode === `single`,
|
||||
multi: this.#selectCount > 1,
|
||||
single: this.#selectCount === 1,
|
||||
},
|
||||
layout: this.#layout,
|
||||
can: {
|
||||
upload: game.user.can(`FILES_UPLOAD`),
|
||||
upload: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -220,6 +238,7 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
};
|
||||
|
||||
_prepareImagesContext(ctx) {
|
||||
ctx.images = [];
|
||||
const allImages = Object.entries(deepClone(this.#imagesDB.data));
|
||||
|
||||
const images = [];
|
||||
|
|
@ -237,7 +256,6 @@ export class ArtBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
|
|||
// Populate ephemeral data for rendering
|
||||
image.id = id;
|
||||
image.selected = this.#selected.has(imagePath(image));
|
||||
image.path = imagePath(image);
|
||||
|
||||
// Convert all of the artist IDs into the actual data
|
||||
image.artists = image.artists
|
||||
|
|
@ -272,10 +290,16 @@ 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`] });
|
||||
};
|
||||
|
||||
async #onLayoutChange(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const newLayout = event.target.value;
|
||||
await this.changeLayout(newLayout);
|
||||
};
|
||||
|
||||
/** @this {ArtBrowser} */
|
||||
static async #nextPage() {
|
||||
this.#page += 1;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -63,11 +63,11 @@ export class ArtistApp extends
|
|||
get title() {
|
||||
if (this._docID && this._doc.name) {
|
||||
return game.i18n.format(
|
||||
`IT.apps.ArtistApp.title.edit`,
|
||||
`TB.apps.ArtistApp.title.edit`,
|
||||
{ name: this._doc.name },
|
||||
);
|
||||
};
|
||||
return game.i18n.localize(`IT.apps.ArtistApp.title.create`);
|
||||
return game.i18n.localize(`TB.apps.ArtistApp.title.create`);
|
||||
};
|
||||
// #endregion Instance Data
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ export class ArtistApp extends
|
|||
/** @this {ArtistApp} */
|
||||
static async #addNewLink() {
|
||||
const link = await promptViaTemplate(
|
||||
`IT.dialogs.Link.title`,
|
||||
`TB.dialogs.Link.title`,
|
||||
`templates/Dialogs/Link.hbs`,
|
||||
);
|
||||
link.isNew = true;
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
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: [`list`] });
|
||||
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: game.user.can(`FILES_UPLOAD`),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_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);
|
||||
|
||||
/*
|
||||
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
|
||||
if (this.filters.name && !artist.name.includes(this.filters.name)) {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Populate ephemeral data for rendering
|
||||
artist.id = id;
|
||||
|
||||
artist.imageCount = summary[id].count;
|
||||
artist.commonTags = Object.values(summary[id].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.#page = 1;
|
||||
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;
|
||||
};
|
||||
|
|
@ -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,26 +47,21 @@ 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) {
|
||||
if (this._doc.name) {
|
||||
return _loc(
|
||||
`IT.apps.ImageApp.title.edit-named`,
|
||||
`TB.apps.ImageApp.title.edit-named`,
|
||||
{ name: this._doc.name },
|
||||
);
|
||||
} else {
|
||||
return _loc(`IT.apps.ImageApp.title.edit-generic`);
|
||||
return _loc(`TB.apps.ImageApp.title.edit-generic`);
|
||||
}
|
||||
};
|
||||
if (this.#isExternal) {
|
||||
return _loc(`IT.apps.ImageApp.title.register`);
|
||||
}
|
||||
return _loc(`IT.apps.ImageApp.title.upload`);
|
||||
return _loc(`TB.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(
|
||||
`TB.notifs.error.document-ID-404`,
|
||||
{ id: this._docID, dbType: this.dbType },
|
||||
));
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
@ -75,7 +73,7 @@ export function DBConnectorMixin(HandlebarsApp) {
|
|||
async isAbleToSave() {
|
||||
if (!game.user.can(`FILES_UPLOAD`)) {
|
||||
ui.notifications.error(
|
||||
`IT.notifs.error.no-upload-permission`,
|
||||
`TB.notifs.error.no-upload-permission`,
|
||||
{ localize: true },
|
||||
);
|
||||
return false;
|
||||
|
|
@ -86,7 +84,7 @@ export function DBConnectorMixin(HandlebarsApp) {
|
|||
const newLastModified = await lastModifiedAt(this.dbPath);
|
||||
if (newLastModified !== this.#lastModified) {
|
||||
ui.notifications.error(
|
||||
`IT.notifs.error.db-out-of-date`,
|
||||
`TB.notifs.error.db-out-of-date`,
|
||||
{ localize: true },
|
||||
);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
export const config = CONFIG.ImageTagger = foundry.utils.deepSeal({
|
||||
WEBP_QUALITY: 0.5,
|
||||
WEBP_IGNORE: [`image/gif`],
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export const __ID__ = `image-tagger`;
|
||||
export const __ID__ = `token-browser`;
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ import { options } from "./options.mjs";
|
|||
|
||||
export default {
|
||||
// MARK: Complex
|
||||
"it-options": options,
|
||||
"it-filePath": filePath,
|
||||
"tb-options": options,
|
||||
"tb-filePath": filePath,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { ArtBrowser } from "../apps/ArtBrowser.mjs";
|
||||
import { __ID__ } from "../consts.mjs";
|
||||
|
||||
Hooks.on(`getHeaderControlsDocumentSheetV2`, (sheet) => {
|
||||
|
||||
const original = sheet.options.actions.editImage;
|
||||
sheet.options.actions.editImage = async (event, target) => {
|
||||
|
||||
if (!game.settings.get(__ID__, `openForEditImage`)) {
|
||||
return original.call(sheet, event, target);
|
||||
};
|
||||
|
||||
if (target.nodeName !== `IMG`) {
|
||||
throw new Error(`The editImage action is available only for IMG elements.`);
|
||||
};
|
||||
|
||||
const src = await ArtBrowser.select(1);
|
||||
if (!src) { return };
|
||||
target.src = src;
|
||||
if (sheet.options.form.submitOnChange) {
|
||||
const submit = new Event(`submit`, { cancelable: true });
|
||||
sheet.form.dispatchEvent(submit);
|
||||
};
|
||||
};
|
||||
});
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
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";
|
||||
|
||||
Hooks.on(`init`, () => {
|
||||
registerUserSettings();
|
||||
registerCustomComponents();
|
||||
globalThis.tb = api;
|
||||
|
||||
Handlebars.registerHelper(helpers);
|
||||
registerCustomComponents();
|
||||
|
||||
// Art sidebar tab
|
||||
CONFIG.ui.sidebar.TABS.art = {
|
||||
|
|
@ -22,6 +21,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,44 +0,0 @@
|
|||
import { ArtBrowser } from "../apps/ArtBrowser.mjs";
|
||||
|
||||
/*
|
||||
This hook handles adding the button for selecting an image using this module
|
||||
into the prototype token config and the token-specific config. Handling all of
|
||||
the relevant locations in both apps with a single hook.
|
||||
*/
|
||||
Hooks.on(`renderTokenApplication`, (app, form) => {
|
||||
const button = document.createElement(`button`);
|
||||
button.type = `button`;
|
||||
button.classList = `icon fa-solid fa-paintbrush`;
|
||||
|
||||
/** @type {HTMLElement | undefined} */
|
||||
const textureSource = form.querySelector(`file-picker[name="texture.src"]`);
|
||||
if (textureSource) {
|
||||
const cloned = button.cloneNode(true);
|
||||
cloned.addEventListener(`click`, async () => {
|
||||
const src = await ArtBrowser.select(1);
|
||||
if (!src) return
|
||||
textureSource.value = src;
|
||||
});
|
||||
textureSource.insertAdjacentElement(`afterend`, cloned);
|
||||
};
|
||||
|
||||
/** @type {HTMLElement | undefined} */
|
||||
const turnMarkerSource = form.querySelector(`file-picker[name="turnMarker.src"]`);
|
||||
if (turnMarkerSource) {
|
||||
const cloned = button.cloneNode(true);
|
||||
cloned.disabled = turnMarkerSource.disabled;
|
||||
cloned.addEventListener(`click`, async () => {
|
||||
const src = await ArtBrowser.select(1);
|
||||
if (!src) return
|
||||
turnMarkerSource.value = src;
|
||||
});
|
||||
|
||||
// Ensure that the disabled state stays in sync
|
||||
const observer = new MutationObserver(() => {
|
||||
cloned.disabled = turnMarkerSource.disabled;
|
||||
});
|
||||
observer.observe(turnMarkerSource, { attributeFilter: [`disabled`] });
|
||||
|
||||
turnMarkerSource.insertAdjacentElement(`afterend`, cloned);
|
||||
};
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import "./hooks/init.mjs";
|
||||
import "./hooks/ready.mjs";
|
||||
import "./hooks/renderTokenApplication.mjs";
|
||||
import "./hooks/getHeaderControlsDocumentSheetV2.mjs";
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { __ID__ } from "../consts.mjs";
|
||||
|
||||
export function registerUserSettings() {
|
||||
game.settings.register(__ID__, `openForEditImage`, {
|
||||
name: `IT.settings.openForEditImage.name`,
|
||||
hint: `IT.settings.openForEditImage.hint`,
|
||||
scope: `user`,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
config: true,
|
||||
});
|
||||
};
|
||||
2
module/token-browser.mjs
Normal file
2
module/token-browser.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import "./hooks/init.mjs";
|
||||
import "./hooks/ready.mjs";
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { filePath } from "../consts.mjs";
|
||||
|
||||
export function imagePath(image) {
|
||||
if (image.external) {
|
||||
return image.path;
|
||||
};
|
||||
return filePath(image.path);
|
||||
};
|
||||
|
|
|
|||
95
package-lock.json
generated
95
package-lock.json
generated
|
|
@ -8,7 +8,6 @@
|
|||
"@eslint/js": "^9.8.0",
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"eslint": "^9.8.0",
|
||||
"glob": "^13.0.0",
|
||||
"globals": "^15.9.0",
|
||||
"scripts": "file:./scripts"
|
||||
}
|
||||
|
|
@ -292,29 +291,6 @@
|
|||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@seald-io/binary-search-tree": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz",
|
||||
|
|
@ -1447,24 +1423,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
|
|
@ -1478,22 +1436,6 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
|
|
@ -1931,16 +1873,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
|
||||
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -1990,16 +1922,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
|
|
@ -2152,23 +2074,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"eslint": "^9.8.0",
|
||||
"globals": "^15.9.0",
|
||||
"scripts": "file:./scripts",
|
||||
"glob": "^13.0.0"
|
||||
"scripts": "file:./scripts"
|
||||
},
|
||||
"scripts": {
|
||||
"link": "node scripts/src/linkFoundry.mjs",
|
||||
|
|
|
|||
2
scripts
2
scripts
|
|
@ -1 +1 @@
|
|||
Subproject commit 0065967ad7f086c63c07d28d834bd7bdf31ccbd5
|
||||
Subproject commit 06fb33b35ff446dee613afe271b6fe2ff976735a
|
||||
|
|
@ -1,9 +1,41 @@
|
|||
.image-tagger.ArtBrowser {
|
||||
.token-browser.ArtBrowser {
|
||||
> .window-content {
|
||||
display: grid;
|
||||
grid-template-columns: 175px auto;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
--spacing: 12px;
|
||||
border-right: var(--sidebar-separator);
|
||||
margin-right: var(--spacing);
|
||||
padding-right: var(--spacing);
|
||||
}
|
||||
|
||||
.image-list {
|
||||
--size: 100px;
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, var(--size));
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: all 100ms ease-in-out;
|
||||
|
||||
&:hover, &:has(input[type="checkbox"]:checked) {
|
||||
background: var(--color-warm-3);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
|
|
@ -27,4 +59,14 @@
|
|||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.paginated {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px; /* Due to the grow element, this actually means 12px */
|
||||
}
|
||||
|
||||
.page-nav {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger.ArtSidebar {
|
||||
.token-browser.ArtSidebar {
|
||||
flex-flow: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
|
|
@ -16,11 +16,4 @@
|
|||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-subtitle);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger.ArtistApp {
|
||||
.token-browser.ArtistApp {
|
||||
ul li button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
.image-tagger.ImageApp {
|
||||
.token-browser.ImageApp {
|
||||
> .window-content {
|
||||
display: grid;
|
||||
grid-template-columns: 200px auto;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
file-picker {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger {
|
||||
.token-browser {
|
||||
> .window-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
.image-tagger.data-browser {
|
||||
min-height: 350px;
|
||||
min-width: 630px;
|
||||
|
||||
> .window-content {
|
||||
display: grid;
|
||||
grid-template-columns: 175px auto;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.filters {
|
||||
--spacing: 12px;
|
||||
border-right: var(--sidebar-separator);
|
||||
margin-right: var(--spacing);
|
||||
padding-right: var(--spacing);
|
||||
}
|
||||
|
||||
.entry-list {
|
||||
--size: 100px;
|
||||
list-style-type: none;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
|
||||
&.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, var(--size));
|
||||
};
|
||||
|
||||
&.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
transition: all 100ms ease-in-out;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover,
|
||||
&[data-selected="true"]
|
||||
&:has(input[type="checkbox"]:checked) {
|
||||
background: var(--color-warm-3);
|
||||
}
|
||||
}
|
||||
|
||||
.paginated {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px; /* Due to the grow element, this actually means 12px */
|
||||
}
|
||||
|
||||
.page-nav {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
.image-tagger > .window-content input[type="checkbox"] {
|
||||
.token-browser > .window-content input[type="checkbox"] {
|
||||
--checkbox-checked-color: var(--color-level-success-border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger > .window-content {
|
||||
.token-browser > .window-content {
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
|
|||
38
styles/elements/radio.css
Normal file
38
styles/elements/radio.css
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
.token-browser > .window-content {
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
input[type="radio"] {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-light-5);
|
||||
cursor: pointer;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
&:has(:checked) {
|
||||
background: var(--color-cool-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger > .window-content {
|
||||
.token-browser > .window-content {
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -8,12 +8,11 @@
|
|||
@import url("./elements/utils.css") layer(elements);
|
||||
@import url("./elements/checkbox.css") layer(elements);
|
||||
@import url("./elements/lists.css") layer(elements);
|
||||
@import url("./elements/radio.css") layer(elements);
|
||||
|
||||
/* Apps */
|
||||
@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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger > .window-content hr {
|
||||
.token-browser > .window-content hr {
|
||||
all: initial;
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.image-tagger > .window-content {
|
||||
.token-browser > .window-content {
|
||||
li {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
<li
|
||||
class="entry image grid"
|
||||
class="image grid"
|
||||
data-image-id="{{image.id}}"
|
||||
>
|
||||
<img
|
||||
src="{{image.path}}"
|
||||
src="{{tb-filePath image.path}}"
|
||||
alt=""
|
||||
>
|
||||
{{#if is.single}}
|
||||
<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,6 +1,6 @@
|
|||
<li class="image">
|
||||
<img
|
||||
src="{{it-filePath image.path}}"
|
||||
src="{{tb-filePath image.path}}"
|
||||
alt=""
|
||||
>
|
||||
{{#if (eq selectMode "single")}}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,33 @@
|
|||
<div class="paginated">
|
||||
<div class="row">
|
||||
<div class="radio-group">
|
||||
<label data-tooltip="Grid layout">
|
||||
<input
|
||||
type="radio"
|
||||
name="layoutType"
|
||||
value="grid"
|
||||
{{checked (eq layout "grid")}}
|
||||
>
|
||||
<it-icon
|
||||
name="icons/layout/grid"
|
||||
var:fill="currentColor"
|
||||
></it-icon>
|
||||
</label>
|
||||
<label data-tooltip="List layout">
|
||||
<input
|
||||
type="radio"
|
||||
name="layoutType"
|
||||
value="list"
|
||||
{{checked (eq layout "list")}}
|
||||
>
|
||||
<it-icon
|
||||
name="icons/layout/list"
|
||||
var:fill="currentColor"
|
||||
></it-icon>
|
||||
</label>
|
||||
</div>
|
||||
{{#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>
|
||||
|
|
@ -11,10 +35,10 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
{{#if images}}
|
||||
<ul class="entry-list grid">
|
||||
<ul class="image-list image-list--{{listLayout}}">
|
||||
{{#each images as | image |}}
|
||||
{{>
|
||||
(it-filePath "templates/ArtBrowser/image/grid.hbs")
|
||||
(concat (tb-filePath "templates/ArtBrowser/image/") @root.layout ".hbs")
|
||||
image=image
|
||||
is=@root.is
|
||||
}}
|
||||
|
|
@ -22,7 +46,7 @@
|
|||
</ul>
|
||||
{{else}}
|
||||
<span class="placeholder">
|
||||
{{ localize "IT.apps.ArtBrowser.no-results" }}
|
||||
{{ localize "" }}
|
||||
</span>
|
||||
{{/if}}
|
||||
<div class="grow"></div>
|
||||
|
|
@ -31,14 +55,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,12 +23,12 @@
|
|||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-artists">
|
||||
{{localize "IT.common.artists"}}
|
||||
Artists
|
||||
</label>
|
||||
<multi-select
|
||||
id="{{meta.idp}}-artists"
|
||||
name="artists"
|
||||
>
|
||||
{{ it-options artists artistList }}
|
||||
{{ tb-options artists artistList }}
|
||||
</multi-select>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
data-app="ArtistList"
|
||||
>
|
||||
{{ 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,13 +1,11 @@
|
|||
<div>
|
||||
<div class="row">
|
||||
<span class="label">
|
||||
{{localize "IT.common.links"}}
|
||||
</span>
|
||||
<span class="label">Links</span>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="{{localize 'IT.apps.ArtistApp.add-link'}}"
|
||||
data-tooltip="IT.apps.ArtistApp.add-link"
|
||||
aria-label="{{localize 'TB.apps.ArtistApp.add-link'}}"
|
||||
data-tooltip="TB.apps.ArtistApp.add-link"
|
||||
data-action="addNewLink"
|
||||
>
|
||||
+
|
||||
|
|
@ -18,7 +16,7 @@
|
|||
<li
|
||||
data-index="{{@key}}"
|
||||
{{#if link.isNew}}
|
||||
data-tooltip="IT.common.unsaved"
|
||||
data-tooltip="TB.common.unsaved"
|
||||
{{/if}}
|
||||
>
|
||||
<a
|
||||
|
|
@ -29,7 +27,7 @@
|
|||
{{link.name}}
|
||||
</a>
|
||||
{{#if link.isNew}}
|
||||
<div class="large" aria-label="{{localize 'IT.common.unsaved'}}">
|
||||
<div class="large" aria-label="{{localize 'TB.common.unsaved'}}">
|
||||
!
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<div class="entry artist">
|
||||
<div class="row">
|
||||
<h2>{{artist.name}}</h2>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
{{localize "IT.apps.ArtistBrowser.image-count" count=artist.imageCount}}
|
||||
</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>{{localize "IT.apps.ArtistBrowser.common-tags"}}</h3>
|
||||
<ul class="chip-list">
|
||||
{{#each artist.commonTags as |tag|}}
|
||||
<li class="chip">{{tag.name}} ({{tag.count}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</section>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<div class="paginated">
|
||||
<div class="row">
|
||||
{{#if can.upload}}
|
||||
<button data-action="createArtist">
|
||||
{{localize "IT.apps.ArtistBrowser.create-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>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<p>
|
||||
{{localize "IT.common.page-reset-warning"}}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
</label>
|
||||
<input
|
||||
id="{{meta.idp}}-name"
|
||||
type="text"
|
||||
name="name"
|
||||
value="{{name}}"
|
||||
>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-sort">
|
||||
{{localize "IT.common.sort-by"}}
|
||||
</label>
|
||||
<select name="sortBy" id="{{meta.idp}}-sort">
|
||||
{{it-options sortBy sortOptions localize=true}}
|
||||
</select>
|
||||
</form>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="form-group">
|
||||
<label for="{{idp}}-name">
|
||||
{{localize "IT.dialogs.Link.name"}}
|
||||
{{localize "TB.dialogs.Link.name"}}
|
||||
</label>
|
||||
<input
|
||||
id="{{idp}}-name"
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{idp}}-url">
|
||||
{{localize "IT.dialogs.Link.url"}}
|
||||
{{localize "TB.dialogs.Link.url"}}
|
||||
</label>
|
||||
<input
|
||||
id="{{idp}}-url"
|
||||
|
|
|
|||
|
|
@ -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,12 +19,12 @@
|
|||
></string-tags>
|
||||
|
||||
<label for="{{meta.idp}}-artists">
|
||||
{{localize "IT.common.artists"}}
|
||||
Artists (Optional)
|
||||
</label>
|
||||
<multi-select
|
||||
id="{{meta.idp}}-artists"
|
||||
name="artists"
|
||||
>
|
||||
{{ it-options image.artists artists }}
|
||||
{{ tb-options image.artists artists }}
|
||||
</multi-select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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