Add initial implementation for AscAssetManager module

This commit introduces the following key features:

- **New `.gitignore`**:
  - Ignores `.vscode`, `.eslintrc.js`, and `dist/module.zip`.

- **Core functionality**:
  - Created `src/main.js` to implement the `AscAssetManager` class with methods to manage file uploads, categories, tags, and settings registration.
  - Introduced hooks for rendering and managing custom upload dialogs and forms.

- **Configuration and settings**:
  - Added `src/settings.js` to register settings for features like enabling/disabling, root directory configuration, and theme selection.

- **Templates**:
  - Added `upload-choose.hbs` for the file selection dialog.
  - Added `upload-form.hbs` for the file metadata customization dialog.

- **Utilities**:
  - Added `src/utils.js` with helper functions for file parsing, metadata handling, and filename creation.

- **Styling**:
  - Added `style.css` for styling upload areas.

- **Module metadata**:
  - Added `module.json` with module details, compatibility, and dependencies.

This commit establishes the foundational structure and functionality for the AscAssetManager module.
This commit is contained in:
2025-01-19 10:52:44 -06:00
commit aba086d7f0
8 changed files with 349 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.vscode
.eslintrc.js
dist/module.zip

164
src/main.js Normal file
View File

@@ -0,0 +1,164 @@
const LOG_PREFIX = "asc-asset-manager | "
import { registerSettings } from "./settings.js";
import { parseFileName, createMetaFileName } from "./utils.js";
export class AscAssetManager {
static ID = 'asc-asset-manager'
static TEMPLATES = {
UPLOAD_CHOOSE:`modules/${this.ID}/templates/upload-choose.hbs`,
UPLOAD_FORM:`modules/${this.ID}/templates/upload-form.hbs`
}
static getDirectory () {
return game.settings.get(this.ID, "rootDirectory")
}
static fileCategories = [
{id: "npcn", label: "NPC (Named)"},
{id: "npcu", label: "NPC (Unnamed)"},
{id: "scene", label: "Scene Background"},
{id: "pc", label: "PC"},
{id: "inset", label: "Inset"},
{id: "vehicle", label: "Vehicle"},
{id: "weapon", label: "Weapon"}
]
static fileTags = [
{id:"tk", label: "Token"},
{id:"sq", label: "Square"}
]
static log(force, ...args) {
console.log(this.ID, '|', ...args);
}
}
Hooks.on("ready", function() {
AscAssetManager.log(false, "Deno nodule reody")
registerSettings(AscAssetManager.ID);
});
Hooks.on("ascAssetManager.renderUploadChoose", () => {
renderTemplate(AscAssetManager.TEMPLATES.UPLOAD_CHOOSE).then((content)=>{
new Dialog({
title: "Choose",
content: content,
buttons: [],
render: (html)=> {
AscAssetManager.log(false, 'Rendering choose window')
const uploadArea = html.find("#upload-area");
const fileInput = html.find("#file-input");
const form = html.find("form")
// Handle drag-and-drop events
uploadArea.on("dragover", (event) => {
event.preventDefault();
uploadArea.css("background-color", "#e0f7fa");
});
uploadArea.on("dragleave", () => {
uploadArea.css("background-color", "");
});
uploadArea.on("drop", (event) => {
event.preventDefault();
uploadArea.css("background-color", "");
const files = event.originalEvent.dataTransfer.files;
if (files.length > 0) {
AscAssetManager.log(false, 'file dropped change')
// Update the file input with the dropped file
const dataTransfer = new DataTransfer();
for (let file of files) {
dataTransfer.items.add(file); // Add the first dropped file
}
fileInput.files = dataTransfer.files;
fileInput.trigger('change')
}
});
fileInput.on("change", (event)=> {
const files = fileInput.files
// Show feedback to the user
uploadArea.find("p").text(`Selected file: ${Array.from(files).map(f=>f.name).join(', ')}`);
})
form.on('submit', (event) => {
for (let file of fileInput.files) {
Hooks.callAll("ascAssetManager.renderUploadForm", {file})
}
})
}
}).render(true)
})
})
Hooks.on("ascAssetManager.renderUploadForm", (data)=>{
const templateData = {fileCategories: AscAssetManager.fileCategories, fileTags: AscAssetManager.fileTags}
renderTemplate(AscAssetManager.TEMPLATES.UPLOAD_FORM, templateData).then(content => {
let {file} = data
new Dialog({
title: "Simple Popup",
content: content,
buttons: [],
render: (html) =>{
AscAssetManager.log('file', file)
const uploadArea = html.find("#upload-area");
const fileInput = html.find("#file-input");
const form = html.find('#upload-form');
const namePreview = html.find('#name-preview')
const baseName = form.find("input[id='baseName']")
fileInput.val(file.name)
const reader = new FileReader();
reader.onload = (e) => {
const img = form.find("img");
const blob = new Blob([e.target.result], { type: file.type });
const blobUrl = URL.createObjectURL(blob)
AscAssetManager.log(false, blobUrl,img)
img.attr("src", blobUrl)
};
reader.readAsArrayBuffer(file)
baseName.val(parseFileName(file.name).fileName)
// Handle click to open file selector
// uploadArea.on("click", () => fileInput.click());
form.on('change', (event)=>{
const fileExtension = parseFileName(fileInput.val()).fileExtension
const fileCategory = form.find("select[name='file-category'] option:checked").val()
const fileTags = form.find("input[name='file-tags']:checked").map(function(){
return $(this).val()
}).get()
AscAssetManager.log(false, 'form changed', {fileCategory, fileTags, baseName})
const new_filename = createMetaFileName(`${baseName.val()}.${fileExtension}`,{
category: fileCategory,
tags: fileTags
})
namePreview.val(new_filename)
})
form.on('submit', (event) => {
AscAssetManager.log(false, "Form Submitted");
event.preventDefault();
const renamedFile = new File([file], namePreview.val(), {type:file.type})
FilePicker.upload(
"data",
AscAssetManager.getDirectory(),
renamedFile,
{
notify: true
}
)
.then((res)=>AscAssetManager.log(false, 'Uploaded!', res))
.catch((e)=>ui.notifications.error('Error uploading', e))
})
}
}).render(true);
})
})

21
src/module.json Normal file
View File

@@ -0,0 +1,21 @@
{
"id": "asc-asset-manager",
"title": "asc-asset-manager",
"version": "0.1.0",
"compatibility": {
"minimum": "12",
"verified": "12"
},
"authors": [
{
"name": "Anthony Correa",
"flags": {}
}
],
"esmodules": [
"./main.js"
],
"styles": [
"./styles/style.css"
]
}

48
src/settings.js Normal file
View File

@@ -0,0 +1,48 @@
export function registerSettings(ID) {
console.log("Registering settings for My Module...");
// Register the "enableFeature" setting
game.settings.register(ID, "enableFeature", {
name: "Enable Feature",
hint: "Enable or disable the special feature of this module.",
scope: "world",
config: true,
type: Boolean,
default: true,
onChange: value => {
console.log(`Enable Feature setting changed to: ${value}`);
}
});
// Register the "customMessage" setting
game.settings.register(ID, "rootDirectory", {
name: "Root Directory",
hint: "Root Directory to save files",
scope: "client",
config: true,
type: String,
default: `worlds/${game.data.world.id}/`,
filePicker: "folder",
onChange: value => {
console.log(`Custom Message changed to: ${value}`);
}
});
// Register a dropdown setting
game.settings.register(ID, "theme", {
name: "Select Theme",
hint: "Choose a theme for the module.",
scope: "world",
config: true,
type: String,
choices: {
light: "Light Theme",
dark: "Dark Theme",
system: "Use System Default"
},
default: "system",
onChange: value => {
console.log(`Theme changed to: ${value}`);
}
});
}

7
src/styles/style.css Normal file
View File

@@ -0,0 +1,7 @@
#upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
cursor: pointer;
background-color: #f9f9f9;
}

View File

@@ -0,0 +1,7 @@
<form action="">
<div id="upload-area">
<p>Drag files here to upload</p>
<input type="file" id="file-input" style="display:none;">
</div>
<button type="submit">Submit</button>
</form>

View File

@@ -0,0 +1,41 @@
<form id="upload-form">
<input type="text" id="file-input" style="" readonly>
<img height=100px>
<div class="form-group">
<label>Category</label>
<div class="form-fields">
<select type="radio" name="file-category" required>
{{#each fileCategories}}
<option value="{{id}}">
{{label}}
</option>
{{/each}}
</select>
</div>
</div>
<div class="form-group">
<label>Name</label>
<input type="text" id="baseName" value="">
</div>
<div class="form-group">
<label for="tags">Tags</label>
<div class="form-fields">
{{#each fileTags}}
<input type="checkbox" name="file-tags" value="{{id}}">
{{label}}
{{/each}}
</div>
</div>
<div class="form-group">
<label>Preview</label>
<div class="form-fields"><input type="text" id="name-preview" readonly></div>
</div>
<div style="margin-top: 15px; text-align: right;">
<button type="submit">Upload</button>
</div>
</form>

58
src/utils.js Normal file
View File

@@ -0,0 +1,58 @@
export function getFileExtension (fileName) {
const regex = new RegExp('[^.]+$');
return fileName.match(regex)?.[0];
}
export function getFileBasename (fileName) {
const regex = new RegExp('^[^.]+');
return fileName.match(regex)?.[0]
}
export function parseFileName (s) {
const pattern = /(?<base>^.+)\.(?<extension>[^.]+?$)/
const match = s.match(pattern)
if (match && match.groups){
return {
fileName: match.groups.base,
fileExtension: match.groups.extension,
}
} else {
return {fileName: s}
}
}
const createFileNameDefaultOptions = {
meta_separator : "__",
category_prefix : "",
tag_prefix: "<>",
tag_separator: "",
lowerCase: true,
noSymbols: true,
}
export function createMetaFileName(originalFileName, meta={category:"", tags:[]}, options) {
const {
meta_separator,
category_prefix,
tag_prefix,
tag_separator,
lowerCase,
noSymbols
} = {...createFileNameDefaultOptions,...options}
let {fileName, fileExtension} = parseFileName(originalFileName)
const category_string = `${category_prefix}${meta.category}`
const tag_string = `${meta.tags.map(tag=>tag_prefix+tag).join('')}`
fileName = `${fileName}${meta_separator}${category_string}${tag_separator}${tag_string}`
if (lowerCase) {
fileName = fileName.toLowerCase()
}
if (noSymbols) {
fileName = fileName.replace(/[^a-z0-9_-]/g,'-')
}
return `${fileName}.${fileExtension}`
}