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