zischenstand
This commit is contained in:
105
src/modules/moulinette-imagesearch/CHANGELOG.md
Normal file
105
src/modules/moulinette-imagesearch/CHANGELOG.md
Normal 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
|
||||
21
src/modules/moulinette-imagesearch/LICENSE.txt
Normal file
21
src/modules/moulinette-imagesearch/LICENSE.txt
Normal 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.
|
||||
79
src/modules/moulinette-imagesearch/README.md
Normal file
79
src/modules/moulinette-imagesearch/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Moulinette Forge Image Search (Foundry VTT)
|
||||
|
||||
[](https://github.com/SvenWerlen/moulinette-imagesearch/releases)
|
||||
[](https://github.com/SvenWerlen/moulinette-imagesearch/blob/main/LICENSE.txt)
|
||||
[](#install)
|
||||

|
||||
[](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!
|
||||
|
||||

|
||||
<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à!
|
||||
|
||||

|
||||

|
||||
|
||||
## <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à!
|
||||
|
||||

|
||||
|
||||
## <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
|
||||
|
||||

|
||||
|
||||
## <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)
|
||||
37
src/modules/moulinette-imagesearch/lang/en.json
Normal file
37
src/modules/moulinette-imagesearch/lang/en.json
Normal 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"
|
||||
}
|
||||
37
src/modules/moulinette-imagesearch/lang/fr.json
Normal file
37
src/modules/moulinette-imagesearch/lang/fr.json
Normal 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"
|
||||
}
|
||||
25
src/modules/moulinette-imagesearch/lang/ja.json
Normal file
25
src/modules/moulinette-imagesearch/lang/ja.json
Normal 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": "この機能を使うとURL(Google画像等)のインポートができます。クリップボードから有効な画像が引き出せませんでした、改めて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": "検索結果"
|
||||
}
|
||||
63
src/modules/moulinette-imagesearch/module.json
Normal file
63
src/modules/moulinette-imagesearch/module.json
Normal 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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
71
src/modules/moulinette-imagesearch/moulinette-imagesearch.js
Normal file
71
src/modules/moulinette-imagesearch/moulinette-imagesearch.js
Normal 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")
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user