diff --git a/README.md b/README.md index b047839..4fecaad 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ Key settings: Maximum directory depth for `.pagelink` discovery under each configured root. `0` means only the root directory itself is checked. +- **omdb_apikey** + OMDb API key used for the movie import toolbar button. The key is sent to the + browser for client-side API requests and will be visible in developer tools. + ### Template style settings The `{{open>...}}` links and directory “open” links use a dedicated color @@ -254,7 +258,43 @@ Supported input examples include: - `2026-01-30 13:45` - `2026-01-30T13:45:00` -### 0.2) Page Link: link a page to a folder +### 0.2) Editor toolbar: Movie Import + +The plugin adds a toolbar button for importing movie metadata from the [OMDb API](https://www.omdbapi.com/). + +**Setup:** +1. Obtain an OMDb API key from https://www.omdbapi.com/apikey.aspx +2. Enter the key in **Admin → luxtools** under "Movie Import (OMDb)". + +**Usage:** +1. Open a page for editing. +2. Click the movie icon in the toolbar. +3. A prompt appears pre-filled with the first heading from the page (e.g. `Project Hail Mary (2026)`). +4. Edit the title if needed and confirm. +5. The plugin queries OMDb and inserts a movie metadata block into the editor. + +**Inserted markup example:** +``` + +{{image>https://...poster.jpg}} +^ Title | Project Hail Mary | +^ Year | 2026 | +^ Genre | Adventure, Drama, Sci-Fi | +^ Director | Phil Lord, Christopher Miller | +^ Actors | Ryan Gosling | +^ Plot | A lone astronaut must save Earth... | + +``` + +**Re-import behavior:** Running the import again replaces the existing movie block +(delimited by `` / `` markers) instead of +appending a duplicate. + +**Security note:** The OMDb API key is passed to the browser and used for +client-side requests. It is visible in browser developer tools and network +traffic. This is an intentional tradeoff for this single-user LAN deployment. + +### 0.3) Page Link: link a page to a folder Page linking uses a page-scoped UUID stored in page metadata. This UUID is used to link the page to a folder that contains a `.pagelink` file with the same UUID. @@ -282,7 +322,7 @@ for example: {{directory>blobs/&recursive=1}} ``` -### 0.3) Calendar widget +### 0.4) Calendar widget Render a basic monthly calendar that links each day to canonical chronological pages: @@ -311,7 +351,7 @@ Notes: - Indicator placement in small mode is configured per slot via the `Display` setting. - Slot colors are reused for both indicators and inline event accents. -### 0.4) Virtual chronological day pages +### 0.5) Virtual chronological day pages When a canonical day page (for example `chronological:2026:02:13`) does not yet exist, luxtools renders a virtual page in normal show mode instead of the @@ -326,7 +366,7 @@ The virtual page includes: The page is only created once you edit and save actual content. -### 0.5) Cache invalidation +### 0.6) Cache invalidation luxtools provides an admin-only **Invalidate Cache** action in the page tools menu. @@ -338,7 +378,7 @@ luxtools provides an admin-only **Invalidate Cache** action in the page tools me permission errors). - Also useful when actively adding external photos to the current day page. -### 0.6) Multi-calendar slot system +### 0.7) Multi-calendar slot system The plugin supports 4 calendar slots, each with independent configuration for a local `.ics` file, CalDAV URL, authentication, and display color. @@ -352,7 +392,7 @@ a local `.ics` file, CalDAV URL, authentication, and display color. Calendar data is always read from local `.ics` files for rendering. If a remote CalDAV source is configured, use the sync feature to populate the local file. -### 0.7) Maintenance task completion +### 0.8) Maintenance task completion Maintenance tasks shown on day pages include a "Complete" button. Clicking it: @@ -368,7 +408,7 @@ Write-back rules: - Recurring events: Completion writes an occurrence override/exception to preserve per-occurrence state rather than modifying the master event. -### 0.8) Event popup +### 0.9) Event popup Clicking any event on a day page opens a popup overlay showing: - Title @@ -379,7 +419,7 @@ Clicking any event on a day page opens a popup overlay showing: Close the popup by clicking outside it or pressing Escape. -### 0.9) Maintenance task list syntax +### 0.10) Maintenance task list syntax Embed a list of open maintenance tasks anywhere on a wiki page: @@ -401,7 +441,7 @@ window are hidden. The default is `30`. Each task shows its date, optional time, summary, and a "Complete" button. -### 0.10) CalDAV sync +### 0.11) CalDAV sync If a slot has a CalDAV URL configured, the admin panel provides a sync button. Triggering sync downloads all calendar objects from the remote CalDAV collection diff --git a/action.php b/action.php index 63782ae..92a20c6 100644 --- a/action.php +++ b/action.php @@ -122,6 +122,7 @@ class action_plugin_luxtools extends ActionPlugin "linkfavicon.js", "calendar-widget.js", "event-popup.js", + "movie-import.js", "main.js", ]; @@ -131,6 +132,11 @@ class action_plugin_luxtools extends ActionPlugin "src" => $base . $script, ]; } + + // Pass OMDb API key to client-side JavaScript. + // Intentional: the key is exposed to the browser for direct OMDb lookups. + global $JSINFO; + $JSINFO['luxtools_omdb_apikey'] = (string)$this->getConf('omdb_apikey'); } /** @@ -909,6 +915,14 @@ class action_plugin_luxtools extends ActionPlugin "icon" => "../../plugins/luxtools/images/date-fix-all.svg", "block" => false, ]; + + // Movie Import: fetch movie metadata from OMDb + $event->data[] = [ + "type" => "LuxtoolsMovieImport", + "title" => $this->getLang("toolbar_movie_title"), + "icon" => "../../plugins/luxtools/images/movie.svg", + "block" => false, + ]; } /** diff --git a/admin/main.php b/admin/main.php index 5b8a0a3..85560fd 100644 --- a/admin/main.php +++ b/admin/main.php @@ -57,6 +57,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin 'calendar_slot4_color', 'calendar_slot4_display', 'pagelink_search_depth', + 'omdb_apikey', ]; public function getMenuText($language) @@ -130,6 +131,8 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin if ($depth < 0) $depth = 0; $newConf['pagelink_search_depth'] = $depth; + $newConf['omdb_apikey'] = trim($INPUT->str('omdb_apikey')); + if ($this->savePluginLocalConf($newConf)) { msg($this->getLang('saved'), 1); } else { @@ -360,6 +363,13 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo ''; echo '
'; + // OMDb API key + echo '

' . hsc($this->getLang('omdb_heading')) . '

'; + echo '
'; + echo '

' . hsc($this->getLang('omdb_apikey_note')) . '

'; + echo ''; echo ''; diff --git a/conf/default.php b/conf/default.php index 600aa32..8dce750 100644 --- a/conf/default.php +++ b/conf/default.php @@ -70,6 +70,9 @@ $conf['calendar_slot4_display'] = 'none'; // Maximum depth when searching for .pagelink files under allowed roots. $conf['pagelink_search_depth'] = 3; +// OMDb API key for movie metadata import (used client-side). +$conf['omdb_apikey'] = ''; + // Image syntax defaults $conf['default_image_width'] = 250; $conf['default_image_align'] = 'right'; // left|right|center diff --git a/images/movie.svg b/images/movie.svg new file mode 100644 index 0000000..205f1e7 --- /dev/null +++ b/images/movie.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/js/movie-import.js b/js/movie-import.js new file mode 100644 index 0000000..ee94ac3 --- /dev/null +++ b/js/movie-import.js @@ -0,0 +1,205 @@ +/* global window, jQuery */ + +/** + * Movie Import toolbar button for luxtools. + * + * Fetches movie metadata from OMDb (client-side) and inserts/replaces + * a wiki markup block in the editor. + * + * The OMDb API key is intentionally passed to the browser via JSINFO. + * It will be visible in browser developer tools and network requests. + * This is an accepted tradeoff for this single-user LAN deployment. + */ +(function () { + 'use strict'; + + var MARKER_BEGIN = ''; + var MARKER_END = ''; + + /** + * Get the OMDb API key from JSINFO (set by PHP in action.php). + */ + function getApiKey() { + if (window.JSINFO && window.JSINFO.luxtools_omdb_apikey) { + return String(window.JSINFO.luxtools_omdb_apikey); + } + return ''; + } + + /** + * Get localized string from DokuWiki's LANG object. + */ + function lang(key, fallback) { + if (window.LANG && window.LANG.plugins && window.LANG.plugins.luxtools && + window.LANG.plugins.luxtools[key]) { + return String(window.LANG.plugins.luxtools[key]); + } + return fallback || key; + } + + /** + * Extract the first heading from the editor text. + * DokuWiki headings use ====== Heading ====== syntax. + * Returns the heading text trimmed, or empty string. + */ + function extractFirstHeading(text) { + var match = text.match(/^={2,6}\s*(.+?)\s*={2,6}\s*$/m); + if (match && match[1]) { + return match[1].trim(); + } + return ''; + } + + /** + * Parse a title string that may contain a trailing year in parentheses. + * e.g. "Project Hail Mary (2026)" -> { title: "Project Hail Mary", year: "2026" } + * e.g. "Inception" -> { title: "Inception", year: null } + */ + function parseTitleYear(raw) { + var match = raw.match(/^(.+?)\s*\((\d{4})\)\s*$/); + if (match) { + return { title: match[1].trim(), year: match[2] }; + } + return { title: raw.trim(), year: null }; + } + + /** + * Fetch movie data from OMDb. + * Returns a Promise that resolves with the parsed JSON response. + */ + function fetchMovie(apiKey, title, year) { + var url = 'https://www.omdbapi.com/?apikey=' + encodeURIComponent(apiKey) + + '&type=movie&t=' + encodeURIComponent(title); + if (year) { + url += '&y=' + encodeURIComponent(year); + } + + return jQuery.ajax({ + url: url, + dataType: 'json', + timeout: 10000 + }); + } + + /** + * Build the wiki markup block for a movie. + */ + function buildMarkup(movie) { + var lines = []; + lines.push(MARKER_BEGIN); + + // Poster image (omit if N/A or empty) + var poster = movie.Poster || ''; + if (poster && poster !== 'N/A') { + lines.push('{{image>' + poster + '}}'); + } + + // Data table + lines.push('^ Title | ' + safe(movie.Title) + ' |'); + lines.push('^ Year | ' + safe(movie.Year) + ' |'); + lines.push('^ Genre | ' + safe(movie.Genre) + ' |'); + lines.push('^ Director | ' + safe(movie.Director) + ' |'); + lines.push('^ Actors | ' + safe(movie.Actors) + ' |'); + lines.push('^ Plot | ' + safe(movie.Plot) + ' |'); + + lines.push(MARKER_END); + return lines.join('\n'); + } + + /** + * Return value for table cell, replacing N/A with empty string. + */ + function safe(val) { + if (!val || val === 'N/A') return ''; + return String(val); + } + + /** + * Get the editor textarea element by id. + */ + function getEditor(edid) { + var id = edid || 'wiki__text'; + return document.getElementById(id); + } + + /** + * Main import action: prompt for title, fetch from OMDb, insert/replace markup. + */ + function importMovie(edid) { + var apiKey = getApiKey(); + if (!apiKey) { + alert(lang('movie_error_no_apikey', 'OMDb API key is not configured. Set it in Admin → luxtools.')); + return; + } + + var editor = getEditor(edid); + if (!editor) return; + + var text = editor.value || ''; + var heading = extractFirstHeading(text); + var defaultTitle = heading || ''; + + var input = prompt(lang('movie_prompt', 'Enter movie title (optionally with year):'), defaultTitle); + if (input === null || input.trim() === '') return; + + var parsed = parseTitleYear(input.trim()); + + fetchMovie(apiKey, parsed.title, parsed.year) + .done(function (data) { + if (!data || data.Response === 'False') { + var errMsg = (data && data.Error) ? data.Error : lang('movie_error_not_found', 'Movie not found.'); + alert(errMsg); + return; + } + + var markup = buildMarkup(data); + insertOrReplace(editor, markup); + }) + .fail(function () { + alert(lang('movie_error_fetch', 'OMDb lookup failed.')); + }); + } + + /** + * Insert movie markup into the editor, replacing an existing movie block if present. + */ + function insertOrReplace(editor, markup) { + var text = editor.value || ''; + + var beginIdx = text.indexOf(MARKER_BEGIN); + var endIdx = text.indexOf(MARKER_END); + + if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) { + // Replace existing block + var before = text.substring(0, beginIdx); + var after = text.substring(endIdx + MARKER_END.length); + editor.value = before + markup + after; + } else { + // Append after the first heading line, or at the end + var headingMatch = text.match(/^(={2,6}\s*.+?\s*={2,6}\s*)$/m); + if (headingMatch) { + var headingEnd = text.indexOf(headingMatch[0]) + headingMatch[0].length; + editor.value = text.substring(0, headingEnd) + '\n\n' + markup + text.substring(headingEnd); + } else { + editor.value = text + '\n\n' + markup; + } + } + + // Trigger input event so DokuWiki knows the content changed + try { + editor.dispatchEvent(new Event('input', { bubbles: true })); + } catch (e) { + // IE fallback + } + } + + // Register toolbar button action handler. + // DokuWiki toolbar looks for window.addBtnAction for custom button types. + window.addBtnActionLuxtoolsMovieImport = function ($btn, props, edid) { + $btn.on('click', function () { + importMovie(edid); + return false; + }); + return 'luxtools-movie-import'; + }; +})(); diff --git a/lang/de/lang.php b/lang/de/lang.php index c241ec4..9d52fc7 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -122,3 +122,12 @@ $lang["cache_purge_cancel"] = "Abbrechen"; $lang["cache_purge_confirm"] = "Cache leeren"; $lang["cache_purge_pagelinks_success"] = "Seitenlink-Cache geleert."; $lang["cache_purge_thumbs_success"] = "Vorschaubild-Cache geleert."; + +$lang["toolbar_movie_title"] = "Film-Import"; +$lang["movie_prompt"] = "Filmtitel eingeben (optional mit Jahr):"; +$lang["movie_error_no_apikey"] = "OMDb-API-Schlüssel nicht konfiguriert. Unter Admin → luxtools einstellen."; +$lang["movie_error_not_found"] = "Film nicht gefunden."; +$lang["movie_error_fetch"] = "OMDb-Abfrage fehlgeschlagen."; +$lang["omdb_heading"] = "Film-Import (OMDb)"; +$lang["omdb_apikey"] = "OMDb-API-Schlüssel"; +$lang["omdb_apikey_note"] = "Der API-Schlüssel wird an den Browser übergeben für clientseitige OMDb-Abfragen. Er ist in den Browser-Entwicklertools und Netzwerkanfragen sichtbar."; diff --git a/lang/de/settings.php b/lang/de/settings.php index d5b1d13..520218d 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -28,3 +28,5 @@ $lang["gallery_thumb_scale"] = "Skalierungsfaktor fuer Galerie-Thumbnails. 2 erz $lang["open_service_url"] = "URL des lokalen Client-Dienstes fuer {{open>...}} (z.B. http://127.0.0.1:8765)."; $lang["pagelink_search_depth"] = "Maximale Verzeichnisebene fuer .pagelink-Suche (0 = nur Root)."; + +$lang["omdb_apikey"] = "OMDb-API-Schlüssel für den Film-Import. Wird clientseitig im Browser verwendet."; diff --git a/lang/en/lang.php b/lang/en/lang.php index e53c896..25b4da4 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -123,3 +123,12 @@ $lang["cache_purge_cancel"] = "Cancel"; $lang["cache_purge_confirm"] = "Purge Cache"; $lang["cache_purge_pagelinks_success"] = "Pagelinks cache purged."; $lang["cache_purge_thumbs_success"] = "Thumbnail cache purged."; + +$lang["toolbar_movie_title"] = "Movie Import"; +$lang["movie_prompt"] = "Enter movie title (optionally with year):"; +$lang["movie_error_no_apikey"] = "OMDb API key is not configured. Set it in Admin → luxtools."; +$lang["movie_error_not_found"] = "Movie not found."; +$lang["movie_error_fetch"] = "OMDb lookup failed."; +$lang["omdb_heading"] = "Movie Import (OMDb)"; +$lang["omdb_apikey"] = "OMDb API key"; +$lang["omdb_apikey_note"] = "The API key is passed to the browser for client-side OMDb lookups. It will be visible in browser developer tools and network requests."; diff --git a/lang/en/settings.php b/lang/en/settings.php index cdc04a0..5ffd094 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -28,3 +28,5 @@ $lang['gallery_thumb_scale'] = 'Gallery thumbnail scale factor. Use 2 for sharpe $lang['open_service_url'] = 'Local client service URL for the {{open>...}} link (e.g. http://127.0.0.1:8765).'; $lang['pagelink_search_depth'] = 'Maximum directory depth for .pagelink search (0 = only root).'; + +$lang['omdb_apikey'] = 'OMDb API key for movie metadata import. Used client-side in the browser.';