Compare commits
1 commit
main
...
feature/ur
| Author | SHA1 | Date | |
|---|---|---|---|
| c70b8f938a |
18 changed files with 178 additions and 393 deletions
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
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
|
||||
run: mkdir -p storage/db && mv importable/*.json storage/db
|
||||
|
||||
- name: Compress files
|
||||
run: zip -r release.zip langs module styles storage templates README.md assets LICENSE ${{env.MANIFEST}}
|
||||
|
|
|
|||
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,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,61 +18,67 @@ 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);
|
||||
};
|
||||
};
|
||||
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),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -12,15 +12,15 @@
|
|||
"tags": "Tags",
|
||||
"select": "Select",
|
||||
"links": "Links",
|
||||
"sort-by": "Sort By",
|
||||
"page-reset-warning":"Changing any of these will reset your page to 1."
|
||||
"sort-by": "Sort By"
|
||||
},
|
||||
"apps": {
|
||||
"ArtBrowser": {
|
||||
"selected": "{current}/{required} Selected",
|
||||
"upload-image": "Upload Image",
|
||||
"no-results": "No results were found for that search",
|
||||
"select-image": "Select image"
|
||||
"select-image": "Select image",
|
||||
"page-reset-warning": "Changing any of these will reset your page to 1."
|
||||
},
|
||||
"ArtistBrowser": {
|
||||
"sort-options": {
|
||||
|
|
@ -48,11 +48,9 @@
|
|||
"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"
|
||||
|
|
@ -76,7 +74,7 @@
|
|||
"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}"
|
||||
"invalid-import-type": "Invalid import type: {type}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@
|
|||
"maximum": 13
|
||||
},
|
||||
"authors": [
|
||||
{ "name": "Oliver", "email": "hello@varify.ca" }
|
||||
{ "name": "Oliver" }
|
||||
],
|
||||
"url": "https://git.varify.ca/Foundry/image-tagger",
|
||||
"bugs": "https://git.varify.ca/Foundry/image-tagger/issues",
|
||||
"manifest": "",
|
||||
"download": "",
|
||||
"esmodules": [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ImageApp } from "./apps/ImageApp.mjs";
|
|||
|
||||
// Utils
|
||||
import { convertToWebp, getFileSize, hashFile, lastModifiedAt } from "./utils/fs.mjs";
|
||||
import { importFromURL } from "./utils/imports.mjs";
|
||||
|
||||
export const api = foundry.utils.deepFreeze({
|
||||
Apps: {
|
||||
|
|
@ -21,5 +22,6 @@ export const api = foundry.utils.deepFreeze({
|
|||
lastModifiedAt,
|
||||
getFileSize,
|
||||
},
|
||||
importFromURL,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`] });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 { 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,8 +47,6 @@ 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: [] };
|
||||
|
||||
|
|
@ -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,11 @@ 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 +105,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 +148,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,7 +168,7 @@ 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];
|
||||
|
||||
|
|
@ -264,38 +214,6 @@ export class ImageApp extends
|
|||
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` ] });
|
||||
};
|
||||
|
||||
/** @this {ImageApp} */
|
||||
static async #removeEditingImage() {
|
||||
this.#file = null;
|
||||
|
|
@ -304,11 +222,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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,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;
|
||||
|
|
|
|||
70
module/utils/imports.mjs
Normal file
70
module/utils/imports.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { getFile } from "./fs.mjs";
|
||||
|
||||
const { Dialog } = foundry.applications.api;
|
||||
const { fetchJsonWithTimeout, mergeObject } = foundry.utils;
|
||||
|
||||
const TRUSTED_DOMAINS = new Set([
|
||||
`git.varify.ca`,
|
||||
`cdn.varify.ca`,
|
||||
window.location.host,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Imports an existing JSON DB into the module's current database.
|
||||
*
|
||||
* @param {string} url The URL pointing to a raw JSON file
|
||||
* @param {`keep`|`replace`|`merge`} type The type of import that should be performed
|
||||
*/
|
||||
export async function importFromURL(url, type) {
|
||||
|
||||
if (![`keep`, `replace`, `merge`].includes(type)) {
|
||||
ui.notifications.error(_loc(`IT.notifs.error.invalid-import-type`, { type }));
|
||||
return false;
|
||||
};
|
||||
|
||||
const domain = new URL(url);
|
||||
if (!TRUSTED_DOMAINS.has(domain.host)) {
|
||||
const confirmed = await Dialog.confirm({
|
||||
content: `An import is trying to happen from: <br>${url}<br>Do you trust this website?`,
|
||||
rejectClose: false,
|
||||
});
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await fetchJsonWithTimeout(url);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
};
|
||||
|
||||
const images = await getFile(`storage/db/images.json`);
|
||||
const artists = await getFile(`storage/db/artists.json`);
|
||||
|
||||
switch (type) {
|
||||
case `keep`: {
|
||||
importKeepExisting(data, images, artists);
|
||||
break;
|
||||
};
|
||||
case `replace`: {
|
||||
importReplaceExisting(data, images, artists);
|
||||
break;
|
||||
};
|
||||
case `merge`: {
|
||||
await importMerge(data, images, artists);
|
||||
break;
|
||||
};
|
||||
};
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
async function importKeepExisting(data, imageDB, artistDB) {};
|
||||
|
||||
async function importReplaceExisting(data, imageDB, artistDB) {};
|
||||
|
||||
async function importMerge(data, imageDB, artistDB) {
|
||||
throw `Merge-based importing is not implemented yet`;
|
||||
};
|
||||
|
|
@ -5,10 +5,6 @@
|
|||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
file-picker {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
header, footer {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<p>
|
||||
{{localize "IT.common.page-reset-warning"}}
|
||||
{{localize "IT.apps.ArtBrowser.page-reset-warning"}}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
<form autocomplete="off" class="filters">
|
||||
<p>
|
||||
{{localize "IT.common.page-reset-warning"}}
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<label for="{{meta.idp}}-name">
|
||||
{{localize "IT.common.name"}}
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{{ localize "IT.apps.ImageApp.image-label" }}
|
||||
</label>
|
||||
{{#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}}
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue