zischenstand

This commit is contained in:
centron\schwoerer
2025-11-14 14:52:43 +01:00
parent 30aa03c6db
commit f054a31b20
8733 changed files with 900639 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [12.0.0] - 2024-04-23
### Changed
- Support for v12
## [11.1.0] - 2024-04-09
### Added
- Support for Avif
- Copy path to downloaded image into clipboard for immediate use
## [11.0.1] - 2023-05-22
### Changed
- Official support for V11
## [10.4.0] - 2022-01-31
### Changed
- Better support for websites like unsplash
- Assume JPG format by default
## [10.3.0] - 2022-01-28
### Changed
- New look-and-feel general availability
## [10.2.0] - 2022-12-22
### Changed
- New interface (auto-scroll lists, breadcrumbs)
- Improved footer
## [10.1.1] - 2022-10-30
### Fixed
- Openverse max number of results is now 20
- 10.1.1: fix V10 compatibility
### Added
- Google Search
## [10.0.2] - 2022-09-04
### Fixed
- 10.0.1: module packaging fix
- 10.0.2: fix warnings in modules.json
### Changed
- Compatibility with V10
- Major version based on FVTT
## [3.1.1] - 2022-02-01
### Fixed
- 3.1.1: typo in jp translation
### Added
- Japanese translations
### Changed
- Help & controls (see core)
## [3.0.1] - 2021-12-23
### Fixed
- 3.0.1: Fixed CC image search (not working since API change)
### Changed
- Support for FVTT 9.x
- Support for FVTT 0.7 and 0.8 removed
## [2.3.2] - 2021-10-31
### Fixed
- 2.3.1: Import image from clipboard (URL) not working
- 2.3.2: Workaround when 'navigator.clipboard' not available
## [2.3.0] - 2021-06-24
### Added
- Support for compact mode
## [2.2.1] - 2021-06-17
### Changed
- Drag & drop as journal article will create folder structure
## [2.1.1] - 2021-06-10
### Added
- No support for new view mode (browse)
### Fixed
- Tiles don't show up if download not yet finished
## [2.0.0] - 2021-05-24
### Added
- Compatibility with FVTT 0.8.5
## [1.4.0] - 2021-05-08
### Added
- Image search via Creative Commons search engine
## [1.3.1] - 2021-04-29
### Added
- Support for S3 as storage
## [1.2.0] - 2021-04-27
### Changed
- Each installation must now provide its own API key for Bing Search
## [1.1.0] - 2021-04-18
### Added
- Image drag & drop capabilities for creating tiles/articles/tokens
## [1.0.1] - 2021-04-16
### Added
- Image search via Bing search engine

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,79 @@
# Moulinette Forge Image Search (Foundry VTT)
[![GitHub tags (latest by date)](https://img.shields.io/github/v/tag/SvenWerlen/moulinette-imagesearch)](https://github.com/SvenWerlen/moulinette-imagesearch/releases)
[![License](https://img.shields.io/github/license/SvenWerlen/moulinette-imagesearch)](https://github.com/SvenWerlen/moulinette-imagesearch/blob/main/LICENSE.txt)
[![GitHub Download](https://img.shields.io/badge/foundryvtt-Download-important)](#install)
![Tested on forge-vtt.com](https://img.shields.io/badge/Forge-supported-success)
[![Support me on Patreon](https://img.shields.io/badge/patreon-Support%20me-informational)](https://www.patreon.com/moulinette)
This is a submodule for [Moulinette Core](https://github.com/SvenWerlen/moulinette-core). See [Moulinette Core](https://github.com/SvenWerlen/moulinette-core) for an overview of all modules.
## Search image and generate article
You need images to enrich your game/campaign ?
* Search using <a href="https://www.bing.com" target="_blank">Microsoft Bing</a>, <a href="https://www.google.com" target="_blank">Google Search</a> or <a href="https://search.openverse.engineering/" target="_blank">Openverse Search</a> engines.
* Preview the image
* Download or generate a journal article
* Forge!
![Image search on Bing](docs/img/search-bing.jpg)
<br>_(Images on the screenshot are from [Microsoft Bing](https://www.bing.com) search engine. Images are publicly available but their license varies.)_
## <a name="configure"/>Generate your own API key and Engine ID for Google Search
The module requires a valid API key and Engine ID for using Google Search. You'll have to create a Google Cloud account and a create a new project. The 100 first search queries per day are free. However, Moulinette always executes 3 queries in order to get 30 results. That means that only the first 33 moulinette searches per day will be free while using Google Search.
* Visit https://console.developers.google.com and create a project.
* Visit https://console.developers.google.com/apis/library/customsearch.googleapis.com and enable "Custom Search API" for your project.
* Visit https://console.developers.google.com/apis/credentials and generate API key credentials for your project.
* Copy the API key and paste it as Google Search API key (module's configuration)
* Visit https://cse.google.com/cse/all and in the web form where you create/edit your custom search engine enable "Image search" option and for "Sites to search" option select "Search the entire web but emphasize included sites".
* Copy the Search engine ID and paste it as Google Search Engine ID (module's configuration)
* Voilà!
![Configure Google Search](docs/img/configure-google-api.jpg)
![Configure Google Search](docs/img/configure-google-engine.jpg)
## <a name="configure"/>Generate your own API key for Bing Search
The module requires a valid API key for using Bing Search. You'll have to create a Microsoft Azure account and a create a new service. The "Free" Tier provides you 1000 requests per month for free.
The following steps assume that you have a basic undestanding of Microsoft Azure and assumes that you already created a subscription
* From the dashboard / home page, click on "Create a resource"
* In the search bar, search for "Bing Search" and select "Bing Search v7"
* Click "Create"
* Enter a name for your service
* Select your existing subscription
* Choose a pricing tier according to your needs (Free tier should fit in most cases)
* Choose an existing Resource Group or create a new
* Check the box to confirm conditions
* Create
* Go to the resource after it has been created
* Select "Key and EndPoint"
* Copy one of the two keys (doesn't matter, both will work)
* In Foundry VTT, under "Configure Settings", specify your key for the "Bing Search API key"
* Voilà!
![Configure Bing Search](docs/img/configure-bing.jpg)
## <a name="install"/>Install the module
To **install** the module from FoundryVTT:
1. Start FVTT and browse to the Game Modules tab in the Configuration and Setup menu
2. Search for "Moulinette Forge" and click install on the desired module
To **manually install** the module (not recommended), follow these instructions:
1. Start FVTT and browse to the Game Modules tab in the Configuration and Setup menu
2. Select the Install Module button and enter the following URL: https://raw.githubusercontent.com/svenwerlen/moulinette-imagesearch/master/module.json
3. Click Install and wait for installation to complete
![Install custom module](https://raw.githubusercontent.com/SvenWerlen/moulinette-core/main/docs/img/moulinette-install.jpg)
## <a name="support"/>Support me on Patreon
If you like my work and want to support me, consider becoming a patreon!
[https://www.patreon.com/moulinette](https://www.patreon.com/moulinette)
You can also join [Moulinette Discord](https://discord.gg/xg3dcMQfP2)

View File

@@ -0,0 +1,37 @@
{
"mtte.codeCopiedClipboardFail": "Code snippet couldn't be copied into your clipboard! Your browser doesn't support it or blocks it.",
"mtte.codeCopiedClipboardSuccess": "Code snippet successfully copied into your clipboard!",
"mtte.configBingKey": "Bing Search API key",
"mtte.configBingKeyHint": "You have to provide your own Bing Search API key. Check documentation for more details",
"mtte.configGoogleEngineId": "Google Search Engine ID (cx)",
"mtte.configGoogleEngineIdHint": "You have to provide your own Google Search Engine ID. Check documentation for more details",
"mtte.configGoogleKey": "Google Search API key",
"mtte.configGoogleKeyHint": "You have to provide your own Google Search API key. Check documentation for more details",
"mtte.configOpenverseEnabled": "Enable Openverse Search",
"mtte.configOpenverseEnabledHint": "Enable/disable Openverse search engine.",
"mtte.confirm": "Confirm",
"mtte.createArticle": "Create article",
"mtte.createArticleToolTip": "Generates an article based on the image",
"mtte.downloadImage": "Download",
"mtte.downloadImageToolTip": "Download image into Foundry VTT",
"mtte.enterImageURL": "Specify URL to image",
"mtte.enterImageURLDescription": "This feature lets you import an image from a URL (ex: Google Images). Moulinette has not been able to extract a valid image URL from your clipboard. Please specify it below.",
"mtte.errorDragDropRequirements": "Drag & Drop capability requires submodule 'Moulinette Tiles'",
"mtte.fromClipboard": "From clipboard {date}",
"mtte.howto": "Help",
"mtte.howtoToolTip": "See the available documentation (dynamic).",
"mtte.imageFrom": "Image from",
"mtte.imageSearch": "Image search",
"mtte.imageSearchDescription": "Instructions : search then click for action",
"mtte.imageSize": "Image size",
"mtte.imageURL": "Image URL",
"mtte.multiplesMacros": "{count} macros selected",
"mtte.noBingKey": "In order to search images, you first need to configure Bing Search API key",
"mtte.noGoogleEngine": "In order to search images, you first need to configure Google Search Engine ID",
"mtte.noGoogleKey": "In order to search images, you first need to configure Google Search API key",
"mtte.openverseDisabled": "In order to search images, you first need to enable Openverse Search in the module's configuration",
"mtte.pasteURL": "Import image from URL (and create article)",
"mtte.searchBy": "Search provided by",
"mtte.searchByCustom": "Image was manually provided (from Clipboard)",
"mtte.searchresult": "Search result"
}

View File

@@ -0,0 +1,37 @@
{
"mtte.codeCopiedClipboardFail": "Le snippet de code n'a pas pu être copié dans votre presse-papier. Votre navigateur ne le supporte pas ou le bloque.",
"mtte.codeCopiedClipboardSuccess": "Le snippet de code a été copié dans votre presse-papier avec succès!",
"mtte.configBingKey": "Clé d'API Bing Search",
"mtte.configBingKeyHint": "Vous devez fournir votre propre clé d'API Bing Search. Consultez la documentation pour plus de détails",
"mtte.configGoogleEngineId": "ID du moteur de recherche (cx)",
"mtte.configGoogleEngineIdHint": "Vous devez fournir votre propre ID du moteur de recherche. Consultez la documentation pour plus de détails.",
"mtte.configGoogleKey": "Clé d'API Google Search",
"mtte.configGoogleKeyHint": "Vous devez fournir votre propre clé d'API Google Search. Consultez la documentation pour plus de détails.",
"mtte.configOpenverseEnabled": "Activer Openverse",
"mtte.configOpenverseEnabledHint": "Activer/désactiver le moteur de recherche Openverse.",
"mtte.confirm": "Confirmer",
"mtte.createArticle": "Créer article",
"mtte.createArticleToolTip": "Génère un article basé sur l'image",
"mtte.downloadImage": "Télécharger",
"mtte.downloadImageToolTip": "Télécharger l'image dans Foundry VTT",
"mtte.enterImageURL": "Spécifier l'adresse (URL) de l'image",
"mtte.enterImageURLDescription": "Cette fonction vous permet d'importer une image directement à partir d'une URL (ex: Google Images). Moulinette n'a pas été capable d'extraire une adresse d'image valide de votre presse-papier. Veuillez en spécifier une ci-dessous.",
"mtte.errorDragDropRequirements": "Les fonctionalités de Glisser-Déposer requièrent le sous-module 'Moulinette Tiles'",
"mtte.fromClipboard": "Depuis presse-papier {date}",
"mtte.howto": "Aide",
"mtte.howtoToolTip": "Accéder aux rubriques d'aide",
"mtte.imageFrom": "Image de",
"mtte.imageSearch": "Rech. image",
"mtte.imageSearchDescription": "Instructions : rechercher et cliquer pour action",
"mtte.imageSize": "Taille de l'image",
"mtte.imageURL": "URL de l'image",
"mtte.multiplesMacros": "{count} macros sélectionnées",
"mtte.noBingKey": "Pour effectuer des recherches d'images, vous devez d'abord configurer votre clé d'API Bing",
"mtte.noGoogleEngine": "Pour effectuer des recherches d'images, vous devez d'abord configurer votre ID du moteur de recherche Google",
"mtte.noGoogleKey": "Pour effectuer des recherches d'images, vous devez d'abord configurer votre clé d'API Google",
"mtte.openverseDisabled": "Pour effectuer des recherches d'images, vous devez d'abord activer la recherche Openverse dans la configuration du module.",
"mtte.pasteURL": "Importer l'image depuis son adresse (URL) et créer un article",
"mtte.searchBy": "Recherche par",
"mtte.searchByCustom": "Image fournie manuellement (depuis le presse-papier)",
"mtte.searchresult": "Résultat de la recherche"
}

View File

@@ -0,0 +1,25 @@
{
"mtte.configBingKey": "Bing検索APIキー",
"mtte.configBingKeyHint": "ご自身のBing検索キーを提供する必要があります。",
"mtte.confirm": "確認",
"mtte.createArticle": "資料作成",
"mtte.createArticleToolTip": "画像をベースに資料を作成します。",
"mtte.downloadImage": "ダウンロード",
"mtte.downloadImageToolTip": "画像をオンセ工房 Foundry VTTにダウンロード",
"mtte.enterImageURL": "URLを画像に変換",
"mtte.enterImageURLDescription": "この機能を使うとURLGoogle画像等のインポートができます。クリップボードから有効な画像が引き出せませんでした、改めてURLを指定してください。",
"mtte.errorDragDropRequirements": "ドラッグ&ドロップの機能を利用するには'Moulinette Tiles'が必要です。",
"mtte.fromClipboard": "{date}のクリップボードから",
"mtte.howto": "ヘルプ",
"mtte.howtoToolTip": "使い方:新たな画像・タイルのインストール方法",
"mtte.imageFrom": "画像元",
"mtte.imageSearch": "画像検索",
"mtte.imageSearchDescription": "使い方:検索してクリックしてください。",
"mtte.imageSize": "画像サイズ",
"mtte.imageURL": "画像URL",
"mtte.noBingKey": "画像を検索するにはBingのAPIキーを入手して設定する必要があります。",
"mtte.pasteURL": "画像をURLからインポートする素材を作る",
"mtte.searchBy": "検索提供元 ",
"mtte.searchByCustom": "画像は手動で提供されました(クリップボード)",
"mtte.searchresult": "検索結果"
}

View File

@@ -0,0 +1,63 @@
{
"title": "Moulinette Image Search (module)",
"description": "This module for Moulinette adds capabilities for searching images and generating articles on the fly.",
"version": "12.0.0",
"authors": [
{
"name": "Sven Werlen",
"flags": {}
}
],
"esmodules": [
"moulinette-imagesearch.js"
],
"styles": [
"moulinette-imagesearch.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json",
"flags": {}
},
{
"lang": "fr",
"name": "French",
"path": "lang/fr.json",
"flags": {}
},
{
"lang": "ja",
"name": "日本語",
"path": "lang/ja.json",
"flags": {}
}
],
"relationships": {
"requires": [
{
"id": "moulinette-core",
"type": "module",
"compatibility": {
"minimum": "11.0.1"
}
},
{
"id": "moulinette-tiles",
"type": "module",
"compatibility": {
"minimum": "11.0.1"
}
}
]
},
"url": "https://github.com/SvenWerlen/moulinette-imagesearch",
"download": "https://github.com/SvenWerlen/moulinette-imagesearch/archive/v-12.0.0.zip",
"manifest": "https://raw.githubusercontent.com/SvenWerlen/moulinette-imagesearch/main/module.json",
"compatibility": {
"minimum": "10",
"verified": "12"
},
"id": "moulinette-imagesearch"
}

View File

@@ -0,0 +1,449 @@
import { MoulinetteSearchResult } from "./moulinette-searchresult.js"
/**
* Forge Module for image search
*/
export class MoulinetteImageSearch extends game.moulinette.applications.MoulinetteForgeModule {
static SEARCH_BING_API = "https://api.bing.microsoft.com/v7.0/images/search"
static SEARCH_GOOGLE_API = "https://customsearch.googleapis.com/customsearch/v1"
static SEARCH_CC_API = "https://api.openverse.engineering/v1/images"
constructor() {
super()
}
supportsModes() { return false }
/**
* Pack lists are Search implementations
*/
async getPackList() {
this.assetsPacks = []
this.assetsPacks.push({ idx: 3, special:"google", publisher: "Google Search", pubWebsite: "https://developers.google.com/custom-search/", name: "Google Custom Search v1", url: "https://developers.google.com/custom-search/v1/overview", license: "depends", isRemote: true })
this.assetsPacks.push({ idx: 1, special:"bing", publisher: "Microsoft Bing", pubWebsite: "https://microsoft.com", name: "Bing Search v7.0", url: "http://bing.com/", license: "depends", isRemote: true })
this.assetsPacks.push({ idx: 2, special:"cc", publisher: "Creative Commons", pubWebsite: "https://opensource.creativecommons.org/", name: "CC Search v1.0", url: "https://opensource.creativecommons.org/archives/cc-search/", license: "depends", isRemote: true })
return duplicate(this.assetsPacks)
}
/**
/* Bing Search implementation
*/
async searchBing(searchTerms, bingKey) {
// execute search
let header = {
method: "GET",
headers: {"Ocp-Apim-Subscription-Key" : bingKey},
}
const params = new URLSearchParams({
q: searchTerms,
//imageType: "photo",
count: 150
})
const response = await fetch(`${MoulinetteImageSearch.SEARCH_BING_API}?${params}`, header).catch(function(e) {
console.log(`MoulinetteClient | Cannot establish connection to server ${MoulinetteImageSearch.SEARCH_BING_API}`, e)
});
if( !response || response.status != 200 ) {
console.error("MoulinetteImageSearch | Invalid response from Bing API", response)
return [];
}
let data = await response.json()
let results = []
data.value.forEach( r => results.push({
src: "Microsoft Bing",
name: r.name,
thumb: r.thumbnailUrl,
url: r.contentUrl,
page: r.hostPageUrl,
width: r.width,
height: r.height,
format: r.encodingFormat}));
return results
}
/**
/* Google Search implementation
*/
async searchGoogle(searchTerms, googleKey, googleCx) {
// execute search
const params = new URLSearchParams({
key: googleKey,
cx: googleCx,
q: searchTerms,
searchType: "image",
num: 10,
start: 1,
filter: 1
})
let results = []
// retrieve 30 results (= 5 requests)
for(let idx=0; idx<3; idx++) {
params.set("start", idx * 10 + 1)
const response = await fetch(`${MoulinetteImageSearch.SEARCH_GOOGLE_API}?${params}`).catch(function(e) {
console.log(`MoulinetteClient | Cannot establish connection to server ${MoulinetteImageSearch.SEARCH_GOOGLE_API}`, e)
});
if( !response || response.status != 200 ) {
console.error("MoulinetteImageSearch | Invalid response from Google API", response)
return [];
}
let data = await response.json()
data.items.forEach( r => results.push({
src: "Google Search",
name: r.title,
thumb: r.image.thumbnailLink,
url: r.link,
page: r.image.contextLink,
width: r.image.width,
height: r.image.height,
format: r.fileFormat.split("/").pop()}));
}
return results
}
/**
/* CreativeCommons Search implementation
*/
async searchCC(searchTerms) {
// execute search
let header = {
method: "GET"
}
const params = new URLSearchParams({
q: searchTerms,
page_size: 20
})
const response = await fetch(`${MoulinetteImageSearch.SEARCH_CC_API}/?format=json&${params}`, header).catch(function(e) {
console.log(`MoulinetteClient | Cannot establish connection to server ${MoulinetteImageSearch.SEARCH_CC_API}`, e)
});
if( !response || response.status != 200 ) {
console.error("MoulinetteImageSearch | Invalid response from CreativeCommons API", response)
return [];
}
let data = await response.json()
let results = []
data.results.forEach( r => {
const format = r.url.substring(r.url.lastIndexOf(".")+1)
results.push({
src: "Creative Commons",
name: r.title,
thumb: r.thumbnail,
url: r.url,
license: r.license,
licenseUrl: r.license_url,
page: r.foreign_landing_url,
noSize: true,
format: format })
});
return results
}
/**
* Implements getAssetList
*/
async getAssetList(searchTerms, packs, publisher) {
let assets = []
const isBing = publisher == "Microsoft Bing"
const isGoogle = publisher == "Google Search"
const isCC = publisher == "Creative Commons"
// error handling
const bingKey = game.settings.get("moulinette-imagesearch", "bing-key")
if( isBing && (!bingKey | bingKey.length == 0)) {
assets.push(`<div class="error">${game.i18n.localize("mtte.noBingKey")}</div>`)
return assets;
}
const googleKey = game.settings.get("moulinette-imagesearch", "google-key")
if(isGoogle && (!googleKey | googleKey.length == 0)) {
assets.push(`<div class="error">${game.i18n.localize("mtte.noGoogleKey")}</div>`)
return assets;
}
const googleEngineId = game.settings.get("moulinette-imagesearch", "google-engine-id")
if(isGoogle && (!googleEngineId | googleEngineId.length == 0)) {
assets.push(`<div class="error">${game.i18n.localize("mtte.noGoogleEngine")}</div>`)
return assets;
}
const openverseEnabled = game.settings.get("moulinette-imagesearch", "openverse-enabled")
if(isCC && !openverseEnabled) {
assets.push(`<div class="error">${game.i18n.localize("mtte.openverseDisabled")}</div>`)
return assets;
}
if(!searchTerms || searchTerms.length == 0) {
return []
}
console.log("Moulinette ImageSearch | Searching images... " + searchTerms)
this.searchResults = []
if((!publisher || isBing) && bingKey && bingKey.length > 0) {
this.searchResults.push(...await this.searchBing(searchTerms, bingKey))
}
if((!publisher || isGoogle) && googleKey && googleKey.length > 0 && googleEngineId && googleEngineId.length > 0) {
this.searchResults.push(...await this.searchGoogle(searchTerms, googleKey, googleEngineId))
}
if((!publisher || isCC) && openverseEnabled) {
this.searchResults.push(...await this.searchCC(searchTerms))
}
this.searchResults.sort((a,b) => 0.5 - Math.random())
let html = ""
let idx = 0;
this.searchResults.forEach( r => {
idx++
assets.push(`<div class="imageresult draggable" title="${r.name}" data-idx="${idx}"><img width="100" height="100" src="${r.thumb}"/></div>`)
})
assets.push(`<div class="text">Hello</div>`)
return assets
}
/**
* Footer: Dropmode
*/
async getFooter() {
return `<div id="footerTiles"></div>`
}
/**
* Updates the footer
*/
async updateFooter() {
// prepare the list of macros
const mode = game.settings.get("moulinette", "tileMode")
const macroCfg = game.settings.get("moulinette", "tileMacros")[mode] // should return a list of _ids
const compendium = game.settings.get("moulinette-tiles", "macroCompendium")
const macroIndex = compendium ? game.packs.get(compendium)?.index.values() : null
const macros = macroIndex ? Array.from(macroIndex).filter(m => macroCfg && macroCfg.includes(m._id)) : []
let macroText = "-"
if( macros.length == 1) {
macroText = macros[0].name
}
else if( macros.length > 1) {
macroText = game.i18n.format("mtte.multiplesMacros", { count: macros.length})
}
const html = await renderTemplate("modules/moulinette-tiles/templates/search-footer.hbs", {
tileSize: game.settings.get("moulinette", "tileSize"),
dropAsTile: mode == "tile",
dropAsArticle: mode == "article",
dropAsActor: mode == "actor",
macros: macroText
})
this.html.find("#footerTiles").html(html)
// dropmode listener
this.html.find(".dropMode").click(event => {
// callback function for appying the results
const parent = this
const callback = async function (mode) {
mode = ["tile","article","actor"].includes(mode) ? mode : "tile"
await game.settings.set("moulinette", "tileMode", mode)
await parent.updateFooter()
}
const dialog = new game.moulinette.applications.MoulinetteOptions("dropmode", callback, { width: 100, height: "auto" })
dialog.position.left = event.pageX - dialog.position.width/2
dialog.position.top = event.pageY - 60 // is auto
dialog.render(true)
})
// tilesize listener
this.html.find(".tileSize").click(event => {
// callback function for appying the results
const parent = this
const callback = async function (size) {
await game.settings.set("moulinette", "tileSize", Number(size))
await parent.updateFooter()
}
const dialog = new game.moulinette.applications.MoulinetteOptions("tilesize", callback, { width: 250, height: "auto" })
dialog.position.left = event.pageX - dialog.position.width/2
dialog.position.top = event.pageY - 100 // is auto
dialog.render(true)
})
// macros listener
this.html.find(".macros").click(event => {
// callback function for appying the results
const parent = this
const callback = async function (macros) {
if(macros) {
const config = game.settings.get("moulinette", "tileMacros")
config[mode] = macros
await game.settings.set("moulinette", "tileMacros", config)
await parent.updateFooter()
}
}
const dialog = new game.moulinette.applications.MoulinetteOptions("macros", callback, { width: 450, height: 400, macros: macros.map(m => m._id) })
dialog.position.left = event.pageX - dialog.position.width/2
dialog.position.top = event.pageY - 100 // is auto
dialog.render(true)
})
}
/**
* Implements listeners
*/
activateListeners(html) {
// keep html for later usage
this.html = html
// image clicked
this.html.find(".imageresult").click(this._onClickAction.bind(this))
// when choose mode
this.html.find(".options input").click(this._onChooseMode.bind(this))
// insert Footer
this.updateFooter()
}
async _onClickAction(event) {
event.preventDefault();
const source = event.currentTarget;
const idx = source.dataset.idx;
if(this.searchResults && idx > 0 && idx <= this.searchResults.length) {
new MoulinetteSearchResult(this.searchResults[idx-1]).render(true)
}
}
_onChooseMode(event) {
const source = event.currentTarget;
let mode = ["tile","article","actor"].includes(source.value) ? source.value : "tile"
game.settings.set("moulinette", "tileMode", mode)
}
/**
* @INFO Function is inspired from the one in submodule Tiles
*/
onDragStart(event) {
// module moulinette-tiles is required for supporting drag/drop
if(!game.moulinette.applications.MoulinetteDropAsActor) {
ui.notifications.error(game.i18n.localize("mtte.errorDragDropRequirements"));
event.preventDefault();
return;
}
const div = event.currentTarget;
const idx = div.dataset.idx;
const mode = game.settings.get("moulinette", "tileMode")
const size = game.settings.get("moulinette", "tileSize")
// invalid action
if(!this.searchResults || idx < 0 || idx > this.searchResults.length) return
const timestamp = new Date().getTime();
const image = this.searchResults[idx-1]
let imageFileName = image.name.replace(/[\W_]+/g,"-").replace(".","")
imageFileName = (imageFileName.length > 30 ? imageFileName.substring(0, 30) : imageFileName) + "-" + timestamp + "." + image.format
// create fake tile
const tile = {
filename: imageFileName,
type: "img",
sas: "",
search: image
}
let dragData = {}
if(mode == "tile") {
dragData = {
type: "Tile",
tile: tile,
pack: { publisher: image.src, name: "Results" },
tileSize: size
};
} else if(mode == "article") {
dragData = {
type: "JournalEntry",
tile: tile,
pack: { publisher: image.src, name: "Results" }
};
} else if(mode == "actor") {
dragData = {
type: "Actor",
tile: tile,
pack: { publisher: image.src, name: "Results" }
};
}
dragData.source = "mtte"
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
}
async onShortcut(type) {
if(type == "paste") {
let text = ""
if(navigator.clipboard) {
text = await navigator.clipboard.readText().catch(err => {
console.error('Moulinette ImageSearch | Failed to read clipboard contents: ', err);
});
}
const content = `<p>${game.i18n.localize("mtte.enterImageURLDescription")}</p>
<div class="form-group"><label><b>${game.i18n.localize("mtte.imageURL")}</b></label>
<label><input class="imageURL" type="text" name="imageURL" placeholder="https://..."></label></div><br/>`
if(!text || !text.startsWith("http")) {
text = await Dialog.prompt({
title: game.i18n.localize("mtte.enterImageURL"),
content: content,
label: game.i18n.localize("mtte.confirm"),
callback: html => {
return html.find(".imageURL").val()
}
});
}
const re = /(?:\.([^.]+))?$/; // regular expression to extract extension
let ext = re.exec(text.split("/").pop().split('?')[0])[1]
ext = ext ? ext : "jpg"
if(!["png","jpg","jpeg","webp","gif","svg","avif"].includes(ext.toLowerCase())) {
return console.error('Moulinette ImageSearch | Invalid image format from URL: ', text);
}
const dateAsString = new Date().toLocaleDateString("en-US", { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
const data = {
custom: true,
noSize: true,
name: game.i18n.format("mtte.fromClipboard", { date: dateAsString }),
page: text,
thumb: text,
url: text,
format: ext,
src: 'Clipboard',
}
new MoulinetteSearchResult(data).render(true)
}
}
async onAction(classList) {
// ACTION - HELP / HOWTO
if(classList.contains("howto")) {
new game.moulinette.applications.MoulinetteHelp("search").render(true)
}
}
}

View File

@@ -0,0 +1,113 @@
/*************************
* Search result
*************************/
export class MoulinetteSearchResult extends FormApplication {
constructor(data) {
super()
this.data = data;
const mode = game.settings.get("moulinette", "tileMode")
const timestamp = new Date().getTime();
const image = data
let imageFileName = image.name.replace(/[\W_]+/g,"-").replace(".","")
imageFileName = (imageFileName.length > 30 ? imageFileName.substring(0, 30) : imageFileName) + "-" + timestamp + "." + image.format
// create fake tile
this.tile = {
filename: imageFileName,
type: "img",
sas: "",
search: image
}
}
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
id: "moulinette-searchresult",
classes: ["mtte", "searchresult"],
title: game.i18n.localize("mtte.searchresult"),
template: "modules/moulinette-imagesearch/templates/searchresult.hbs",
width: 420,
height: "auto",
dragDrop: [{dragSelector: ".imageresult"}],
closeOnSubmit: true,
submitOnClose: false,
});
}
getData() {
let domain = (new URL(this.data.page));
this.data["domain"] = domain.hostname
return this.data
}
async _updateObject(event) {
event.preventDefault();
// Download asset
const data = { tile: this.tile }
const cTiles = await import("../../moulinette-tiles/modules/moulinette-tiles.js")
const folder = await cTiles.MoulinetteTiles.getOrCreateArticleFolder(this.data.src, "Results")
await cTiles.MoulinetteTiles.downloadAsset(data)
// copy into the clipboard
navigator.clipboard.writeText(data.img).then(() => {
ui.notifications.info(game.i18n.localize("mtte.codeCopiedClipboardSuccess"))
})
.catch(() => {
ui.notifications.warn(game.i18n.localize("mtte.codeCopiedClipboardFail"))
console.log("Path to image is: " + data.img)
});
// create article if requested
if(event.submitter.className == "createArticle") {
ui.journal.activate() // give focus to journal
const article = await game.moulinette.applications.Moulinette.generateArticle(this.data.name, data.img, folder._id)
article.sheet.render(true)
}
}
_onDragStart(event) {
// module moulinette-tiles is required for supporting drag/drop
if(!game.moulinette.applications.MoulinetteDropAsActor) {
ui.notifications.error(game.i18n.localize("mtte.errorDragDropRequirements"));
event.preventDefault();
return;
}
const mode = game.settings.get("moulinette", "tileMode")
let dragData = {}
if(mode == "tile") {
dragData = {
type: "Tile",
tile: this.tile,
pack: { publisher: this.data.src, name: "Results" },
tileSize: 100
};
} else if(mode == "article") {
dragData = {
type: "JournalEntry",
tile: this.tile,
pack: { publisher: this.data.src, name: "Results" }
};
} else if(mode == "actor") {
dragData = {
type: "Actor",
tile: this.tile,
pack: { publisher: this.data.src, name: "Results" }
};
}
dragData.source = "mtte"
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
}
activateListeners(html) {
super.activateListeners(html);
this.bringToTop()
html.find(".thumb").css('background', `url(${this.data.thumb}) 50% 50% no-repeat`)
}
}

View File

@@ -0,0 +1,14 @@
.mtte.forge .imageresult {
display: inline-flex;
cursor: pointer;
}
.mtte.forge .imageresult:hover {
opacity: 50%;
}
.mtte .imageresult img {
object-fit: contain;
border: none;
margin: 2px;
}

View File

@@ -0,0 +1,71 @@
Hooks.once("init", async function () {
console.log("Moulinette ImageSearch | Init")
game.settings.register("moulinette", "tileMode", { scope: "world", config: false, type: String, default: "tile" })
game.settings.register("moulinette-imagesearch", "openverse-enabled", {
name: game.i18n.localize("mtte.configOpenverseEnabled"),
hint: game.i18n.localize("mtte.configOpenverseEnabledHint"),
scope: "world",
config: true,
default: true,
type: Boolean
});
game.settings.register("moulinette-imagesearch", "bing-key", {
name: game.i18n.localize("mtte.configBingKey"),
hint: game.i18n.localize("mtte.configBingKeyHint"),
scope: "world",
config: true,
default: "",
type: String
});
game.settings.register("moulinette-imagesearch", "google-key", {
name: game.i18n.localize("mtte.configGoogleKey"),
hint: game.i18n.localize("mtte.configGoogleKeyHint"),
scope: "world",
config: true,
default: "",
type: String
});
game.settings.register("moulinette-imagesearch", "google-engine-id", {
name: game.i18n.localize("mtte.configGoogleEngineId"),
hint: game.i18n.localize("mtte.configGoogleEngineIdHint"),
scope: "world",
config: true,
default: "",
type: String
});
})
/**
* Ready: define new moulinette forge module
*/
Hooks.once("ready", async function () {
if (game.user.isGM) {
// create default home folder for image search
await game.moulinette.applications.MoulinetteFileUtil.createFolderRecursive("moulinette/images/search");
const moduleClass = (await import("./modules/moulinette-imagesearch.js")).MoulinetteImageSearch
game.moulinette.forge.push({
id: "imagesearch",
layer: "notes",
icon: "fas fa-search",
name: game.i18n.localize("mtte.imageSearch"),
description: game.i18n.localize("mtte.imageSearchDescription"),
instance: new moduleClass(),
actions: [
{id: "howto", icon: "fas fa-question-circle" ,name: game.i18n.localize("mtte.howto"), help: game.i18n.localize("mtte.howtoToolTip") }
],
shortcuts: [{
id: "paste",
name: game.i18n.localize("mtte.pasteURL"),
icon: "fas fa-clipboard"
}],
})
console.log("Moulinette ImageSearch | Module loaded")
}
});

View File

@@ -0,0 +1,24 @@
<form autocomplete="off" onsubmit="event.preventDefault();">
<h3>{{name}}</h3>
<div class="imageresult draggable"><img width="400" height="400" src="{{url}}"/></div>
<p>
<i>
{{#if custom}}{{localize "mtte.searchByCustom"}}{{/if}}
{{#unless custom}}{{localize "mtte.searchBy" }} {{src}}{{/unless}}
</i>
</p>
<p>
{{localize "mtte.imageFrom"}} <a href="{{page}}" target="_blank">{{domain}}</a>
</p>
{{#unless noSize}}
<p>
{{localize "mtte.imageSize"}} {{width}}x{{height}} px.
</p>
{{/unless}}
<footer>
<div class="actions">
<button class="download" title="{{localize "mtte.downloadImageToolTip"}}"><i class="fas fa-cloud-download-alt"></i> {{localize "mtte.downloadImage"}}</button>
<button class="createArticle" title="{{localize "mtte.createArticleToolTip"}}"><i class="fas fa-book-open"></i> {{localize "mtte.createArticle"}}</button>
</div>
</footer>
</form>