cloning original project from github

This commit is contained in:
Adam 2024-01-28 15:59:53 -06:00
commit 18318349c1
130 changed files with 20423 additions and 0 deletions

333
README-old.md Normal file
View File

@ -0,0 +1,333 @@
# Journal to Canvas Slideshow
- [WARNING](#warning)
- [Journal to Canvas Slideshow](#journal-to-canvas-slideshow)
- [Feature Requests & The Future of Journal to Canvas Slideshow 01-16-2022](#feature-requests--the-future-of-journal-to-canvas-slideshow-01-16-2022)
- [WARNING](#warning)
- [About the Project](#about-the-project)
- [Built With](#built-with)
- [Installation and Getting Started](#installation-and-getting-started)
- [How to Use](#how-to-use)
- [Full Video Tutorial (Walkthrough of New Features Included)](#full-video-tutorial-walkthrough-of-new-features-included)
- [Written Tutorial with Gifs](#written-tutorial-with-gifs)
- [Videos in Journal](#videos-in-journal)
- [Display In Window and Module Settings](#display-in-window-and-module-settings)
- [Auto-Activate or Auto-Show Toggle](#auto-activate-or-auto-show-toggle)
- [Changelog](#changelog)
- [**v0.1.8 - v0.1.9** - 2022-01-16](#v018---v019---2022-01-16)
- [**v0.1.7** - 2021-08-26](#v017---2021-08-26)
- [**v0.1.6** - 2021-06-09](#v016---2021-06-09)
- [**v0.1.5** - 2021-05-23](#v015---2021-05-23)
- [Default Tile Control Tools](#default-tile-control-tools)
- [Tile Control Tools with Hide Tile Buttons Turned On](#tile-control-tools-with-hide-tile-buttons-turned-on)
- [**v0.1.4** - 2021-03-19](#v014---2021-03-19)
- [**v0.1.3** - 2021-01-22](#v013---2021-01-22)
- [**v0.1.2** - 2021-01-03](#v012---2021-01-03)
- [**v0.1.1** - 2020-12-28](#v011---2020-12-28)
- [**Added**](#added)
- [**Changes**](#changes)
- [Roadmap](#roadmap)
- [Code Explanation](#code-explanation)
- [Motivation](#motivation)
- [Credits](#credits)
- [Contact Me](#contact-me)
<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
## Feature Requests & The Future of Journal to Canvas Slideshow 01-16-2022
With my school workload, I unfortunately don't have time to implement any new features for this module going forward. I will try to fix any bugs that pop up, tweak any minor issues, and continue to keep it updated for major Foundry versions. If you want more/cooler/more flexible features, I would suggest checking out @DarKDinDoN 's amazing module Share Media, which was inspired by and improves upon this module :)
https://github.com/DarKDinDoN/share-media
## WARNING
If your image's source is from somewhere online, there's a chance there will be a CORS issue, and clicking on the image in the journal won't change the tile due to this error. Take a look at the dev console (F12 on Windows) and see if there's an error like this: https://i.imgur.com/SBHQPka.png
If so, for now, try saving/downloading the image and placing it in your Foundry data folder somewhere, then in the journal entry, have the image's source link to that file path instead. I'll try to see if I can find a way around this.
# About the Project
This project brings functionality to images and videos in your Foundry journal entries and allows you to create a Display Scene. You can click on journal images and videos, which will change a tile in the Display Scene to match the image in the journal.
!["Demonstration of module"](https://media4.giphy.com/media/nCBKGGweEGVYObw3ZB/giphy.gif)
This is meant to make sharing art with your players while narrating or reading your notes a lot more seamless. It fits well with Theater of the Mind style play.
## Built With
- JavaScript
- JQuery
- HTML
# Installation and Getting Started
To install this module, go to the Configuration and Setup options in Foundry VTT, click on "Add-on Modules", then "Install Module".
There will be a text box toward the bottom where you can paste this Manifest URL:
https://raw.githubusercontent.com/EvanesceExotica/Journal-To-Canvas-Slideshow/master/module.json
Then click "Install".
# How to Use
## Full Video Tutorial (Walkthrough of New Features Included)
Please click on the image below for a full video tutorial with walk-throughs of new features included:
[![Alt text](https://img.youtube.com/vi/t4NX55vs9gU/0.jpg)](https://www.youtube.com/watch?v=t4NX55vs9gU)
(Click this link if the above doesn't work for some reason):
https://youtu.be/t4NX55vs9gU
---
## Written Tutorial with Gifs
(**NOTE:** The instructions below are a bit outdated compared to the above video which includes explanations of new features and settings, but they still generally apply. )
1. Open your game in FoundryVTT and navigate to the "Scenes" tab.
2. At the bottom of this tab, there will be a button that reads "Create or Show Display Scene".
3. Click that button. A new scene will be generated named "Display", with a single tile with a dark background.
!["Demonstration of Create or Show Display Scene button"](https://media4.giphy.com/media/PHOiojIZhG3hyg4uIm/giphy.gif)
4. If you click on this button again while the Display scene already exists, it will activate the Display scene instead of creating a new one. ~~(**Warning**: _Do not_ rename this scene, as the script searches for a scene named "Display" specifically. I may try to change this in the future so it keeps track of and searches for the scene's ID instead.)~~ (No longer true in v0.1.4 -- see Change Log below for details)
5. You can change the scene's background image and add extra tiles for decoration, ~~but
make sure that the very first tile on the canvas is the tile you wish to display your images.~~ (No longer necessary in version 0.1.4 -- see Change Log below for details)
6. Open up a journal entry with images (**Note**: This should work with Image Mode journal entries too). You should notice the images highlight and gain a shadow when you hover over them.
7. Clicking on them will cause the tile in the Display scene to resize/reposition and change to match the image you clicked on.
8. Have fun!
---
## Videos in Journal
This project does support .webm and .mp4 files, however inserting a video into a journal entry requires a different approach than using an image.
1. Check out this page: https://www.w3schools.com/html/html5_video.asp and scroll down a bit to the part that says "Example".
2. Open up your journal entry, click the edit button, then click the button along the top that looks like this "< >" to access the entry's source code.
!["Clicking on source code button"](https://i.imgur.com/dUgvvBS.png?1)
3. You can copy and paste the code example shown on the above w3schools site into the source code, and change the file path in quotations (where it says <source="movie.mp4" type="video/mp4">, change the 'movie.mp4' part to be the file path of your .webm or .mp4 video and the type to the matching type "mp4" or "webm").
!["Placing video element in source code"](https://i.imgur.com/uXnoRxU.png=800x600)
4. You can then click on the video, and it will change the Display tile.
!["Clicking on vid to change tile"](https://media0.giphy.com/media/rF4mjVrm5L6Y4MrmSx/giphy.gif)
---
## Display In Window and Module Settings
The module now includes a feature to display your journal images in a window rather than in a dedicated scene. This will allow you to click through and present your 'slideshow' to your players even while they're on another scene, like a battlemap.
!["Display in Window"](https://media.giphy.com/media/2ubtah0ZPWcWtpzyh6/giphy.gif)
(Note: If above gif doesn't display, please click this link to see it: https://media.giphy.com/media/2ubtah0ZPWcWtpzyh6/source.gif)
In order to use this new feature
1. navigate to the "Journal" tab and click on the button that says "Create or Show Display Entry". A journal will be created named "Display Journal".
(_Note_: You can click this button again after the Display Journal is created to show the Display Journal to all of your players.)
2. Navigate to "Journal to Canvas Slideshow"'s settings in the "Module Settings" tab of the "Configure Game Settings" window.
3. You will see a setting called "Display Location", and you can switch it from "Scene" to "Window".
!["Module settings"](https://i.imgur.com/djMriuR.jpg)
4. This will allow clicked-on journal images to be shown in the Display Journal rather than in the Display scene, as demonstrated in the above gif.
( _Note_: For right now, to 'clear' the Display Journal, (while it's selected as the Display Location in the module settings), you can click on the same "ClearDisplay" button under the Tiles section of the scene controls as you would use to clear the tile in the Display scene. This will set the image to a blank/transparent image. I intend to add a button to the actual window later on. )
### Auto-Activate or Auto-Show Toggle
A toggleable option is also included in the module settings to automatically activate the "Display" scene OR automatically show the "Display Journal" window to players when you click on a journal image, depending on which option you have selected for the Display Location setting.
!["Module settings1"](https://i.imgur.com/k3CwDBa.jpg)
(_Note_: A notification appears each time you click a journal image while having this setting turned on in conjunction with the "Window" option for the Display Location setting. This is because it uses the same functionality as the "Show Players" button at the top of a journal in image mode. I'll have to figure out how to change that if possible.)
# Changelog
## **v0.1.8 - v0.1.9** - 2022-01-16
**CHANGED**
- Updated for Foundry version 9. Check in "releases" for the version still compatible with version 8.
## **v0.1.7** - 2021-08-26
**CHANGED**
- Integrated features from pull requests, such as item images now being able to be clicked on and displayed. (Thanks, @DarKDinDoN !)
- Added setting to hide or change how "Toggle Display Location" button in journal header displays.
## **v0.1.6** - 2021-06-09
Updated module to work with Foundry v8.6
## **v0.1.5** - 2021-05-23
**ADDED**
**Major**
- NEW: Ability to _right click_ on actor sheet character images to display them the same as journal images.
- NEW: Ability to display Journal-to-Canvas-Slideshow tools within a dialog rather than as tile control tools.
See the settings for **Use Actor Sheet Images** and **Hide Tile Buttons** in the updated module settings below.
!["New Settings"](https://i.imgur.com/AfHLPSG.png)
### Default Tile Control Tools
The default tile control tools with the Hide Tile Buttons setting disabled.
A new button is there called "Switch Display Location" that will display a dialog that allows you to switch display locations without needing to go into the module's settings.
!["Switch Display Location Button"](https://i.imgur.com/3XLHTku.png)
!["Switch Display Location Dialog"](https://i.imgur.com/CBSidW0.png)
**Note**: Journal entries now have a button in the header that allows you to switch the display location as well.
**Note**: You can switch away from the tile control tools and then back again to "refresh" if you enable or disable the Hide Tile Buttons setting.
---
### Tile Control Tools with Hide Tile Buttons Turned On
With the Hide Tile Buttons setting enabled, all Journal-to-Canvas-Slideshow buttons will not be displayed except for the "Clear Display" button, and a new button that says "Show Slideshow Config".
To show the other functions, click on the button in the tile control tools that says "Show Slideshow Config".
!["Hide Tile Buttons Setting Turned On"](https://i.imgur.com/6a7oxpt.png)
The following dialog will appear with buttons with all the functionality, such as creating Display and Bounding Tiles, Setting a URL image, and switching between display locations.
!["Hide Tile Buttons Dialog"](https://i.imgur.com/u5DWfMc.png)
---
**CHANGED**:
- Many features now work with VIEWED scene rather than ACTIVE scene, such as the bounding tiles.
---
## **v0.1.4** - 2021-03-19
**ADDED**
**Major**:
- NEW: Bounding Tiles implemented by @Occidio
- NEW: Display Tiles that along with Bounding Tiles can be added to _any scene_.
- NEW: Display images via copy-pasting URL feature implemented by @p4535992
- NEW: Display in Window feature alternative implemented by @DarKDinDoN
- NEW: Extra settings to accomodate the above new features -- please check the settings menu and reselect your prefered settings.
**Changes**:
**Major:**
- Special "Display Tiles" now created via button in Tile controls menu. Flagged by script, so no longer have to be very first tile in scene.
- **Warning**: Please replace regular tile in pre-made Display Scenes with new Display Tile, else the script will not detect them.
## **v0.1.3** - 2021-01-22
**ADDED**
**Major**:
- NEW: Added option to display journal images in a window rather than display scene
- NEW: Module settings
## **v0.1.2** - 2021-01-03
**Major:**
- Fixed an incompatability issue with the Call of Cthulhu 7e (CoC7) system.
## **v0.1.1** - 2020-12-28
### **Added**
**Major:**
- Added "Clear Display" button in Tiles scene control buttons. Will set 'slideshow' tile to a transparent image.
**Minor:**
- More visual effects when hovering over and clicking images in journal, for more user feedback
- Changed cursor to pointer on hover of journal images
### **Changes**
- Clicking on image in journal no longer activates the 'Display' scene if a different scene is active. Plan to add functionality later to toggle this behavior.
(Red arrow pointing at new 'Clear Display' button')
!["Location of clear button"](https://i.imgur.com/aPtU9QL.jpg)
!["Showing off updates"](https://media2.giphy.com/media/sIKIPBhN3c5vLPVxGu/giphy.gif)
# Roadmap
- I next intend to add a way to more easily toggle between the various different settings (Display in Window vs Display in Scene, etc.) without needing to go all the way to the settings menu.
- I may possibily implement a way to have multiple Display Tiles in a single scene, but I will need to think of the best way to implement this.
# Code Explanation
The real 'magic' basically surrounds a single tile. Each time I click on an image in the journal entry, the tile's image source is updated with the source changed to that of the image in the journal that I clicked on.
The script basically goes through the images in a journal entry when it renders, adds a 'clickable-image' class to them, and then looks through the elements with that class and adds an event handler when they're clicked.
This event handler handles changing the tile's source in a specific scene called "Display" to that of the image in the journal.
# Motivation
I love being able to share art with my players. Though I know some groups prefer complete Theater of the Mind using only imagination, images help me and my group with immersion, improv, and with 'pseudo-TOTM' battles when I don't want to use a battlemap.
The motivation behind this project is to have a journal entry with many images, that you can click through to show your players art for characters, locations, monsters, etc. while not having to miss a beat if you also need to narrate.
Years ago I tried to do this using Google Slides, but it was clunky and a pain in the butt. Then another VTT (GM Forge) came along that had this feature and I absolutely loved it, but the VTT stopped being supported.
I then tried to replicate the feature in other VTTs but nothing quite worked the way I wanted.
Upon getting Foundry VTT, I finally learned Javascript, with some help from some StackOverflow posts and reading over the source code for the Image-Drop, Sound-Link and Journal-Links modules helped me figure out how to implement this feature on my own.
# Credits
The following users for their awesome contributions to this module:
@p4535992 https://github.com/p4535992
@Occidio https://github.com/Occidio
@DarKDinDoN https://github.com/DarKDinDoN
@zeel01 for the implementation of their MultiMediaPopout class from their module Show art: https://github.com/zeel01/TokenHUDArtButton
GMForge -- this feature was originally built into [GMForge](https://store.steampowered.com/app/842250/GM_Forge__Virtual_Tabletop/) which I used while it was still supported. I adored this feature and wanted to bring it to Foundry VTT, as I could never quite find another solution that scratched this itch and satisfied me in the same way.
My figuring out how to do this in FoundryVTT is thanks largely to studying the source code of the modules [Drag Upload](https://github.com/cswendrowski/FoundryVTT-Drag-Upload) by _cswendrowski_, [Image-Drop](https://gitlab.com/mesfoliesludiques/foundryvtt-image-drop) by _U~man_, [Journal-Links](https://github.com/Sigafoos/journal-links) by _Sigafoos_ and [Sound-Link](https://github.com/superseva/sound-link) by _superseva_.
Thanks to Joe Neeves whose tabletop art from Gumroad: https://gumroad.com/limonium provided the decorative tiles.
# Contact Me
_Discord_: Eva#3782
_Email_: EvanesceExotica@gmail.com

116
README.md Normal file
View File

@ -0,0 +1,116 @@
- [JTCS - Art Gallery](#jtcs---art-gallery)
- [Features, Walkthrough and Changelog](#features-walkthrough-and-changelog)
- [Walkthrough/Tutorial](#walkthroughtutorial)
- [Major Feature Additions and Improvements](#major-feature-additions-and-improvements)
- [**IMPROVED**](#improved)
- [**NEW**](#new)
- [Previous Versions Documentation](#previous-versions-documentation)
- [Changelog/Release Notes](#changelogrelease-notes)
- [Recommended Modules](#recommended-modules)
- [Contributors & Thanks](#contributors--thanks)
- [Code Contributors](#code-contributors)
- [Visual FX/UI/Assets Contributions](#visual-fxuiassets-contributions)
# JTCS - Art Gallery
Journal to Canvas Slideshow (Now renamed to "JTCS - Art Gallery") has received a major overhaul and several big feature updates.
Now with support for Foundry v10!
In addition to big updates to the core features, and major QOL improvements, the biggest new feature is called the "Art Gallery" which allows you to display multiple different images on multiple different "Art Tiles" in the same scene, bound by "Frame Tiles" that keep the Art Tiles scaled within a certain size.
Here is a video demonstration:
<https://user-images.githubusercontent.com/13098820/193938899-f5920be7-6148-4ac7-9738-8a5ee7d420e9.mp4>
Note: While the wooden scene background and overlay art is included as part of the module in a compendium pack, the tabletop "trinket" assets seen in above video are NOT included as part of the module. They are by [Joe Neeves (Limonium) on Gumroad](https://limonium.gumroad.com/?recommended_by=library).
## Features, Walkthrough and Changelog
### Walkthrough/Tutorial
A detailed walkthrough of the new features can be found here: [Features and Walkthrough](features-and-walkthrough.md)
### Major Feature Additions and Improvements
#### **IMPROVED**
- Added support for Foundry VTT v10, with backwards compability for v9
- Improved image-share controls in actor, item, and journal sheets
- Various Display methods can be easily accessed and activated by hovering over an image and clicking on one of these controls, rather than having to change the method via settings
- the Original "click on an image to display it on the canvas" functionality remains intact.
<img alt="Image Controls Demo" src="https://user-images.githubusercontent.com/13098820/193946807-644aed5c-e6ad-402f-a85f-91947343dbf7.png" width="45%"/>
#### **NEW**
- Gallery Tiles
- "Gallery Tiles" feature introduced, allowing the creation of "Art Tiles" and "Frame Tiles" (which are an overhauled and much more robust version of the old 'Display Tiles' and 'Bounding Tiles' feature)
- Gallery Tiles can be created, linked, configured, and given unique names in a new configuration application called the "Scene Gallery Config"
<img alt="Scene Gallery Config App - Light Mode" src="https://user-images.githubusercontent.com/13098820/193947720-ed4a388f-e22f-466c-b14b-b26c64042c7c.png" width="45%"/>
- Settings and Customization
- JTCS Art Gallery Settings application that can be launched from multiple locations and includes several customization options
- Canvas tiles highlight with colored overlays whenever you hover a connected UI item, to ensure you can easily find them.
- overlay colors are customizable
- Gallery Tile color customization options in the JTCS Art Gallery Settings App
<img alt="Tile Colors Demo" src="https://user-images.githubusercontent.com/13098820/193948186-86e8f4b8-7803-48bc-acef-93bbf54a0a67.png" width="75%"/>
- Demonstration of how the colors translate to affect the UI elements and canvas tile overlays
<img width="75%" alt="Color Demo Template" src="https://user-images.githubusercontent.com/13098820/193948287-2004ca17-a594-4d92-aec5-ad6e616abc52.png">
- Color customization of elements UI in JTCS Art Gallery apps, including a default light and dark theme.
<img alt="Background Color Change Demo" src="https://user-images.githubusercontent.com/13098820/193948120-316f5f8c-9ea9-4ca2-b42f-cdc3ea7f8eb8.png" width="75%"/>
<img alt="Scene Gallery Config App - Dark Mode" src="https://user-images.githubusercontent.com/13098820/193947490-3baf8588-c679-4375-be76-0ad88ff892de.png" width="45%"/>
<img alt="Scene Gallery Config App - Light Mode" src="https://user-images.githubusercontent.com/13098820/193947720-ed4a388f-e22f-466c-b14b-b26c64042c7c.png" width="45%"/>
- Compendiums
- Compendium pack of macros with featuring utilities to make moving and scaling tiles easier
- Compendium pack of premade scenes displaying demo setups of Gallery tiles, including a scene meant to act as your default "Display Scene"
- Compendium pack of Journal Entries including a scene meant to act as your default "Display Journal"
### Previous Versions Documentation
For access to the older/previous version documentation, please see [Old Readme](README-old.md)
### Changelog/Release Notes
Go to [Release Notes](release-notes.md) to view the full changelog/release notes.
## Recommended Modules
- [Tile Sort] by [theripper93](https://github.com/theripper93) - highly recommended for taking the pain out of layering tiles
- [Quickscale](https://foundryvtt.com/packages/quickscale) by [unsoluble](https://foundryvtt.com/community/unsoluble) - will make scaling and rotating tiles easier
- [FX Master](https://foundryvtt.com/packages/fxmaster) by [ghost](https://foundryvtt.com/community/saluu) - for cool visual effects on your Art Gallery scenes
- [Token Magic FX](https://foundryvtt.com/packages/tokenmagic) by [SecretFire](https://foundryvtt.com/community/galaktor) - for adding add filters and effects to tiles
## Contributors & Thanks
Thanks to everyone, both users and contributors, for being patient with me as this project has evolved. This module was one of my first forays into the realm of web development, and I've learned so much since its inception, both through schooling and the painstaking but fulfilling process of gradually trying to make my modules better and better.
This update was a huge undertaking for me, but aso an amazing learning experience, and I hope to continue adding improvements as needed.
I hope everyone enjoys!
### Code Contributors
Thank you to everyone who contributed, added and suggested features, and helped me out while I was still a beginner, including:
- <https://github.com/Occidio> - @Occidio
- <https://github.com/MaximeTKT> - @MaximeTKT
- <https://github.com/p4535992> - @P4535992
### Visual FX/UI/Assets Contributions
- FoundryVTT UI Theme shown in demo vids and images is [Polished UI](https://foundryvtt.com/packages/polished-ui) by [erizocosmico](https://foundryvtt.com/community/erizocosmico)
- Visual FX - [FX Master](https://foundryvtt.com/packages/fxmaster)
- Tabletop Trinket Assets by Joe Neeves (Limonium) on Gumroad - [Limonium's Gumroad Library](https://limonium.gumroad.com/?recommended_by=library)

View File

@ -0,0 +1,20 @@
# Attributions:
## Arrow Icons:
### Url
https://game-icons.net/tags/arrow.html
### File Paths
"assets\contract.svg"
"assets\multi-directions.svg"
### Authors:
Lorc, Delapouite & contributors
### License
CC BY 3.0

BIN
assets/Bounding_Tile.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
assets/Character-Frames.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/DarkBackground.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/FramesBG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

1
assets/contract.svg Normal file
View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="" style="" transform="translate(0,0)"><path d="M96 64L64 96l48 48-48 48h128V64l-48 48-48-48zm224 0v128h128l-48-48 48-48-32-32-48 48-48-48zM64 320l48 48-48 48 32 32 48-48 48 48V320H64zm256 0v128l48-48 48 48 32-32-48-48 48-48H320z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 385 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
{
"name": "Video Demo",
"content": "<p><video controls=\"controls\" width=\"320\" height=\"240\">\n <source src=\"DemoImages/Videos/Waterfalls.mp4\" type=\"video/mp4\" />\n</video></p>\n<p><video controls=\"controls\" width=\"320\" height=\"240\">\n <source src=\"DemoImages/Videos/StarrySparkles.mp4\" type=\"video/mp4\" />\n</video></p>",
"flags": {
"core": {
"sourceId": "JournalEntry.k3tNlbsTTVCKviiK"
},
"exportSource": {
"world": "moduletestworld",
"system": "cyphersystem",
"coreVersion": "9.269",
"systemVersion": "1.32.3"
}
}
}

View File

@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="" style="" transform="translate(0,0)"><path d="M256 23.9A232.1 232.1 0 0 0 23.9 256 232.1 232.1 0 0 0 256 488.1 232.1 232.1 0 0 0 488.1 256 232.1 232.1 0 0 0 256 23.9zm0 15.87L301.3 153h-90.6l36.9-92.34 8.4-20.89zM256 183c40.2 0 73 32.8 73 73s-32.8 73-73 73-73-32.8-73-73 32.8-73 73-73zm0 18c-30.5 0-55 24.5-55 55s24.5 55 55 55 55-24.5 55-55-24.5-55-55-55zm-103 9.7v90.6L39.77 256l100.93-40.4 12.3-4.9zm206 0L472.2 256 359 301.3v-90.6zM256 231c13.7 0 25 11.3 25 25s-11.3 25-25 25-25-11.3-25-25 11.3-25 25-25zm-45.3 128h90.6L256 472.2l-40.4-100.9-4.9-12.3z" fill="#fff" fill-opacity="1"></path></g></svg>

After

Width:  |  Height:  |  Size: 712 B

9
debug.log Normal file
View File

@ -0,0 +1,9 @@
[0826/121332.698:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/121332.918:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/122428.170:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/122428.356:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/122447.230:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/122447.395:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/122748.143:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/123336.898:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)
[0826/123337.074:ERROR:registration_protocol_win.cc(102)] CreateFile: The system cannot find the file specified. (0x2)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

308
features-and-walkthrough.md Normal file
View File

@ -0,0 +1,308 @@
# JTCS Art Gallery
### Table of Contents
- [JTCS Art Gallery](#jtcs-art-gallery)
- [Table of Contents](#table-of-contents)
- [Display Methods](#display-methods)
- [Window Popouts, Art Journal and Art Scene - (Upgraded Features)](#window-popouts-art-journal-and-art-scene---upgraded-features)
- [URL Image Sharing](#url-image-sharing)
- [URL Sharing Limitations (IMPORTANT)](#url-sharing-limitations-important)
- [URL Image Sharing (Cont.)](#url-image-sharing-cont)
- [The Art Gallery - (New Features!)](#the-art-gallery---new-features)
- [Art Gallery Config](#art-gallery-config)
- [Default Art Tiles](#default-art-tiles)
- [Art Tiles and Frame Tiles](#art-tiles-and-frame-tiles)
- [Frame Tiles vs Decorative Tiles](#frame-tiles-vs-decorative-tiles)
- [Art and Frame Tiles (Cont.)](#art-and-frame-tiles-cont)
- [Gallery Tile Creation](#gallery-tile-creation)
- [Linking Preexisting Tiles](#linking-preexisting-tiles)
- [URL Image Sharing on Specific Art Tiles](#url-image-sharing-on-specific-art-tiles)
- [Utilities, Settings and Customization](#utilities-settings-and-customization)
- [Color Theme and Color Customization](#color-theme-and-color-customization)
- [Custom Colors](#custom-colors)
- [Default Dark and Light Theme](#default-dark-and-light-theme)
- [Tips and Utilities](#tips-and-utilities)
- [Fading Overlapping Elements](#fading-overlapping-elements)
- [Fading Tiles](#fading-tiles)
- [Fading Journals](#fading-journals)
- [Fading the Scene Gallery Config App](#fading-the-scene-gallery-config-app)
(**Note**: Some of the footage below is a teeny bit outdated, with small visual and functional bugs having being fixed since they were recorded; If your interface looks a little bit different than in the videos below, it is due to those updates; but all major functionality should be the same )
# Display Methods
- Hovering over an image on an actor, item, or journal entry sheet will display controls that represent different methods by which you can display that image.
- these controls can be toggled on and off by type or on each individual sheet (see "[Utilities, Settings and Customization](#utilities-settings-and-customization)" section for more information)
- You can also click* on the image itself, which will send it to whichever tile is selected as "Default" in that scene (see "[Default Art Tiles](#default-art-tiles)" section for more information)
## Important Note about Clicking Foundry V10*
In FoundryVTT Version 10, left-clicking on an image in a Journal Sheet now has the core functionality of displaying an image in a window popout.
Instead of overriding this functionality, I instead allowed it to be possible to right-click on Journal Sheet images instead. This right-click functionality was already implemented for images on Item and Actor sheets, as left-clicking on those images causes the file-picker to open.
While both right-clicking and left-clicking in Journal Sheets will send the image to the "Default" tile as stated above, right-clicking will avoid opening the Image Popout.
<https://user-images.githubusercontent.com/13098820/193417395-8f9aef27-7d09-47c2-8f66-a53917edae40.mp4>
> display on default tile demo
## Window Popouts, Art Journal and Art Scene - (Upgraded Features)
Two of the options in the controls allow you to display images in a window popout, or a dedicated Journal Entry
(Note: The window popout option will render a new "popout" window each time you click it, allowing multiple windows for multiple different images, while the Journal Entry option will simply change the image in that specific journal, showing one image at a time, and providing more of a "slideshow" effect.
<https://user-images.githubusercontent.com/13098820/193417364-234769f5-4feb-4d5f-8b2e-14582dfecda9.mp4>
> journal entry popover demo
You can also have a dedicated "Art Scene" that images are sent to. Images displayed through this method will automatically display on the "Default Art Tile" in that Art Scene (see "[Default Art Tiles](#default-art-tiles)" section for more info).
<https://user-images.githubusercontent.com/13098820/193417382-25c0aaca-ccc5-4232-b53e-95fe7bb06205.mp4>
> art scene demo
## URL Image Sharing
You can also share images via URLs through the Scene Gallery Config App.
Clicking on the first button at the top will open up a Dialog window. Here you can paste your image URL, and select whichever display method you would like.
<https://user-images.githubusercontent.com/13098820/193662712-8ae3a10d-e917-44a1-8a59-84575101445b.mp4>
---
### URL Sharing Limitations (IMPORTANT)
Do be aware that for web security reasons, not all image URLs will work.
Some sites do not allow resources like images to be shared "externally".
See these Wikipedia articles for more information:
- [Same-Origin Policy | Wikipedia](https://en.wikipedia.org/wiki/Same-origin_policy)
- [Cross-Origin Resource Sharing | Wikipedia](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
---
### URL Image Sharing (Cont.)
The display methods in the URL Image Sharing Dialog work pretty much identically those available when hovering over a Journal Entry, Actor or Item Sheet.
The below options are included in both:
- Window - will render a new "popout" window each time you click it, allowing multiple windows for multiple different images
- Art Journal - will simply change the image in your Art Journal, showing one image at a time, and providing a "slideshow" effect.
- Art Scene - will automatically display on the "Default Art Tile" in that Art Scene (see "[Default Art Tiles](#default-art-tiles)" section for more info).
<img src="https://user-images.githubusercontent.com/13098820/193738467-8b321398-8ab8-41a9-8b9d-8b764542ac08.png" width="70%"/>
Note: A fourth option to Share URLs on specific tiles will be explained below in the Art Gallery section.
## The Art Gallery - (New Features!)
Clicking the top button on the Sheet Image controls will display a button list of all of the tiles in the scene, and clicking on one of those buttons will display the specific image on that tile
<https://user-images.githubusercontent.com/13098820/193417360-70ceb7b4-0ab2-4160-afe9-6ee620d22ab5.mp4>
> art gallery display demo
### Art Gallery Config
Open the Art Gallery Config to see all of the Gallery Tiles a scene.
You can open it by clicking on a button in the "Tiles" controls, or via utility buttons on any Journal Entry, Actor, or Item sheet.
Contextual instructions about each type of tile will show to the right
Click on the floating i button to toggle contextual instructions on and off.
<https://user-images.githubusercontent.com/13098820/193417374-e2e42fbd-684f-4e08-93f2-426473364348.mp4>
> config highlight demo
The Gallery Tiles shown in the "Art Gallery Config" automatically change depending on which scene you're viewing.
Upon switching a scene, the Config will update to show that particular scene's Art Gallery tiles.
<https://user-images.githubusercontent.com/13098820/193417372-181cfcd0-06ce-4aaa-b8a6-eb71bb9000d4.mp4>
> change config based on scene
### Default Art Tiles
Select a "Default Art Tile" for each scene in the Art Gallery config with Ctrl/Cmd + Click.
(The "Default" tile will automatically be set to the first "linked" Art Tile in a scene )
<https://user-images.githubusercontent.com/13098820/193417379-e38485a1-492d-425d-87de-4862e6406131.mp4>
> default Art Tile demo
Clicking on a sheet image itself, rather than any of its controls, will automatically send it to the "Default Art Tile" in the current scene
<https://user-images.githubusercontent.com/13098820/193417395-8f9aef27-7d09-47c2-8f66-a53917edae40.mp4>
> display on default tile demo
Note: If you haven't manually selected a Default Art Tile, the first Art Tile in a scene that is linked to a tile on the canvas will be chosen as the Default Art Tile.
If there is no Art Tile that fits these conditions, you will receive a notification that there are no Default Art Tiles in the scene.
### Art Tiles and Frame Tiles
- A frame tile acts like an "Frame" for the Art Tile.
- The "Frame" Tile will contain the Art Tile within it, making sure it gets no larger than the frame, but maintaining the image's original dimensions/aspect ratio.
- While an "Art Tile" can only have one "Frame Tile", a "Frame" tile can have more than one "Art Tile" linked to it, which can be useful for many reasons.
---
### Frame Tiles vs Decorative Tiles
Frame Tiles are completely transparent by default, and are used specifically for sizing and positioning Art Tiles, not really meant for displaying anything themselves. If you want the visual of a "Frame" (like a picture frame, a door, a window, etc.) overlayed on top of your Art Tiles, like is demonstrated in the included "Premade Gallery Scene", you could either use 'decorative tiles', tiles on the canvas that are not linked to an Art or Frame tile, and position them manually, or use two Art Tiles bound to the same Frame Tile, with one on top to represent the actual visual "Frame", and another below to represent the "Art".
Here's an example of how you could layer the various Gallery tile types with decorative tiles and/or scene foreground/background images
<img alt="tile layer demo diagram" src="https://user-images.githubusercontent.com/13098820/193488971-98cd1084-e597-451f-a42c-a8dc8b90bb26.png" width="70%" height="auto"/>
---
### Art and Frame Tiles (Cont.)
By default after an Art Tile is created, or if you select "Use Canvas as Frame", the Art Tile will treat the scene canvas's boundaries as its frame, getting no bigger than that.
<https://user-images.githubusercontent.com/13098820/193417331-c6b03f54-550a-40fc-b377-6e18da26ab3c.mp4>
> Art frame tile bounding demo
You can change an Art Tile's "Frame" by selecting a new Frame in the dropdown on the ArtTile.
Hovering over an Art Tile in the Art Gallery Config will also highlight the Frame Tile it was linked to
<https://user-images.githubusercontent.com/13098820/193417365-a8ff798e-dc84-4309-893e-7f0e9d7a5802.mp4>
> frame and art tile link demo
### Gallery Tile Creation
When creating a new scene or viewing one without Art Gallery tiles, the Art Gallery Config will be blank.
You can create a new Gallery tile of either type by clicking on the "Create new Frame Tile" or "New Art Tile" buttons at the bottom of each column in the Art Gallery Config.
By default, these tiles will be "Unlinked", meaning they aren't connected to a 'physical' tile on the canvas. Unlinked tiles will show a warning on them indicating that they aren't linked.
To link an Gallery tile to a 'physical' tile on the Canvas, there are two methods.
One is to click the "Plus" button on an unlinked tile, which will create a new "blank" tile linked.
<https://user-images.githubusercontent.com/13098820/193417377-b3f0c34d-ae4d-4dcd-b7bb-cc55f1dfd7b8.mp4>
> create new tile demo
A new Art Tile will be created with a black gradient as its image, while a new frame tile will have a completely transparent image (however you can still highlight both by hovering over them in the Art Gallery Config)
You can change the 'default' image for both Gallery tile types in the settings, as this also affects what image the Gallery tile 'resets' to if you 'clear' it (more on this functionality to be explained later)
### Linking Preexisting Tiles
If you have pre-existing canvas tiles you wish to turn _into_ Gallery Tiles, you can do that too.
Upon creating a new Gallery Tile in the Art Gallery Config, one of the buttons that will appear has a "link" icon.
Clicking on that button display a list of all the "Unlinked" tile objects in your current scene.
You can hover over each item in the list to highlight the specific tile on the canvas, and then click to select which one you would like to "link" to the Gallery Tile.
<https://user-images.githubusercontent.com/13098820/193632443-f18bc1aa-14f2-486b-bf62-6ef2c23d4028.mp4>
This works the same for both Art and Frame Tiles.
### URL Image Sharing on Specific Art Tiles
- Clicking the icon highlighted below on a Gallery Tile item in the Scene Gallery Tile Config will bring up a small textbox.
<img alt="share url on tile icon" src="https://user-images.githubusercontent.com/13098820/193692119-81cd5cde-19ba-46fa-ae9b-fbc12dfef015.png" width="70%" height="auto"/>
<img alt="share url on tile textbox" src="https://user-images.githubusercontent.com/13098820/193694983-58a64c65-89d0-4dd7-a5f7-63c361dd37ca.png" width="70%" height="auto"/>
- Here you can paste your Image URL and press Enter/Return (or click outside of the text box), and the image pointed to by the URL will be shared on the specific Art Tile.
<https://user-images.githubusercontent.com/13098820/193691106-490f4dc6-88fe-4875-9184-b11f2a522ddc.mp4>
# Utilities, Settings and Customization
You can configure and customize various settings for this module.
The JTCS Art Gallery settings application can either be accessed as usual through the "Module Settings" tab in Foundry's default settings, or by clicking on this Gear icon in the Art Gallery Config app or on a sheet that has the controls toggled on.
<img alt="launching settings app" src="https://user-images.githubusercontent.com/13098820/193644403-03a3c076-7848-487f-9703-e2c1c7a26edb.png" width="500" height="auto"/>
<img alt="JTCS Settings App Window" src="https://user-images.githubusercontent.com/13098820/193644547-9af4fdef-a68f-4d10-ae77-c225b018f4c1.png" width="500" height="auto"/>
> Here is what the JTCS Art Gallery Settings App looks like
## Color Theme and Color Customization
### Custom Colors
You can change the colors of various UI elements in the JTCS Art Gallery.
<https://user-images.githubusercontent.com/13098820/193647498-54f13886-6206-47d9-891c-585421e1cd2b.mp4>
(Selecting a new color, and clicking "Apply" to see how it appears. )
The "Accent Color" property affects the colors of buttons, controls and inputs, including the color of the controls in Actor, Journal, and Item Sheets.
<img alt="color change controls" src="https://user-images.githubusercontent.com/13098820/193647516-718d0c16-b386-4fb6-abb4-cea865b29396.png" width="70%"/>
The "Background Color" property affects the background color of the "JTCS Art Gallery Settings" application and the "Scene Gallery Config" application.
<img alt="color change controls" src="https://user-images.githubusercontent.com/13098820/193652877-2b492dc6-eaa2-46b3-a5a7-795a8a844a4e.png" width="70%"/>
The "Tile Indicator Colors" affect the colors that represent the different types of Gallery Tiles, both in the UI elements that represent them, and in the color they are highlighted on the canvas when hovering over the items in the UI.
<img width="70%" alt="frame art default color" src="https://user-images.githubusercontent.com/13098820/193652903-07fa9141-8d49-4c45-ae66-69231e12ee7f.png"/>
<img width="961" alt="ColorDemoTemplateFlat" src="https://user-images.githubusercontent.com/13098820/193660437-e01e36c9-1048-4abe-848f-215889f8add2.png">
### Default Dark and Light Theme
There are two default themes included, one Dark and one Light.
You can switch between them by clicking one of the buttons in the JTCS Art Gallery Settings app, but do be aware that doing so will overwrite any custom colors you have chosen.
<https://user-images.githubusercontent.com/13098820/193636813-99a3c9fd-2e36-4af6-8324-7d42c853bc96.mp4>
# Tips and Utilities
## Fading Overlapping Elements
There are various ways to fade parts of the UI or tiles on the screen to better see the Highlighted Gallery Tiles on the Canvas.
### Fading Tiles
Clicking the button with two overlapping squares on a Gallery Tile item in the Scene Gallery Config App will make all tiles in the scene except the one whose button you clicked upon fade out/become partially translucent, allowing you to better see the highlighted area that represents each Gallery Tile if you have lots of tiles overlaid on top of each other.
<img src="https://user-images.githubusercontent.com/13098820/193483999-c455fab0-c7b8-47c7-8884-0c4255da3e94.png" width="70%"/>
### Fading Journals
You can fade out the UI of the Journal Entry, Actor, and Item Sheets by clicking the Icon that looks like an Eye in the controls at the Sheets' bottom.
<https://user-images.githubusercontent.com/13098820/193676995-9468b148-8ff8-4345-9ab5-92bd2cdf9e42.mp4>
The transparency/fade can be toggled off by clicking the eye button once again.
<img alt="fade toggle demo" src="https://user-images.githubusercontent.com/13098820/193677026-b2e2f5ea-0d12-4aab-9e56-bbe6e80ac1f6.gif" width="50%"/>
(Note: (QOL/UX Improvement) This feature could be improved so that the Fade Toggle Button itself is fully opaque and visible regardless of if the fade feature is toggled on or off)
### Fading the Scene Gallery Config App
This goes similarly for the Scene Gallery Config App.
<https://user-images.githubusercontent.com/13098820/193676928-3709b5c5-2333-4584-8753-404edbde77d9.mp4>
(Note: The value dictating how transparent the document Sheets and the Scene Gallery Config App become when the fade function is toggled on can be changed in the JTCS Art Gallery Settings. )
<img alt="fade section in config" src="https://user-images.githubusercontent.com/13098820/193680424-a2c4ca00-e01d-4953-bd18-bad417e602a3.png" width="350" height="auto"/>

View File

@ -0,0 +1,20 @@
{
"folders": [
{
"name": "journal-to-canvas-slideshow",
"path": "."
}
],
"settings": {
"gotoSymbolStack.currentStackPosition": 0,
"gotoSymbolStack.filePositionInfo": [],
"gotoSymbolStack.maxStackPosition": 0,
"cSpell.words": ["JTCS", "PIXI", "Popouts"],
"workbench.tree.renderIndentGuides": "always",
"todoist.projectId": 2297684184,
"dimmer.enabled": false
},
"extensions": {
"recommendations": ["riazxrazor.html-to-jsx"]
}
}

83
module.json Normal file
View File

@ -0,0 +1,83 @@
{
"id": "journal-to-canvas-slideshow",
"title": "JTCS - Art Gallery",
"description": "Show art from journal, actor, or item sheets to your players in a variety of ways, now including displaying multiple images simultaneously in tile-based 'Art Gallery'. (Previously named 'Journal-to-Canvas-Slideshow')",
"authors": [
{
"name": "Eva",
"email": "evanesceexotica@gmail.com",
"discord": "Eva#3782"
}
],
"socket": true,
"version": "0.2.4",
"compatibility": {
"minimum": "9",
"verified": "10",
"maximum": "11"
},
"relationships": [
null
],
"packs": [
{
"name": "jtcs-premade-scenes",
"label": "JTCS Premade Display Scenes",
"path": "packs/jtcs-premade-display-scenes",
"type": "Scene",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "jtcs-premade-journals",
"label": "JTCS Premade Journal Entries",
"path": "packs/jtcs-premade-journal-entries",
"type": "JournalEntry",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "jtcs-utility-macros",
"label": "JTCS Utility Macros",
"path": "packs/jtcs-utility-macros",
"type": "Macro",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
}
],
"esmodules": [
"scripts/shim.js",
"scripts/ClickImageInJournal.js",
"scripts/classes/MultiMediaPopout.js",
"scripts/classes/SlideshowConfig.test.js",
"scripts/SlideshowConfig.js",
"scripts/init.js",
"scripts/DragDropIntoJournal.js",
"scripts/classes/ArtTileManager.js",
"scripts/classes/CanvasIndicators.js",
"scripts/debug-mode.js",
"scripts/SheetImageApp.js",
"scripts/classes/HelperFunctions.js",
"scripts/classes/ImageDisplayManager.js",
"scripts/classes/JTCSSettingsApplication.js",
"scripts/data/JTCS-Actions.js",
"scripts/data/templates.js",
"scripts/data/SlideshowConfigActions.js",
"scripts/classes/PopoverGenerator.js",
"scripts/hooks.js",
"scripts/data/ModuleManager.js",
"scripts/classes/SheetImage.test.js"
],
"styles": [
"styles/styles.css"
],
"url": "https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow",
"manifest": "https://raw.githubusercontent.com/EvanesceExotica/Journal-To-Canvas-Slideshow/master/module.json",
"download": "https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow/archive/refs/heads/master.zip"
}

6011
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"type": "module",
"devDependencies": {
"jest": "^29.0.1"
},
"scripts": {
"test": "jest"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000006

View File

View File

@ -0,0 +1,8 @@
2024/01/28-21:30:42.602202 ffff6efaf0c0 Recovering log #4
2024/01/28-21:30:42.606423 ffff6efaf0c0 Delete type=3 #2
2024/01/28-21:30:42.606450 ffff6efaf0c0 Delete type=0 #4
2024/01/28-21:52:42.254216 ffff6d77f0c0 Level-0 table #9: started
2024/01/28-21:52:42.254447 ffff6d77f0c0 Level-0 table #9: 0 bytes OK
2024/01/28-21:52:42.257169 ffff6d77f0c0 Delete type=0 #7
2024/01/28-21:52:42.264379 ffff6d77f0c0 Manual compaction at level-0 from '!scenes!TNeiA03pFhWGh8h5' @ 72057594037927935 : 1 .. '!scenes.tiles!xQZR86kbSU9xusuY.uNmT1zSreNqANa8F' @ 0 : 0; will stop at (end)
2024/01/28-21:52:42.264500 ffff6d77f0c0 Manual compaction at level-1 from '!scenes!TNeiA03pFhWGh8h5' @ 72057594037927935 : 1 .. '!scenes.tiles!xQZR86kbSU9xusuY.uNmT1zSreNqANa8F' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,5 @@
2024/01/28-21:29:21.936697 ffff6efaf0c0 Delete type=3 #1
2024/01/28-21:30:22.962266 ffff6d77f0c0 Level-0 table #5: started
2024/01/28-21:30:22.967312 ffff6d77f0c0 Level-0 table #5: 104736 bytes OK
2024/01/28-21:30:22.970386 ffff6d77f0c0 Delete type=0 #3
2024/01/28-21:30:22.975099 ffff6d77f0c0 Manual compaction at level-0 from '!scenes!TNeiA03pFhWGh8h5' @ 72057594037927935 : 1 .. '!scenes.tiles!xQZR86kbSU9xusuY.uNmT1zSreNqANa8F' @ 0 : 0; will stop at (end)

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1 @@
MANIFEST-000006

View File

View File

@ -0,0 +1,8 @@
2024/01/28-21:30:42.609711 ffff6f7bf0c0 Recovering log #4
2024/01/28-21:30:42.612534 ffff6f7bf0c0 Delete type=3 #2
2024/01/28-21:30:42.612559 ffff6f7bf0c0 Delete type=0 #4
2024/01/28-21:52:42.257240 ffff6d77f0c0 Level-0 table #9: started
2024/01/28-21:52:42.257315 ffff6d77f0c0 Level-0 table #9: 0 bytes OK
2024/01/28-21:52:42.259240 ffff6d77f0c0 Delete type=0 #7
2024/01/28-21:52:42.264393 ffff6d77f0c0 Manual compaction at level-0 from '!journal!HC7VLG78jjZDmV1j' @ 72057594037927935 : 1 .. '!journal!icIWzXBCIddrrqa7' @ 0 : 0; will stop at (end)
2024/01/28-21:52:42.264621 ffff6d77f0c0 Manual compaction at level-1 from '!journal!HC7VLG78jjZDmV1j' @ 72057594037927935 : 1 .. '!journal!icIWzXBCIddrrqa7' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,5 @@
2024/01/28-21:29:21.964026 ffff6efaf0c0 Delete type=3 #1
2024/01/28-21:30:22.960215 ffff6d77f0c0 Level-0 table #5: started
2024/01/28-21:30:22.961363 ffff6d77f0c0 Level-0 table #5: 3413 bytes OK
2024/01/28-21:30:22.962176 ffff6d77f0c0 Delete type=0 #3
2024/01/28-21:30:22.975083 ffff6d77f0c0 Manual compaction at level-0 from '!journal!HC7VLG78jjZDmV1j' @ 72057594037927935 : 1 .. '!journal!icIWzXBCIddrrqa7' @ 0 : 0; will stop at (end)

Binary file not shown.

View File

@ -0,0 +1,2 @@
{"name":"Art","content":"<p><img src=\"modules/journal-to-canvas-slideshow/demo-images/pd19-20091_1.webp\" width=\"263\" height=\"326\" /></p>\n<p> </p>\n<p><img src=\"modules/journal-to-canvas-slideshow/demo-images/pd23-011-jj.webp\" width=\"95\" height=\"259\" /></p>\n<p><img src=\"modules/journal-to-canvas-slideshow/demo-images/pd42-63143351-rob.webp\" width=\"266\" height=\"363\" /></p>\n<p><img src=\"modules/journal-to-canvas-slideshow/demo-images/pd20-360102-num.webp\" width=\"209\" height=\"321\" /></p>","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"journal-to-canvas-slideshow":{"clickableImages":[{"name":"modules/journal-to-canvas-slideshow/demo-images/pd19-20091_1","scenesData":[{"sceneID":"oFmiUCX9yecjiqAl","selectedTileID":"0bZbcLBkydz1wS3M"}],"displayLocation":"displayScene"},{"name":"modules/journal-to-canvas-slideshow/demo-images/pd20-360102-num","scenesData":[{"sceneID":"XVGuxZbIURYCqfrG","selectedTileID":"d0nJR0i1fdnb1w3h"}]},{"name":"modules/journal-to-canvas-slideshow/demo-images/pd23-011-jj","scenesData":[{"sceneID":"oFmiUCX9yecjiqAl","selectedTileID":"0bZbcLBkydz1wS3M"}]}],"showControls":true},"core":{"sourceId":"JournalEntry.wAR3aNF5T1hgUAhb"}},"_id":"vs0CjdTc62AcAxW1"}
{"name":"Display Journal","content":"","img":"modules/journal-to-canvas-slideshow/demo-images/pd19-20091_1.webp","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"core":{"sourceId":"JournalEntry.nZtJ4p6xOoDldYTH"}},"_id":"xTyXBKG7oXwOJQIV"}

View File

@ -0,0 +1,5 @@
{"name":"Tile Toggle Locked Status","type":"script","author":"FLvo1qb3UkC40yKG","img":"icons/svg/padlock.svg","scope":"global","command":"// originally by Anthony Vadala and Shawn Dibble\n//https://github.com/foundry-vtt-community/macros/blob/main/misc/tile_toggle_locked_status.js\n\n//Simple macro to loop through ALL SELECTED TILES and toggle their locked status.\n//In other words:\n //If an individual tile is unlocked, this macro will lock it.\n //If an individual tile is locked, this macro will unlock it.\n\n const tiles = canvas.background.controlled.length ? canvas.background.controlled : canvas.foreground.controlled;\n const updates = tiles.map(tile => ({ _id: tile.id, locked: !tile.data.locked }));\n canvas.scene.updateEmbeddedDocuments(\"Tile\", updates);","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"core":{"sourceId":"Macro.FcmAATox0lTQuPvn"}},"_id":"DO68MDHqstmJJh2S"}
{"name":"Open JTCS-Art-Gallery Config","type":"script","author":"FLvo1qb3UkC40yKG","img":"icons/svg/card-hand.svg","scope":"global","command":"if(!game.JTCSlideshowConfig) game.JTCSlideshowConfig = new SlideshowConfig()\nif(!game.JTCSlideshowConfig.rendered)\n game.JTCSlideshowConfig.render(true)\nelse\n game.JTCSlideshowConfig.bringToTop()","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.WWxPRiTaL2mg8lql"}},"_id":"RyYAGTXo26QPi6ZB"}
{"name":"Tile Scale Manager","type":"script","author":"FLvo1qb3UkC40yKG","img":"modules/journal-to-canvas-slideshow/assets/contract.svg","scope":"global","command":"(()=>{\n \n let buttonValues = {\n \"scaleLarger\": 2.0,\n \"scaleSmaller\": 0.5\n}\nlet axisValues = [\" \", \"width\", \"height\"]\n\nrenderDialog()\n\n\nfunction processButtonObject(){\n \n return object;\n}\n\nfunction renderDialog(){\n let options= {\n width: 600,\n id: \"JTCS-custom-dialog\",\n classes: [\"dialog-grid-3\"]\n }\n let dialog;\n let object = {}\n for(const key in buttonValues){\n let value = buttonValues[key]\n \n for(const axis of axisValues){\n object[`${key}${axis}`] = {\n label: `x${value} ${axis}`,\n callback: ()=> {\n scaleAndMove(value, axis)\n dialog.render(true)\n \n }\n }\n \n }\n }\n dialog = new Dialog({\n title: 'Tile Controls',\n content: `click the buttons below to scale a tile`,\n buttons: object,\n}, options).render(true);\n}\n\nfunction scaleAndMove(scale, axis){\n game.JTCS.utils.manager.scaleControlledTiles(scale, axis)\n renderDialog()\n}\n \n})();","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"core":{"sourceId":"Macro.s2LknTdxNh4N69p4"}},"_id":"UCJcOT7EH8WaUhAE"}
{"name":"Tile Movement Manager","type":"script","author":"FLvo1qb3UkC40yKG","img":"modules/journal-to-canvas-slideshow/assets/multi-directions.svg","scope":"global","command":"(()=>{\n \n let buttonValues = {\n \"moveTiny\": 0.5,\n \"moveSmall\": 1.0,\n \"moveLarge\": 10,\n \"moveHuge\": 20,\n}\nlet directions = [1, -1]\nlet axisValues = [\"x\", \"y\"]\n\nrenderDialog()\n\n\n\n\nfunction renderDialog(){\n let options= {\n width: 600,\n id: \"JTCS-custom-dialog\",\n classes: [\"dialog-grid\"]\n }\n\n let dialog;\n let object = {};\n for(const key in buttonValues){\n for(const axis of axisValues){\n for(const direction of directions){\n let value = buttonValues[key] * direction\n let label = getDirectionName(axis, value) + \" \" + buttonValues[key].toString()\n let directionName = (direction >= 1)\n object[label] = {\n label: label,\n callback: ()=> {\n scaleAndMove(value, axis)\n dialog.render(true)\n \n }\n }\n } \n }\n \n}\n\n dialog = new Dialog({\n title: 'Tile Controls',\n content: `click the buttons below to move a tile`,\n buttons: object,\n }, options).render(true)\n \n}\n\nfunction getDirectionName(axis, amount){\n let directionName = \"\";\n if(axis === \"x\"){\n if(amount < 0){\n directionName = \"left\"\n \n } else{\n \n directionName = \"right\"\n }\n }\n if(axis === \"y\"){\n if( amount < 0){\n directionName = \"up\"\n \n }else{\n directionName = \"down\"\n \n }\n \n \n }\n return directionName;\n \n \n}\n\nfunction scaleAndMove(amount, direction){\n game.JTCS.utils.manager.moveControlledTiles(amount, direction)\n //renderDialog()\n}\n \n})();","folder":null,"sort":0,"permission":{"default":0,"FLvo1qb3UkC40yKG":3},"flags":{"core":{"sourceId":"Macro.gKU43tEiULxIIRVw"}},"_id":"moGBJ148Fro1pbUP"}
{"name":"Fit Tile to Scene","type":"script","author":"FLvo1qb3UkC40yKG","img":"icons/skills/targeting/crosshair-bars-yellow.webp","scope":"global","command":"renderDialog();\n\nfunction renderDialog(){\n //filter out things that aren't tiles\n let viewedScene = game.scenes.viewed;\n let sceneTiles = canvas.background.controlled.filter((obj)=> obj.document.documentName === 'Tile');\n if(sceneTiles.length === 0){\n ui.notifications.warn(\"No tiles selected\");\n return;\n }\n let d = new Dialog({\n title: 'Fit Tile to Scene',\n content: `Along which dimension should the tile fit?`,\n buttons: {\n width: {\n label: 'Width',\n callback: ()=>{fitTo('width')}\n },\n\n height: {\n label: 'Height',\n callback: ()=>{fitTo('height')}\n }, \n both: {\n label: 'Both',\n callback: ()=>{fitTo('both')}\n },\n }\n}).render(true);\n}\n\nfunction fitTo(whichDimension){\n //filter out things that aren't tiles\n let viewedScene = game.scenes.viewed;\n\n let sceneTiles = canvas.background.controlled.filter((obj)=> obj.document.documentName === 'Tile');\n let dimensions = viewedScene.dimensions;\n\n //get array of tile ids for update\n let tileData = sceneTiles.map((tile) => {return{_id: tile.data._id}})\n let updateData = {}\n \n switch (whichDimension) {\n case 'width':\n // code\n updateData.width = dimensions.sceneRect.width;\n break;\n case 'height':\n updateData.height = dimensions.sceneRect.height;\n break;\n case 'both':\n updateData.width = dimensions.sceneWidth;\n updateData.height = dimensions.sceneHeight;\n break;\n }\n tileData = tileData.map((data)=> {return {...data, ...updateData}})\n updateTiles(tileData)\n}\n\nfunction updateTiles(updateData){\n let viewedScene = game.scenes.viewed;\n\n viewedScene.updateEmbeddedDocuments(\"Tile\", updateData);\n}","folder":null,"sort":0,"permission":{"default":0,"xjKGzWFbtOifTl9H":3,"FLvo1qb3UkC40yKG":3},"flags":{"advanced-macros":{"runAsGM":false},"core":{"sourceId":"Macro.KWmUj6RKO4bzYdk8"}},"_id":"wbmIqlU6VPkKlTPP"}

Binary file not shown.

View File

View File

@ -0,0 +1 @@
MANIFEST-000006

View File

View File

@ -0,0 +1,8 @@
2024/01/28-21:30:42.613745 ffff6efaf0c0 Recovering log #4
2024/01/28-21:30:42.615845 ffff6efaf0c0 Delete type=3 #2
2024/01/28-21:30:42.615863 ffff6efaf0c0 Delete type=0 #4
2024/01/28-21:52:42.259282 ffff6d77f0c0 Level-0 table #9: started
2024/01/28-21:52:42.259306 ffff6d77f0c0 Level-0 table #9: 0 bytes OK
2024/01/28-21:52:42.260622 ffff6d77f0c0 Delete type=0 #7
2024/01/28-21:52:42.264407 ffff6d77f0c0 Manual compaction at level-0 from '!macros!DO68MDHqstmJJh2S' @ 72057594037927935 : 1 .. '!macros!wbmIqlU6VPkKlTPP' @ 0 : 0; will stop at (end)
2024/01/28-21:52:42.264611 ffff6d77f0c0 Manual compaction at level-1 from '!macros!DO68MDHqstmJJh2S' @ 72057594037927935 : 1 .. '!macros!wbmIqlU6VPkKlTPP' @ 0 : 0; will stop at (end)

View File

@ -0,0 +1,5 @@
2024/01/28-21:29:21.977996 ffff6df8f0c0 Delete type=3 #1
2024/01/28-21:30:22.970645 ffff6d77f0c0 Level-0 table #5: started
2024/01/28-21:30:22.972148 ffff6d77f0c0 Level-0 table #5: 3863 bytes OK
2024/01/28-21:30:22.974952 ffff6d77f0c0 Delete type=0 #3
2024/01/28-21:30:22.975222 ffff6d77f0c0 Manual compaction at level-0 from '!macros!DO68MDHqstmJJh2S' @ 72057594037927935 : 1 .. '!macros!wbmIqlU6VPkKlTPP' @ 0 : 0; will stop at (end)

Binary file not shown.

205
release-notes.md Normal file
View File

@ -0,0 +1,205 @@
# Release Notes
## v0.2.4 - 10/17/2022
### MAJOR
#### Removed
- Due to changes in FoundryV10 making it easier to see indicators, removed the "fadeTileButton" from the Scene Gallery Config (in v10 version only)
### MINOR
#### Added
- Added tests to work with [Quench](https://github.com/Ethaks/FVTT-Quench)
## v.0.2.3
### Added
#### Major
- Added "auto-view" and "auto-activate" options in settings for "Art Journal" and "Art Scene"
## v.0.2.2
**CHANGED**
**Major**
- Added support for v10 with backwards compatibility
- v10 version **REQUIRES** right-click on sheet images instead of left-click, to avoid interfering with default JournalEntryPage functionality when clicking on an image
## v.0.2.1 - 10/06/2022
**PATCHED**
**Major**
- Fixed bug with src not being fetched from video elements
## v.0.2.0 - 10/05/2022
**CHANGED**
**Major**
- Improved image-share controls in actor, item, and journal sheets
- Various Display methods can be accessed and activated by hovering over an image and clicking on one of these controls
- the original "click on the image to display it on the canvas" functionality remains intact.
**ADDED**
**Major**
- Scene Gallery Config
- Configuration settings
- Settings and Customization
- settings application that can be launched from multiple locations and includes customization options
- colored overlays shown on tiles on the canvas whenever you hover a connected UI item, to ensure you can easily find them. - overlay colors are customizable
- Color customization of elements UI in JTCS Art Gallery apps, including a default light and dark theme.
- Compendiums
- Compendium pack of macros with featuring utilities to make moving and scaling tiles easier
- Compendium pack of premade scenes displaying demo setups of Gallery tiles, including a scene meant to act as your default "Display Scene"
- Compendium pack of Journal Entries including a scene meant to act as your default "Display Journal"
**REMOVED**
- Tile Tool Controls added by the module, including controls/dialog to change Display Method and share URL image.
- The above tools have been replaced by "Scene Gallery Config" App, which can be accessed from the same place.
## **v0.1.8 - v0.1.9** - 2022-01-16
**CHANGED**
- Updated for Foundry version 9. Check in "releases" for the version still compatible with version 8.
## **v0.1.7** - 2021-08-26
**CHANGED**
- Integrated features from pull requests, such as item images now being able to be clicked on and displayed. (Thanks, @DarKDinDoN !)
- Added setting to hide or change how "Toggle Display Location" button in journal header displays.
## **v0.1.6** - 2021-06-09
Updated module to work with Foundry v8.6
## **v0.1.5** - 2021-05-23
**ADDED**
**Major**
- NEW: Ability to _right click_ on actor sheet character images to display them the same as journal images.
- NEW: Ability to display Journal-to-Canvas-Slideshow tools within a dialog rather than as tile control tools.
See the settings for **Use Actor Sheet Images** and **Hide Tile Buttons** in the updated module settings below.
!["New Settings"](https://i.imgur.com/AfHLPSG.png)
### Default Tile Control Tools
The default tile control tools with the Hide Tile Buttons setting disabled.
A new button is there called "Switch Display Location" that will display a dialog that allows you to switch display locations without needing to go into the module's settings.
!["Switch Display Location Button"](https://i.imgur.com/3XLHTku.png)
!["Switch Display Location Dialog"](https://i.imgur.com/CBSidW0.png)
**Note**: Journal entries now have a button in the header that allows you to switch the display location as well.
**Note**: You can switch away from the tile control tools and then back again to "refresh" if you enable or disable the Hide Tile Buttons setting.
---
### Tile Control Tools with Hide Tile Buttons Turned On
With the Hide Tile Buttons setting enabled, all Journal-to-Canvas-Slideshow buttons will not be displayed except for the "Clear Display" button, and a new button that says "Show Slideshow Config".
To show the other functions, click on the button in the tile control tools that says "Show Slideshow Config".
!["Hide Tile Buttons Setting Turned On"](https://i.imgur.com/6a7oxpt.png)
The following dialog will appear with buttons with all the functionality, such as creating Display and Bounding Tiles, Setting a URL image, and switching between display locations.
!["Hide Tile Buttons Dialog"](https://i.imgur.com/u5DWfMc.png)
---
**CHANGED**:
- Many features now work with VIEWED scene rather than ACTIVE scene, such as the bounding tiles.
---
## **v0.1.4** - 2021-03-19
**ADDED**
**Major**:
- NEW: Bounding Tiles implemented by @Occidio
- NEW: Display Tiles that along with Bounding Tiles can be added to _any scene_.
- NEW: Display images via copy-pasting URL feature implemented by @p4535992
- NEW: Display in Window feature alternative implemented by @DarKDinDoN
- NEW: Extra settings to accomodate the above new features -- please check the settings menu and reselect your prefered settings.
**Changes**:
**Major:**
- Special "Display Tiles" now created via button in Tile controls menu. Flagged by script, so no longer have to be very first tile in scene.
- **Warning**: Please replace regular tile in pre-made Display Scenes with new Display Tile, else the script will not detect them.
## **v0.1.3** - 2021-01-22
**ADDED**
**Major**:
- NEW: Added option to display journal images in a window rather than display scene
- NEW: Module settings
## **v0.1.2** - 2021-01-03
**Major:**
- Fixed an incompatability issue with the Call of Cthulhu 7e (CoC7) system.
## **v0.1.1** - 2020-12-28
### **Added**
**Major:**
- Added "Clear Display" button in Tiles scene control buttons. Will set 'slideshow' tile to a transparent image.
**Minor:**
- More visual effects when hovering over and clicking images in journal, for more user feedback
- Changed cursor to pointer on hover of journal images
### **Changes**
- Clicking on image in journal no longer activates the 'Display' scene if a different scene is active. Plan to add functionality later to toggle this behavior.
(Red arrow pointing at new 'Clear Display' button')
!["Location of clear button"](https://i.imgur.com/aPtU9QL.jpg)
!["Showing off updates"](https://media2.giphy.com/media/sIKIPBhN3c5vLPVxGu/giphy.gif)
# Roadmap
- I next intend to add a way to more easily toggle between the various different settings (Display in Window vs Display in Scene, etc.) without needing to go all the way to the settings menu.
- I may possibily implement a way to have multiple Display Tiles in a single scene, but I will need to think of the best way to implement this.
- Clicking on image in journal no longer activates the 'Display' scene if a different scene is active. Plan to add functionality later to toggle this behavior.
(Red arrow pointing at new 'Clear Display' button')
!["Location of clear button"](https://i.imgur.com/aPtU9QL.jpg)
!["Showing off updates"](https://media2.giphy.com/media/sIKIPBhN3c5vLPVxGu/giphy.gif)

View File

@ -0,0 +1,23 @@
function setEventListeners(html, app) {
//look for the images and videos with the clickable image class, and add event listeners for being hovered over (to highlight and dehighlight),
//and event listeners for the "displayImage" function when clicked
// execute(html, app);
wait().then(execute.bind(null, [html, app]));
}
function wait(callback) {
return new Promise(function (resolve, reject) {
resolve();
});
}
function execute(args) {
let [html, app] = args;
// this one is for actor sheets. Right click to keep it from conflicting with the default behavior of selecting an image for the actor.
if (checkSettingEquals("useActorSheetImages", true)) {
html.find(".rightClickableImage").each((i, img) => {
img.addEventListener("contextmenu", (event) => determineLocation(event, app), false);
});
}
}

View File

@ -0,0 +1,9 @@
//what component are you testing
//const
//what is the actual output
const actual = 'what is the actual output?';
const expected = 'What is the expected output?';
//what is the expected output
//assert.equal(actual, expected, 'What should the feature do?');
//test('what should the feature do?', () => { expect(actual).toBe(expected);});

View File

@ -0,0 +1,81 @@
Hooks.once("init", async function () {});
let currentJournalId;
async function addImage(app, url, currentJournalId) {
var journalEntry;
//create image tag with url of item
var containerParagraph;
var image = new Image();
//append the child to the body of the journal entry -- gotta figure out how to add it to the journal entry specifically
var journalDiv = document.getElementById(currentJournalId);
var journalForm = journalDiv.getElementsByTagName("form")[0];
var editorContent = journalForm.getElementsByClassName("editor-content")[0];
containerParagraph = journalForm.getElementsByClassName("editor-content")[0].querySelector("p"); //appendChild(containerParagraph);
if (containerParagraph == null) {
containerParagraph = document.createElement("p");
journalForm.getElementsByClassName("editor-content")[0].appendChild(containerParagraph);
}
containerParagraph.appendChild(image);
image.src = url;
app.object.data.content = editorContent.innerHTML;
app.object.update({
content: app.object.data.content,
});
await app.submit();
}
async function handleDrop(app, event, currentJournalId) {
event.preventDefault();
var files = event.dataTransfer.files;
for (let f of files) {
checkSource(app, f, currentJournalId);
}
}
//implemented and tweaked these methods from DragUpload by cswendrowski
//https://github.com/cswendrowski/FoundryVTT-Drag-Upload/blob/master/dragupload.js
async function checkSource(app, file, currentJournalId) {
if (typeof ForgeVTT != "undefined" && ForgeVTT.usingTheForge) {
source = "forgevtt";
} else {
var source = "data";
}
let response;
if (file.isExternalUrl) {
response = {
path: file.url,
};
} else {
response = await FilePicker.upload(
source,
game.settings.get("journal-to-canvas-slideshow", "imageSaveLocation"),
file,
{}
);
}
addImage(app, response.path, currentJournalId);
}
Hooks.on("renderJournalSheet", (app, html, options) => {
currentJournalId = html[0].id;
var journalDiv = html[0];
if (!journalDiv.querySelector("div.editor")) {
return;
}
journalDiv.querySelector("div.editor").addEventListener("drop", (event) => {
handleDrop(app, event, currentJournalId);
});
});
Hooks.once("init", async function () {
game.settings.register("DragDropIntoJournal", "imageSaveLocation", {
name: "Image Save Location",
hint: "Where in your Data folder would you like to save the images you drag into journal entries? Input the file path to your prefered folder here.",
scope: "client",
config: true,
type: String,
default: "",
});
});

View File

@ -0,0 +1,284 @@
"use strict";
import { HelperFunctions } from "./classes/HelperFunctions.js";
import { SheetImageDataController } from "./SheetImageDataController.js";
import { SheetImageApp } from "./SheetImageApp.js";
import { SlideshowConfig } from "./SlideshowConfig.js";
import { extraActions } from "./data/SlideshowConfigActions.js";
import { ArtTileManager } from "./classes/ArtTileManager.js";
import { ImageDisplayManager } from "./classes/ImageDisplayManager.js";
import { JTCSSettingsApplication } from "./classes/JTCSSettingsApplication.js";
import { universalInterfaceActions as UIA } from "./data/Universal-Actions.js";
export const sheetControls = [
{
action: "sheet.click.toggleImageControls",
tooltip: "Toggle the image controls on this sheet",
icon: "fas fa-sliders-h",
toggle: true,
activeOn: "showControls",
},
{
action: "sheet.click.fadeJournal",
icon: "fas fa-eye-slash",
tooltip: "Fade sheet background to see canvas",
// toggle: true,
},
{
action: "sheet.click.openSlideshowConfig",
tooltip: "open Gallery Tile Config Application for the current scene",
icon: "fas fa-cubes",
},
{
action: "sheet.click.openSettingsApp",
tooltip: "open JTCS Art Gallery Settings",
icon: "fas fa-cog",
},
// {
// action: "sheet.click.toggleInstructions",
// tooltip: "show or hide contextual instructions",
// icon: "fas fa-info",
// },
// {
// action: "sheet.click.showURLShareDialog",
// tooltip: "Share a URL Image with your players",
// icon: "fas fa-external-link",
// },
];
export const dedicatedDisplayControls = [
{
label: "view",
callback: (controls) => {},
},
{
label: "showToAll",
callback: () => {},
},
{
label: "showToSome",
callback: () => {},
},
];
export const sheetImageActions = {
sheet: {
click: {
fadeJournal: {
onClick: async (event, options) => {
await SheetImageApp.addFadeStylesToSheet(event);
UIA.toggleActiveStyles(event);
},
},
// setDefaultTileInScene: {
// onClick: async (event, options) => {
// //TODO:
// //? show the tiles
// },
// },
toggleImageControls: {
onClick: async (event, options) => {
let { app } = options;
let journalEntry = app.object;
let currentSetting = await HelperFunctions.getFlagValue(
journalEntry,
"showControls",
"",
false
);
//if current setting is false, or doesn't exist, set it to true
if (!currentSetting || currentSetting.length === 0) {
await HelperFunctions.setFlagValue(
journalEntry,
"showControls",
true
);
} else {
await HelperFunctions.setFlagValue(
journalEntry,
"showControls",
false
);
}
UIA.toggleActiveStyles(event);
},
},
changeControlsPosition: {
onClick: async (event, options) => {
let positions = [
"top-left",
"top-right",
"bottom-left",
"bottom-right",
];
await HelperFunctions.getFlagValue(journalEntry, "controlsPosition");
await HelperFunctions.setFlagValue(
journalEntry,
"controlsPosition",
true
);
},
},
openSlideshowConfig: {
onClick: async () => {
await UIA.renderAnotherApp("JTCSlideshowConfig", SlideshowConfig);
},
},
openSettingsApp: {
onClick: async () => {
await UIA.renderAnotherApp(
"JTCSSettingsApp",
JTCSSettingsApplication
);
},
},
showURLShareDialog: {
onClick: async (event, options) =>
await extraActions.showURLShareDialog(event, options),
},
toggleInstructions: {
onClick: (event, option) => {
const parentElement = event.currentTarget.closest("editor");
UIA.toggleShowAnotherElement(event, {
parentItem: parentElement,
targetClassSelector: "instructions",
});
},
},
},
},
image: {
click: {
sendImageDataToDisplay: {
onClick: async (event, options) => {
// event.stopPropagation();
options.method = "anyScene";
// event.stopImmediatePropagation();
// bundle all the necessary data into an object
let sheetImageData =
await SheetImageDataController.wrapSheetImageData(options);
await ImageDisplayManager.determineDisplayMethod(sheetImageData);
},
},
},
hover: {
showTileIndicator: {
onHover: async (event, options) => {
//!get the default tile
//get the default art tile in this scene
let tileID = await ArtTileManager.getDefaultArtTileID(
game.scenes.viewed
);
let isLeave =
event.type === "mouseleave" || event.type === "mouseout"
? true
: false;
let tile = await ArtTileManager.getTileObjectByID(tileID);
if (isLeave) {
await game.JTCS.indicatorUtils.hideTileIndicator(tile);
} else {
await game.JTCS.indicatorUtils.showTileIndicator(tile);
}
},
},
},
},
tileButton: {
hover: {
showTileIndicator: {
onHover: async (event, data = {}) => {
let isLeave =
event.type === "mouseout" || event.type === "mouseleave";
let tileID = event.currentTarget.dataset.id; //this should grab the value from the radio button itself
let tile = await game.JTCS.tileUtils.getTileObjectByID(tileID);
if (isLeave) {
await game.JTCS.indicatorUtils.hideTileIndicator(tile);
} else {
await game.JTCS.indicatorUtils.showTileIndicator(tile);
}
},
},
},
click: {
displayImageOnTile: {
onClick: async (event, options) => {
let { imgElement } = options;
let tileID = event.currentTarget.dataset.id;
if (event.ctrlKey) {
const appName = "JTCSlideshowConfig";
await UIA.renderAnotherApp(appName, SlideshowConfig);
if (game[appName]) {
const configElement = game[appName].element;
configElement.focus();
const tileItem = configElement.find(`[data-id='${tileID}']`);
if (tileItem && tileItem[0]) {
tileItem[0].scrollIntoView();
tileItem[0].focus();
}
}
return;
}
let url = ImageDisplayManager.getImageSource(imgElement);
const frameID = await ArtTileManager.getGalleryTileDataFromID(
tileID,
"linkedBoundingTile"
);
await ImageDisplayManager.updateTileObjectTexture(
tileID,
frameID,
url,
"anyScene"
);
//once clicked, hide the buttons
UIA.toggleHideAncestor(event, {
...options,
ancestorSelector: "#displayTileButtons",
});
},
},
},
},
displayActionButton: {
click: {
sendImageDataToDisplay: {
onClick: async (event, options) => {
let { app, html, imgElement } = options;
let method = event.currentTarget.dataset.method;
let sheetImageData =
await SheetImageDataController.wrapSheetImageData({
...options,
method: method,
imgElement,
});
await ImageDisplayManager.determineDisplayMethod(sheetImageData);
//TODO: add this functionality in later for prompting the user for what to do next
// if (method === "journalEntry" || method === "artScene") {
// let property = method === "journalEntry" ? "journal" : "scene";
// const autoActivate = await HelperFunctions.getSettingValue(
// "artGallerySettings",
// `dedicatedDisplayData.${property}.autoActivate`
// );
// if (!autoActivate) {
// UIA.toggleShowAnotherElement(event, options);
// UIA.toggleActiveStyles(event);
// }
// }
},
},
revealTileButtons: {
onClick: async (event, options) => {
UIA.toggleShowAnotherElement(event, options);
UIA.toggleActiveStyles(event);
},
},
},
},
};

315
scripts/SheetImageApp.js Normal file
View File

@ -0,0 +1,315 @@
"use strict";
import { log } from "./debug-mode.js";
import { HelperFunctions } from "./classes/HelperFunctions.js";
import { sheetImageActions, sheetControls } from "./SheetImageActions.js";
import { SheetImageDataController } from "./SheetImageDataController.js";
import { artTileManager, helpers } from "./data/ModuleManager.js";
import { ArtTileManager } from "./classes/ArtTileManager.js";
import { universalInterfaceActions } from "./data/Universal-Actions.js";
export class SheetImageApp {
static displayMethods = [
{
name: "anyScene",
icon: "fas fa-vector-square",
tooltip: "choose Art Tile in current scene to display image on",
},
{
name: "window",
icon: "fas fa-external-link-alt",
tooltip: "Display image in pop-out window",
},
{
name: "journalEntry",
icon: "fas fa-book-open",
tooltip: "display image in your chosen 'Art Journal'",
},
{
name: "artScene",
icon: "far fa-image",
tooltip: "display image in your chosen 'Art Scene'",
},
];
/**
*
* @param {*} app - the application (sheet) that this is being called from
* @param {*} html
*/
static async applyImageClasses(app, html) {
if (game.user.isGM) {
const whichSheets = await HelperFunctions.getSettingValue(
"artGallerySettings",
"sheetSettings.modularChoices"
);
const doc = app.document;
let onThisSheet = await HelperFunctions.getFlagValue(
doc,
"showControls",
"",
false
);
let documentName = doc.documentName;
documentName = documentName.charAt(0).toLowerCase() + documentName.slice(1);
// if (documentName === "item" && doc.parent) {
// //if it's an embedded item in a sheet
// return;
// }
// for v10 +
if (game.version >= 10) {
if (documentName === "journalEntryPage") {
documentName = "journalEntry";
onThisSheet = await HelperFunctions.getFlagValue(
doc.parent,
"showControls",
"",
false
);
}
}
let selectorString = "img, video, .lightbox-image";
if (whichSheets[documentName] || onThisSheet === true) {
if (onThisSheet) {
if (documentName === "journalEntry" && game.version < 10) {
html.find(selectorString).addClass("clickableImage");
} else {
html.find(selectorString).addClass("rightClickableImage");
}
//inject the controls into every image that has the clickableImage or rightClickableImage classes
Array.from(
html.find(".clickableImage, .rightClickableImage")
).forEach((img) => SheetImageApp.injectImageControls(img, app));
}
}
if (doc.documentName !== "JournalEntryPage") {
SheetImageApp.injectSheetWideControls(app);
}
}
}
static async applySheetFadeSettings(journalSheet) {
//get opacity, and whether or not journals should be faded
let opacityValue = (
await game.JTCS.utils.getSettingValue(
"artGallerySettings",
"sheetFadeOpacityData"
)
).value;
let shouldFadeImages = (
await game.JTCS.utils.getSettingValue(
"artGallerySettings",
"fadeSheetImagesData"
)
).chosen;
//set a CSS variable on the journal sheet to grab the opacity in css
let sheetElement = journalSheet.element;
sheetElement[0].style.setProperty("--journal-fade", opacityValue + "%");
//set the window content's data-attribute to "data-fade-all" so it fades the journal's opacity, and not just the background color
if (shouldFadeImages === "fadeAll") {
sheetElement.find(".window-content").attr("data-fade-all", true);
// sheetElementStyle.setProperty("--fade-all", true);
}
}
/**
* When the journal sheet renders, we're going to add controls over each image
* @param {HTMLElement} imgElement - the image HTML element
* @param {*} journalSheet - the journal sheet we're searching within
*/
static async injectImageControls(imgElement, journalSheet) {
let template = "modules/journal-to-canvas-slideshow/templates/image-controls.hbs";
// game.JTCS.templates["image-controls"]
const defaultArtTileID = await ArtTileManager.getDefaultArtTileID();
const imageName = await SheetImageDataController.convertImageSourceToID(
imgElement
);
imgElement.dataset.name = imageName;
//get the art tiles in the scene
let displayTiles = await ArtTileManager.getSceneSlideshowTiles("art", true);
displayTiles = displayTiles.filter((tile) => !tile.missing);
displayTiles = displayTiles.map((tile) => {
return {
tile: tile,
randomID: foundry.utils.randomID(),
};
});
let users = game.users.contents;
let renderHtml = await renderTemplate(template, {
currentSceneName: game.scenes.viewed.name,
displayMethods: SheetImageApp.displayMethods,
displayTiles: displayTiles,
defaultArtTileID: defaultArtTileID,
imgPath: imageName,
users: users,
// ...imageFlagData,
});
//wrap each image in a clickableImageContainer
$(imgElement).wrap("<div class='clickableImageContainer'></div>");
$(imgElement).parent().append(renderHtml);
await SheetImageApp.activateImageEventListeners({
controlsContainer: $(imgElement).parent(),
journalSheet: journalSheet,
imgElement: imgElement,
});
}
static async injectSheetWideControls(journalSheet) {
let template = game.JTCS.templates["sheet-wide-controls"];
await SheetImageApp.applySheetFadeSettings(journalSheet);
let isActive = await HelperFunctions.getFlagValue(
journalSheet.document,
"showControls",
"",
false
);
let controlsData = sheetControls.map((control) =>
control.toggle ? { ...control, active: isActive } : control
);
let renderHtml = await renderTemplate(template, {
controls: controlsData,
isActive,
});
let selector = ".window-content";
if (journalSheet.document.documentName === "JournalEntryPage") {
selector = ".journal-page-content";
}
let $editorElement = $(journalSheet.element[0].querySelector(selector));
$editorElement.prepend(renderHtml);
let controlsContainer = $("#sheet-controls");
await SheetImageApp.activateSheetWideEventListeners({
controlsContainer,
journalSheet,
isActive,
});
}
static async activateSheetWideEventListeners(options) {
const { journalSheet, isActive } = options;
const controlsContainer = journalSheet.element.find("#sheet-controls");
$(controlsContainer)
.off("click", "[data-action]")
.on("click", "[data-action]", async (event) => {
SheetImageApp.handleAction(event, journalSheet, "action", false);
});
const controlsToggleButton = $(controlsContainer).find(
"[data-action='sheet.click.toggleImageControls']"
)[0];
if (isActive) {
$(controlsToggleButton).addClass("active");
}
universalInterfaceActions.toggleHideAllSiblings(null, controlsToggleButton);
}
// handle any interaction event
static async handleAction(event, journalSheet, actionType = "action", isItem = true) {
event.preventDefault();
let targetElement = $(event.currentTarget);
let imgElement;
//"isItem" stands for if it's a sheet-wide control or an item-specific control
if (isItem) {
//if our target element is not an image, get the closest image from our clickableImageContainer parent
//else just get the current target itself
if (
targetElement.prop("nodeName") !== "IMG" ||
targetElement.prop("nodeName") !== "VIDEO"
) {
imgElement = targetElement[0]
.closest(".clickableImageContainer")
.querySelector("img, video");
} else {
imgElement = targetElement[0];
}
//if our target element is a label, get the input before it instead
targetElement.prop("nodeName") === "LABEL" &&
(targetElement = targetElement.prev());
}
let action = targetElement.data()[actionType];
let handlerPropertyString = "onClick";
switch (actionType) {
case "hoverAction":
handlerPropertyString = "onHover";
break;
case "changeAction":
handlerPropertyString = "onChange";
break;
}
let actionData = getProperty(sheetImageActions, action);
if (actionData && actionData.hasOwnProperty(handlerPropertyString)) {
//call the event handler stored on this object
let options = {
action: action,
app: journalSheet,
html: journalSheet.element,
...(imgElement && {
parentItem: imgElement.closest(".clickableImageContainer"),
}),
imgElement: imgElement,
};
actionData[handlerPropertyString](event, options);
}
}
/**
*
* @param data - the data object
*/
static async activateImageEventListeners(data) {
let { journalSheet, imgElement, controlsContainer } = data;
let html = journalSheet.element;
//add data actions to the images
$(imgElement).attr("data-hover-action", "image.hover.showTileIndicator");
$(imgElement).attr("data-action", "image.click.sendImageDataToDisplay");
$(controlsContainer)
.off("click contextmenu", "[data-action]")
.on(
"click contextmenu",
"[data-action]",
async (event) =>
await SheetImageApp.handleAction(event, journalSheet, "action")
);
$(controlsContainer)
.off("mouseenter mouseleave", "[data-hover-action]")
.on(
"mouseenter mouseleave",
"[data-hover-action]",
async (event) =>
await SheetImageApp.handleAction(event, journalSheet, "hoverAction")
);
$(controlsContainer)
.off("change", "[data-change-action]")
.on(
"change",
"[data-change-action]",
async (event) =>
await SheetImageApp.handleAction(event, journalSheet, "changeAction")
);
}
static async addFadeStylesToSheet(event) {
event.preventDefault();
let windowContent = event.currentTarget.closest(".window-content");
let faded =
windowContent.classList.contains("fade") ||
windowContent.classList.contains("fade-all");
if (faded) {
windowContent.classList.remove("fade");
} else {
windowContent.classList.add("fade");
}
}
}

View File

@ -0,0 +1,163 @@
import { HelperFunctions } from "./classes/HelperFunctions.js";
import { log, MODULE_ID } from "./debug-mode.js";
// Model: The backend that contains all the data logic
// View: The frontend or graphical user interface (GUI)
// Controller: The brains of the application that controls how data is displayed
// import { artGalleryDefaultSettings } from "./settings.js";
export class SheetImageDataController {
static checkFlags(documentCollectionName, flagName = "journal-to-canvas-slideshow") {
let flaggedJournalEntries = game[documentCollectionName].contents.filter(
(entry) => entry.data.flags["journal-to-canvas-slideshow"]
);
return flaggedJournalEntries;
}
static async getAllFlaggedSheets() {
//get every journal entry with flags associated with this
let flaggedJournals = SheetImageDataController.checkFlags("journal");
let flaggedActors = SheetImageDataController.checkFlags("actors");
let flaggedItems = SheetImageDataController.checkFlags("items");
let flaggedDocs = [...flaggedJournals, ...flaggedActors, ...flaggedItems];
return flaggedDocs;
}
/**
*Remove any tile data from docs that has an id that was deleted
* @param {*} removedTileID - remove any reference to a tile with this ID
*/
static async removeTileDataFromDocs(removedTileID, sceneID) {
let flaggedDocs = await SheetImageDataController.getAllFlaggedSheets();
for (let doc of flaggedDocs) {
let clickableImages = await HelperFunctions.getFlagValue(
doc,
"clickableImages"
);
clickableImages = clickableImages.map((item) => {
return {
...item,
scenesData: item.scenesData.filter(
(sceneData) => removedTileID !== sceneData.selectedTileID
),
};
});
// map array of objects, and filter out any scenesdata objects in scenesDay array that hold the removed tile id
await HelperFunctions.setFlagValue(doc, "clickableImages", clickableImages);
}
}
/**
* Store image data in flags
* @param {App} journalSheet - the journal sheet whose images we're storing in the flag
* @param {HTMLElement} imgElement - the image HTML element
* @param {Obj} newImgData - the data being stored
*/
static async updateImageFlags(journalSheet, imgElement, newImgData) {
let journalEntry = journalSheet.object;
let imageName = await SheetImageDataController.convertImageSourceToID(imgElement);
let clickableImages = await HelperFunctions.getFlagValue(
journalEntry,
"clickableImages"
);
let foundImage = clickableImages.find((imgData) => imgData.name === imageName);
if (foundImage) {
setProperty(foundImage, newImgData);
// clickableImages = clickableImages.map((imgData) => {
// // if the ids match, update the matching one with the new displayName
// return imgData.name === imageName ? { ...imgData, ...newImgData } : imgData; //else just return the original
// });
} else {
clickableImages.push({ name: imageName, ...newImgData });
}
await HelperFunctions.setFlagValue(
journalEntry,
"clickableImages",
clickableImages
);
await HelperFunctions.setFlagValue(
journalEntry,
"linkedImageTilesByID",
clickableImages
);
}
// /**
// * Return data specific to the current viewed scene for the particular image in the journal entry, which should change when the scene does
// * @param {Object} imageFlagData - the data from the flag for this particular image in the journal entry
// * @returns the data specific to the current viewed scene
// */
// static async getSceneSpecificImageData(imageFlagData) {
// let currentSceneID = game.scenes.viewed.data._id;
// return imageFlagData.scenesData?.find((obj) => obj.sceneID === currentSceneID); //want to get the specific data for the current scene
// }
static async getGalleryTileIDsFromImage(imageElement, journalSheet) {
let imageData = await SheetImageDataController.getJournalImageFlagData(
journalSheet.object,
imageElement
);
if (!imageData) {
console.error("could not get data from that sheet and element");
return;
}
let flaggedTiles = await game.JTCS.tileUtils.getSceneSlideshowTiles("", true);
let frameTileID = await game.JTCS.tileUtils.getLinkedFrameID(
artTileID,
flaggedTiles
);
if (!artTileID) {
console.error("Image data has no tile ID");
return;
}
return {
artTileID: artTileID,
frameTileID: frameTileID,
};
}
/**
* Convert the image's path without extention to use as as an identifier to store it in flags
* @param {Element} imgElement - the image element
* @returns a string path
*/
static async convertImageSourceToID(imgElement) {
let name = imgElement.getAttribute("src");
name = name.replace(/\.(gif|jpe?g|tiff?|png|webp|bmp)/g, "");
return name;
}
/**
* get the flag data for this image in this journal entry
* @param {Document} journalEntry - the journal entry whose flags we're looking in
* @param {Element} imgElement - an HTML Image Element
* @returns an object containing the data saved in the flags
*/
static async getJournalImageFlagData(journalEntry, imgElement) {
let clickableImages =
(await HelperFunctions.getFlagValue(journalEntry, "clickableImages")) || [];
// let clickableImages = (await journalEntry.getFlag("journal-to-canvas-slideshow", "clickableImages")) || [];
let foundData = clickableImages.find((imgData) => {
return imgData.name == imgElement.dataset["name"];
});
return { journalID: journalEntry.id, ...foundData };
}
/**
* Returns all the necessary data in a bundled object
* @param {Object} options - bundled options like the app, the html element, and the imgElement
* @returns Object
*/
static async wrapSheetImageData(options) {
let { app, html, imgElement } = options;
// let imageData = await SheetImageDataController.getJournalImageFlagData(app.object, imgElement);
// let galleryTileIDs = await SheetImageDataController.getGalleryTileIDsFromImage(imgElement, app);
let sheetImageData = {
imageElement: imgElement,
// ...imageData,
// ...galleryTileIDs,
method: options.method || "window", //if we don't have a location set, default to window
};
return sheetImageData;
}
}

215
scripts/SlideshowConfig.js Normal file
View File

@ -0,0 +1,215 @@
"use strict";
import { log, MODULE_ID } from "./debug-mode.js";
import { slideshowDefaultSettingsData } from "./data/SlideshowConfigActions.js";
import { Popover } from "./classes/PopoverGenerator.js";
import { HelperFunctions } from "./classes/HelperFunctions.js";
export class SlideshowConfig extends Application {
constructor(data = {}) {
super();
this.data = data;
this.element.find(".window-content").attr("data-fade-all");
}
/**
* @override
*/
async _render(force, options = {}) {
return super._render(force, options);
}
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["form"],
popOut: true,
resizable: true,
height: 500,
template:
"modules/journal-to-canvas-slideshow/templates/scene-tiles-config.hbs",
id: "slideshow-config",
title: "Scene Gallery Config",
scrollY: ["ul"],
});
}
//for saving tab layouts and such
renderWithData() {
this.render(true, this.data);
}
hideButtons(html) {
Array.from(
html.querySelectorAll(".tile-list-item .actions .icon-button")
).forEach((btn) => {
btn = $(btn);
let data = btn.data();
let parentData = btn.closest(".tile-list-item").data();
let type = parentData.type;
let action = data.action.split(".").pop();
let itemActionsObject =
slideshowDefaultSettingsData.itemActions.click.actions;
let filteredActionKeys = [];
let v9onlyKeys = [];
Object.keys(itemActionsObject).forEach((itemAction) => {
if (getProperty(itemActionsObject, itemAction).v9Only === true) {
v9onlyKeys.push(itemAction);
}
});
Object.keys(itemActionsObject).forEach((itemAction) => {
if (getProperty(itemActionsObject, itemAction).artTileOnly) {
filteredActionKeys.push(itemAction);
}
});
filteredActionKeys.forEach((itemAction) => {
if (type === "frame" && itemAction === action) {
btn.css({ display: "none" });
}
});
v9onlyKeys.forEach((itemAction) => {
if (game.version >= 10 && itemAction === action) {
btn.css({ display: "none" });
}
});
});
}
/**
* Handles all types of actions (click, hover, etc.) and finds their relevant functions
* @param {Event} event - the passed in event that triggered this
* @param {String} actionType - the type of action "action, hover action, changeAction, etc"
*/
async handleAction(event, actionType = "action") {
event.preventDefault();
let targetElement = $(event.currentTarget);
//if our target element is a label, get the input before it instead
targetElement.prop("nodeName") === "LABEL" &&
(targetElement = targetElement.prev());
let action = targetElement.data()[actionType];
let handlerPropertyString = "onClick";
let parentLI = targetElement[0].closest(".tile-list-item, .popover");
let tileID = parentLI?.dataset?.id;
switch (actionType) {
case "hoverAction":
handlerPropertyString = "onHover";
break;
case "changeAction":
handlerPropertyString = "onChange";
break;
}
let actionData = getProperty(slideshowDefaultSettingsData, action);
if (actionData && actionData.hasOwnProperty(handlerPropertyString)) {
//call the event handler stored on this object
let app = game.JTCSlideshowConfig;
let options = {
action: action,
tileID: tileID,
parentLI: parentLI,
app: app,
html: app.element,
};
actionData[handlerPropertyString](event, options);
}
}
async getData() {
//return data to template
let artTileDataArray = await game.JTCS.tileUtils.getSceneSlideshowTiles(
"art",
true
);
let frameTileDataArray = await game.JTCS.tileUtils.getSceneSlideshowTiles(
"frame",
true
);
let unlinkedTileIDs = await game.JTCS.tileUtils.getUnlinkedTileIDs([
...artTileDataArray,
...frameTileDataArray,
]);
let areConfigInstructionsVisible = await HelperFunctions.getSettingValue(
"areConfigInstructionsVisible"
);
let allJournals = game.journal.contents;
let artJournal = await game.JTCS.utils.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.journal.value"
);
let artJournalData = {
options: allJournals,
value: artJournal,
};
let allScenes = await game.JTCS.tileUtils.getAllScenesWithSlideshowData();
let artScene = await game.JTCS.utils.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.scene.value"
);
let defaultArtTileID = await game.JTCS.tileUtils.manager.getDefaultArtTileID();
let artSceneData = {
options: allScenes,
value: artScene,
};
return {
shouldActivateDisplayScene: this.shouldActivateDisplayScene,
artTiles: artTileDataArray,
defaultArtTileID,
frameTiles: frameTileDataArray,
unlinkedTiles: unlinkedTileIDs,
currentSceneName: game.scenes.viewed.name,
artSceneData: artSceneData,
artJournalData: artJournalData,
partials: game.JTCS.templates,
areConfigInstructionsVisible,
settingsData: slideshowDefaultSettingsData,
...this.data,
};
}
async activateListeners(html) {
// super.activateListeners(html);
html = $(html[0].closest(".window-app"));
this.hideButtons(html[0]);
html.find(".window-content").attr("data-fade-all", true);
// await this.setUIColors(html);
// this._handleToggle(html);
html.off("click").on(
"click",
"[data-action]",
async (event) => await this.handleAction(event, "action", html)
);
let hoverEventString = "mouseenter mouseleave";
let hoverActionSelectorString = `[data-hover-action],
[data-hover-action] + label`;
html.off(hoverEventString, hoverActionSelectorString).on(
hoverEventString,
hoverActionSelectorString,
async (event) => await this.handleAction(event, "hoverAction")
);
// let tooltipActionSelectorString = "[data-tooltip]";
// html.off(hoverEventString, tooltipActionSelectorString).on(
// hoverEventString,
// tooltipActionSelectorString,
// async (event) => await Popover.generateTooltip(event, html, ".popover, .tile-list-item", "tooltip")
// );
html.off("change").on(
"change",
"[data-change-action]",
async (event) => await this.handleAction(event, "changeAction")
);
}
}
window.SlideshowConfig = SlideshowConfig;

View File

@ -0,0 +1,4 @@
export class SlideshowLayer extends TilesLayer{
}

View File

@ -0,0 +1,597 @@
import { MODULE_ID } from "../debug-mode.js";
import { HelperFunctions, HelperFunctions as HF } from "./HelperFunctions.js";
import { ImageDisplayManager } from "./ImageDisplayManager.js";
/**
* This class manages the Art and Bounding Tiles, creating them, showing them in the Config, and
* getting and setting their values
*/
export class ArtTileManager {
/**
*
* @param {String} oldTileID - the id of a tile that's now missing
* @param {String newTileID - the id of a new tile we're linking it to
*/
static async updateTileDataID(oldTileID, newTileID) {
//this is an array of objects
let tileDataArray = await ArtTileManager.getSceneSlideshowTiles("", false);
//find object with id
let index = tileDataArray.findIndex((tileData) => tileData.id === oldTileID);
let tileObject = tileDataArray[index]; //.find((tileData) => tileData.id === oldTileID);
tileObject = { ...tileObject, id: newTileID };
//here we ensure that the info for the linked tiles are also getting updated. Find ones that match our old ID, and update them to our new id
//replace the object at its original index with the object w/ the new id
if (tileObject && index !== undefined) {
tileDataArray.splice(index, 1, tileObject);
if (tileObject.isBoundingTile) {
ArtTileManager.updateLinkedArtTiles(oldTileID, newTileID, tileDataArray);
}
await ArtTileManager.updateAllSceneTileFlags(tileDataArray);
}
}
static async updateLinkedArtTiles(oldFrameID, newFrameID, tileDataArray) {
//get art tiles that had us as their bounding tile
let linkedArtTiles = [];
let updatedLinkedArtTiles = [];
linkedArtTiles = tileDataArray.filter(
(tileData) => tileData.linkedBoundingTile === oldFrameID
);
//update them with our new ID
updatedLinkedArtTiles = linkedArtTiles.map((tileData) => {
return {
...tileData,
linkedBoundingTile: newFrameID,
};
});
updatedLinkedArtTiles.forEach((atData) => {
let atIndex = tileDataArray.findIndex((item) => item.id === atData.id);
tileDataArray.splice(atIndex, 1, atData);
});
return tileDataArray;
}
static async getDefaultData(isBoundingTile, linkedBoundingTile = "") {
//determine its default name based on whether it's a bounding or display tile
let displayName = isBoundingTile ? "frameTile" : "displayTile";
displayName = await ArtTileManager.incrementTileDisplayName(displayName);
//increment it if one already exists with that name
return {
displayName: `${displayName}`,
isBoundingTile: isBoundingTile,
linkedBoundingTile: linkedBoundingTile,
};
}
static async incrementTileDisplayName(name) {
let finalName = name;
let tileDataArray = await ArtTileManager.getSceneSlideshowTiles();
let conflictingTile = tileDataArray.find((tileData) => {
return tileData.displayName.includes(name);
});
if (conflictingTile) {
let digit = conflictingTile.displayName.match(/\d+/g);
if (digit) {
digit = parseInt(digit);
digit++;
} else {
digit = 1;
}
finalName = finalName + digit;
}
return finalName;
}
static async createTileInScene(isFrameTile) {
let ourScene = game.scenes.viewed;
let pathProperty = isFrameTile ? "frameTilePath" : "artTilePath";
let imageManager = ImageDisplayManager;
let imgPath = await HF.getSettingValue(
"artGallerySettings",
`defaultTileImages.paths.${pathProperty}`
);
if (!imgPath) {
return;
}
const tex = await loadTexture(imgPath);
let sceneWidth = game.version >= 10 ? ourScene.width : ourScene.data.width;
let sceneHeight = game.version >= 10 ? ourScene.height : ourScene.data.height;
let dimensionObject = imageManager.calculateAspectRatioFit(
tex.width,
tex.height,
sceneWidth,
sceneHeight
);
let newTile = await ourScene.createEmbeddedDocuments("Tile", [
{
img: imgPath,
width: dimensionObject.width,
height: dimensionObject.height,
x: sceneWidth / 2 - dimensionObject.width / 2,
y: sceneHeight / 2 - dimensionObject.height / 2,
},
]);
return newTile;
}
static async createOrFindDefaultFrameTile() {
let frameTile;
let tileDataArray = (await ArtTileManager.getSceneSlideshowTiles()) || [];
if (tileDataArray.length === 0) {
ui.notifications.warn(
"No frame tile detected in scene. Creating new frame tile alongside display tile"
);
frameTile = await ArtTileManager.createFrameTileObject();
return frameTile[0].id;
} else {
ui.notifications.warn(
"No frame tile provided to when creating display tile. Linking display tile to first frame tile in scene"
);
frameTile = tileDataArray[0];
return frameTile.id;
}
}
/**
* Return any tiles that have data, but their id doesn't match any tile in the scene
* @param {Array} flaggedTiles - array of tiles with Slideshow data
* @returns array of tiles that have data but aren't connected to a tile in the scene
*/
static async getMissingTiles(flaggedTiles) {
let currentScene = game.scenes.viewed;
let sceneTileIDs = currentScene.tiles.map((tile) => tile.id);
let missingTiles = flaggedTiles.filter(
(tileData) => !sceneTileIDs.includes(tileData.id)
);
return missingTiles;
}
/**
* Get tiles that aren't linked to any slideshow data
* @param {Array} flaggedTiles - array of tiles with Slideshow data
* @returns array of IDs of tiles that aren't linked to any slideshow data
*/
static async getUnlinkedTileIDs(flaggedTiles) {
let currentScene = game.scenes.viewed;
let flaggedTileIDs = flaggedTiles.map((tileData) => tileData.id);
let sceneTileIDs = currentScene.tiles.map((tile) => tile.id);
let unlinkedTiles = sceneTileIDs.filter(
(tileID) => !flaggedTileIDs.includes(tileID)
);
return unlinkedTiles;
}
/**
*
* @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one
* @param {String} tileObjectID - the ID of the tile being created
* @param {Boolean} isBoundingTile - whether or not its a frame tile instead of an art tile
*/
static async createTileData(linkedFrameTileId, tileObjectID, isBoundingTile = false) {
let defaultData = await ArtTileManager.getDefaultData(
isBoundingTile,
linkedFrameTileId
);
await ArtTileManager.updateSceneTileFlags(defaultData, tileObjectID);
}
/**
* Create a Tile in the current scene that is linked to the Tile Data we're passing in
* @param {Object} options - the options object
* @param {Boolean} options.isFrameTile - whether or not its a frame tile or an art tile
* @param {String} options.linkedFrameTileID - the ID of the linked frame tile
* @param {String} options.unlinkedDataID - the ID of the unlinked art gallery tile we want to link to a tile in the scene
* @returns the new tile object in the scene that is linked to our previously unlinked data
*/
static async createAndLinkSceneTile(
options = {
isFrameTile: false,
linkedFrameTileID: "",
unlinkedDataID: "",
}
) {
let { isFrameTile, linkedFrameTileID, unlinkedDataID } = options;
let newTile = await ArtTileManager.createTileInScene(isFrameTile);
if (newTile) {
let tileObjectID = newTile[0].id;
//
if (!unlinkedDataID) {
console.log("Scene Gallery Config - Creating new tile data");
await ArtTileManager.createTileData(
linkedFrameTileID,
tileObjectID,
false
);
} else {
console.log(
"Scene Gallery Config | updating already created tile data, and linking it"
);
await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID);
}
} else {
ui.notifications.error("New art gallery tile couldn't be created");
}
return newTile;
}
/**
*
* @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one
* @param {*} unlinkedDataID - if we're creating a new tile from the config rather than from the tile itself, it may have an unlinkedId
* @returns - the created art tile
*/
static async createArtTileObject(_linkedFrameTileId = "", unlinkedDataID = "") {
let linkedFrameTileId = _linkedFrameTileId;
let newTile = await ArtTileManager.createTileInScene(false);
if (newTile) {
let tileObjectID = newTile[0].id;
if (!unlinkedDataID) {
await ArtTileManager.createTileData(
linkedFrameTileId,
tileObjectID,
false
);
} else {
await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID);
}
} else {
ui.notifications.error("New display tile couldn't be created");
}
return newTile;
}
/**
* @param {String} linkedFrameTileId - the frame tile linked to this art tile, if it is one
* @param {*} unlinkedDataID - if we're creating a new tile from the config rather than from the tile itself, it may have an unlinkedId
* @returns - the created frame tile
* */
static async createFrameTileObject(unlinkedDataID = "") {
let newTile = await ArtTileManager.createTileInScene(true);
if (newTile) {
let tileObjectID = newTile[0].id;
// let tileObjectID = newTile[0].document.id;
if (!unlinkedDataID) {
await ArtTileManager.createTileData("", tileObjectID, true);
} else {
await ArtTileManager.updateTileDataID(unlinkedDataID, tileObjectID);
}
} else {
ui.notifications.error("New frame tile couldn't be created");
}
return newTile;
}
static async convertToNewSystem() {
let currentScene = game.scenes.viewed;
let sceneTiles = currentScene.tiles.contents;
let boundingTile;
let displayTile;
sceneTiles.forEach(async (tile) => {
let flag = await tile.getFlag("journal-to-canvas-slideshow", "name");
switch (flag.name) {
case "boundingTile":
boundingTile = tile;
break;
case "displayTile":
displayTile = tile;
break;
default:
break;
}
});
if (boundingTile) {
convertBoundingTile(boundingTile.data);
if (displayTile) {
convertDisplayTile(displayTile.data, boundingTile.id);
}
} else {
if (displayTile) {
convertDisplayTile(displayTile.data);
}
}
}
static convertBoundingTile(tileData) {
let defaultData = {
displayName: "BoundingTile1",
isBoundingTile: true,
linkedBoundingTile: "",
};
updateSceneTileFlags(defaultData, tileData.id);
}
static convertDisplayTile(tileData, linkedBoundingTileId = "") {
let defaultData = {
displayName: "DisplayTile1",
isBoundingTile: false,
linkedBoundingTile: linkedBoundingTileId,
};
updateSceneTileFlags(defaultData, tileData.id);
}
/**
* Set the default art gallery art tile in this scene
* @param {String} tileID - the id of the art gallery tile that was clicked
* @param {Object} currentScene - the current scene doc
*/
static async setDefaultArtTileID(tileID, currentScene) {
if (!currentScene) currentScene = game.scenes.viewed;
await currentScene.setFlag(MODULE_ID, "defaultArtTileID", tileID);
Hooks.callAll("updateDefaultArtTile", { updateData: tileID, currentScene });
}
/**
* Gets the default art gallery tile in this scene, or returns the first tile in scene if not found
* returns undefined if that is also not found
* @param {Object} currentScene - the current scene doc
*/
static async getDefaultArtTileID(currentScene) {
if (!currentScene) currentScene = game.scenes.viewed;
//get all the art tiles in the scene, filtering out the ones that aren't unlinked/missing
let artTiles = (
await ArtTileManager.getSceneSlideshowTiles("art", true, {
currentSceneID: currentScene.id,
})
).filter((item) => !item.missing);
let defaultArtTileID = await currentScene.getFlag(MODULE_ID, "defaultArtTileID");
// if the defaultArtTileID stored in our flags is
let shouldReplaceID = false;
const found = artTiles.find((tileData) => tileData.id === defaultArtTileID);
if (!found) {
//a tile with this ID wasn't in the scene, so replace it with a the first art tile that IS linked, or if there are none, replace it with an empty string
shouldReplaceID = true;
if (artTiles.length > 0) {
defaultArtTileID = artTiles[0].id;
} else {
defaultArtTileID = "";
}
}
// should
if (shouldReplaceID) {
await currentScene.setFlag(MODULE_ID, "defaultArtTileID", defaultArtTileID);
}
return defaultArtTileID;
}
static async getGalleryTileDataFromID(tileID, property = "", currentSceneID = "") {
if (!currentSceneID) currentSceneID = game.scenes.viewed.current;
let flaggedTiles = await ArtTileManager.getSceneSlideshowTiles("", false, {
currentSceneID,
});
let ourTile = flaggedTiles.find((data) => data.id === tileID);
if (property) {
if (ourTile) {
return ourTile[property];
} else {
return "";
}
} else {
return ourTile;
}
}
/**
* get tiles that have been stored by this module in a flag on this scene
* @returns array of display tile data stored in "slideshowTiles" tag
*/
static async getSceneSlideshowTiles(
type = "",
shouldCheckIfExists = false,
options = { currentSceneID: "" }
) {
let { currentSceneID } = options;
let currentScene = game.scenes.viewed;
if (currentSceneID) {
currentScene = game.scenes.get(currentSceneID);
}
let flaggedTiles =
(await currentScene.getFlag(
"journal-to-canvas-slideshow",
"slideshowTiles"
)) || [];
//check if the tile exists in the scene, and add a "missing" element if it does
if (shouldCheckIfExists) {
//get the ids of all the tiles in the scene
let sceneTileIDs = currentScene.tiles.map((tile) => tile.id);
// if our tile data's id isn't included in the ids of tiles in the scene, add a missing property
flaggedTiles = flaggedTiles.map((tileData) =>
!sceneTileIDs.includes(tileData.id)
? { ...tileData, missing: true }
: tileData
);
}
if (type === "frame") {
return ArtTileManager.getFrameTiles(flaggedTiles);
} else if (type === "art") {
return ArtTileManager.getDisplayTiles(flaggedTiles);
}
return flaggedTiles;
}
/**
* Returns the "bounding" or "frame" tiles from the scene's displayTiles flag
* @param {Array} flaggedTiles - array of Display Tiles from a scene flag
* @returns filtered array with only the bounding tiles
*/
static getFrameTiles(flaggedTiles) {
return flaggedTiles.filter((tileData) => tileData.isBoundingTile);
}
/**
* Returns the Display tiles from the scene's displayTiles flag array
* @param {Array} flaggedTiles - array of SlideshowTiles from the scene's flag
* @returns filtered array with only the display tiles
*/
static getDisplayTiles(flaggedTiles) {
return flaggedTiles.filter((tileData) => !tileData.isBoundingTile);
}
static async renderTileConfig(tileID, sceneID = "") {
let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID);
if (tile) {
await tile.sheet.render(true);
} else {
ArtTileManager.displayTileNotFoundError(tileID);
}
}
static async toggleTileZ(tileID, toFront = true) {
let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID);
if (!tile) return;
const zIndex = tile.object.zIndex;
if (toFront) {
//save the default z-index
tile.defaultZ = zIndex;
await game.scenes.viewed.updateEmbeddedDocuments("Tile", {
_id: tileID,
object: { zIndex: 300 },
});
} else {
await game.scenes.viewed.updateEmbeddedDocuments("Tile", {
_id: tileID,
object: { zIndex: tile.defaultZ },
});
}
}
static async selectTile(tileID, sceneID = "") {
let tile = await game.scenes.viewed.getEmbeddedDocument("Tile", tileID);
if (tile) {
await game.JTCS.utils.swapTools();
tile.object.control({
releaseOthers: true,
});
} else {
ArtTileManager.displayTileNotFoundError(tileID);
}
}
static async getLinkedFrameID(tileID, flaggedTiles) {
let tileData = await ArtTileManager.getTileDataFromFlag(tileID, flaggedTiles);
if (!tileData) {
console.error("Could not find tile data with that ID");
}
return tileData.linkedBoundingTile;
}
/**
* Get the DisplayTile data
* @param {string} tileID - the id of the tile in scene we're looking to filter
* @param {Array} flaggedTiles - the flagged tiles
* @returns the flag data
*/
static async getTileDataFromFlag(tileID, flaggedTiles) {
let defaultData = ArtTileManager.getDefaultData(false, "");
if (!flaggedTiles) {
return defaultData;
}
let flaggedTile = flaggedTiles.find((tileData) => tileData.id === tileID);
//if we find a tile
if (flaggedTile) {
return flaggedTile;
} else {
return defaultData;
}
}
static async getTileObjectByID(tileID, sceneID = "") {
let ourScene = game.scenes.viewed;
if (sceneID) ourScene = game.scenes.get(sceneID);
let tile = await ourScene.getEmbeddedDocument("Tile", tileID);
if (tileID.includes === "new") {
// console.log("New tile created");
} else {
if (!tile) {
ArtTileManager.displayTileNotFoundError(tileID);
}
}
return tile;
}
static displayTileNotFoundError(tileID, sceneID = "") {
console.error("JTCS can't find tile in scene with ID " + tileID);
}
/**
* Update a tile in this scene with new data, or create a new one
* @param {Object} displayData - the data we want to update with
* @param {String} tileID - the id of the tile we want to update
*/
static async updateSceneTileFlags(displayData, tileID) {
if (!tileID) {
return;
}
let currentScene = game.scenes.viewed;
let tiles = (await ArtTileManager.getSceneSlideshowTiles()) || [];
if (tiles.find((tile) => tile.id === tileID)) {
tiles = tiles.map((tileData) => {
// if the ids match, update the matching one with the new displayName
return tileData.id === tileID
? { ...tileData, ...displayData }
: tileData; //else just return the original
});
} else {
tiles.push({ id: tileID, ...displayData });
}
await ArtTileManager.updateAllSceneTileFlags(tiles);
}
/**
* Replace the slideshow tileData array stored in scene flags with the array passed in
* @param {Array} tiles - the tiles array we want to update our flag with
*/
static async updateAllSceneTileFlags(tiles, currentSceneID = "") {
let currentScene = game.scenes.get(currentSceneID);
if (!currentScene) currentScene = game.scenes.viewed;
await currentScene.setFlag(
"journal-to-canvas-slideshow",
"slideshowTiles",
tiles
);
Hooks.callAll("updateArtGalleryTiles", { currentScene, updateData: tiles });
}
static async deleteSceneTileData(tileID) {
let tiles = await ArtTileManager.getSceneSlideshowTiles();
//filter out the tile that matches this
let deletedTileData = tiles.find((tileData) => tileData.id === tileID);
tiles = tiles.filter((tileData) => tileData.id !== tileID);
if (deletedTileData.isBoundingTile) {
await ArtTileManager.updateLinkedArtTiles(tileID, "", tiles);
}
await ArtTileManager.updateAllSceneTileFlags(tiles);
//call hook to delete the art tile data
}
static async getAllScenesWithSlideshowData() {
let slideshowScenes =
game.scenes.contents.filter((scene) => {
if (game.version >= 10)
return scene.flags[`${MODULE_ID}`]?.slideshowTiles;
else return scene.data.flags[`${MODULE_ID}`]?.slideshowTiles;
}) || [];
return slideshowScenes;
}
}

View File

@ -0,0 +1,160 @@
import { ArtTileManager } from "./ArtTileManager.js";
import { HelperFunctions } from "./HelperFunctions.js";
export class CanvasIndicators {
static async setUpIndicators(foundTileData, tileDoc) {
if (!tileDoc) {
ui.notifications.error("No tile doc was provided.");
return;
}
let type = "unlinked";
if (foundTileData) {
type = foundTileData.isBoundingTile ? "frame" : "art";
const defaultTileID = await ArtTileManager.getDefaultArtTileID();
if (foundTileData.id === defaultTileID) {
type = "default";
}
}
await CanvasIndicators.createTileIndicator(tileDoc, type);
await CanvasIndicators.hideTileIndicator(tileDoc);
}
static async getColors() {
let colors = {};
let settingsColors = await HelperFunctions.getSettingValue(
"artGallerySettings",
"indicatorColorData.colors"
);
colors.frameTileColor = settingsColors.frameTileColor || "#ff3300";
colors.artTileColor = settingsColors.artTileColor || "#2f2190";
colors.unlinkedTileColor = settingsColors.unlinkedTileColor || "#a2ff00";
colors.defaultTileColor = settingsColors.defaultTileColor || "#e75eff";
return colors;
}
/**
* for v10, create an indicator that better reflects the image
* @author TheRipper93 (original author)
* @author Eva (added small changes better to fit module)
* https://github.com/theripper93/tile-sort/blob/master/scripts/main.js
* @returns - the created sprite
*/
static createV10Indicator(tile, fillAlpha, color) {
let tileImg = tile.mesh;
if (!tileImg || !tileImg.texture.baseTexture) return;
let sprite = new PIXI.Sprite.from(tileImg.texture);
sprite.isSprite = true;
sprite.width = tile.document.width;
sprite.height = tile.document.height;
sprite.angle = tileImg.angle;
sprite.alpha = fillAlpha;
sprite.tint = color;
sprite.name = "tilesorthighlight";
return sprite;
}
static async createTileIndicator(tileDocument, type = "art") {
if (!tileDocument) {
ui.notifications.warn("Tile document not supplied.");
return;
}
//add check for if it's v10
const isV10 = game.version >= 10 ? true : false;
let tileDimensions = {
width: isV10 ? tileDocument.width : tileDocument.data.width,
height: isV10 ? tileDocument.height : tileDocument.data.height,
x: isV10 ? tileDocument.x : tileDocument.data.x,
y: isV10 ? tileDocument.y : tileDocument.data.y,
};
let tileObject = tileDocument.object;
if (!tileObject) {
return;
}
if (tileObject && tileObject.overlayContainer) {
//destroy the overlayContainer PIXI Container stored on the tileObject
tileObject.overlayContainer.destroy();
//delete the property itself that was storing it
delete tileObject.overlayContainer;
}
let colors = await CanvasIndicators.getColors();
let color;
let fillAlpha = 0.5;
let lineWidth;
switch (type) {
case "frame":
color = colors.frameTileColor;
lineWidth = 15;
break;
case "art":
color = colors.artTileColor;
lineWidth = 5;
break;
case "unlinked":
color = colors.unlinkedTileColor;
lineWidth = 15;
break;
case "default":
color = colors.defaultTileColor;
lineWidth = 15;
break;
}
color = color.substring(1);
if (color.length === 8) {
color = HelperFunctions.hex8To6(color);
}
color = `0x${color}`;
tileObject.overlayContainer = tileObject.addChild(new PIXI.Container());
let overlayGraphic;
let overlaySprite;
if (game.version >= 10 && (type === "art" || type === "default")) {
overlaySprite = CanvasIndicators.createV10Indicator(
tileObject,
fillAlpha,
color
);
tileObject.overlayContainer.addChild(overlaySprite);
}
overlayGraphic = new PIXI.Graphics();
const whiteColor = 0xffffff;
fillAlpha = overlaySprite ? 0.25 : 0.5;
overlayGraphic.beginFill(whiteColor, fillAlpha);
overlayGraphic.lineStyle(lineWidth, color, 1);
overlayGraphic.tint = color;
overlayGraphic.drawRect(0, 0, tileDimensions.width, tileDimensions.height);
overlayGraphic.endFill();
tileObject.overlayContainer.addChild(overlayGraphic);
tileObject.overlayContainer.alpha = 0;
}
static async showTileIndicator(tileDocument, alpha = 1) {
if (!tileDocument || !tileDocument.object) {
console.warn("Tile document not supplied.");
return;
}
let tileObject = tileDocument.object;
if (tileObject.overlayContainer) {
tileObject.overlayContainer.alpha = alpha;
} else {
console.error("No overlay container found");
}
}
static async hideTileIndicator(tileDocument) {
if (!tileDocument || !tileDocument.object) {
console.warn("No tile document supplied");
return;
}
let tileObject = tileDocument.object;
if (tileObject && tileObject.overlayContainer) {
tileObject.overlayContainer.alpha = 0;
} else {
console.error("No overlay container found");
}
}
}

View File

@ -0,0 +1 @@
class CustomDialog extends Dialog {}

View File

@ -0,0 +1,588 @@
import { log, MODULE_ID } from "../debug-mode.js";
import { artGalleryDefaultSettings } from "../settings.js";
export class HelperFunctions {
static MODULE_ID = "journal-to-canvas-slideshow";
/**
* pass in a string and capitalize each word in the string
* @param {String} string - the string whose words we want to capitalize
* @param {String} delimiter - a delimiter separating each word
* @returns A string with each word capitalized and the same delimiters
*/
static capitalizeEachWord(string, delimiter = " ") {
let sentenceArray;
let capitalizedString;
if (!delimiter) {
// if the delimiter is an empty string, split it by capital letters, as if camelCase
sentenceArray = string.split(/(?=[A-Z])/).map((s) => s.toLowerCase());
capitalizedString = sentenceArray
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(" ");
} else {
sentenceArray = string.split(delimiter);
capitalizedString = sentenceArray
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(delimiter);
}
return capitalizedString;
}
static async resetArtGallerySettings() {
await HelperFunctions.setSettingValue("artGallerySettings", {}, "", false);
await HelperFunctions.setSettingValue(
"artGallerySettings",
artGalleryDefaultSettings,
"",
true
);
// await game.settings.set(MODULE_ID, "artGallerySettings", newSettings);
}
static async swapTools(layerName = "background", tool = "select") {
if (game.version >= 10) {
if ((layerName = "background")) layerName = "tiles";
}
ui.controls.controls.find((c) => c.layer === layerName).activeTool = tool;
let ourLayer = game.canvas.layers.find((l) => l.options.name === layerName);
if (ourLayer) {
if (ourLayer && !ourLayer.active) {
ourLayer?.activate();
} else {
ui.controls.render(true);
}
} else {
console.error("Can't find that layer", ourLayer);
}
}
/**
* Scale a tile's size, or one dimension (length or width) of a tile
* @param {Number} scale - the ratio to scale the tile by
* @param {String} axis - the tile's width or the tile's height or both
*/
static async scaleControlledTiles(scale = 0.5, axis = " ") {
const ourScene = game.scenes.viewed;
const layerName = game.version >= 10 ? "tiles" : "background";
const sceneTiles = canvas[layerName].controlled.filter(
(obj) => obj.document.documentName === "Tile"
);
let updateObjects = [];
sceneTiles.forEach((tile) => {
let tileWidth = game.version >= 10 ? tile.width : tile.data.width;
let tileHeight = game.version >= 10 ? tile.height : tile.data.height;
let width = duplicate(tileWidth);
let height = duplicate(tileHeight);
height *= scale;
width *= scale;
updateObjects.push({
_id: tile.id,
...(axis === " " && { height: height, width: width }),
...(axis === "height" && { height: height }),
...(axis === "width" && { width: width }),
});
});
ourScene.updateEmbeddedDocuments("Tile", updateObjects);
}
// Move tiles
// originally By @cole$9640
//slightly edited by @Eva into a modular method
static async moveControlledTiles(amount = 10, axis = "x") {
const ourScene = game.scenes.viewed;
let tiles;
if (game.version >= 10) {
tiles = canvas.tiles.controlled;
} else {
tiles =
canvas.background.controlled.length === 0
? canvas.foreground.controlled
: canvas.background.controlled;
}
if (tiles.length) {
const updates = tiles
.filter((tile) => {
if (game.version >= 10) return !tile.locked;
else return !tile.data.locked;
})
.map((tile) => ({
_id: tile.id,
[axis]: tile[axis] + amount,
}));
ourScene.updateEmbeddedDocuments("Tile", updates);
} else {
ui.notifications.notify("Please select at least one tile.");
}
}
static async setFlagValue(document, flagName, updateData, nestedKey = "") {
await document.setFlag(MODULE_ID, flagName, updateData);
}
/**
* Get the value of a document's flag
* @param {Object} document - the document whose flags we want to set (Scene, Actor, Item, etc)
* @param {String} flagName - the name of the flag
* @param {String} nestedKey - a string of nested properties separated by dot notation that we want to set
* @param {*} returnIfEmpty - a value to return if the flag is undefined
* @returns
*/
static async getFlagValue(document, flagName, nestedKey = "", returnIfEmpty = []) {
let flagData = await document.getFlag(MODULE_ID, flagName);
if (!flagData) {
flagData = returnIfEmpty;
}
return flagData;
}
/**
* Sets a value, using the "flattenObject" and "expandObject" utilities to reach a nested property
* @param {String} settingName - the name of the setting
* @param {*} updateData - the value you want to set a property to
* @param {String} nestedKey - a string of dot-separated values to refer to a nested property
*/
static async setSettingValue(
settingName,
updateData,
nestedKey = "",
isFormData = false
) {
if (isFormData) {
let currentSettingData = game.settings.get(
HelperFunctions.MODULE_ID,
settingName
);
updateData = expandObject(updateData); //get expanded object version of formdata keys, which were strings in dot notation previously
updateData = mergeObject(currentSettingData, updateData);
// let updated = await game.settings.set(HelperFunctions.MODULE_ID, settingName, currentSettingData);
// console.warn(updated);
}
if (nestedKey) {
let settingData = game.settings.get(HelperFunctions.MODULE_ID, settingName);
setProperty(settingData, nestedKey, updateData);
await game.settings.set(HelperFunctions.MODULE_ID, settingName, settingData);
} else {
await game.settings.set(HelperFunctions.MODULE_ID, settingName, updateData);
}
}
/* --------------------------------- Colors --------------------------------- */
/**
*
* Get the contrasting color for any hex color
* (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com
* Derived from work by Brian Suda, https://24ways.org/2010/calculating-color-contrast/
* @param {String} A hexcolor value
* @return {String} The contrasting color (black or white)
**/
static getContrast(hexcolor) {
// If a leading # is provided, remove it
if (hexcolor.slice(0, 1) === "#") {
hexcolor = hexcolor.slice(1);
}
// If a three-character hexcode, make six-character
if (hexcolor.length === 3) {
hexcolor = hexcolor
.split("")
.map(function (hex) {
return hex + hex;
})
.join("");
}
// Convert to RGB value
var r = parseInt(hexcolor.substr(0, 2), 16);
var g = parseInt(hexcolor.substr(2, 2), 16);
var b = parseInt(hexcolor.substr(4, 2), 16);
// Get YIQ ratio
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
// Check contrast
//@author: slightly modified this
return yiq; // >= 128 ? "black" : "white";
// return yiq >= 128 ? "black" : "white";
}
/**
* Will return 1 if color is darker than gray, -1 if color is lighter than gray
* @param {String} hexColor - string hex color
* @returns {Number} -1 or +1
*/
static lighterOrDarker(hexColor) {
const HF = HelperFunctions;
let yiq = HF.getContrast(hexColor);
//the 128 is like a value between 0 and 255, so gray
//checking to see if the contrast value is greater than gray (black) or less than gray (white)
return yiq >= 128 ? -1 : 1;
}
/**
*
* @param {String} backgroundColor - our background color
* @param {String} accentColor - the color we're adjusting to find a tint with better contrast
* @returns a color lightened to have the right contrast
*/
static getColorWithContrast(backgroundColor, accentColor) {
const HF = HelperFunctions;
backgroundColor = HF.hex8To6(backgroundColor);
accentColor = HF.hex8To6(accentColor);
const contrastValue = HF.getContrastBetween(backgroundColor, accentColor);
const direction = contrastValue > 0 ? 1 : -1;
// const text = contrastValue < 0 ? "We should darken color" : "we should lighten color";
let adjustedColor = HF.hex8To6(accentColor);
for (
let adjustAmount = 0, times = 0;
times < 15;
adjustAmount += direction * 10, times += 1
) {
adjustedColor = HF.LightenDarkenColor(accentColor, adjustAmount);
const hasEnoughContrast = HF.checkIfColorsContrastEnough(
backgroundColor,
adjustedColor
);
if (hasEnoughContrast) {
break;
}
}
return adjustedColor;
}
static getContrastBetween(backgroundColor, accentColor) {
const HF = HelperFunctions;
let contrast1 = HF.getContrast(backgroundColor);
let contrast2 = HF.getContrast(accentColor);
//the 128 is like a value between 0 and 255, so gray.
//if luminance? is grater than 128, it's between gray and white, so return a dark color
//if luminance? is less than 128, it's between black and gray, so return a light color
let contrastBetween = contrast2 - contrast1;
return contrastBetween;
}
static checkIfColorsContrastEnough(hexColor1, hexColor2) {
const HF = HelperFunctions;
let contrast1 = HF.getContrast(hexColor1);
let contrast2 = HF.getContrast(hexColor2);
//the 128 is like a value between 0 and 255, so gray.
//if luminance? is grater than 128, it's between gray and white, so return a dark color
//if luminance? is less than 128, it's between black and gray, so return a light color
let contrastBetween = Math.abs(contrast2 - contrast1);
return contrastBetween >= 128 ? true : false;
}
/**
* Programmatically lighten or darken a color
* @author "Pimp Trizkit" on Stackoverflow
* @link https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
* @returns a lightened or darkened color
*/
static LightenDarkenColor(col, amt) {
var usePound = false;
if (col[0] == "#") {
col = col.slice(1);
usePound = true;
}
var num = parseInt(col, 16);
var r = (num >> 16) + amt;
if (r > 255) r = 255;
else if (r < 0) r = 0;
var b = ((num >> 8) & 0x00ff) + amt;
if (b > 255) b = 255;
else if (b < 0) b = 0;
var g = (num & 0x0000ff) + amt;
if (g > 255) g = 255;
else if (g < 0) g = 0;
var string = "000000" + (g | (b << 8) | (r << 16)).toString(16);
return (usePound ? "#" : "") + string.substring(string.length - 6);
// return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16);
}
/**
* set the custom colors for the indicators and color scheme in the JTCS Apps
* @param {String} settingPropertyString - the string name of the property in the settings
*/
static async getColorDataFromSettings(settingPropertyString) {
const HF = HelperFunctions;
let colorData = await HelperFunctions.getSettingValue(
"artGallerySettings",
settingPropertyString
);
let { colors, propertyNames, colorVariations } = colorData;
Object.keys(colors).forEach((colorKey) => {
const value = HF.hex8To6(colors[colorKey]);
const propertyName = propertyNames[colorKey];
let shouldMakeVariants = false;
if (colorVariations) {
shouldMakeVariants = colorVariations[colorKey];
}
HF.setRootStyleProperty(propertyName, value, shouldMakeVariants);
});
// add these extra bits on for now
if (settingPropertyString === "colorSchemeData") {
let { accentColor, backgroundColor } = colors;
// accentColor = accentColor;//HF.getColorWithContrast(backgroundColor, accentColor);
backgroundColor = HF.hex8To6(backgroundColor);
const colorNeutral =
HF.getContrast(backgroundColor) >= 128 ? "black" : "white";
const textColor = HF.getContrast(accentColor) >= 128 ? "black" : "white";
HF.setRootStyleProperty("--JTCS-text-color-on-bg", colorNeutral); //for text on the background color
HF.setRootStyleProperty("--JTCS-text-color-on-fill", textColor); //for text on buttons and filled labels
HF.setRootStyleProperty("--JTCS-accent-color", accentColor); //HF.getColorWithContrast(backgroundColor, accentColor));
}
}
/**
* set the custom colors for the indicators and color scheme in the JTCS Apps
*/
static async setUIColors() {
await this.getColorDataFromSettings("indicatorColorData");
await this.getColorDataFromSettings("colorSchemeData");
}
/**
* Set properties of custom colors on root element for use in our CSS
* @param {String} propertyName - the name of the CSS custom property we want to set on the root element
* @param {String} value - a value representing a hex code color
* @param {*} makeVariations - whether we should generate light and dark variants on this color for better contrast
*/
static setRootStyleProperty(propertyName, value, makeVariations = false) {
const html = document.documentElement;
const HF = HelperFunctions;
value = HF.hex8To6(value);
html.style.setProperty(propertyName, value);
if (makeVariations) {
const direction = HF.lighterOrDarker(value);
const shouldDarken = direction < 0 ? true : false;
const text =
direction < 0 ? "We should darken color" : "we should lighten color";
let startNumber = !shouldDarken ? 80 : 0;
let step = !shouldDarken ? -10 : 10;
for (var number = startNumber; Math.abs(number) < 90; number += step) {
const variantPropName = `${propertyName}-${number
.toString()
.padStart(2, "0")}`;
// const amount = number;
const amount = shouldDarken ? number * -1 : number;
const variantValue = HF.LightenDarkenColor(value, amount);
html.style.setProperty(variantPropName, variantValue);
}
if (propertyName.includes("background-color")) {
const htmlStyle = getComputedStyle(html);
let inputBG = htmlStyle.getPropertyValue("--JTCS-background-color");
let elevationBG = htmlStyle.getPropertyValue("--JTCS-background-color");
let borderColor = htmlStyle.getPropertyValue(
"--JTCS-background-color-70"
);
let shadowColor = htmlStyle.getPropertyValue(
"--JTCS-background-color-50"
);
let dangerColor = htmlStyle.getPropertyValue("--color-danger-base");
let warningColor = htmlStyle.getPropertyValue("--color-warning-base");
let infoColor = htmlStyle.getPropertyValue("--color-info-base");
let successColor = htmlStyle.getPropertyValue("--color-success-base");
let tileItemColor = "transparent";
if (!shouldDarken) {
inputBG = getComputedStyle(html).getPropertyValue(
"--JTCS-background-color-20"
);
borderColor = "transparent"; //getComputedStyle(html).getPropertyValue("--JTCS-background-color");
shadowColor = "transparent";
elevationBG = htmlStyle.getPropertyValue(
"--JTCS-background-color-10"
);
dangerColor = htmlStyle.getPropertyValue("--color-danger-light");
dangerColor = htmlStyle.getPropertyValue("--color-danger-light");
infoColor = htmlStyle.getPropertyValue("--color-info-light");
successColor = htmlStyle.getPropertyValue("--color-success-light");
tileItemColor = elevationBG;
}
html.style.setProperty("--JTCS-box-shadow-color", shadowColor);
html.style.setProperty("--JTCS-input-background-color", inputBG);
html.style.setProperty("--JTCS-border-color", borderColor);
html.style.setProperty("--JTCS-elevation-BG-color", elevationBG);
html.style.setProperty("--JTCS-danger-color", dangerColor);
html.style.setProperty("--JTCS-warning-color", warningColor);
html.style.setProperty("--JTCS-info-color", dangerColor);
html.style.setProperty("--JTCS-success-color", warningColor);
html.style.setProperty("--JTCS-tile-item-bg-color", tileItemColor);
}
}
}
/**
* Checks if color is in hex8 format, and if so slices string to make it hex6
* @param {String} hexColor - a hex color code with "#" up front
* @returns {String}
*/
static hex8To6(hexColor) {
let hexColorMod = hexColor;
if (hexColor.slice(1).length > 6) {
hexColorMod = hexColor.slice(0, -2);
}
return hexColorMod;
}
static async getSettingValue(settingName, nestedKey = "") {
let settingData = await game.settings.get(HelperFunctions.MODULE_ID, settingName);
if (settingData !== undefined && settingData !== null) {
if (nestedKey) {
let nestedSettingData = getProperty(settingData, nestedKey);
return nestedSettingData;
}
return settingData;
} else {
console.error("Cannot find setting with name " + settingName);
}
}
static async checkSettingEquals(settingName, compareToValue) {
if (game.settings.get(HelperFunctions.MODULE_ID, settingName) == compareToValue) {
return true;
}
return false;
}
static async showWelcomeMessage() {
let options = {};
let d = new Dialog({
title: "Welcome Message",
content: `<div><p>
<h2>Journal To Canvas Slideshow Has Updated</h2>
<p>Journal to Canvas Slideshow is now known as "JTCS - Art Gallery"</p>
<p>The module has received a huge overhaul, updates and improvements to preexisting features, and the addition of brand new features.
</p>
<video width="100%" controls>
<source src="https://user-images.githubusercontent.com/13098820/193938899-f5920be7-6148-4ac7-9738-8a5ee7d420e9.mp4"
type="video/mp4"/>
Feature demo</video>
<ol style="list-style-type:decimal">
<li>
<a href="https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow/blob/master/features-and-walkthrough.md">
View a detailed guide and walkthrough of the new features here
</a>
</li>
<li><a href="https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow/blob/master/README.md">ReadMe and Feature List</a></li>
<li><a href="https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow/blob/master/release-notes.md">Release Notes</a></li>
</ol>
</p>
<p>Note: This welcome message can be turned on and off in the module settings, but will be enabled after updates to inform you of important changes.</p>
</div> `,
buttons: {
disable: {
label: "Disable Welcome Message",
callback: async () => await HelperFunctions.disableWelcomeMessage(),
},
continue: {
label: "Continue without Disabling",
},
},
});
d.render(true);
}
static async disableWelcomeMessage() {
//disable the welcome message
await HelperFunctions.setSettingValue("showWelcomeMessage", false);
}
static isImage(url) {
return /\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(url);
}
/**
* Validating text input
* @param {String} inputValue - the input value
* @param {string} validationType - the type of input, what are we looking for, an image, video, etc.
* @param {function} onInvalid - callback function for if our input is invalid
* @returns true or false depending on if our input is valid or not
*/
static validateInput(inputValue, validationType, onInvalid = "") {
let valid = false;
switch (validationType) {
case "image":
valid = HelperFunctions.isImage(inputValue);
break;
default:
valid = inputValue !== undefined;
break;
}
return valid;
}
static getElementPositionAndDimension(element) {
return {};
}
static isOutsideClick(event) {
if ($(event.target).closest(".popover").length) {
//click was on the popover
return false;
}
//if our click is outside of our popover element
return true;
}
/**
* Create a dialog
* @param {String} title - the title of the dialog
* @param {String} templatePath - the path to the template being used for this dialog
* @param {Object} data - the data object
* @param {Object} data.buttons - the buttons at the bottom of the prompt
*/
static async createDialog(title, templatePath, data) {
const options = {
width: 600,
// height: 250,
id: "JTCS-custom-dialog",
};
let renderedHTML = await renderTemplate(templatePath, data);
let d = new Dialog(
{
title: title,
content: renderedHTML,
buttons: data.buttons,
},
options
).render(true);
}
static async createEventActionObject(
name,
callback,
shouldRenderAppOnAction = false
) {
return {
name: name,
callback: callback,
shouldRenderAppOnAction: shouldRenderAppOnAction,
};
}
static editorsActive(sheet) {
let hasActiveEditors = Object.values(sheet.editors).some(
(editor) => editor.active
);
return hasActiveEditors;
}
///
}

View File

@ -0,0 +1,469 @@
import { ArtTileManager } from "./ArtTileManager.js";
import { HelperFunctions } from "./HelperFunctions.js";
import ImageVideoPopout from "./MultiMediaPopout.js";
/**
* This class manages the images specifically, setting and clearing the tiles' images
*/
export class ImageDisplayManager {
static async getTilesFromArtScene() {
let artSceneID = await game.JTCS.utils.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.scene.value"
);
let ourScene = game.scenes.get(artSceneID);
let artTiles = await game.JTCS.tileUtils.getSceneSlideshowTiles("art", true, {
currentSceneID: artSceneID,
});
let artTileID = artTiles[0].id;
let frameTiles = await game.JTCS.tileUtils.getSceneSlideshowTiles("frame", true, {
currentSceneID: artSceneID,
});
let frameTileID = artTiles[0].linkedBoundingTile || frameTiles[0].id;
return {
ourScene: ourScene,
artTileID: artTileID,
frameTileID: frameTileID,
};
}
static async updateTileObjectTexture(
artTileID,
frameTileID,
url,
method,
sceneID = ""
) {
let ourScene = game.scenes.get(sceneID);
if (!ourScene) ourScene = game.scenes.viewed;
let artTile = ourScene.tiles.get(artTileID);
let frameTile = ourScene.tiles.get(frameTileID);
//load the texture from the source
if (!artTile || !url) {
ui.notifications.error("Tile or image not found");
console.error(url, artTile, artTileID);
return;
}
const tex = await loadTexture(url);
if (!tex) {
ui.notifications.error(
`Error loading texture from '${url}'. Access to URL likely blocked by CORS policy.`
);
return;
}
let imageUpdate;
if (!frameTile) {
imageUpdate = await ImageDisplayManager.scaleArtTileToScene(
artTile,
tex,
url,
sceneID
);
} else {
imageUpdate = await ImageDisplayManager.scaleArtTileToFrameTile(
artTile,
frameTile,
tex,
url,
sceneID
);
}
const updated = await ourScene
.updateEmbeddedDocuments("Tile", [imageUpdate])
.catch((error) =>
ui.notifications.error(
`Default art tile in ${ourScene.name} couldn't be updated`
)
);
if (updated && method === "artScene") {
const { autoActivate, autoView } = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.scene"
);
if (autoActivate) {
ourScene.activate();
}
if (autoView) {
ourScene.view();
}
if (
game.user.isGM && //if we're GM
((!autoActivate && !autoView) || (ourScene.active && !ourScene.viewed)) //if the scene is neither set to activate or to view, notify that the image updated.
// if the scene is active but not viewed, notify that the image updated
) {
ui.notifications.info(
`Default Tile in Art Scene '${ourScene.name}' successfully updated`
);
}
}
}
static async scaleArtTileToScene(displayTile, tex, url, sceneID = "") {
let displayScene = game.scenes.get(sceneID);
if (!displayScene) displayScene = game.scenes.viewed;
let displaySceneWidth =
game.version >= 10 ? displayScene.width : displayScene.data.width;
let displaySceneHeight =
game.version >= 10 ? displayScene.height : displayScene.data.height;
let dimensionObject = await ImageDisplayManager.calculateAspectRatioFit(
tex.width,
tex.height,
displaySceneWidth,
displaySceneHeight
// displayScene.data.width,
// displayScene.data.height
);
//scale down factor is how big the tile will be in the scene
//make this scale down factor configurable at some point
let scaleDownFactor = 200;
dimensionObject.width -= scaleDownFactor;
dimensionObject.height -= scaleDownFactor;
//half of the scene's width or height is the center -- we're subtracting by half of the image's width or height to account for the offset because it's measuring from top/left instead of center
//separate objects depending on the texture's dimensions --
//create an 'update' object for if the image is wide (width is bigger than height)
let wideImageUpdate = {
_id: displayTile.id,
width: dimensionObject.width,
height: dimensionObject.height,
img: url,
x: scaleDownFactor / 2,
y: displaySceneHeight / 2 - dimensionObject.height / 2,
};
//create an 'update' object for if the image is tall (height is bigger than width)
let tallImageUpdate = {
_id: displayTile.id,
width: dimensionObject.width,
height: dimensionObject.height,
img: url,
y: scaleDownFactor / 2,
x: displaySceneWidth / 2 - dimensionObject.width / 2,
};
//https://stackoverflow.com/questions/38675447/how-do-i-get-the-center-of-an-image-in-javascript
//^used the above StackOverflow post to help me figure that out
//Determine if the image or video is wide, tall, or same dimensions and update depending on that
if (dimensionObject.height > dimensionObject.width) {
//if the height is longer than the width, use the tall image object
return tallImageUpdate;
} else if (dimensionObject.width > dimensionObject.height) {
//if the width is longer than the height, use the wide image object
return wideImageUpdate;
}
//if the image length and width are pretty much the same, just default to the wide image update object
return wideImageUpdate;
}
static async scaleArtTileToFrameTile(artTile, frameTile, tex, url) {
const frameTileWidth =
game.version >= 10 ? frameTile.width : frameTile.data.width;
const frameTileHeight =
game.version >= 10 ? frameTile.height : frameTile.data.height;
const frameTileX = game.version >= 10 ? frameTile.x : frameTile.data.x;
const frameTileY = game.version >= 10 ? frameTile.y : frameTile.data.y;
let dimensionObject = ImageDisplayManager.calculateAspectRatioFit(
tex.width,
tex.height,
frameTileWidth,
frameTileHeight
);
let imageUpdate = {
_id: artTile.id,
width: dimensionObject.width,
height: dimensionObject.height,
img: url,
y: frameTileY,
x: frameTileX,
};
//Ensure image is centered to bounding tile (stops images hugging the top left corner of the bounding box).
let boundingMiddle = {
x: frameTileX + frameTileWidth / 2,
y: frameTileY + frameTileHeight / 2,
};
let imageMiddle = {
x: imageUpdate.x + imageUpdate.width / 2,
y: imageUpdate.y + imageUpdate.height / 2,
};
imageUpdate.x += boundingMiddle.x - imageMiddle.x;
imageUpdate.y += boundingMiddle.y - imageMiddle.y;
return imageUpdate;
}
/** Used snippet from the below stackOverflow answer to help me with proportionally resizing the images*/
/*https://stackoverflow.com/questions/3971841/how-to-resize-images-proportionally-keeping-the-aspect-ratio*/
static calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) {
let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
return {
width: srcWidth * ratio,
height: srcHeight * ratio,
};
}
static async displayImageInWindow(method, url) {
if (method === "journalEntry") {
let dedicatedDisplayData = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData"
);
const displayJournalID = dedicatedDisplayData.journal.value;
const displayJournal = game.journal.get(displayJournalID);
//if we would like to display in a dedicated journal entry
if (!displayJournal) {
//couldn't find display journal, so return
if (game.user.isGM) {
ui.notifications.error(
`No Art Journal entry set! Please set your art journal in the module settings or Art Gallery Config`
);
}
return;
} else {
displayJournal.render(true);
}
//get the file type of the image url via regex match
var fileTypePattern = /\.[0-9a-z]+$/i;
var fileType = url.match(fileTypePattern);
let journalMode = "image";
let update;
if (fileType == ".mp4" || fileType == ".webm") {
if (game.version < 10) {
// if the file type is a video and we're before v10, we have to do a bit of a wonky workaround
let videoHTML = `<div style="height:100%; display: flex; flex-direction: column; justify-content:center; align-items:center;">
<video width="100%" height="auto" autoplay loop>
<source src=${url} type="video/mp4">
<source src=${url} type="video/webm">
</video>
</div>
`;
update = {
_id: displayJournal._id,
content: videoHTML,
img: "",
};
imageMode = "text";
} else {
//if we're after v10
update = {
_id: displayJournal.id,
src: url,
video: {
loop: true,
autoplay: true,
},
type: "video",
};
}
} else {
//change the background image to be the clicked image in the journal
if (game.version < 10) {
update = {
_id: displayJournal.id,
content: "",
img: url,
};
} else {
update = {
// _id: displayJournal.id,
src: url,
type: "image",
};
}
}
let updated;
if (game.version < 10) {
updated = await displayJournal.update(update, {});
} else {
const firstPage = displayJournal.pages.contents[0];
updated = await firstPage?.update({ _id: firstPage.id, ...update });
}
if (updated === null && game.user.isGM) {
ui.notifications.error(
`Could not display image in Art Journal '${displayJournal.name}.'`
);
}
const { autoActivate, autoView } = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.journal"
);
if (autoActivate) displayJournal.show(journalMode, true);
if (autoView && !displayJournal.sheet.rendered)
displayJournal.sheet.render(true);
if (game.user.isGM && !autoActivate && !autoView) {
ui.notifications.info(
`Image in Art Journal '${displayJournal.name}' successfully updated`
);
}
} else if (method === "window") {
//if we would like to display in a new popout window
let popout = new ImageVideoPopout(url, {
shareable: true,
})
.render(true)
.shareImage();
}
}
/**
* determine the location of the display
* @param {*} imageElement - the imageElement
* @param {*} location - the location we want to display our image in
* @param {*} journalSheet - the journal sheet in which we're performing these actions
* @param {*} url
*/
static async determineDisplayMethod(sheetImageData = { method: "window", url: "" }) {
let { method, imageElement, url } = sheetImageData;
if (!url && imageElement) {
url = ImageDisplayManager.getImageSource(imageElement);
} else if (!url && !imageElement) {
console.error("No image data passed to this method", url, imageElement);
}
//on click, this method will determine if the image should open in a scene or in a display journal
switch (method) {
case "artScene":
let artSceneID = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.scene.value"
);
let artScene = game.scenes.get(artSceneID);
if (artScene) {
let defaultArtTileID = await ArtTileManager.getDefaultArtTileID(
artScene
);
if (!defaultArtTileID) {
ui.notifications.error(
"No valid 'Art Tile' found in scene '" + artScene.name + "'"
);
return;
}
let frameTileID = await ArtTileManager.getGalleryTileDataFromID(
defaultArtTileID,
"linkedBoundingTile",
artSceneID
);
await ImageDisplayManager.updateTileObjectTexture(
defaultArtTileID,
frameTileID,
url,
method,
artSceneID
);
}
break;
case "anyScene":
let { artTileID, frameTileID } = sheetImageData;
//if the setting is to display it in a scene, proceed as normal
if (method === "anyScene" && !artTileID) {
artTileID = await ArtTileManager.getDefaultArtTileID(
game.scenes.viewed
);
if (!artTileID) {
ui.notifications.error(
"No valid 'Art Tile' found in current scene"
);
return;
}
frameTileID = await ArtTileManager.getGalleryTileDataFromID(
artTileID,
"linkedBoundingTile"
);
}
await ImageDisplayManager.updateTileObjectTexture(
artTileID,
frameTileID,
url,
method
);
break;
case "journalEntry":
case "window":
await ImageDisplayManager.displayImageInWindow(method, url);
break;
}
}
static getImageSource(imageElement) {
let type = imageElement.nodeName;
let url;
if (type == "IMG") {
//if it's an image element
url = imageElement.getAttribute("src");
} else if (type == "VIDEO") {
//if it's a video element
url = imageElement.getElementsByTagName("source")[0]?.getAttribute("src");
if (!url) {
url = imageElement.getAttribute("src");
}
} else if (type == "DIV" && imageElement.classList.contains("lightbox-image")) {
//if it's a lightbox image on an image-mode journal
//https://stackoverflow.com/questions/14013131/how-to-get-background-image-url-of-an-element-using-javascript --
let imgStyle = imageElement.style;
url = imgStyle.backgroundImage.slice(4, -1).replace(/['"]/g, "");
} else {
ui.notifications.error("Type not supported");
url = null;
}
if (!url) {
ui.notifications.error("url not found");
}
return url;
//load the texture from the source
}
static async clearDisplayWindow() {
if (!findDisplayJournal()) {
return;
}
let url =
"/modules/journal-to-canvas-slideshow/artwork/HD_transparent_picture.png";
let update = {
_id: displayJournal.id,
img: url,
content: `<div></div>`,
};
const updated = await displayJournal.update(update, {});
}
static async clearTile(tileID, options = {}) {
let { ourScene } = options;
if (!ourScene) ourScene = game.scenes.viewed;
const isBoundingTile = await ArtTileManager.getGalleryTileDataFromID(
tileID,
"isBoundingTile"
);
let propertyPath = "defaultTileImages.paths.artTilePath";
if (isBoundingTile) {
propertyPath = "defaultTileImages.paths.frameTilePath";
}
const clearImagePath = await HelperFunctions.getSettingValue(
"artGallerySettings",
propertyPath
);
var clearTileUpdate = {
_id: tileID,
img: clearImagePath,
};
await ourScene.updateEmbeddedDocuments("Tile", [clearTileUpdate]);
}
}

View File

@ -0,0 +1,321 @@
import { log, MODULE_ID } from "../debug-mode.js";
import { artGalleryDefaultSettings, colorThemes } from "../settings.js";
import { universalInterfaceActions as UIA } from "../data/Universal-Actions.js";
import { HelperFunctions, HelperFunctions as HF } from "./HelperFunctions.js";
import { ArtTileManager } from "./ArtTileManager.js";
const settingsActions = {
global: {
resetColors: {
onClick: () => {},
},
},
item: {},
};
/**
* Form app to handle JTCS settings
*/
export class JTCSSettingsApplication extends FormApplication {
constructor(data = {}) {
super();
// this.data = data;
this.data = game.JTCS.utils.getSettingValue("artGallerySettings");
}
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["form"],
width: 600,
popOut: true,
resizable: true,
minimizable: true,
submitOnClose: false,
closeOnSubmit: true,
submitOnChange: false,
template: `modules/${MODULE_ID}/templates/JTCS-settings-app.hbs`,
id: "JTCSSettingsApplication",
title: " JTCSSettings Application",
scrollY: [".form-content"],
onsubmit: (event) => {},
});
}
async addArtSceneAndJournalData(dedicatedDisplayData) {
const { journal, scene } = dedicatedDisplayData;
function mapNameAndId(contents) {
const newData = {};
contents.forEach((c) => {
newData[c.id] = c.name;
});
return newData;
}
const allJournals = mapNameAndId(game.journal.contents);
const artJournalID = journal.value;
const artJournalData = {
options: allJournals,
value: artJournalID,
};
const allScenes = mapNameAndId(
await ArtTileManager.getAllScenesWithSlideshowData()
);
const artSceneID = scene.value;
const artSceneData = {
options: allScenes,
value: artSceneID,
};
return {
...dedicatedDisplayData,
journal: {
...journal,
artJournalData,
},
scene: {
...scene,
artSceneData,
},
};
}
async getData() {
// Send data to the template
// let data = game.JTCS.utils.getSettingValue("artGallerySettings");
this.data = await game.JTCS.utils.getSettingValue("artGallerySettings");
let data = { ...this.data };
let newDisplayData = await this.addArtSceneAndJournalData(
data.dedicatedDisplayData
);
setProperty(data, "dedicatedDisplayData", newDisplayData);
return data;
}
activateListeners(html) {
super.activateListeners(html);
html.off("click").on(
"click",
"[data-action]",
this._handleButtonClick.bind(this)
);
this._handleChange();
}
async handleAction(event, actionType) {}
async _handleChange() {
$(`select, input[type='checkbox'], input[type='radio'], input[type='text']`).on(
"change",
async function (event) {
let { value, name, checked, type } = event.currentTarget;
let propertyString = "";
switch (type) {
case "select":
case "radio":
//the radio or select data is nested in a parent object that holds both its choices and the chosen value
//so we will want to get that parent object instead of the object itself
propertyString = name.split(".").splice(0, 2).join(".");
break;
case "checkbox":
value = checked; //if its a checkbox, set its value to whether or not it is checked
break;
case "text":
propertyString = name.split(".").splice(0, 1).join(".");
break;
default:
propertyString = name;
break;
}
let settingsObject = getProperty(
artGalleryDefaultSettings,
propertyString
);
if (settingsObject && settingsObject.hasOwnProperty("onChange")) {
let ourApp = game.JTCSSettingsApp;
settingsObject.onChange(event, {
value: value,
app: ourApp,
html: ourApp.element,
});
}
}
);
}
async _handleButtonClick(event) {
let clickedElement = $(event.currentTarget);
let action = clickedElement.data().action;
let type = clickedElement[0].type;
if (type === "submit") {
this.element.find("form").on("submit", (e) => {
if (action === "closeOnSubmit") {
this.close();
}
});
return;
}
event.stopPropagation();
event.preventDefault();
let backgroundColor = this.element.find("#backgroundColor").val();
let outerWrapper = clickedElement.closest(".outer-wrapper");
const accentElement = outerWrapper.find("[data-responsive-color]");
let accentColor = accentElement.val();
let contrastNotification = outerWrapper.find(".inline-notification");
if (action === "checkContrast") {
let hasEnoughContrast = HF.checkIfColorsContrastEnough(
backgroundColor,
accentColor
);
if (!hasEnoughContrast && contrastNotification.length === 0) {
await UIA.renderInlineNotification(event, "outer-wrapper", {
message:
"This color does not have enough contrast with the background color you've chosen. Text and buttons might be unreadable.",
notificationType: "warning",
});
} else if (hasEnoughContrast && contrastNotification.length > 0) {
contrastNotification.remove();
}
} else if (action === "toggleAutoContrastOff") {
const templatePath = game.JTCS.templates["delete-confirmation-prompt"];
const buttons = {
cancel: {
label: "Cancel",
icon: "<i class='fas fa-undo'></i>",
},
delete: {
label: "Turn Off Auto-Contrast",
icon: "<i class='fas fa-power-off'></i>",
callback: async () => {
await HF.setSettingValue(
"artGallerySettings",
false,
"colorSchemeData.autoContrast"
);
this.render(true);
},
},
};
const data = {
icon: "fas fa-exclamation",
heading: "Turn off Auto-Contrast?",
destructiveActionText: `Turn off auto contrast`,
explanation: `This will make it so your chosen colors aren't automatically adjusted to contrast with your background color;
<br/> However you risk choosing colors that make text illegible`,
buttons,
};
await HF.createDialog("Turn Off Auto Contrast", templatePath, data);
} else if (action === "toggleAutoContrastOn") {
await HF.setSettingValue(
"artGallerySettings",
true,
"colorSchemeData.autoContrast"
);
this.render(true);
} else if (action === "resetColor") {
let key = accentElement.attr("name");
let theme = await HF.getSettingValue(
"artGallerySettings",
"colorSchemeData.theme"
);
// const newScheme = mergeObject(currentSettings, colorThemes[theme]);
const defaultValue = getProperty(colorThemes[theme], key);
await HF.setSettingValue("artGallerySettings", defaultValue, key);
this.render(true);
// accentElement.val(defaultValue);
} else if (action === "scrollTo") {
const parentItem = clickedElement.closest("form");
UIA.scrollOtherElementIntoView(event, { parentItem });
} else if (action === "applyChanges") {
const form = event.currentTarget.closest("form");
const formData = {};
Array.from(form.querySelectorAll("input, select")).forEach((input) => {
let value = input.value;
if (input.type === "checkbox") {
value = input.checked;
}
formData[input.name] = value;
});
await HF.setSettingValue("artGallerySettings", formData, "", true);
this.render(true);
} else if (action === "setDarkTheme" || action === "setLightTheme") {
const theme = action === "setDarkTheme" ? "dark" : "light";
const templatePath = game.JTCS.templates["delete-confirmation-prompt"];
const buttons = {
cancel: {
label: "Cancel",
icon: "<i class='fas fa-undo'></i>",
},
reset: {
label: `Apply Default ${HF.capitalizeEachWord(theme)} Theme`,
icon: "<i class='fas fa-power-off'></i>",
callback: async () => {
//store the chosen theme as a setting
await HF.setSettingValue(
"artGallerySettings",
theme,
"colorSchemeData.theme"
);
const currentSettings = await HF.getSettingValue(
"artGallerySettings"
);
const newScheme = mergeObject(
currentSettings,
colorThemes[theme]
);
await HF.setSettingValue("artGallerySettings", newScheme);
this.render(true);
},
},
};
const data = {
icon: "fas fa-exclamation",
heading: `Apply Default ${HF.capitalizeEachWord(theme)} Theme?`,
destructiveActionText: `Apply Default ${HF.capitalizeEachWord(
theme
)} Theme`,
explanation: `This will overwrite any custom colors you've picked out`,
buttons,
};
await HF.createDialog("Apply Default Theme", templatePath, data);
}
}
async _updateObject(event, formData) {
const autoContrast = await HF.getSettingValue(
"artGallerySettings",
"colorSchemeData.autoContrast"
);
if (autoContrast) {
//if auto-contrast is turned on
const bgColorKey = "colorSchemeData.colors.backgroundColor";
for (const key in formData) {
if (key.includes(".colors.") && !key.includes(bgColorKey)) {
//for all the colors, convert the colors
const bgColor = formData[bgColorKey];
const fgColor = formData[key];
const hasEnoughContrast = HF.checkIfColorsContrastEnough(
bgColor,
fgColor
);
//only alter the contrast if there isn't enough already
if (!hasEnoughContrast) {
formData[key] = HF.getColorWithContrast(bgColor, fgColor);
}
}
}
}
await game.JTCS.utils.setSettingValue("artGallerySettings", formData, "", true);
await game.JTCSSettingsApp.render(true);
}
}
window.JTCSSettingsApplication = JTCSSettingsApplication;

View File

@ -0,0 +1,69 @@
/* Code below written by zeelo1 https://github.com/zeel01/TokenHUDArtButton/blob/master/artbutton.js -- to handle videos in the popout
/* with some tweaks to fit my module
/**
* Capable of handling images, as well as .mp4 and .webm video
* not very sophisticated.
*
* @class ImageVideoPopout
* @extends {ImagePopout}
*/
export default class ImageVideoPopout extends ImagePopout {
/**
* Creates an instance of MultiMediaPopout.
*
* @param {string} src
* @param {object} [options={}]
* @memberof ImageVideoPopout
*/
constructor(src, options = {}) {
super(src, options);
this.video = [".mp4", "webm"].includes(
src.slice(-4).toLowerCase()
);
this.options.template = "modules/journal-to-canvas-slideshow/templates/media-popout.html";
}
/** @override */
async getData(options) {
let data = await super.getData();
data.isVideo = this.video;
return data;
}
/**
* Share the displayed image with other connected Users
*/
shareImage() {
game.socket.emit("module.journal-to-canvas-slideshow", {
image: this.object,
title: this.options.title,
uuid: this.options.uuid
});
}
/**
* Handle a received request to display media.
*
* @override
* @param {string} image - The path to the image/media resource.
* @param {string} title - The title for the popout title bar.
* @param {string} uuid
* @return {ImageVideoPopout}
* @private
*/
static _handleShareMedia({ image, title, uuid } = {}) {
return new ImageVideoPopout(image, {
title: title,
uuid: uuid,
shareable: false,
editable: false
}).render(true);
}
}
Hooks.once("ready", () => {
game.socket.on("module.journal-to-canvas-slideshow", ImageVideoPopout._handleShareMedia);
});

View File

@ -0,0 +1,270 @@
"use strict";
import { HelperFunctions } from "./HelperFunctions.js";
import { universalInterfaceActions as UIA } from "../data/Universal-Actions.js";
export class Popover {
static defaultElementData = {
popoverElement: {
target: null,
hideEvents: [],
},
sourceElement: {
target: null,
hideEvents: [],
},
parentElement: {
target: null,
hideEvents: [],
},
};
static async processPopoverData(
sourceElement,
parentElement,
templateData,
elementData = Popover.defaultElementData,
sourceEvent = ""
) {
// -- RENDER THE POPOVER
elementData.parentElement.target = parentElement;
elementData.sourceElement.target = sourceElement;
let elementDataArray = Object.keys(elementData).map((key) => {
let newData = elementData[key];
newData.name = key;
return newData;
});
let popover = await Popover.createAndPositionPopover(
templateData,
elementDataArray,
sourceEvent
);
return popover;
}
/**
*
* @param {Event} event - the event (usually hover) that generated the tooltip
* @param {JQueryObject} $html - the html of the entire app
* @param {String} dataElementSelector - a string to select the parent element with the relevant data for this tooltip
*/
static async generateTooltip(event, $html, dataElementSelector, sourceEvent) {
if (!$html.jquery) $html = $($html);
let sourceElement = event.currentTarget;
let parentDataElement = sourceElement.closest(dataElementSelector);
let templateData = Popover.createTemplateData(parentDataElement, "tooltip", {
content: sourceElement.dataset.tooltipText,
});
let popover = await Popover.processPopoverData(
sourceElement,
$html,
templateData,
{
...Popover.defaultElementData,
},
sourceEvent
);
let eventString = "mouseenter mouseleave";
let selectorString = `[data-tooltip], .popover[data-popover-id="tooltip"]`;
$html
.off(eventString, selectorString)
.on(eventString, selectorString, async (event) => {
let { type, currentTarget } = event;
let isMouseOver = type === "mouseover" || type === "mouseenter";
!currentTarget.jquery && (currentTarget = $(currentTarget));
//if our current target is our source element or the popover itself
if (currentTarget.is($(sourceElement)) || currentTarget.is($(popover))) {
if (!popover.hoveredElements) popover.hoveredElements = [];
let { hoveredElements } = popover;
let isInArray = Popover.JqObjectInArray(
hoveredElements,
currentTarget
);
if (isMouseOver) {
//if we're already tracking it, remove it
if (!isInArray) {
hoveredElements.push(currentTarget);
} else {
}
popover.hoveredElements = hoveredElements;
} else {
if (isInArray) {
//if we're already tracking it, remove it
hoveredElements = hoveredElements.filter(
(el) => !el.is(currentTarget)
);
}
popover.hoveredElements = hoveredElements;
if (popover.hoveredElements.length === 0) {
await Popover.hideAndDeletePopover(popover);
}
}
}
});
}
static JqObjectInArray(objectArray, searchObject) {
let isIncluded = false;
objectArray.forEach((obj) => {
if (obj.is(searchObject)) {
isIncluded = true;
}
});
return isIncluded;
}
static createTemplateData(parentLI, partialName, context = {}) {
let template = { frameId: "", id: "", type: "" };
let dataset = filterObject($(parentLI).data(), template);
if (!dataset) {
dataset = {};
}
let popoverId = partialName;
dataset.popoverId = popoverId; //to keep there from being multiples of the same popover
return {
passedPartial: partialName,
dataset: dataset,
passedPartialContext: context,
};
}
static validateInput(inputValue, validationType, onInvalid = "") {
let valid = false;
switch (validationType) {
case "image":
valid = HelperFunctions.isImage(inputValue);
break;
default:
valid = inputValue !== undefined;
break;
}
return valid;
}
/**
* Generates a popover (tooltip, etc), and positions it from the source element
* boundingClientRect data
* @param {Object} templateData - the data to be passed to the popover
* @param {Application} parentApp - the parent application rendering the popover
* @param {HTMLElement} sourceElement - the element that is the "source" of the popover (a button, input, etc.)
*/
static async createAndPositionPopover(
templateData,
elementDataArray = [],
sourceEvent = ""
) {
let elements = elementDataArray.map((data) => data.target);
let [popoverElement, sourceElement, parentElement] = elements; //destructure the passed-in elements
let boundingRect = sourceElement.getBoundingClientRect();
let popoverTemplate = game.JTCS.templates["popover"];
// popoverElement = parentElement.find(`.popover[data-popover-id="${templateData.dataset.popoverId}"]`);
let another = parentElement.find(`.popover`);
popoverElement = parentElement.find(
`.popover[data-popover-id="${templateData.dataset.popoverId}"]`
);
let areTheSame = another.is(popoverElement);
if (another && !areTheSame) {
await Popover.hideAndDeletePopover(another);
}
if (popoverElement.length === 0) {
//if it doesn't already exist, create it
let renderedHTML = await renderTemplate(popoverTemplate, templateData);
parentElement.append(renderedHTML);
popoverElement = parentElement.find(
`.popover[data-popover-id="${templateData.dataset.popoverId}"`
);
UIA.fade(popoverElement);
}
let popoverData = elementDataArray.find((data) => data.name === "popoverElement");
popoverData.target = popoverElement;
popoverElement.css({ position: "absolute" });
popoverElement.offset({
top: boundingRect.top + boundingRect.height,
left: boundingRect.left,
});
popoverElement.focus({ focusVisible: true });
//set up a "Click Out" event handler
let popoverId = templateData.dataset.popoverId;
// //hideEvents should be list of events to hide the popover on (like blur, change, mouseout, etc)
// elementDataArray.forEach((data) => {
// let targetElement = data.target;
// data.hideEvents.forEach((eventData) => {
// let handler;
// let selector;
// let eventName;
// let options;
// if (typeof eventData === "string") {
// //if it's a simple string, just set the handler to immediaetly hide the popover on this event
// eventName = eventData;
// handler = async (event) => await Popover.hideAndDeletePopover(popoverElement);
// selector = "*";
// } else if (typeof eventData === "object") {
// //if it's an object, we'll want to do something (like validate input) first before hiding
// eventName = eventData.eventName;
// //pass the popover element and the hide function to the wrapperFunction
// options = {
// ...eventData.options,
// popover: popoverElement,
// hideFunction: Popover.hideAndDeletePopover,
// };
// handler = async (event, options) => {
// await eventData.wrapperFunction(event, options);
// };
// selector = eventData.selector;
// }
// $(targetElement)
// .off(eventName, selector)
// .on(eventName, selector, async (event) => await handler(event, options));
// });
// });
// let popoverID = popoverElement.data().popoverId;
$(document)
.off("click")
.on("click", async (event) => {
//make sure the button that originated the click wasn't
//the same one being handled by this document
if (Popover.isOutsideClick(event, sourceElement)) {
await Popover.hideAndDeletePopover(popoverElement);
}
});
return popoverElement;
}
static isOutsideClick(event, sourceElement) {
let wasOnPopover = $(event.target).closest(".popover").length > 0;
let wasOnSourceElement = $(event.target).is($(sourceElement));
if (wasOnPopover || wasOnSourceElement) {
//click was on the popover
return false;
}
//if our click is outside of our popover element
return true;
}
static async hideAndDeletePopover(popoverElement) {
if (popoverElement.timeout) {
//if the popover is already counting down to a timeout, cancel it
clearTimeout(popoverElement.timeout);
}
let popoverTimeout = setTimeout(() => {
UIA.fade(popoverElement, { isFadeOut: true });
}, 400);
//save that timeout's id on the popover
popoverElement.timeout = popoverTimeout;
}
}

View File

@ -0,0 +1 @@
const dummyData = {};

View File

@ -0,0 +1,166 @@
import { HelperFunctions } from "./HelperFunctions.js";
import { TestUtils } from "../tests/test-utils.js";
import { ArtTileManager } from "./ArtTileManager.js";
export async function sheetImageDisplayTest(context) {
const { describe, it, assert, expect, should } = context;
describe("Sheet image display suite", async function () {
const {
dispatchChange,
dispatchMouseDown,
getTileObject,
getDocData,
resizeTile,
changeTileImage,
getArtGallerySettings,
getDocIdFromApp,
getAppFromWindow,
getAreasOfDocs,
deleteTestScene,
duplicateTestScene,
initializeScene,
returnClassListAsArray,
returnComputedStyles,
checkAppElementForId,
getChildElements,
clickButton,
clickActionButton,
} = TestUtils;
let sheetApp,
sheetElement,
scene,
sheetControls,
imageControls,
clickableImageContainers,
ourImageContainer,
artJournal,
artScene;
const clickableContainerSelector = ".clickableImageContainer";
const imageControlsSelector = ".clickableImageControls";
const sheetControlsSelector = "#sheet-controls";
let sheetGlobalActionStrings = ["togglelmageControls", "openSlideshowConfig", "openSettingsApp", "fadeJournal"];
/**
* set the sheet data from the test journal
*/
async function getSheetData() {
sheetControls = getChildElements(sheetElement, sheetControlsSelector);
imageControls = getChildElements(sheetElement, imageControlsSelector, true);
clickableImageContainers = getChildElements(sheetElement, clickableContainerSelector, true);
ourImageContainer = clickableImageContainers[1];
}
before(async () => {
let sourceScene = await initializeScene("Premade Gallery Scene");
scene = await duplicateTestScene(sourceScene);
// sheetApp = await game.journal.getName("Art").sheet._render(true);
sheetApp = await game.journal.getName("Art").sheet.render(true);
await quench.utils.pause(900);
sheetElement = sheetApp.element;
await getSheetData();
});
after(async () => {
await deleteTestScene(scene);
});
async function getDefaultDisplayIDs() {
let artSceneID = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.scene.value"
);
let artJournalID = await HelperFunctions.getSettingValue(
"artGallerySettings",
"dedicatedDisplayData.journal.value"
);
artScene = game.scenes.get(artSceneID);
artJournal = game.scenes.get(artJournalID);
}
describe("Testing individual image controls", async function () {
let ourImage;
let src;
let displayMethod = "window";
const clickImageControlButton = async () => {
ourImageContainer.querySelector(`[data-method='${displayMethod}']`).click();
await quench.utils.pause(900);
};
async function compareWindowContent(type, displayMethodAppName, searchText = "") {
let windowApp = getAppFromWindow(type, searchText);
let windowElement = windowApp.element;
assert.exists(windowElement[0]);
let windowImageSrc = windowElement[0].querySelector("img")?.getAttribute("src");
if (game.version < 10 && displayMethodAppName.toLowerCase().includes("journal"))
windowImageSrc = returnComputedStyles(windowElement[0], ".lightbox-image", "background-image");
// windowImageSrc = getComputedStyle(
// windowElement[0].querySelector(".lightbox-image")
// ).getPropertyValue("background-image");
let message = `${displayMethodAppName} src should equal clicked image src`;
if (game.version >= 10) expect(windowImageSrc, message).to.equal(src);
else expect(windowImageSrc, message).to.have.string(src);
windowApp.close();
}
async function compareTileContent(ourScene) {
let defaultArtTileID = await ArtTileManager.getDefaultArtTileID(ourScene);
let tileDoc = await getTileObject(defaultArtTileID, ourScene.id);
let textureProperty = game.version >= 10 ? "texture.src" : "img";
let tileSrc = await getDocData(tileDoc, textureProperty);
expect(tileSrc, `${displayMethod} image src should equal clicked image src`).to.equal(src);
}
before(async () => {
await getDefaultDisplayIDs();
ourImage = ourImageContainer.querySelector("img");
src = ourImage.getAttribute("src");
});
let doBeforeEach = clickImageControlButton;
beforeEach(doBeforeEach);
afterEach(async () => {
await getSheetData();
});
it("Renders a popout window with the apporpriate image", async () => {
await compareWindowContent(ImagePopout, "ImagePopout");
displayMethod = "journalEntry";
});
it("Renders the art journal sheet with the appropriate image", async () => {
await compareWindowContent(JournalSheet, "Art Journal", "Display Journal");
// //TODO: Ensure the id of this journal entry matches the Art Journal entry
displayMethod = "artScene";
});
it("Updates the default Art Tile in the Art Scene with the appropriate image", async () => {
await compareTileContent(artScene);
displayMethod = "anyScene";
});
it("Updates the default Art Tile in the current scene with the appropriate image", async () => {
let tiles = scene.tiles.contents;
// compare the slideshow tiles to the tiles that are in the display
let buttons = Array.from(ourImageContainer.querySelectorAll(".displayTiles button"));
for (let button of buttons) {
button.click();
let id = button.dataset.id;
await quench.utils.pause(200);
let tile = tiles.find((tile) => tile.id === id);
let textureProperty = game.version >= 10 ? "texture.src" : "img";
let tileSrc = await getDocData(tile, textureProperty);
let imgSrc = ourImage.getAttribute("src");
expect(tileSrc, "Tile path should equal image path").to.equal(imgSrc);
}
displayMethod = "anyScene";
});
doBeforeEach = async () => {
ourImage.click();
await quench.utils.pause(900);
};
it("Updates the Art Tile associated with the particular sheet image", async () => {
await compareTileContent(game.scenes.viewed);
});
});
});
}

View File

@ -0,0 +1,651 @@
import { SlideshowConfig } from "../SlideshowConfig.js";
import { HelperFunctions } from "./HelperFunctions.js";
import { ArtTileManager } from "./ArtTileManager.js";
import { ImageDisplayManager } from "./ImageDisplayManager.js";
import { JTCSSettingsApplication } from "./JTCSSettingsApplication.js";
import { TestUtils } from "../tests/test-utils.js";
import { sheetImageDisplayTest } from "./SheetImage.test.js";
const slideshowConfigTest = async (context) => {
const { describe, it, assert, expect, should } = context;
const {
dispatchChange,
dispatchMouseDown,
getTileObject,
getDocData,
resizeTile,
changeTileImage,
getArtGallerySettings,
getDocIdFromApp,
getAppFromWindow,
getAreasOfDocs,
deleteTestScene,
duplicateTestScene,
initializeScene,
returnClassListAsArray,
returnComputedStyles,
checkAppElementForId,
getDefaultImageSrc,
} = TestUtils;
describe("Slideshow Config Test Suite", async function () {
let configApp, configElement, scene, defaultImageSrc;
async function clickGlobalButton(actionName) {
configElement[0]
.querySelector(
`[data-action='globalActions.click.actions.${actionName}']`
)
.click();
await quench.utils.pause(900);
}
/**
* @description - renders the Scene Config
*/
async function renderConfig() {
configApp = new SlideshowConfig();
await configApp._render(true);
configElement = configApp.element;
}
async function getTestData() {
defaultImageSrc = await getArtGallerySettings(
"defaultTileImages.paths.artTilePath"
);
}
before(async () => {
let sourceScene = await initializeScene();
scene = await duplicateTestScene(sourceScene);
await getTestData();
await renderConfig();
});
after(async () => {
await deleteTestScene(scene);
});
describe("Testing Linked Gallery Tile Item Actions in overflow menu", async function () {
let tileID,
tileData,
tileDoc,
tileItemPrefixString,
ourTileElement,
tileElements,
overflowMenu,
ourButton,
originalTileID;
before(async () => {
await getTileData();
originalTileID = tileID; //store the very first tile
});
beforeEach(async () => {
await doBeforeEach();
});
let doBeforeEach = async () => {
await toggleOverflowMenu();
await changeTileImage(tileID, defaultImageSrc);
await getTileData();
};
async function getTileData() {
tileID = await ArtTileManager.getDefaultArtTileID(scene);
tileData = await ArtTileManager.getGalleryTileDataFromID(tileID);
tileDoc = await getTileObject(tileID);
tileItemPrefixString = "[data-action='itemActions.click.actions.";
tileElements = $(configElement).find(
`.tile-list-item:not([data-is-default])`
);
ourTileElement = $(configElement).find(
`.tile-list-item[data-is-default]`
)[0];
}
// get tile Item id
async function clickActionButton(actionName, element = overflowMenu) {
const actionQueryString = combine(actionName);
await clickButton(element, actionQueryString);
}
async function clickButton(element, selector) {
ourButton = element.querySelector(selector);
ourButton.click();
await quench.utils.pause(900);
}
// async function getFameTileData() {
// let frameTileID = tileData.linkedBoundingTile;
// let frameTileData = await ArtTileManager.getGalleryTileDataFromID(
// frameTileID
// );
// return frameTileData;
// }
function combine(actionName) {
let string = `${tileItemPrefixString}${actionName}']`;
return string;
}
async function toggleOverflowMenu() {
await clickActionButton("toggleOverflowMenu", ourTileElement);
//get the popover, which will be a child of the parent app's element
overflowMenu = configElement[0].querySelector(".popover");
return overflowMenu;
}
it("renders the overflow menu when the 'toggle overflow menu'", async function () {
assert.notEqual(
ourTileElement,
undefined,
"Our tile element should be defined"
);
assert.notEqual(
overflowMenu,
undefined,
"Our overflow menu element should be defined"
);
});
it("selects the tile when the 'Select Tile' action is clicked", async function () {
await clickActionButton("selectTile");
const layerName = game.version >= 10 ? "tiles" : "background";
const selectedTileID = canvas[layerName].controlled[0].id;
expect(selectedTileID).to.equal(tileID);
});
it("renders the Tile's configuration app when the 'render tile config' action is clicked", async () => {
await clickActionButton("renderTileConfig");
const app = getAppFromWindow(TileConfig);
let id = getDocIdFromApp(app);
expect(id).to.equal(tileID);
await app.close();
});
it("Updates the tile's dimensions to be within that of its frame tile's dimensions when the 'Fit Tile to Frame' button is clicked", async () => {
//resize the tile to be bigger than scene
await resizeTile(tileDoc, scene);
//get the frame data, whether frame tile or scene
let { linkedBoundingTile } = tileData;
let frameDoc;
if (linkedBoundingTile) {
frameDoc = await getTileObject(linkedBoundingTile);
} else {
frameDoc = game.scenes.viewed;
}
//get the area of the frame, and that of the art tile while resized (which should be bigger)
const { frameArea, artArea: oldArtArea } = await getAreasOfDocs(
frameDoc,
tileDoc
);
expect(oldArtArea).to.be.above(frameArea);
// press the Fit Tile to Frame Button
await clickActionButton("fitTileToFrame");
// get the new area of the art tile
const { artArea: newArtArea } = await getAreasOfDocs(frameDoc, tileDoc);
//ensure the art tile area is smaller than the frame's area now
expect(newArtArea).to.be.below(frameArea);
});
it("resets the Tile Image back to default when Clear Tile Image button is clicked", async () => {
if (!defaultImageSrc) {
defaultImageSrc = await getDefaultImageSrc("art");
}
assert.isDefined(defaultImageSrc, "default image source is defined");
//change the current tile's image (without url, will default to another image)
await changeTileImage(tileID);
// get tile's current image
let textureProperty = game.version >= 10 ? "texture.src" : "img";
const oldImgSrc = await getDocData(tileDoc, textureProperty);
assert.isDefined(oldImgSrc, "Image source is defined");
//check to see if it changed
expect(
oldImgSrc,
"Tile's old texture src should NOT equal default image path"
).to.not.equal(defaultImageSrc);
// * & click Clear Tile Image Button
await clickActionButton("clearTileImage");
const newImgSrc = await getDocData(tileDoc, textureProperty);
// - test Tile Image now matches 'default' image saved
expect(
newImgSrc,
"Tile's texture src should equal default image path"
).to.equal(defaultImageSrc);
});
/**
* simulates clicking the "deleteTileData" action button on a tile item
* @returns returns the dialog rendered after the click
*/
async function clickDeleteTileDataButton() {
//click "Delete Gallery Tile Data" Button and wait for prompt
await clickActionButton("deleteTileData", overflowMenu);
// - ~ Prompt is created, asking if user wants to delete data
let app = getAppFromWindow(Dialog);
let dialogElement = app.element;
//get the dialog element from ui.windows
return { app, dialogElement };
}
it("renders a Delete Data confirmation prompt", async () => {
//click "Delete Gallery Tile Data" Button
let { dialogElement, app } = await clickDeleteTileDataButton();
assert.isDefined(dialogElement, "Delete dialog is defined");
expect(app.element.text()).to.include("Delete");
await app.close();
});
it("deletes the tile data on 'Delete'", async () => {
// ! - & On approve, dialog closes, tile data is deleted
let { dialogElement } = await clickDeleteTileDataButton();
await clickButton(dialogElement[0], ".dialog-button.delete");
// - ~ Scene Config App updates
let sceneTiles = await ArtTileManager.getSceneSlideshowTiles("art", true);
let ourOriginalTile = sceneTiles.find((tile) => tile.id === tileID);
expect(ourOriginalTile).to.not.exist;
await getTileData();
});
it("closes the dialog button on cancel", async () => {
// ? - & On cancel, dialog closes, nothing happens
let { dialogElement } = await clickDeleteTileDataButton();
await clickButton(dialogElement[0], ".dialog-button.cancel");
//app should be undefined now after cancel is clicked
let app = getAppFromWindow(Dialog, "Delete URL");
assert.isUndefined(app);
});
async function renderURLSharePopover() {
await clickActionButton("shareURLOnTile", ourTileElement);
let popoverId = "input-with-error";
let urlShareElement = configElement[0].querySelector(
`.popover[data-popover-id='${popoverId}']`
);
let inputBox = urlShareElement.querySelector("input");
return { urlShareElement, inputBox };
}
it("renders the SHARE URL IMAGE popover box", async () => {
doBeforeEach = async () => {
await getTileData();
};
// Click on "Share URL Image" button
let { urlShareElement, inputBox } = await renderURLSharePopover();
assert.exists(urlShareElement, "the popover should exist");
assert.exists(inputBox, "the input box should exist");
});
it("on change, notifies the user when an invalid url is provided", async () => {
let { inputBox } = await renderURLSharePopover();
inputBox.value = "test";
dispatchChange(inputBox);
quench.utils.pause(100);
let notification = ui.notifications.active[0][0];
assert.exists(notification, "The notification exists");
let includesErrorClass = notification.classList.contains("error");
assert.isTrue(includesErrorClass, "This is an error notification");
});
it("on change, if url is valid and not CORS, sets the tile image to equal the url", async () => {
let { inputBox } = await renderURLSharePopover();
let textureProperty = game.version >= 10 ? "texture.src" : "img";
let oldImg = await getDocData(tileDoc, textureProperty);
//enter placeholder png
inputBox.value =
"https://images.pexels.com/photos/934067/pexels-photo-934067.jpeg";
dispatchChange(inputBox);
await quench.utils.pause(900);
await getTileData(); //reseting the tile data again
let newImg = await getDocData(tileDoc, textureProperty);
expect(newImg, "New Tile Image shouldn't Equal old img").to.not.equal(
oldImg
);
});
it("sets the tile to be the default tile", async () => {
// Ctrl + Click (Set Default Action)
let otherTileElement = Array.from(tileElements).filter(
(tileEl) => tileEl.dataset.type === "art"
)[0];
assert.exists(otherTileElement, "A secondary tile element exists");
let oldDefaultID = tileID;
dispatchMouseDown(otherTileElement); //dispatching an event witfh ctrl pressed?
await quench.utils.pause(900);
// - ~ Tile is set as default tile in current scene
let newDefaultId = await ArtTileManager.getDefaultArtTileID(scene);
expect(newDefaultId).to.not.equal(oldDefaultID);
//TODO - test border colors and indicator colors
// - $ Tile Item now has color change to match.
// - $ Tile Indicator Color changes to match default color as set in settings
});
});
describe("Slideshow config global actions", async () => {
before(async () => {
await renderConfig();
});
it("renders the JTCS Art Gallery settings Application", async () => {
await clickGlobalButton("showModuleSettings");
let app = getAppFromWindow(JTCSSettingsApplication);
assert.exists(app, "The app has been rendered");
await app.close();
});
it("renders the URL Share Dialog", async () => {
await clickGlobalButton("showURLShareDialog");
let app = getAppFromWindow(Dialog);
assert.exists(app, "The app has been rendered");
expect(app.element.text()).to.include("Share URL");
await app.close();
});
it("fades out the Scene Gallery Config", async () => {
let classList;
let opacityValue;
const recheckClassList = () => {
let el = configElement;
let sel = ".window-content";
classList = returnClassListAsArray(el, sel);
opacityValue = parseFloat(returnComputedStyles(el, sel, "opacity"));
};
//by default shouldn't include fade
recheckClassList();
expect(classList).to.not.include("fade");
expect(opacityValue).to.equal(1.0);
//should include fade after fade button is clicked
await clickGlobalButton("toggleSheetOpacity");
recheckClassList();
expect(classList).to.include("fade");
//check opacity
expect(opacityValue).to.equal(0.5);
//should not includ fade after fade button is clicked once more
await clickGlobalButton("toggleSheetOpacity");
recheckClassList();
expect(classList).to.not.include("fade");
expect(opacityValue).to.equal(1.0);
});
});
});
};
const unlinkedTilesTest = async (context) => {
const { describe, it, assert, expect, should } = context;
const {
testFitToFrame,
getDefaultImageSrc,
renderConfig,
dispatchEvent,
getTileObject,
getDocData,
changeTileImage,
getArtGallerySettings,
deleteTestScene,
duplicateTestScene,
initializeScene,
returnClassListAsArray,
returnComputedStyles,
} = TestUtils;
const itemActionPrefixString = "[data-action='itemActions.click.actions.";
let configApp, configElement, scene, defaultImageSrc, newArtTileBtn, newFrameTileBtn;
async function bundleTestData() {
let sourceScene = await initializeScene("Empty Tile Scene");
scene = await duplicateTestScene(sourceScene);
await quench.utils.pause(300);
defaultImageSrc = await getDefaultImageSrc();
({ configApp, configElement } = await renderConfig());
newArtTileBtn = configElement.find(
".wrapper.art-tiles .new-tile-list-item button"
);
newFrameTileBtn = configElement.find(
".wrapper.frame-tiles .new-tile-list-item button"
);
}
//reset all of the references
async function getConfigData() {
configElement = $(document.documentElement.querySelector("#slideshow-config"));
newArtTileBtn = configElement.find(
".wrapper.art-tiles .new-tile-list-item button"
);
newFrameTileBtn = configElement.find(
".wrapper.frame-tiles .new-tile-list-item button"
);
}
describe("It tests the creation and linking of new tiles", async function () {
before(async () => {
await bundleTestData();
});
after(async () => {
await deleteTestScene(scene);
});
// beforeEach(async ()=> {
// await getConfigData()
// })
async function getTileListItem(type) {
let tileListElement = configElement.find(
`.tile-list-item[data-type='${type}']:not(.new-tile-list-item)`
)[0];
return tileListElement;
}
async function createNewTileListItem(type) {
let btn = type === "art" ? newArtTileBtn : newFrameTileBtn;
btn.click();
await quench.utils.pause(900);
await getConfigData();
return await getTileListItem(type);
let tileListElement = configElement.find(
`.tile-list-item[data-type='${type}']:not(.new-tile-list-item)`
)[0];
return tileListElement;
}
function getInlineBadge(tileListElement) {
let unlinkedNotificationBadge = tileListElement.querySelector(
".inline-notification[data-variant='warning']"
);
return unlinkedNotificationBadge;
}
async function checkInlineBadge(tileListElement, shouldExist = true) {
let unlinkedNotificationBadge = getInlineBadge(tileListElement);
if (shouldExist) {
assert.exists(
unlinkedNotificationBadge,
"The unlinked notification badge should exist"
);
} else {
expect(
unlinkedNotificationBadge,
"The unlinked notification badge should not exist"
).to.not.exist;
}
}
async function clickUnlinkedActionButton(actionName, type) {
const fullActionString = `${itemActionPrefixString}${actionName}']`;
const tileListItem = await getTileListItem(type);
const btn = tileListItem.querySelector(fullActionString);
assert.exists(btn);
btn.click();
await quench.utils.pause(900);
await getConfigData();
// return await getConfigData();
}
async function createAndCheckNewLinkedTile(type) {
let tiles;
/**
* Get the tiles in the scene
*/
function getTiles() {
tiles = scene.tiles.contents;
}
getTiles();
let length = tiles.length;
// expect(tiles, "Scene should have no tiles at first").to.be.empty;
const actionName = "createNewGalleryTile";
await clickUnlinkedActionButton(actionName, type);
getTiles();
expect(tiles).to.have.length.above(length);
//STUB - get the tile list item element
let tileListElement = await getTileListItem(type);
let tileID = tileListElement.dataset.id;
let tileDoc = await getTileObject(tileID);
let textureProperty = game.version >= 10 ? "texture.src" : "img";
let tileDocSrc = await getDocData(tileDoc, textureProperty);
let tileDocID = tileDoc.id; //await getDocData(tileDoc, "id");
//STUB - Test that the Tile ID doesn't contain "unlinked" anymore
expect(
tileID,
"Tile id shouldn't have 'unlinked' anymore"
).to.not.contain.oneOf(["unlinked"]);
//STUB - Test that the Tile has the Default Frame Tile Image as stored in the settings
let defaultSrc = await getDefaultImageSrc(type);
expect(
tileDocSrc,
"The tile should have the same image as the defalt frame tile image"
).to.equal(defaultSrc);
// STUB - test that the Tile List Item no longer has the 'unlinked' badge
await checkInlineBadge(tileListElement, false);
//STUB - Test that the created tile's id and the current tile's id now match
expect(tileID).to.equal(tileDocID);
}
function getSelectElementAndOptions(tileListItem) {
let frameSelect = tileListItem.querySelector("select");
let options = Array.from(frameSelect.querySelectorAll("option"));
return {
frameSelect,
options,
};
}
async function clickOptionElement(tileListItem) {
let { frameSelect, options } = getSelectElementAndOptions(tileListItem);
let frameOption1 = options[1];
assert.exists(frameOption1);
let optionValue = frameOption1.value;
frameSelect.value = optionValue;
await quench.utils.pause(400);
}
it("creates a new Scene Gallery Tile Element in the config, when 'new art tile button' is clicked", async function () {
let tileListElement = await createNewTileListItem("art");
assert.exists(
tileListElement,
"This new art Gallery Tile List Item element should exist"
);
await checkInlineBadge(tileListElement);
// TODO - assert also that the art tile has the Canvas as its frame
});
it("Creates a new Frame Gallery Tile Element in the config, when the 'new Frame Tile Button' is clicked", async function () {
let tileListElement = await createNewTileListItem("frame");
assert.exists(
tileListElement,
"This new FRAME Gallery Tile List Item element should exist"
);
await checkInlineBadge(tileListElement);
});
it("Creates a new ART tile on the canvas when the 'Button' is pressed", async function () {
await createAndCheckNewLinkedTile("art");
});
it("Creates a new FRAME tile on the canvas when the 'Button' is pressed", async function () {
await createAndCheckNewLinkedTile("frame");
});
it("Checks to see if selecting an Art Tile for the Frame Tile updates the art tile in the config", async function () {
// test that the selected Frame Tile updates in the config
let frameSelect, options, selectedValue, tileListItem;
async function getFrameAndTileListItem() {
tileListItem = await getTileListItem("art");
}
async function getFrameSelectAndOptions() {
tileListItem = await getTileListItem("art");
frameSelect = getSelectElementAndOptions(tileListItem).frameSelect;
options = getSelectElementAndOptions(tileListItem).options;
assert.exists(frameSelect);
selectedValue = frameSelect.value;
}
// get the selected option element, and click it, then wait
await getFrameSelectAndOptions();
let oldValue = selectedValue;
await clickOptionElement(tileListItem);
//get the selected option and elements again, and check the select's value
await getFrameSelectAndOptions();
//expect the option element to be a different element now
expect(
selectedValue,
"Selected frame Value should have changed"
).to.not.equal(oldValue);
// await getConfigData();
const frameTileID = selectedValue;
const artTileID = tileListItem.dataset.id;
const frameTileListItem = configElement[0].querySelector(
`.tile-list-item[data-id='${frameTileID}']`
);
//TODO - uncomment the below code, and test the border color upon hover
// dispatchEvent(tileListItem, MouseEvent, "mouseover");
// await quench.utils.pause(100);
// let artBorderColor = returnComputedStyles(tileListItem, "", "border-color");
// let frameBorderColor = returnComputedStyles(
// frameTileListItem,
// "",
// "border-color"
// );
// test that the Art Tile fits within the designated Frame Tile
const url =
"https://images.pexels.com/photos/934067/pexels-photo-934067.jpeg";
//update the art tile, and see if it fits within the frame tile now
await ImageDisplayManager.updateTileObjectTexture(
artTileID,
frameTileID,
url,
"anyScene"
);
let { artArea, frameArea } = await testFitToFrame(frameTileID, artTileID);
expect(
artArea,
"The art area should be smaller than the frame area"
).to.be.below(frameArea);
});
});
};
const settingsToggleTest = () => {};
Hooks.on("quenchReady", async (quench) => {
quench.registerBatch(
"Slideshow Config Test",
async (context) => {
await slideshowConfigTest(context);
},
{ displayName: "QUENCH: SlideshowConfig Test Suite" }
);
quench.registerBatch(
"Sheet image display test",
async (context) => {
await sheetImageDisplayTest(context);
},
{ displayName: "QUENCH: Image Display Test Suite" }
);
quench.registerBatch(
"Config Unlinked Test",
async (context) => {
await unlinkedTilesTest(context);
},
{ displayName: "QUENCH: Unlinked tiles test" }
);
});

View File

@ -0,0 +1,37 @@
import { MODULE_ID } from "../debug-mode.js";
import { ImageDisplayManager } from "../classes/ImageDisplayManager.js";
export class JTCSActions {
static async onDisplayActionClick(event, options = {}) {
let { method, url } = options;
await ImageDisplayManager.determineDisplayMethod({
method: method,
url: url,
});
}
static displayActions = {
anyScene: {
label: "Current Scene",
icon: "fas fa-vector-square",
tooltip: "display image on the Default Art Tile in the current scene",
onClick: JTCSActions.onDisplayActionClick,
},
window: {
label: "Popout Window",
icon: "fas fa-external-link-alt",
tooltip: "display image in pop-out window",
onClick: (event, options) => JTCSActions.onDisplayActionClick(event, "window", url),
},
journalEntry: {
label: "Art Journal",
icon: "fas fa-book-open",
tooltip: "display image in your chosen 'Art Journal'",
onClick: JTCSActions.onDisplayActionClick,
},
artScene: {
label: "Art Scene",
icon: "far fa-image",
tooltip: "display image in your chosen 'Art Scene'",
onClick: JTCSActions.onDisplayActionClick,
},
};
}

View File

@ -0,0 +1,28 @@
import { ArtTileManager } from "../classes/ArtTileManager.js";
import { CanvasIndicators } from "../classes/CanvasIndicators.js";
import { HelperFunctions } from "../classes/HelperFunctions.js";
import { ImageDisplayManager } from "../classes/ImageDisplayManager.js";
import ImageVideoPopout from "../classes/MultiMediaPopout.js";
import { SheetImageApp } from "../SheetImageApp.js";
import { SheetImageDataController } from "../SheetImageDataController.js";
import { SlideshowConfig } from "../SlideshowConfig.js";
const JTCSModules = {
ArtTileManager,
CanvasIndicators,
HelperFunctions,
ImageDisplayManager,
ImageVideoPopout,
// SheetImageApp,
SheetImageDataController,
SlideshowConfig,
};
export const {
artTileManager,
canvasIndicatorManager,
helpers,
imageDisplayManager,
sheetImageApp,
sheetImageDataController,
slideshowConfig,
} = JTCSModules;

View File

@ -0,0 +1,880 @@
"use strict";
import { JTCSSettingsApplication } from "../classes/JTCSSettingsApplication.js";
import { JTCSActions } from "./JTCS-Actions.js";
import { Popover } from "../classes/PopoverGenerator.js";
import { universalInterfaceActions as UIA } from "./Universal-Actions.js";
import { HelperFunctions } from "../classes/HelperFunctions.js";
import { ImageDisplayManager } from "../classes/ImageDisplayManager.js";
import { ArtTileManager } from "../classes/ArtTileManager.js";
/**
* Show instructions
*/
export const extraActions = {
renderTileConfig: async (event, options = {}) => {
let { tileID } = options;
await ArtTileManager.renderTileConfig(tileID);
},
selectTile: async (event, options = {}) => {
let { tileID } = options;
await ArtTileManager.selectTile(tileID);
},
updateTileData: async (event, options = {}) => {
let clickedElement = $(event.currentTarget);
let { name, value } = event.currentTarget;
let { tileID, app, parentLI } = options;
let action = clickedElement.data().action || clickedElement.data().changeAction;
let { type } = $(parentLI).data();
let isNewTile = false;
let isBoundingTile = type === "frame";
if (action.includes("createNewTileData")) {
isNewTile = true;
tileID = `unlinked${foundry.utils.randomID()}`;
name = "displayName";
value = `Untitled ${type} Tile`;
} else {
// if we're already a Slideshow tile, look for our ID
tileID = clickedElement[0].closest(".tile-list-item, .popover").dataset.id;
}
if (tileID) {
let updateData = {
id: tileID,
[name]: value,
...(isNewTile ? { isBoundingTile: isBoundingTile } : {}),
};
await ArtTileManager.updateSceneTileFlags(updateData, tileID);
await app.renderWithData();
}
},
deleteTileData: async (event, options = {}) => {
const { app, tileID, parentLI } = options;
let type = parentLI.dataset.type;
let displayName = await ArtTileManager.getGalleryTileDataFromID(
tileID,
"displayName"
);
const templatePath = game.JTCS.templates["delete-confirmation-prompt"];
const buttons = {
cancel: {
label: "Cancel",
icon: "<i class='fas fa-undo'></i>",
},
delete: {
label: "Delete Gallery Tile",
icon: "<i class='fas fa-trash'></i>",
callback: async () => {
await ArtTileManager.deleteSceneTileData(tileID);
await app.renderWithData();
},
},
};
type = type.charAt(0).toUpperCase() + type.slice(1);
const data = {
icon: "fas fa-trash",
heading: "Delete Art Tile Data?",
destructiveActionText: `delete this ${type} tile data?`,
explanation: `This will permanently delete`,
lossDataList: [`${type} Tile '${displayName}'`],
explanation2: `With no way to get it back`,
buttons,
};
await HelperFunctions.createDialog("Delete Art Tile", templatePath, data);
},
highlightItemAndTile: async (event, options = {}) => {
let { parentLI, tileID, missing } = options;
// if (missing) {
// return;
// }
let { type, frameId: frameID } = $(parentLI).data();
let isLeave = event.type === "mouseleave" ? true : false;
// we want every hover over a tile to highlight the tiles it is linked to
let hoveredElement = $(event.currentTarget);
// let type = hoveredElement.data().type;
// let id = hoveredElement.data().id;
let otherListItems = [];
if (type === "frame") frameID = tileID;
if (frameID) {
otherListItems = Array.from(
hoveredElement[0].closest(".tilesInScene").querySelectorAll("li")
).filter(
//get list items with the opposite tile type
(item) => {
let passed = true;
if (item.dataset.type === type) {
passed = false;
}
if (item.dataset.flag === "ignoreHover") {
passed = false;
}
return passed;
}
);
//filter out list items
otherListItems = otherListItems.filter((element) => {
let dataset = Object.values({ ...element.dataset }).join(" ");
let match = false;
if (type === "art") {
//for art tiles, we're looking for frameTiles in the list that match the frame id
match = dataset.includes(frameID);
} else if (type === "frame") {
//for frame tiles, we're looking for art tiles in the list that have our id
match = dataset.includes(tileID);
}
return match;
});
}
// if (!tileID) {
// return;
// }
let tile;
if (!missing) tile = await ArtTileManager.getTileObjectByID(tileID);
if (isLeave) {
hoveredElement.removeClass("accent border-accent");
$(otherListItems).removeClass("accent border-accent");
if (!missing) game.JTCS.indicatorUtils.hideTileIndicator(tile);
} else {
hoveredElement.addClass("accent border-accent");
$(otherListItems).addClass("accent border-accent");
if (!missing) game.JTCS.indicatorUtils.showTileIndicator(tile);
}
},
setDefaultTileInScene: async (event, options = {}) => {
if (event.ctrlKey || event.metaKey) {
//if the ctrl or meta (cmd) key on mac is pressed
let { tileID, parentLI } = options;
let type = parentLI.dataset.type;
let tileInScene = await ArtTileManager.getTileObjectByID(tileID);
let displayName = await ArtTileManager.getGalleryTileDataFromID(
tileID,
"displayName"
);
if (tileInScene) {
await ArtTileManager.setDefaultArtTileID(tileID, game.scenes.viewed);
} else {
ui.notifications.warn(
`Gallery ${HelperFunctions.capitalizeEachWord(type)}
Tile "${displayName}"
must be linked to a tile in this scene
before it can be set as default`
);
}
}
},
showURLShareDialog: async (event, options = {}) => {
let wrappedActions = {};
let { displayActions } = JTCSActions;
for (let actionName in displayActions) {
wrappedActions[actionName] = { ...displayActions[actionName] };
//converting properties to fit the dialog's schema
wrappedActions[
actionName
].icon = `<i class='${displayActions[actionName].icon}'></i>`;
// wrappedActions[actionName].label = HelperFunctions.capitalizeEachWord(actionName, "");
wrappedActions[actionName].callback = async (html) => {
let urlInput = html.find("input[name='urlInput']");
let url = urlInput.val();
if (url !== "") {
await ImageDisplayManager.determineDisplayMethod({
method: actionName,
url: url,
});
}
};
}
delete wrappedActions.anyScene;
let buttons = {
...wrappedActions,
cancel: {
label: "Cancel",
},
};
let templatePath = game.JTCS.templates["share-url-partial"];
await HelperFunctions.createDialog("Share URL", templatePath, {
buttons: buttons,
partials: game.JTCS.templates,
value: "",
});
},
showInstructions: async (event, options = {}) => {
const { html, type, isDefault, missing, frameId, tileID } = options;
const areVisible = await HelperFunctions.getSettingValue(
"areConfigInstructionsVisible"
);
const instructionsElement = html.find("#JTCS-config-instructions");
const isLeave =
event.type === "mouseleave" || event.type === "mouseout" ? true : false;
if ((!instructionsElement && !isLeave) || !areVisible) {
//if the instructions element already exists and we're mousing over, or the instruction visibility has been toggled off
//return
return;
}
let instructionsContent = "<div class='instructions__content JTCS-hidden'>";
// add different instruction content depending on the tile's type (art or frame), whether it's unlinked/missing, and whether, if it's an art tile, it's currently set to the default art tile.
const defaultVariantText = isDefault ? "<em>another</em>" : "this";
switch (type) {
case "art":
instructionsContent += `<p id="isArtTile">
${isDefault ? "This art tile is set as Default." : ""}
<code>Ctr + Click</code> on ${defaultVariantText} <span class="art-color">Art Tile</span> to set it to <span class='default-color'>Default</span>.
<br/>
If you don't specify where a Sheet Image will display, it will automatically display on the <span class="default-color">"Default"</span>
<span class="art-color">Art Tile</span> in a scene.
</p>`;
break;
case "frame":
if (!missing) {
instructionsContent += `<p id="isFrameTile"><span class="frame-color">Frame tiles</span> act as "boundaries" for <span class="art-color">Art Tiles</span>, like a 'picture frame'. <br/>
When you link an <span class="art-color">Art Tile</span> to a <span class="frame-color">Frame tile</span>, the <span class="art-color">Art Tile</span> will get no larger than the <span class="frame-color">Frame Tile</span>.</p>`;
}
break;
}
if (missing) {
const tileName = await ArtTileManager.getGalleryTileDataFromID(
tileID,
"displayName"
);
let suffix = `<span class='${type}-color'>${HelperFunctions.capitalizeEachWord(
type
)} Tile "${tileName}"</span>`;
instructionsContent += `<p id="missing">This <span class='${type}-color'>${HelperFunctions.capitalizeEachWord(
type
)} Tile</span> tile is <b>unlinked</b> to any tile on the canvas in this scene.</p>
<ul>
<li>Click on <i class="fas fa-plus"></i> to add a new Tile object to the canvas, linked to ${suffix}</li>
<li>Click on <i class="fas fa-link"></i> to link a pre-existing Tile object on the canvas to ${suffix}</li>
</ul>
`;
}
if (type === "art" && !frameId)
instructionsContent += `<p id="noFrameTile">This <span class="art-color">Art Tile</span> will be bound by the scene's canvas.</p>`;
else if (type === "art" && frameId) {
const frameTileName = await ArtTileManager.getGalleryTileDataFromID(
frameId,
"displayName"
);
instructionsContent += `<p id="hasFrameTile">This <span class="art-color">Art Tile</span> will be bound by frame tile <span class="frame-color">${frameTileName}</span> </p>`;
}
instructionsContent += "</div>";
let content = instructionsElement.find(".instructions__content");
instructionsElement.contentHidden = true;
if (!isLeave) {
content.replaceWith(`${instructionsContent}`);
let element = instructionsElement[0];
UIA.toggleShowAnotherElement(event, {
parentItem: element,
targetClassSelector: "instructions__content",
fadeIn: false,
});
} else {
if (!instructionsElement.contentHidden) {
instructionsElement.contentHidden = true;
}
if (instructionsElement.timeout) {
clearTimeout(instructionsElement.timeout);
}
instructionsElement.timeout = setTimeout(async () => {
await UIA.fade(content, {
duration: 200,
isFadeOut: true,
onFadeOut: async () => {
content.replaceWith(
`<div class="instructions__content JTCS-hidden"></div>`
);
instructionsElement.contentHidden = true;
// content.addClass("JTCS-hidden");
},
});
}, 300);
}
},
toggleInstructionsVisible: async (event, options = {}) => {
let areVisible = await HelperFunctions.getSettingValue(
"areConfigInstructionsVisible"
);
await HelperFunctions.setSettingValue(
"areConfigInstructionsVisible",
!areVisible
);
UIA.toggleActiveStyles(event);
},
/**
* Lower the opacity of every other tile in the scene, to see this one more clearly
* This is disabled in v10, as the indicators are visible regardless
* @param {HTMLEvent} event - the triggering event
* @param {Object options - an options object
* @param {String} options.tileID - the ID of the Art Gallery tile we're operating upon
*/
toggleTilesOpacity: async (event, options = {}) => {
const { tileID } = options;
const btn = event.currentTarget;
const clickAction = btn.dataset.action;
const removeFade = btn.classList.contains("active") ? true : false;
const alphaValue = removeFade ? 1.0 : 0.5;
//get other buttons from other Tile Items that may be set to active, and so we can toggle them off
let otherToggleFadeButtons = btn
.closest(".tilesInScene")
.querySelectorAll(`[data-action='${clickAction}'].active`);
//filter any button that has the same parent tile item out
otherToggleFadeButtons = Array.from(otherToggleFadeButtons).filter(
(fadeBtn) => fadeBtn.closest(".tile-list-item").dataset.id !== tileID
);
const otherTiles = game.scenes.viewed.tiles.contents.filter(
(tile) => tile.id !== tileID
);
//check for game version
if (game.version >= 10) {
let update = otherTiles.map((data) => {
return {
_id: data.id,
alpha: alphaValue,
};
});
update.push({ _id: tileID, alpha: 1.0 });
await game.scenes.viewed.updateEmbeddedDocuments("Tile", update);
} else {
otherTiles.forEach((tile) => {
tile.object.alpha = alphaValue;
});
const ourTile = game.scenes.viewed.tiles.get(tileID);
ourTile.object.alpha = 1.0;
}
//toggle this button active
if (otherToggleFadeButtons.length > 0) {
UIA.clearOtherActiveStyles(
event,
btn,
`[data-action='${clickAction}']`,
".tilesInScene"
);
}
UIA.toggleActiveStyles(event);
//toggle any other active buttons to be inactive
// Array.from(otherToggleFadeButtons).forEach((el) => UIA.toggleActiveStyles(event, el));
},
toggleSheetOpacity: async (event, options = {}) => {
UIA.fadeSheetOpacity(event);
UIA.toggleActiveStyles(event);
},
};
export const slideshowDefaultSettingsData = {
globalActions: {
click: {
propertyString: "globalActions.click.actions",
actions: {
showURLShareDialog: {
icon: "fas fa-external-link-alt",
tooltipText: "Share a URL link with your players",
onClick: extraActions.showURLShareDialog,
},
showModuleSettings: {
icon: "fas fa-cog",
tooltipText: "Open JTCS Art Gallery Settings",
onClick: (event, options = {}) => {
UIA.renderAnotherApp("JTCSSettingsApp", JTCSSettingsApplication);
},
},
toggleInstructionsVisible: {
icon: "fas fa-eye-slash",
tooltipText: "toggle instruction visibility",
renderedInTemplate: true,
onClick: async (event, options) =>
await extraActions.toggleInstructionsVisible(event, options),
},
toggleSheetOpacity: {
icon: "fas fa-eye-slash",
tooltipText: "fade this application to better see the canvas",
onClick: async (event, options) =>
extraActions.toggleSheetOpacity(event, options),
},
// showArtScenes: {
// icon: "fas fa-map",
// tooltipText: "Show art scenes",
// onClick: async (event, options = {}) => {
// //display the art scenes (scenes that currently have slideshow data)
// let artScenes = await ArtTileManager.getAllScenesWithSlideshowData();
// let chosenArtScene = await HelperFunctions.getSettingValue(
// "artGallerySettings",
// "dedicatedDisplayData.scene.value"
// );
// let artSceneItems = {};
// artScenes.forEach((scene) => {
// artSceneItems[scene.name] = {
// icon: scene.thumbnail,
// dataset: {},
// };
// });
// let context = {
// propertyString: "globalActions.click.actions",
// items: artSceneItems,
// };
// let templateData = Popover.createTemplateData(parentLI, "item-menu", context);
// let elementData = { ...Popover.defaultElementData };
// elementData["popoverElement"] = {
// targetElement: null,
// hideEvents: [
// {
// eventName: "change",
// selector: "input",
// wrapperFunction: async (event) => {
// let url = event.currentTarget.value;
// let valid = HelperFunctions.manager.validateInput(url, "image");
// if (valid) {
// await ImageDisplayManager.updateTileObjectTexture(
// tileID,
// frameTileID,
// url,
// "anyTile"
// );
// } else {
// ui.notifications.error("URL not an image");
// //TODO: show error?
// }
// },
// },
// ],
// };
// let popover = await Popover.processPopoverData(
// event.currentTarget,
// app.element,
// templateData,
// elementData
// );
// popover[0].querySelector("input").focus({ focusVisible: true });
// },
// },
// showArtSheets: {},
createNewTileData: {
icon: "fas fa-plus",
tooltipText: "Create new tile data",
renderedInTemplate: true,
onClick: async (event, options) =>
await extraActions.updateTileData(event, options),
},
},
},
change: {
propertyString: "globalActions.change.actions",
actions: {
setArtScene: {
onChange: async (event, options = {}) => {
let { app } = options;
let value = event.currentTarget.value;
await HelperFunctions.setSettingValue(
"artGallerySettings",
value,
"dedicatedDisplayData.scene.value"
);
await app.renderWithData();
},
},
setArtJournal: {
onChange: async (event, options = {}) => {
let { app } = options;
let value = event.currentTarget.value;
await HelperFunctions.setSettingValue(
"artGallerySettings",
value,
"dedicatedDisplayData.journal.value"
);
await app.renderWithData();
},
},
},
},
hover: {
actions: {},
},
},
itemActions: {
change: {
propertyString: "itemActions.change.actions",
actions: {
setLinkedTile: {
onChange: async (event, options = {}) => {
let { app, tileID, targetElement } = options;
if (!targetElement) targetElement = event.currentTarget;
if (!app) app = game.JTCSlideshowConfig;
let selectedID = targetElement.value;
await ArtTileManager.updateTileDataID(tileID, selectedID);
if (app.rendered) {
await app.renderWithData();
}
},
},
setFrameTile: {
onChange: async (event, options) =>
await extraActions.updateTileData(event, options),
},
setDisplayName: {
onChange: async (event, options) => {
//validate the input first
let isValid = await UIA.validateInput(event, {
noWhitespaceStart: {
notificationType: "danger",
message:
"Please enter a name that doesn't start with a white space",
},
});
if (isValid) {
await extraActions.updateTileData(event, options);
}
},
},
shareURL: {
onChange: async (event, options = {}) => {
const { tileID, parentLI } = options;
const frameTileID = parentLI.dataset.frameId;
const url = event.currentTarget.value;
const valid = HelperFunctions.validateInput(url, "image");
if (valid) {
await ImageDisplayManager.updateTileObjectTexture(
tileID,
frameTileID,
url,
"anyScene"
);
} else {
ui.notifications.error("URL not an image");
}
},
},
},
},
hover: {
propertyString: "itemActions.hover.actions",
actions: {
highlightTile: {
onHover: async (event, options = {}) => {
let isLeave =
event.type === "mouseout" || event.type === "mouseleave";
let { targetElement } = options;
if (!targetElement) targetElement = event.currentTarget;
if (targetElement.tagName === "LABEL") {
targetElement = targetElement.previousElementSibling;
}
let tileID = targetElement.dataset.id;
let tile = await ArtTileManager.getTileObjectByID(tileID);
if (!isLeave) {
await game.JTCS.indicatorUtils.showTileIndicator(tile);
} else {
await game.JTCS.indicatorUtils.hideTileIndicator(tile);
}
},
},
highlightItemAndTile: {
//TODO: refactor the name of this property to include the "showInstructions" method
onHover: async (event, options = {}) => {
const dataset = $(options.parentLI).data();
await extraActions.highlightItemAndTile(event, {
...options,
...dataset,
});
await extraActions.showInstructions(event, {
...options,
...dataset,
});
},
},
},
},
click: {
//for buttons
propertyString: "itemActions.click.actions",
actions: {
setAsDefault: {
onClick: async (event, options = {}) => {
await extraActions.setDefaultTileInScene(event, options);
},
renderNever: true,
},
shareURLOnTile: {
icon: "fas fa-external-link-alt",
tooltipText: "Share image from a url on this tile",
onClick: async (event, options = {}) => {
let { tileID, parentLI, app, html } = options;
let frameTileID = parentLI.dataset.frameId;
let context = {
name: "shareUrl",
id: "shareURL",
changeAction: "itemActions.change.actions.shareURL",
label: "Share URL",
};
let templateData = Popover.createTemplateData(
parentLI,
"input-with-error",
context
);
let elementData = { ...Popover.defaultElementData };
elementData["popoverElement"] = {
targetElement: null,
// hideEvents: [
// {
// eventName: "change",
// selector: "input",
// wrapperFunction: async (event) => {
// let url = event.currentTarget.value;
// let valid = HelperFunctions.manager.validateInput(url, "image");
// if (valid) {
// await ImageDisplayManager.updateTileObjectTexture(
// tileID,
// frameTileID,
// url,
// "anyScene"
// );
// } else {
// ui.notifications.error("URL not an image");
// }
// },
// },
// ],
};
let popover = await Popover.processPopoverData(
// event.currentTarget,
event.target,
app.element,
templateData,
elementData
);
popover[0].querySelector("input").focus({ focusVisible: true });
await app.activateListeners(app.element);
},
overflow: false,
artTileOnly: true,
},
createNewGalleryTile: {
icon: "fas fa-plus",
tooltipText:
"Create a new tile object on the canvas in this scene, linked to this art gallery item",
overflow: false,
renderOnMissing: true,
onClick: async (event, options = {}) => {
let { tileID, parentLI, app } = options;
let isFrameTile = parentLI.dataset.type === "frame";
await ArtTileManager.createAndLinkSceneTile({
unlinkedDataID: tileID,
isFrameTile: isFrameTile,
});
await app.renderWithData();
},
},
showUnlinkedTiles: {
icon: "fas fa-link",
tooltipText:
"Show tiles objects on canvas that aren't linked to any art or frame tile data",
onClick: async (event, options = {}) => {
let { app, tileID, parentLI } = options;
let frameTileID;
if (!frameTileID) frameTileID = parentLI.dataset.frameId;
let artTileDataArray =
await ArtTileManager.getSceneSlideshowTiles("", true);
let unlinkedTilesIDs = await ArtTileManager.getUnlinkedTileIDs(
artTileDataArray
);
let context = {
artTileDataArray: artTileDataArray,
unlinkedTilesIDs: unlinkedTilesIDs,
};
let templateData = Popover.createTemplateData(
parentLI,
"tile-link-partial",
context
);
let elementData = { ...Popover.defaultElementData };
// elementData["popoverElement"].hideEvents.push({
// eventName: "change",
// selector: "input, input + label",
// wrapperFunction: async (event) => {},
// });
// -- RENDER THE POPOVER
let popover = await Popover.processPopoverData(
event.target,
app.element,
templateData,
elementData
);
await app.activateListeners(app.element);
popover[0].querySelector("input").focus({ focusVisible: true });
return;
},
overflow: false,
renderOnMissing: true,
},
toggleTilesOpacity: {
icon: "fas fa-clone",
tooltipText: "fade out other tiles in scene to better see this one",
onClick: async (event, options = {}) =>
extraActions.toggleTilesOpacity(event, options),
v9Only: true,
},
// the overflow menu should be last
toggleOverflowMenu: {
icon: "fas fa-ellipsis-v",
tooltipText: "show menu of extra options for this art tile",
overflow: false,
renderAlways: true,
onClick: async (event, options = {}) => {
const { app, tileID, parentLI } = options;
if (!tileID) tileID = parentLI.dataset.tileID;
const type = parentLI.dataset.type;
const parentItemMissing = parentLI.dataset.missing ? true : false;
const actions =
slideshowDefaultSettingsData.itemActions.click.actions;
let overflowActions = {};
for (let actionKey in actions) {
let { overflow, renderOnMissing, renderAlways, artTileOnly } =
actions[actionKey];
if (!renderOnMissing) renderOnMissing = false; //if render on missing is undefined, set it to false
const shouldRender = renderOnMissing === parentItemMissing; //fif the parent item's missing status and the button's conditional rendering status are equal
const mismatchedTypes = type === "frame" && artTileOnly; //if the item should only render on an art tile
if (
overflow &&
(shouldRender || renderAlways) &&
!mismatchedTypes
) {
//if it's an action to renderon the overflow menu
overflowActions[actionKey] = actions[actionKey];
}
}
const context = {
propertyString: "itemActions.click.actions",
items: overflowActions,
};
let templateData = Popover.createTemplateData(
parentLI,
"item-menu",
context
);
const elementData = { ...Popover.defaultElementData };
let popover = await Popover.processPopoverData(
event.target,
app.element,
templateData,
elementData
);
await app.activateListeners(app.element);
popover.focus({ focusVisible: true });
},
},
//overflow menu items
selectTile: {
text: "Select tile object",
tooltipText: "Select the tile object in this scene",
icon: "fas fa-vector-square",
overflow: true,
renderOnMissing: false,
onClick: async (event, options = {}) =>
await extraActions.selectTile(event, options),
},
fitTileToFrame: {
icon: "fas fa-expand",
tooltipText: "Fit this art tile to its frame",
onClick: async (event, options = {}) => {
const { parentLI, tileID } = options;
const { type, frameId } = $(parentLI).data();
if (type === "art") {
let path;
if (game.version >= 10) {
path = game.scenes.viewed.tiles.get(tileID).texture.src;
} else {
path = game.scenes.viewed.tiles.get(tileID).data.img;
}
await ImageDisplayManager.updateTileObjectTexture(
tileID,
frameId,
path,
"anyScene"
);
}
},
overflow: true,
artTileOnly: true,
},
renderTileConfig: {
text: "Render tile object config",
tooltipText:
"Render the config for the tile object linked to this tile",
icon: "fas fa-cog",
overflow: true,
onClick: async (event, options = {}) =>
await extraActions.renderTileConfig(event, options),
renderOnMissing: false,
},
clearTileImage: {
icon: "fas fa-times-circle",
text: "Clear Tile Image",
tooltipText:
"'Clear' this tile's image, or reset it to your chosen default",
overflow: true,
onClick: async (event, options = {}) => {
let { tileID } = options;
// let tileID = parentElement.dataset.id;
await ImageDisplayManager.clearTile(tileID);
},
extraClass: "danger-text",
},
// bringTileToFront: {
// text: "Bring tile to front",
// tooltipText: "Bring the linked tile to the front"
// icon: "fas fa-arrow-to-top",
// },
deleteTileData: {
icon: "fas fa-trash",
tooltipText: `delete this" this.type "tile data?" "<br/>"
"(this will not delete the tile object in the scene itself)`,
overflow: true,
renderAlways: true,
extraClass: "danger-text",
onClick: async (event, options = {}) =>
extraActions.deleteTileData(event, options),
},
},
},
},
};

View File

@ -0,0 +1,270 @@
const validatorExpressions = {
noWhitespaceStart: /^[\S]+/,
};
const notificationIcons = {
danger: "fas fa-exclamation-circle",
warning: "fas fa-exclamation-triangle",
};
/**
* Hide (or show) all of an element's sibling elements
* @param {event} event - the event that triggered this
*/
function toggleHideAllSiblings(event, currentTarget) {
if (!event && !currentTarget) return;
if (!currentTarget) currentTarget = event.currentTarget;
const siblings = Array.from(currentTarget.parentNode.children).filter(
(item) => !item.isSameNode(currentTarget)
);
if (currentTarget.classList.contains("active")) {
siblings.forEach((el) => el.classList.remove("JTCS-hidden"));
} else {
siblings.forEach((el) => el.classList.add("JTCS-hidden"));
}
}
function fadeSheetOpacity(event, selector = ".window-content") {
event.preventDefault();
const windowContent = event.currentTarget.closest(selector);
const faded =
windowContent.classList.contains("fade") ||
windowContent.classList.contains("fade-all");
if (faded) {
windowContent.classList.remove("fade");
} else {
windowContent.classList.add("fade");
}
}
/**
* Insert a notification inline
* @param {*} event - the event that triggered this notification
* @param {*} ancestorSelector - the ancestor element into which we want to insert this notification element
* @param {Object} options - options to customize this notification
* @param {String} options.message - the notification message
* @param {String} options.notificationType - the type of notification, to affect its icon and styling
*/
async function renderInlineNotification(
event,
ancestorSelector = "formGroup",
options = {}
) {
let { notificationType, icon } = options;
if (!icon) {
if (notificationType) {
options.icon = notificationIcons[notificationType];
} else {
notificationType = "error";
options.icon = notificationIcons[notificationType];
}
}
const parentItem = event.currentTarget.closest(`.${ancestorSelector}`);
let template = game.JTCS.templates["notification-badge"];
let renderHTML = await renderTemplate(template, options);
parentItem.insertAdjacentHTML("beforeend", renderHTML);
}
function setAnimDefaults(animOptions) {
const defaultOptions = {
isFadeOut: false,
duration: 300,
onFadeOut: ($el, event) => {
$el.remove();
},
};
return mergeObject(defaultOptions, animOptions);
}
/**
*
* for fading objects in and out when they enter or exit the DOM
* @param {JQuery} $element - Jquery object representing element to fade
* @param {Object} options - the options object
* @param {Number} options.duration - default fade animation duration
* @param {Boolean} options.isFadeOut - a boolean determining whether or not this should fade in our out
* @param {Function} options.onFadeOut - the callback to handle what happens when the fade animation is complete
*/
async function fade($element, options = {}) {
const { duration, isFadeOut, onFadeOut, onCancel } = setAnimDefaults(options);
if ($element.length === 0) return;
let fadeAnim = $element[0].animate(
[
// keyframes
{ opacity: isFadeOut ? "100%" : "0%" },
{ opacity: isFadeOut ? "0%" : "100%" },
],
{
// timing options
duration: duration,
}
);
fadeAnim.addEventListener("finish", (event) => {
if (isFadeOut) {
onFadeOut($element, event);
}
});
fadeAnim.addEventListener("cancel", (event) => {
if (onCancel) {
onCancel($element, event);
}
});
return fadeAnim;
}
/**
* Handle the adding and removal of classes and triggering of animations to hide and fade elements
* @param {JQuery} $element - the JQuery object representing DOM Element we want to show or hide
*/
async function handleVisibilityTransitions($element) {
//if the class already has hidden, set it to fadeIn rather than out
const isFadeOut = $element.hasClass("JTCS-hidden") ? false : true;
//if we're fading in, remove the hidden class
if (!isFadeOut) $($element).removeClass("JTCS-hidden");
//set our fade animation options
let options = {
isFadeOut,
onFadeOut: ($element, event) => $element.addClass("JTCS-hidden"),
};
//handle the fade animation
//? Fade will handle the opacity, while our "JTCS-hidden" class handles everything else (transform, clip rect, position absolute, etc.)
fade($($element), options);
}
function toggleActiveStyles(event, el, useInitialTarget = true) {
if (!el) {
el = event.currentTarget;
if (useInitialTarget) {
//use target instead of currentTarget
el = event.target;
}
}
if (el.classList.contains("active")) {
el.classList.remove("active");
} else {
el.classList.add("active");
}
}
/**
* Turn off other elements that have active styles
* @param {HTMLEvent} event - the triggering event
* @param {Element} el - the element on which to apply the active styles
* @param {String} otherSelector - a selector to find other elements that are set to "active"
*/
function clearOtherActiveStyles(event, el, otherSelector, parentSelector) {
const parentItem = el.closest(parentSelector);
let others = Array.from(parentItem.querySelectorAll(otherSelector)).filter(
(item) => !item.isSameNode(el)
);
others = others.filter((other) => other.classList.contains("active"));
others.forEach((other) => toggleActiveStyles(event, other));
}
export const universalInterfaceActions = {
/**
*
* Show or hide another element
* @param {HTMLEvent} event - the event that provoked this
* @param {Object} options - options object
* @param {HTMLElement} options.parentItem - the parent item in which to find the element we want to hide/show
* @param {String} options.targetClassSelector - the class of the item we want to show
*/
toggleShowAnotherElement: (event, options) => {
let { parentItem, targetClassSelector, fadeIn = true } = options;
let el = event.currentTarget;
let targetID = el?.dataset.targetId;
let target;
if (targetID) {
target = parentItem.querySelector(`#${targetID}`);
} else {
target = parentItem.querySelector(`.${targetClassSelector}`);
}
if (target) {
if (fadeIn) {
handleVisibilityTransitions($(target));
} else {
$(target).removeClass("JTCS-hidden");
}
}
},
toggleActiveStyles: toggleActiveStyles,
clearOtherActiveStyles: clearOtherActiveStyles,
fadeSheetOpacity: fadeSheetOpacity,
toggleHideSelf: (event) => {
let el = event.currentTarget;
el.classList.toggle("JTCS-hidden");
},
toggleHideAncestor: (event, options) => {
let { ancestorSelector } = options;
let el = event.currentTarget;
el.closest(ancestorSelector).classList.toggle("JTCS-hidden");
// parentItem.classList.toggle("JTCS-hidden");
},
toggleHideAllSiblings,
scrollOtherElementIntoView: (event, options) => {
const { parentItem: $parentItem } = options;
let currentTarget = event.currentTarget;
let scrollTargetID = currentTarget.dataset.target;
let scrollTarget = $parentItem.find(`#${scrollTargetID}`);
scrollTarget[0].scrollIntoView();
clearOtherActiveStyles(
event,
currentTarget,
"[data-action='scrollTo']",
"#JTCSsettingsHeader"
);
toggleActiveStyles(event, currentTarget);
},
renderAnotherApp: async (appName, constructor) => {
//if global variable's not initialized, initialize it
if (!game[appName]) game[appName] = new constructor();
//if it's not rendered, render it
if (!game[appName].rendered) {
await game[appName].render(true);
// window[appName] = constructor;
} else {
//if it is rendered, bring it to the top
await game[appName].bringToTop();
}
},
renderInlineNotification: renderInlineNotification,
fade,
validateInput: async (
event,
validators = {
noWhitespaceStart: {
notificationType: "error",
message: "Please enter a value that doesn't start with a white space",
},
}
) => {
const { value } = event.currentTarget;
const validatorKeys = Object.keys(validators);
let allValid = true;
/// do validation here
let firstInvalidObject = {};
validatorKeys.forEach((key) => {
const regexp = validatorExpressions[key];
let isValid = regexp.test(value);
if (!isValid) {
//if one expression doesn't match
//set the full "allValid" boolean to false
allValid = false;
firstInvalidObject = validators[key];
let { notificationType: type } = firstInvalidObject;
firstInvalidObject.icon = notificationIcons[type];
}
});
//if one of the validators returns invalid, show a notification
if (!allValid) {
await renderInlineNotification(event, "form-group", firstInvalidObject);
}
return allValid;
},
};

View File

@ -0,0 +1,4 @@
const notificationIcons = {
error: "fas fa-exclamation-circle",
warning: "fas fa-exclamation-triangle",
};

54
scripts/data/templates.js Normal file
View File

@ -0,0 +1,54 @@
import { MODULE_ID } from "../debug-mode.js";
const baseTemplatePath = `modules/${MODULE_ID}/templates/`;
const templateBaseNames = [
`tile-list-item.hbs`,
"tooltip.hbs",
"control-button.hbs",
`new-tile-list-item.hbs`,
`icon-button.hbs`,
`display-tile-config.hbs`,
`image-controls.hbs`,
`tile-link-partial.hbs`,
`input-with-error.hbs`,
`share-url-partial.hbs`,
`fieldset-button-group.hbs`,
`checkbox-chip-group.hbs`,
`notification-badge.hbs`,
`popover.hbs`,
`item-list.hbs`,
`item-menu.hbs`,
`sheet-wide-controls.hbs`,
`badge.hbs`,
`delete-confirmation-prompt.hbs`,
`scene-tile-wrapper.hbs`,
`color-picker.hbs`,
`checkbox.hbs`,
];
/**
* @param {*} templateBaseNameArray
* @returns
*/
export function generateTemplates() {
let templates = templateBaseNames.map((baseName) =>
createTemplatePathString(baseName)
);
return templates;
}
export function createTemplatePathString(templateBaseName) {
return `${baseTemplatePath}${templateBaseName}`;
}
export function mapTemplates(templates) {
if (!templates) {
templates = generateTemplates();
}
let mappedTemplates = {};
templates.forEach((path) => {
let baseName = path.split("/").pop().split(".").shift();
mappedTemplates[baseName] = path;
});
return mappedTemplates;
}

50
scripts/debug-mode.js Normal file
View File

@ -0,0 +1,50 @@
export const MODULE_ID = "journal-to-canvas-slideshow";
import { SlideshowConfig } from "./SlideshowConfig.js";
// import { JTCSSettingsApplication } from "./classes/JTCSSettingsApplication.js";
Hooks.once("devModeReady", ({ registerPackageDebugFlag }) => {
registerPackageDebugFlag(MODULE_ID);
});
// Hooks.on("canvasReady", ({}) => {
// const isDebugging = game.modules.get("_dev-mode")?.api?.getPackageDebugValue(MODULE_ID);
// //re-render the tile config
// const autoRenderDocs = {
// // journal: ["Art"],
// };
// const appNames = {
// JTCSlideshowConfig: new SlideshowConfig(),
// // JTCSSettingsApp: new JTCSSettingsApplication(),
// };
// if (game.user.isGM && isDebugging) {
// Object.keys(appNames).forEach((key) => {
// autoRenderApp(key, appNames[key]);
// });
// Object.keys(autoRenderDocs).forEach((collectionName) => {
// autoRenderDocs[collectionName].forEach((name) => {
// game[collectionName].getName(name).sheet.render(true);
// });
// });
// // if (!game[appName]) game[appName] = appNames[appName]; //.render(true)
// // game[appName].render(true);
// // game.journal.getName("Art").sheet.render(true);
// }
// });
function autoRenderApp(appName, instance) {
if (!game[appName]) game[appName] = instance;
game[appName].render(true);
}
export function log(force, ...args) {
try {
const isDebugging = game.modules.get("_dev-mode")?.api?.getPackageDebugValue(MODULE_ID);
if (force || isDebugging) {
console.log(MODULE_ID, "|", ...args);
}
} catch (e) {}
}
// ...

View File

@ -0,0 +1,111 @@
import { HelperFunctions } from "../classes/HelperFunctions.js";
export const registerHelpers = function () {
Handlebars.registerHelper("flattenObject", (object) => {
return flattenObject(object);
});
/**
* Returns a property string that matches a dot-notation-chain for nested objects
*/
Handlebars.registerHelper("getPropertyString", (rootObject, parentKey, childKey) => {
const parentObject = getProperty(rootObject, parentKey);
const flat = flattenObject(parentObject);
const newFlat = Object.keys(flat).map((key) => `${parentKey}.${key}`);
return newFlat.find((key) => key.includes(childKey));
});
Handlebars.registerHelper("applyTemplate", function (subTemplateId, context) {
var subTemplate = Handlebars.compile($("#" + subTemplateId).html());
var innerContent = context.fn({});
var subTemplateArgs = _.extend({}, context.hash, {
content: new Handlebars.SafeString(innerContent),
});
return subTemplate(subTemplateArgs);
});
Handlebars.registerHelper(
"checkAll",
function (anyOrAll = "all", valueToEqual = true, ...conditions) {
conditions.pop();
//if the property has every object, and every object is true
if (anyOrAll === "all") {
return conditions.every((condition) => {
return condition === valueToEqual;
});
} else {
return conditions.some((condition) => {
return condition === valueToEqual;
});
}
}
);
Handlebars.registerHelper("filter", function (object, conditionName, conditionValue) {
let array = Object.entries(object).filter(
([key, data]) => data[conditionName] === conditionValue || data.renderAlways
);
return Object.fromEntries(array);
});
Handlebars.registerHelper("camelCaseToDashCase", function (string) {
let dashedString = string.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
return dashedString;
});
Handlebars.registerHelper("safeString", function (string) {
return Handlebars.SafeString(string);
});
Handlebars.registerHelper("camelCaseToCapitalString", function (string) {
return HelperFunctions.capitalizeEachWord(string, "");
// let sentence = string.split(/(?=[A-Z])/).map((s) => s.toLowerCase());
// sentence = sentence.map((s) => s.charAt(0).toUpperCase() + s.slice(1));
// sentence = sentence.join(" ");
// return sentence;
});
Handlebars.registerHelper("camelCaseToArray", function (string, shouldJoin = false) {
let sentence = string.split(/(?=[A-Z])/).map((s) => s.toLowerCase());
if (shouldJoin) {
sentence = sentence.join(" ");
}
return sentence;
});
Handlebars.registerHelper("combineToString", function (...args) {
args.pop();
let sentence = args.join(" ");
return new Handlebars.SafeString(sentence);
});
Handlebars.registerHelper(
"wrapInSpan",
function (stringToWrap, classList = "accent") {
let wrappedString = `<span class=${classList}>${stringToWrap}</span>`;
return new Handlebars.SafeString(wrappedString);
}
);
Handlebars.registerHelper(
"wrapInElement",
function (stringToWrap, tagName, classList = "") {
let wrappedString = `<${tagName} class=${classList}>${stringToWrap}</${tagName}>`;
return new Handlebars.SafeString(wrappedString);
}
);
Handlebars.registerHelper("generateChildPartials", function (object) {});
Handlebars.registerHelper("ternary", function (test, yes, no) {
return test ? yes : no;
});
Handlebars.registerHelper("dynamicPartial", function (key, partials = "") {
if (!partials || !Array.isArray(partials)) {
partials = game.JTCS.templates;
}
let partialPath = partials[key];
return new Handlebars.SafeString(partialPath);
});
Handlebars.registerHelper("withTooltip", function () {
let wrappedString = `<span class=${classList}>${stringToWrap}</span>`;
});
};

View File

@ -0,0 +1,16 @@
const baseTemplatePath = `modules/${MODULE_ID}/templates/`;
const templateBaseNames = [
`tile-list-item.hbs`,
"tooltip.hbs",
"control-button.hbs",
`new-tile-list-item.hbs`,
`icon-button.hbs`,
`display-tile-config.hbs`,
`image-controls.hbs`,
`tile-link-partial.hbs`,
];
export const registerPartials = function () {
let templates = generateTemplates(templateBaseNames);
loadTemplates(templates);
};

217
scripts/hooks.js Normal file
View File

@ -0,0 +1,217 @@
import { JTCSModules } from "./init.js";
import { HelperFunctions } from "./classes/HelperFunctions.js";
import { ArtTileManager } from "./classes/ArtTileManager.js";
/**
* This sets up most hooks we want to respond to in our code,
* grouping hooks with identical
* callbacks into arrays on an object
* @returns object containing registerHooks function
*/
export const setupHookHandlers = async () => {
async function renderSlideshowConfig(...args) {
if (args[2]?.diff && args[1]?.alpha) {
//TODO: ? this was a workaround for v10, keeping Scene Gallery Config from re-rendering on update of tile alpha, but should remove
//don't update if the changes include alpha
return;
}
if (game.JTCSlideshowConfig && game.JTCSlideshowConfig.rendered) {
game.JTCSlideshowConfig.render(false);
}
}
/**
* Show a toggle in the journal sheet's header to toggle whether the journal
* has controls on or off
*/
async function renderImageControls(app, html) {
if (!game.user.isGM) {
return;
}
await JTCSModules.SheetImageApp.applyImageClasses(app, html);
}
async function updateGalleryTileIndicator(tileDoc) {
let tileID = tileDoc.id; //this gets the id from the tile's document itself
let sceneGalleryTiles = await JTCSModules.ArtTileManager.getSceneSlideshowTiles(
"",
true
);
let foundTileData = await JTCSModules.ArtTileManager.getTileDataFromFlag(
tileID,
sceneGalleryTiles
); //this is looking for tiles that are already linked
await JTCSModules.CanvasIndicators.setUpIndicators(foundTileData, tileDoc);
}
async function updateAllGalleryIndicators(scene) {
let tiles = scene.tiles;
let artTileDataArray = await JTCSModules.ArtTileManager.getSceneSlideshowTiles(
"",
true
);
tiles.forEach(async (tileDoc) => {
let foundTileData = artTileDataArray.find(
(tileData) => tileData.id === tileDoc.id
);
await JTCSModules.CanvasIndicators.setUpIndicators(foundTileData, tileDoc);
});
}
async function addJTCSControls(controls) {
if (!game.user.isGM) {
return;
}
const tileControls = controls.find((control) => control?.name === "tiles");
tileControls.tools.push({
name: "ShowJTCSConfig",
title: "Show Slideshow Config",
icon: "far fa-image",
onClick: () => {
new JTCSModules.SlideshowConfig().render(true);
},
button: true,
});
}
/**
* Re render the image sheet for fresh controls whenever the JTCSSettings, or the SlideshowConfig data for the current scene (individual tile data or the default tile, for instance) is updated
* @param {Object} options - option arguments to be passed to this
* @param {String} options.origin - the origin of the Hook - was it the settings, or a flag update for the current scene's slideshow settings?
* @param {Object} options.currentScene - the scene, usually the current scene
* @param {String} options.tileID - the ID of the tile, if updated
*/
function rerenderImageSheet(options) {
const { origin, currentScene, updateData } = options;
let renderedSheets = Object.values(window.ui.windows).filter(
(item) => item.document?.documentName
);
renderedSheets.forEach((sheet) => {
const docType = sheet.document.documentName.toLowerCase();
//if our type of document is set to "true" as rendering controls in the settings
//and it's not currently being edited
//and we're not telling it to render anyway
let editorsActive = HelperFunctions.editorsActive(sheet);
if (editorsActive !== true) {
sheet.render();
}
});
}
const hookHandlers = {
rerenderImageSheet: {
//when the art gallery tiles update, re-render the sheets
hooks: [
"updateArtGalleryTiles",
"updateDefaultArtTile",
"updateJTCSSettings",
"canvasReady",
],
handlerFunction: rerenderImageSheet,
},
renderImageControls: {
hooks: [
"renderItemSheet",
"renderActorSheet",
"renderJournalSheet",
"renderJournalPageSheet",
"update",
],
// hooks: ["renderJournalSheet"],
handlerFunction: renderImageControls,
},
renderSlideshowConfig: {
hooks: [
"createTile",
// "updateTile",
"preUpdateTile",
"deleteTile",
"canvasReady",
"createJournalEntry",
"updateJournalEntry",
"deleteJournalEntry",
"updateJTCSSettings",
// "updateArtGalleryTiles"
"updateDefaultArtTile",
],
handlerFunction: renderSlideshowConfig,
},
updateCanvasIndicators: {
hooks: ["createTile", "updateTile", "deleteTile"],
handlerFunction: updateGalleryTileIndicator,
specialHooks: [
{
hookName: "canvasReady",
handlerFunction: async (canvas) => {
updateAllGalleryIndicators(canvas.scene);
},
},
{
hookName: "updateJTCSSettings",
handlerFunction: async (options) => {
// let { currentScene } = options;
let currentScene = game.scenes.viewed;
await updateAllGalleryIndicators(currentScene);
},
},
{
hookName: "updateArtGalleryTiles",
handlerFunction: async (options) => {
// let { currentScene } = options;
let currentScene = game.scenes.viewed;
await updateAllGalleryIndicators(currentScene);
},
},
{
hookName: "updateDefaultArtTile",
handlerFunction: async (options) => {
let currentScene = game.scenes.viewed;
await updateAllGalleryIndicators(currentScene);
},
},
],
},
addJTCSControls: {
hooks: ["getSceneControlButtons"],
handlerFunction: addJTCSControls,
},
updateUIColors: {
hooks: ["updateJTCSSettings"],
handlerFunction: async () => {
await HelperFunctions.setUIColors();
},
},
updateDefaultArtTile: {
hooks: ["deleteTile"],
handlerFunction: async (tileDoc) => {
// let tiles = (await ArtTileManager.getSceneSlideshowTiles("art", true)).filter((item)=> !item.missing)
// if(tiles){
// }
// await ArtTileManager.updateDefaultArtTile(tileDoc);
},
},
};
async function registerHooks() {
for (let handlerKey in hookHandlers) {
let handler = hookHandlers[handlerKey];
if (handler.specialHooks) {
handler.specialHooks.forEach((specialHookData) => {
let { hookName, handlerFunction: callback } = specialHookData;
Hooks.on(hookName, callback);
});
}
for (let hookName of handler.hooks) {
Hooks.on(hookName, handler.handlerFunction);
// Hooks.once(hookName, handler.handlerFunction);
}
}
}
return {
registerHooks: registerHooks,
};
};

244
scripts/init.js Normal file
View File

@ -0,0 +1,244 @@
"use strict";
import { ArtTileManager } from "./classes/ArtTileManager.js";
import { CanvasIndicators } from "./classes/CanvasIndicators.js";
import { HelperFunctions } from "./classes/HelperFunctions.js";
import { ImageDisplayManager } from "./classes/ImageDisplayManager.js";
import ImageVideoPopout from "./classes/MultiMediaPopout.js";
import { JTCSActions } from "./data/JTCS-Actions.js";
import { SheetImageApp } from "./SheetImageApp.js";
import { SheetImageDataController } from "./SheetImageDataController.js";
import { SlideshowConfig } from "./SlideshowConfig.js";
import { log, MODULE_ID } from "./debug-mode.js";
import {
generateTemplates,
createTemplatePathString,
mapTemplates,
} from "./data/templates.js";
import { registerHelpers } from "./handlebars/register-helpers.js";
import { registerSettings } from "./settings.js";
import { setupHookHandlers } from "./hooks.js";
import { universalInterfaceActions as UIA } from "./data/Universal-Actions.js";
export const JTCSModules = {
ArtTileManager,
CanvasIndicators,
HelperFunctions,
ImageDisplayManager,
ImageVideoPopout,
JTCSActions,
SheetImageApp,
SheetImageDataController,
SlideshowConfig,
};
Hooks.once("ready", () => {
try {
window.Ardittristan.ColorSetting.tester;
} catch {
ui.notifications.notify(
'Journal to Canvas Slideshow requires the "lib - ColorSettings" module. Please make sure you have it installed and enabled.',
"error"
);
}
});
Hooks.on("init", async () => {
console.log("Initializing Journal to Canvas Slideshow");
//register settings
registerSettings();
//register handlebars helpers
registerHelpers();
libWrapper.register(
"journal-to-canvas-slideshow",
"TextEditor._onDropEditorData",
function (wrapped, ...args) {
let event = args[0];
let editor = args[1];
var files = event.dataTransfer.files;
let containsImage = false;
for (let f of files) {
let type = f["type"].split("/")[0];
if (type === "image") {
containsImage = true;
insertImageIntoJournal(f, editor);
}
}
if (!containsImage) {
console.log("TextEditor._onDropEditorData called");
return wrapped(...args);
}
},
"MIXED"
);
//map of template names w/ short keys
let templates = generateTemplates();
let mappedTemplates = mapTemplates(templates);
// once settings are set up, create our API object
game.modules.get("journal-to-canvas-slideshow").api = {
templates: mappedTemplates,
imageUtils: {
manager: ImageDisplayManager,
displayImageInScene: ImageDisplayManager.displayImageInScene,
updateTileObjectTexture: ImageDisplayManager.updateTileObjectTexture,
scaleToScene: ImageDisplayManager.scaleArtTileToScene,
scaleToBoundingTile: ImageDisplayManager.scaleArtTileToFrameTile,
},
tileUtils: {
manager: ArtTileManager,
getLinkedFrameID: ArtTileManager.getLinkedFrameID,
createAndLinkSceneTile: ArtTileManager.createAndLinkSceneTile,
// createArtTileObject: ArtTileManager.createArtTileObject,
// createFrameTileObject: ArtTileManager.createFrameTileObject,
getSceneSlideshowTiles: ArtTileManager.getSceneSlideshowTiles,
getDefaultData: ArtTileManager.getDefaultData,
getFrameTiles: ArtTileManager.getFrameTiles,
getDisplayTiles: ArtTileManager.getDisplayTiles,
getTileObjectByID: ArtTileManager.getTileObjectByID,
selectTile: ArtTileManager.selectTile,
renderTileConfig: ArtTileManager.renderTileConfig,
updateTileDataID: ArtTileManager.updateTileDataID,
updateSceneTileFlags: ArtTileManager.updateSceneTileFlags,
getTileDataFromFlag: ArtTileManager.getTileDataFromFlag,
getUnlinkedTileIDs: ArtTileManager.getUnlinkedTileIDs,
getMissingTiles: ArtTileManager.getMissingTiles,
deleteSceneTileData: ArtTileManager.deleteSceneTileData,
getAllScenesWithSlideshowData: ArtTileManager.getAllScenesWithSlideshowData,
},
indicatorUtils: {
createTileIndicator: CanvasIndicators.createTileIndicator,
deleteTileIndicator: CanvasIndicators.deleteTileIndicator,
hideTileIndicator: CanvasIndicators.hideTileIndicator,
showTileIndicator: CanvasIndicators.showTileIndicator,
setUpIndicators: CanvasIndicators.setUpIndicators,
},
utils: {
manager: HelperFunctions,
createDialog: HelperFunctions.createDialog,
swapTools: HelperFunctions.swapTools,
setSettingValue: HelperFunctions.setSettingValue,
getSettingValue: HelperFunctions.getSettingValue,
createTemplatePathString: createTemplatePathString,
createEventActionObject: HelperFunctions.createEventActionObject,
},
sheetImageUtils: {
manager: SheetImageApp,
},
};
//load templates
loadTemplates(templates);
// now that we've created our API, inform other modules we are ready
// provide a reference to the module api as the hook arguments for good measure
Hooks.callAll(
"journalToCanvasSlideshowReady",
game.modules.get("journal-to-canvas-slideshow").api
);
});
Hooks.on("journalToCanvasSlideshowReady", async (api) => {
game.JTCS = api;
await (await setupHookHandlers()).registerHooks();
await HelperFunctions.setUIColors();
// await setupHookHandlers.registerHooks();
// setUpSheetRenderHooks();
// setUpIndicatorHooks();
});
Hooks.once("renderSlideshowConfig", (app) => {
game.JTCSlideshowConfig = app;
});
async function insertImageIntoJournal(file, editor) {
if (typeof ForgeVTT != "undefined" && ForgeVTT.usingTheForge) {
source = "forgevtt";
} else {
var source = "data";
}
let response;
if (file.isExternalUrl) {
response = {
path: file.url,
};
} else {
response = await FilePicker.upload(
source,
"/upload",
// game.settings.get("journal-to-canvas-slideshow", "imageSaveLocation"),
file,
{}
);
}
let contentToInsert = `<p><img src="${response.path}" width="512" height="512" /></p>`;
if (contentToInsert) editor.insertContent(contentToInsert);
}
Hooks.once("canvasReady", async () => {
const showWelcomeMessage = await HelperFunctions.getSettingValue(
"showWelcomeMessage"
);
if (showWelcomeMessage && game.user.isGM) {
await HelperFunctions.showWelcomeMessage();
}
});
Hooks.on("canvasReady", async (canvas) => {
//get tile data from scene flags
let artTileDataArray = (await ArtTileManager.getSceneSlideshowTiles("", true)).filter(
(item) => !item.missing
);
game.scenes.viewed.tiles.forEach(async (tileDoc) => {
let foundTileData = artTileDataArray.find(
(tileData) => tileData.id === tileDoc.id
);
await CanvasIndicators.setUpIndicators(foundTileData, tileDoc);
});
});
/**
* For rendering a button in the Tile Config that shows the linked Art Tile
*/
Hooks.on("renderTileConfig", async (app, element) => {
let currentScene = game.scenes.viewed;
//get tiles with flags
let flaggedTiles = await ArtTileManager.getSceneSlideshowTiles();
let defaultData = await ArtTileManager.getDefaultData();
//get data from tiles
if (flaggedTiles) {
let tileID = game.version >= 10 ? app.object._id : app.object.data._id;
defaultData = await ArtTileManager.getTileDataFromFlag(tileID, flaggedTiles);
defaultData = { ...defaultData };
defaultData.boundingTiles = await game.JTCS.tileUtils.getFrameTiles(flaggedTiles);
}
if (element[0] && !element[0]?.querySelector("#displayTileData")) {
//if we don't have this data
// let template = "modules/journal-to-canvas-slideshow/templates/display-tile-config.hbs";
const showConfigButton = document.createElement("button");
showConfigButton.textContent = "Open Gallery Config";
showConfigButton.setAttribute("id", defaultData.id);
showConfigButton.setAttribute("type", "button");
const target = $(element).find(`[name="tint"]`).parent().parent();
target.after(showConfigButton);
$(showConfigButton).on("click", async (event) => {
event.preventDefault();
await UIA.renderAnotherApp("JTCSlideshowConfig", SlideshowConfig);
// let btn = event.currentTarget;
// if (game.JTCSlideshowConfig) {
// game.JTCSlideshowConfig.render(true);
// } else {
// game.JTCSlideshowConfig = new SlideshowConfig().render(true);
// }
});
}
});

189
scripts/settings.js Normal file
View File

@ -0,0 +1,189 @@
"use strict";
import { MODULE_ID, log } from "./debug-mode.js";
import { JTCSSettingsApplication } from "./classes/JTCSSettingsApplication.js";
import { HelperFunctions } from "./classes/HelperFunctions.js";
const assetFolderBasePath = `modules/${MODULE_ID}/assets/`;
export const colorThemes = {
dark: {
colorSchemeData: {
colors: {
backgroundColor: "#010527",
accentColor: "#9876ff",
},
},
indicatorColorData: {
colors: {
frameTileColor: "#2ec4b6",
unlinkedTileColor: "#ff5369",
artTileColor: "#ff9f1c",
defaultTileColor: "#ff60f4",
},
},
},
light: {
colorSchemeData: {
colors: {
accentColor: "#582eff",
backgroundColor: "#ffffff",
},
},
indicatorColorData: {
colors: {
frameTileColor: "#b44b00",
artTileColor: "#009ec5",
unlinkedTileColor: "#33ac4b",
defaultTileColor: "#df00b2",
},
},
},
};
export const artGalleryDefaultSettings = {
sheetSettings: {
name: "Sheet Types",
hint: `Which types of sheets would you like to show clickable image controls? <br/><br/>
<span class="accent"> Note: These options determine if these sheet types have the controls visible by default. You'll still be able to toggle controls on and off on each individual sheet regardless. </span>`,
modularChoices: {
journalEntry: true,
actor: true,
item: true,
},
},
colorSchemeData: {
theme: "light",
name: "Custom Color Scheme",
hint: `What colors would you like to use on parts of the JTCS UI? This will affect things like buttons, checkboxes, borders, etc.
<br/> <br/> <span class="accent"> Hint: Click 'Apply Changes' to refresh this window and immediately see how your chosen colors look.</accent>
`,
colors: {
backgroundColor: "#010527",
accentColor: "#9876ff",
},
propertyNames: {
accentColor: "--JTCS-accent-color",
backgroundColor: "--JTCS-background-color",
},
colorVariations: {
accentColor: true,
backgroundColor: true,
},
autoContrast: true,
},
dedicatedDisplayData: {
name: "Art Journal and Art Scene",
hint: `Select your Art Journal and Art Scene.
These are "dedicated" displays, meaning if you choose the "Art Journal" or "Art Scene" actions on the sheet image controls or within the URL Share Dialog,
images will be sent directly to that scene or journal.
<br/><br/>
<a href="https://github.com/EvanesceExotica/Journal-To-Canvas-Slideshow/blob/master/features-and-walkthrough.md#window-popouts-art-journal-and-art-scene---upgraded-features">View the Features and Walkthrough document for a demonstration and More Info</a>
.`,
journal: {
name: "Art Journal",
value: "",
hint: `Select your Art Journal, then choose additional functionality for what automatically happens when the image is changed
<br/> <br/> <b>Auto Activate</b> - will automatically show the Journal Entry to you and all of your players
<br/> <br/> <b>Auto View </b> - will render the journal entry for you but not your players (useful if you wish to check that the image properly updated)
`,
autoActivate: false,
autoView: false,
},
scene: {
name: "Art Scene",
value: "",
hint: `Select your Art Scene, then choose additional functionality for what automatically happens when the image is changed:
<br/> <br/> <b>Auto Activate</b> - will automatically activate the scene for you and all of your players
<br/> <br/> <b>Auto View</b> - will automatically view the scene for you (useful if you wish to check that the default tile image actually updated)
<br/> <br/> Note: only scenes with a Default Art Tile will be able to be picked as your 'Art Scene'
`,
autoActivate: false,
autoView: false,
},
},
sheetFadeOpacityData: {
name: "Sheet Fade Opacity",
hint: "Change the opacity of the background when the sheet fades. The minimum is 20, nearly completely transparent, 100 means completely opaque. <br/> You must refresh any open journals after changing this value to see the difference.",
value: 60,
},
fadeSheetImagesData: {
name: "Fade Sheet Images",
hint: "When fading a JournalEntry, Actor, or Item sheet, should the images fade as well as the background?",
chosen: "fadeAll",
choices: {
fadeBackground: "Fade Background and UI Only",
fadeAll: "Fade Background, UI AND Images",
},
},
indicatorColorData: {
name: "Tile Indicator Colors",
hint: "Choose colors for the tile indicators, and the tile accent colors in the settings",
colors: {
frameTileColor: "#2ec4b6",
unlinkedTileColor: "#e71d36",
artTileColor: "#ff9f1c",
defaultTileColor: "#b260ff",
// frameTileColor: "#ed3000",
// artTileColor: "#009ec5",
// unlinkedTileColor: "#33ac4b",
// defaultTileColor: "#df00b2",
},
propertyNames: {
frameTileColor: "--data-frame-color",
artTileColor: "--data-art-color",
unlinkedTileColor: "--data-unlinked-color",
defaultTileColor: "--data-default-color",
},
},
defaultTileImages: {
name: "Default Tile Images",
hint: "Choose images for the Art and Frame tiles when they're first created, and for art tiles to reset to when the tile is 'cleared'",
paths: {
frameTilePath: `${assetFolderBasePath}Bounding_Tile.webp`,
artTilePath: `${assetFolderBasePath}DarkBackground.webp`,
},
},
};
export const registerSettings = async function () {
await game.settings.registerMenu(MODULE_ID, "JTCSSettingsMenu", {
name: "JTCS Art Gallery Settings",
label: "Open JTCS Art Gallery Settings",
hint: "Configure extra Journal to Canvas Slideshow settings",
icon: "fas fa-bars",
type: JTCSSettingsApplication,
restricted: true,
});
await game.settings.register(MODULE_ID, "artGallerySettings", {
scope: "world", // "world" = sync to db, "client" = local storage
config: false, // we will use the menu above to edit this setting
type: Object,
default: artGalleryDefaultSettings,
onChange: async (event) => {
const updateData = await HelperFunctions.getSettingValue(
"artGallerySettings"
);
Hooks.callAll("updateJTCSSettings", { origin: "JTCSSettings", updateData });
},
});
await game.settings.register(
"journal-to-canvas-slideshow",
"areConfigInstructionsVisible",
{
name: "Visible Art Gallery Tile Config Instructions",
hint: "Toggle whether the Art Gallery Configuration App will show instructions at the bottom of the application when you hover or not",
scope: "world",
config: true,
type: Boolean,
default: true,
}
);
game.settings.register("journal-to-canvas-slideshow", "showWelcomeMessage", {
name: "Show Welcome Message",
scope: "client",
type: Boolean,
config: true,
default: true,
});
};

158
scripts/shim.js Normal file
View File

@ -0,0 +1,158 @@
// SPDX-License-Identifier: MIT
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro
"use strict";
// A shim for the libWrapper library
export let libWrapper = undefined;
export const VERSIONS = [1, 12, 1];
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", "g");
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", "g");
// Main shim code
Hooks.once("init", () => {
// Check if the real module is already loaded - if so, use it
if (globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
libWrapper = globalThis.libWrapper;
return;
}
// Fallback implementation
libWrapper = class {
static get is_fallback() {
return true;
}
static get WRAPPER() {
return "WRAPPER";
}
static get MIXED() {
return "MIXED";
}
static get OVERRIDE() {
return "OVERRIDE";
}
static register(package_id, target, fn, type = "MIXED", { chain = undefined, bind = [] } = {}) {
const is_setter = target.endsWith("#set");
target = !is_setter ? target : target.slice(0, -4);
const split = target.match(TGT_SPLIT_RE).map((x) => x.replace(/\\(.)/g, "$1").replace(TGT_CLEANUP_RE, ""));
const root_nm = split.splice(0, 1)[0];
let obj, fn_name;
if (split.length == 0) {
obj = globalThis;
fn_name = root_nm;
} else {
const _eval = eval;
fn_name = split.pop();
obj = split.reduce((x, y) => x[y], globalThis[root_nm] ?? _eval(root_nm));
}
let iObj = obj;
let descriptor = null;
while (iObj) {
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
if (descriptor) break;
iObj = Object.getPrototypeOf(iObj);
}
if (!descriptor || descriptor?.configurable === false)
throw new Error(
`libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`
);
let original = null;
const wrapper =
chain ?? (type.toUpperCase?.() != "OVERRIDE" && type != 3)
? function (...args) {
return fn.call(this, original.bind(this), ...bind, ...args);
}
: function (...args) {
return fn.call(this, ...bind, ...args);
};
if (!is_setter) {
if (descriptor.value) {
original = descriptor.value;
descriptor.value = wrapper;
} else {
original = descriptor.get;
descriptor.get = wrapper;
}
} else {
if (!descriptor.set) throw new Error(`libWrapper Shim: '${target}' does not have a setter`);
original = descriptor.set;
descriptor.set = wrapper;
}
descriptor.configurable = true;
Object.defineProperty(obj, fn_name, descriptor);
}
};
//************** USER CUSTOMIZABLE:
// Set up the ready hook that shows the "libWrapper not installed" warning dialog. Remove if undesired.
{
//************** USER CUSTOMIZABLE:
// Package ID & Package Title - by default attempts to auto-detect, but you might want to hardcode your package ID and title here to avoid potential auto-detect issues
const [PACKAGE_ID, PACKAGE_TITLE] = (() => {
const match = (import.meta?.url ?? Error().stack)?.match(/\/(worlds|systems|modules)\/(.+)(?=\/)/i);
if (match?.length !== 3) return [null, null];
const dirs = match[2].split("/");
if (match[1] === "worlds")
return dirs.find((n) => n && game.world.id === n) ? [game.world.id, game.world.title] : [null, null];
if (match[1] === "systems")
return dirs.find((n) => n && game.system.id === n)
? [game.system.id, game.system.data.title]
: [null, null];
const id = dirs.find((n) => n && game.modules.has(n));
return [id, game.modules.get(id)?.data?.title];
})();
if (!PACKAGE_ID || !PACKAGE_TITLE) {
console.error(
"libWrapper Shim: Could not auto-detect package ID and/or title. The libWrapper fallback warning dialog will be disabled."
);
return;
}
Hooks.once("ready", () => {
//************** USER CUSTOMIZABLE:
// Title and message for the dialog shown when the real libWrapper is not installed.
const FALLBACK_MESSAGE_TITLE = PACKAGE_TITLE;
const FALLBACK_MESSAGE = `
<p><b>'${PACKAGE_TITLE}' depends on the 'libWrapper' module, which is not present.</b></p>
<p>A fallback implementation will be used, which increases the chance of compatibility issues with other modules.</p>
<small><p>'libWrapper' is a library which provides package developers with a simple way to modify core Foundry VTT code, while reducing the likelihood of conflict with other packages.</p>
<p>You can install it from the "Add-on Modules" tab in the <a href="javascript:game.shutDown()">Foundry VTT Setup</a>, from the <a href="https://foundryvtt.com/packages/lib-wrapper">Foundry VTT package repository</a>, or from <a href="https://github.com/ruipin/fvtt-lib-wrapper/">libWrapper's Github page</a>.</p></small>
`;
// Settings key used for the "Don't remind me again" setting
const DONT_REMIND_AGAIN_KEY = "libwrapper-dont-remind-again";
// Dialog code
console.warn(`${PACKAGE_TITLE}: libWrapper not present, using fallback implementation.`);
game.settings.register(PACKAGE_ID, DONT_REMIND_AGAIN_KEY, {
name: "",
default: false,
type: Boolean,
scope: "world",
config: false,
});
if (game.user.isGM && !game.settings.get(PACKAGE_ID, DONT_REMIND_AGAIN_KEY)) {
new Dialog({
title: FALLBACK_MESSAGE_TITLE,
content: FALLBACK_MESSAGE,
buttons: {
ok: { icon: '<i class="fas fa-check"></i>', label: "Understood" },
dont_remind: {
icon: '<i class="fas fa-times"></i>',
label: "Don't remind me again",
callback: () => game.settings.set(PACKAGE_ID, DONT_REMIND_AGAIN_KEY, true),
},
},
}).render(true);
}
});
}
});

302
scripts/tests/test-utils.js Normal file
View File

@ -0,0 +1,302 @@
"use strict";
import { ArtTileManager } from "../classes/ArtTileManager.js";
import { HelperFunctions } from "../classes/HelperFunctions.js";
import { ImageDisplayManager } from "../classes/ImageDisplayManager.js";
export class TestUtils {
/**
* Dispatch a simulated event, for when the simple element.click() won't do
* @param {HTMLElement} element - the element we're toggling
* @param {*} eventInterface - the interface such as MouseEvent, ChangeEvent, KeyboardEvent
* @param {String} eventName - the name of the event, such as "mouseenter", "change", "keydown"
* @param {Object} options - object for extra options to pass to the event
* @param {Boolean} options.bubbles - whether the event bubbles
* @param {Boolean} options.ctrlKey - whether we should consider the ctrl key pressed or not
* @example
*
*/
static dispatchEvent(
element,
eventInterface,
eventName,
options = {
bubbles: true,
ctrlKey: false,
}
) {
element.dispatchEvent(new eventInterface(eventName, options));
}
static dispatchMouseEnter(element) {
element.dispatchEvent(
new MouseEvent("mouseenter", {
bubbles: true,
})
);
}
/**
* Simulate keypress
* @param {*} element
*/
static dispatchKeypress(element, keyCode) {
element.dispatchEvent(
new KeyboardEvent("keydown", { key: keyCode, bubbles: true })
);
}
/**
* Simulate clicks, especially those with Ctrl or other keys pressed
* @param {HTMLElement} element
*/
static dispatchMouseDown(element) {
element.dispatchEvent(
new MouseEvent("click", {
ctrlKey: true, // if you aren't going to use them.
bubbles: true,
metaKey: true, // these are here for example's sake.
})
);
}
/**
* Simulate a "change" event on an element
* @param {HTMLElement} element - the element we're simulatnig an event uppon
*/
static dispatchChange(element) {
// element.fireEvent("onchange");
const e = new Event("change", { bubbles: true });
element.dispatchEvent(e);
// element.dispatchEvent("onchange");
}
static async getTileObject(tileID, sceneID = "") {
let tileObject = await ArtTileManager.getTileObjectByID(tileID, sceneID);
return tileObject;
}
static async getDocData(document, property = "") {
let data = game.version >= 10 ? document : document.data;
if (property) {
return foundry.utils.getProperty(data, property);
} else {
return data;
}
}
static async resizeTile(tileDoc, scene) {
//give it random dimensions bigger than scene
let width = (await TestUtils.getDocData(scene, "width")) + 30;
let height = (await TestUtils.getDocData(scene, "height")) + 30;
let updateData = {
_id: tileDoc.id,
width,
height,
};
await scene.updateEmbeddedDocuments("Tile", [updateData]);
}
/**
* change the tile's image to a test image
*/
static async changeTileImage(tileID, url = "") {
if (!url)
url = "/modules/journal-to-canvas-slideshow/demo-images/pd19-20049_1.webp";
await ImageDisplayManager.updateTileObjectTexture(tileID, "", url, "anyScene");
}
static async getArtGallerySettings(nestedPropertyString = "") {
let settings = await HelperFunctions.getSettingValue(
"artGallerySettings",
nestedPropertyString
);
return settings;
}
/**
* returns the "Computed Styles" Object or a property within if specified
* @param {JQuery or HTMLElement} element - the element whose styles we're getting
* @param {String} selector - the selector of the element
* @param {String} property - the style property we want to return
* @returns the Object representing the computed styles, or a property within
*/
static returnComputedStyles(element, selector, property = "") {
if (element.jquery) {
element = element[0];
}
let selectedElement = element;
if (selector) selectedElement = element.querySelector(selector);
if (!property) return getComputedStyle(selectedElement);
else return getComputedStyle(selectedElement).getPropertyValue(property);
}
static returnClassListAsArray(element, selector) {
if (element.jquery) {
element = element[0];
}
return Array.from(element.querySelector(selector).classList);
}
static getDocIdFromApp(app) {
return app.document.id;
}
/**
* We want to test each of the qualifications for if a frame tile
* is properly linked to an art tile here
*
* @param {Object} frameTile - the frame tile
* @param {Object} artTile - the art tile
*/
static async testFitToFrame(frameTileID, artTileID) {
const TU = TestUtils;
let artTileDoc = await TU.getTileObject(artTileID);
let frameDoc = await TU.getTileObject(frameTileID);
//TODO - test if the art tile fits within the frame (tile or scene)
let areas = await TU.getAreasOfDocs(frameDoc, artTileDoc);
return areas;
//TODO - test if the Config UI Updates to show that this art tile now has the frame tile as its child
}
static async duplicateTestScene(sourceScene) {
// let dupedSceneData;
let dupedSceneData = sourceScene.clone({ name: "Test Scene" });
if (game.version < 10) {
dupedSceneData = {
...foundry.utils.duplicate(sourceScene),
_id: foundry.utils.randomID(),
name: "Test Scene",
};
}
let scene = await Scene.create(dupedSceneData);
await scene.activate();
await scene.view();
return scene;
}
static async initializeScene(name = "Display") {
let sourceScene = game.scenes.getName(name);
await sourceScene.view();
return sourceScene;
}
/**
* Get the auto-view or auto-activate settings of the dedicatedDisplayData property
* @param {String} sceneOrJournal - whether we want to access the scene or the journal sub-object
* @param {String} viewOrActivate - whether we want to get the view or activate property
*/
static async getAutoViewOrActivate(
sceneOrJournal = "scene",
viewOrActivate = "view"
) {
const key = `dedicatedDisplayData[${sceneOrJournal}].auto${viewOrActivate}`;
const current = await HelperFunctions.getSettingValue(`artGallerySettings`, key);
return { key, current };
}
/**
* Toggle the auto-view or auto-activate settings of the dedicatedDisplayData property
* @param {String} sceneOrJournal - whether we want to access the scene or the journal sub-object
* @param {String} viewOrActivate - whether we want to toggle the view or activate property
*/
static async toggleAutoViewOrActivate(...args) {
let { key, current } = await TestUtils.getAutoViewOrActivate(...args); //(sceneOrJournal, viewOrActivate)
// const key = `dedicatedDisplayData[${sceneOrJournal}].auto${viewOrActivate}`;
// const current = await HelperFunctions.getSettingValue(`artGallerySettings`, key);
await HelperFunctions.setSettingValue("artGallerySettings", key, !current);
}
static async clickGlobalButton(configElement, actionName) {
configElement[0]
.querySelector(`[data-action='globalActions.click.actions.${actionName}']`)
.click();
await quench.utils.pause(900);
}
/**
* @description - renders the Scene Config
*/
static async renderConfig() {
let configApp = new SlideshowConfig();
await configApp._render(true);
let configElement = configApp.element;
return { configApp, configElement };
}
static async getDefaultImageSrc(type = "art") {
let defaultImageSrc = await TestUtils.getArtGallerySettings(
`defaultTileImages.paths.${type}TilePath`
);
return defaultImageSrc;
}
static async clickActionButton(actionName, element, options = { quench }) {
const actionQueryString = combine(actionName);
await clickButton(element, actionQueryString, quench);
}
/**
* click a button, optionally one with a data-action attribute
* @param {HTMLElement} element - the element to click
* @param {Selector} selector - the selector of the button we want to find
* @param {Object} options - options object
* @param {Object} options.quench - the quench thing to pause the button
* @param {String} options.actionName - the individual action name
* @param {String} options.parentPath - the parent path to find the options' name
*/
static async clickButton(element, selector, options) {
const { quench, actionName, parentPath } = options;
let ourButton = element.querySelector(selector);
ourButton.click();
await quench.utils.pause(900);
}
static getChildElements(element, selector, multiple = false) {
if (element.jquery) {
element = element[0];
}
if (multiple) {
return Array.from(element.querySelectorAll(selector));
} else {
return element.querySelector(selector);
}
}
static getAppFromWindow(type, searchText = "") {
let windows = Object.values(ui.windows);
function predicate(w) {
let allTrue = false;
if (w instanceof type) {
if (!searchText) {
allTrue = true;
} else {
if (w.element && w.element.text().includes(searchText))
allTrue = true;
}
return allTrue;
}
}
return Object.values(ui.windows).filter(predicate)[0];
}
static checkAppElementForId(app, id) {
let elementId = app.element?.attr("id");
if (!app.element || elementId != id) {
return false;
} else {
return true;
}
}
static async deleteTestScene(scene) {
let dupedSceneId = scene.id;
await Scene.deleteDocuments([dupedSceneId]);
}
static async getDimensions(doc) {
let width = await TestUtils.getDocData(doc, "width");
let height = await TestUtils.getDocData(doc, "height");
return {
width,
height,
};
}
static async getAreasOfDocs(frameDoc, artTileDoc) {
const frameDimensions = await TestUtils.getDimensions(frameDoc);
const frameArea = frameDimensions.width * frameDimensions.height;
const artDimensions = await TestUtils.getDimensions(artTileDoc);
const artArea = artDimensions.width * artDimensions.height;
return { artArea, frameArea };
}
}

17
styles/_animations.scss Normal file
View File

@ -0,0 +1,17 @@
@keyframes fade {
from {
opacity: 0%;
}
to {
opacity: 100%;
}
}
@keyframes fadeOut {
from {
opacity: 100%;
}
to {
opacity: 0%;
}
}

64
styles/_colors.scss Normal file
View File

@ -0,0 +1,64 @@
$colors: (
secondary: (
darkest: hsl(221, 98%, 20%),
800: hsl(215, 98%, 30%),
700: hsl(210, 98%, 40%),
600: hsl(205, 98%, 52%),
base: hsl(199, 98%, 63%),
400: hsl(190, 99%, 70%),
300: hsl(185, 100%, 80%),
lightest: hsl(175, 100%, 90%),
),
primary: (
darkest: hsl(240, 58%, 28%),
800: hsl(240, 58%, 30%),
700: hsl(240, 58%, 40%),
600: hsl(240, 60%, 50%),
base: hsl(240, 60%, 60%),
400: hsl(240, 60%, 68%),
300: hsl(240, 65%, 75%),
200: hsl(240, 75%, 85%),
lightest: hsl(240, 86%, 92%),
),
deep-purple: (
darkest: hsl(269, 100%, 8%),
700: hsl(269, 100%, 18%),
600: hsl(269, 100%, 28%),
base: hsl(269, 100%, 38%),
),
neutral: (
darkest: hsl(240, 17%, 20%),
700: hsl(240, 17%, 30%),
base: hsl(240, 12%, 50%),
300: hsl(240, 17%, 70%),
lightest: hsl(240, 17%, 90%),
),
glass: (
no-blur: hsla(0, 0%, 44%, 0.478),
blur: hsla(0, 0%, 44%, 0.267),
border: hsla(0, 0%, 100%, 0.198),
no-blur-dark: hsla(240, 11%, 14%, 0.769),
blur-dark: hsla(240, 11%, 14%, 0.161),
border-dark: hsla(240, 10%, 40%, 0.568),
),
danger: (
dark: hsl(0, 89%, 20%),
base: hsl(0, 59%, 50%),
light: hsl(0, 99%, 70%),
),
warning: (
dark: hsl(28, 100%, 30%),
base: hsl(23, 80%, 50%),
light: hsl(21, 100%, 65%),
),
success: (
dark: hsl(100, 100%, 30%),
base: hsl(100, 100%, 50%),
light: hsl(100, 100%, 70%),
),
info: (
dark: hsl(200, 100%, 25%),
base: hsl(200, 60%, 40%),
light: hsl(200, 80%, 60%),
),
);

229
styles/_mixins.scss Normal file
View File

@ -0,0 +1,229 @@
@mixin with-absolute-pseudo($centeredX: true, $centeredY: true, $isBefore: true) {
$pseudoSelector: before;
@if ($isBefore != true) {
$pseudoSelector: after;
}
position: relative;
&:#{$pseudoSelector} {
position: absolute;
content: "";
@if ($centeredX and not $centeredY) {
left: 50%;
transform: translateX(-50%);
} @else if ($centeredY and not $centeredX) {
top: 50%;
transform: translateY(-50%);
} @else if($centeredX and $centeredY) {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.blank-button {
border: none;
background: transparent;
margin: unset;
padding: unset;
}
@function returnAllHeadings($startLevel: 1, $endLevel: 6) {
$selectorList: [];
@for $var from $startLevel to $endLevel {
$headerSelector: "h#{$var}";
$selectorList: join($selectorList, $headerSelector);
}
@return $selectorList;
}
@mixin printSelectors($selectorList, $propertyMap) {
@for $i from 1 to length($selectorList) {
#{nth($selectorList, $i)} {
@each $property, $value in $propertyMap {
#{$property}: $value;
}
}
}
}
@mixin text-shadow($shadow-color: black) {
text-shadow: -1px -1px 0 $shadow-color, 0 -1px 0 $shadow-color,
1px -1px 0 $shadow-color, 1px 0 0 $shadow-color, 1px 1px 0 $shadow-color,
0 1px 0 $shadow-color, -1px 1px 0 $shadow-color, -1px 0 0 $shadow-color;
}
@mixin JTCS-accent-ghost($primary-color: var(--JTCS-accent-color), $border-side: "") {
background-color: transparent;
color: $primary-color;
border#{$border-side}: 2px solid $primary-color;
transition: background-color 120ms ease-in, color 120ms ease-in;
&:hover,
&:focus,
&.active {
transition: background-color 120ms ease-out, color 120ms ease-out;
background-color: $primary-color;
// color: var(--JTCS-text-color-on-bg);
color: var(--JTCS-text-color-on-fill);
}
}
@mixin JTCS-accent-ghost-input-span($primary-color: var(--JTCS-accent-color)) {
input:disabled + label {
opacity: 50%;
}
display: inline-flex;
&[data-variant="visible-tick"] {
input[type="radio"],
input[type="checkbox"] {
@extend .JTCS-hidden;
+ label {
background-color: transparent;
color: $primary-color;
border: 2px solid $primary-color;
transition: background-color 120ms ease-in, color 120ms ease-in;
&:hover,
&:focus {
transition: background-color 120ms ease-out, color 120ms ease-out;
background-color: $primary-color;
color: var(--JTCS-text-color-on-fill);
}
.tick-circle,
.tick-box {
border: 1px solid $primary-color;
display: inline-flex;
width: 1rem;
height: 1rem;
background-color: var(--JTCS-background-color);
}
.tick-circle {
border-radius: 50%;
}
}
&:checked {
+ label {
color: var(--JTCS-text-color-on-fill);
background-color: $primary-color;
border: 2px solid $primary-color;
outline: 1.5px solid transparent;
outline-offset: -1px;
// transition: color 200ms ease-out, background-color 200ms ease-out, border-color 200ms ease-out;
transition: outline-color 200ms ease-out, box-shadow 200ms ease-out;
&:hover {
outline-color: white;
box-shadow: 0px 0px 4px 2px $primary-color;
color: var(--JTCS-text-color-on-fill);
// color: var(--JTCS-text-color-on-bg);
}
// background-color: var(--accent-color);
.tick-circle,
.tick-box {
@include with-absolute-pseudo($isBefore: false);
&:after {
width: 0.5rem;
height: 0.5rem;
}
}
.tick-circle {
&:after {
border-radius: 50%;
background-color: $primary-color;
}
}
.tick-box {
&:after {
content: "";
color: $primary-color;
top: unset;
}
}
}
}
}
}
}
@mixin JTCS-accent-fill(
$primary-color: var(--JTCS-accent-color),
$include-shadow: false,
$hoverStyle: "scale",
$is-icon-button: false
) {
background-color: $primary-color;
@if $include-shadow {
box-shadow: 0px 0px 8px 2px $primary-color;
} @else {
box-shadow: unset;
}
color: var(--JTCS-text-color-on-fill);
border: unset;
@if $hoverStyle == "scale" {
transform: scale(1);
transition: transform 120ms ease-in-out;
transition: transform 120ms ease-in-out;
} @else if $hoverStyle == "slide-in" {
position: relative;
overflow: hidden;
i {
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute;
right: 100%;
transform: translateX(0);
transition: transform 220ms ease-in;
background-color: var(--JTCS-accent-color-20);
height: 100%;
width: 30%;
}
} @else if $hoverStyle == "border" {
position: relative;
border: 2px solid transparent;
transition: border-color 150ms ease-in;
// &:after {
// position: absolute;
// content: "";
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%);
// width: 100%;
// height: 100%;
// z-index: 1;
// border-radius: 3px;
// border: 2px solid var(--JTCS-accent-color-20);
// opacity: 0;
// transition: opacity 240ms ease-in;
// }
}
&:hover,
&:focus {
@if $hoverStyle == "scale" {
transform: scale(1.12);
} @else if $hoverStyle == "slide-in" {
i {
transform: translateX(100%);
transition: transform 220ms ease-out;
}
} @else if $hoverStyle == "border" {
border: 2px solid var(--JTCS-accent-color-80);
transition: border-color 150ms ease-out;
// &:after {
// opacity: 100%;
// transition: opacity 240ms ease-out;
// }
}
}
}
@mixin generateTypes(
$list: (
"frame",
"art",
"unlinked",
"default",
),
$propertyPrefix: ""
) {
@each $var in $list {
.#{$var}-color {
#{$propertyPrefix}color: var(--data-#{$var}-color);
}
}
}

5
styles/_spacing.scss Normal file
View File

@ -0,0 +1,5 @@
$spacing: (
"small": 1rem,
"medium": 2rem,
"large": 3rem,
);

1252
styles/_utility.scss Normal file

File diff suppressed because it is too large Load Diff

32
styles/_variables.scss Normal file
View File

@ -0,0 +1,32 @@
@use "colors" as *;
@use "spacing" as *;
:root {
--foundry-dark-gray: rgba(52, 52, 52, 0.85);
--foundry-purple-background-hover: rgba(60, 0, 120, 0.5);
--foundry-purple-shadow-hover: #9b8dff;
--foundry-purple-border: #3b1893;
--foundry-purple-background: rgba(30, 0, 60, 0.5);
--box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.36) inset;
$themeColours: (
"pink": #dc51ac,
"red": #d64651,
"orange": #e55937,
);
@each $themeColour, $i in $themeColours {
body {
&.#{$themeColour} {
background-color: $i;
}
}
}
@each $color, $shades in $colors {
@each $shade, $value in $shades {
--color-#{$color}-#{$shade}: #{$value};
}
}
@each $size, $value in $spacing {
--space-#{$size}: #{$value};
}
}

2859
styles/styles.css Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More