feat: add darktable web API interface with gallery, image detail, and settings screens
- Implemented main entry point for React application in `main.jsx`. - Created `Gallery` component for displaying images with infinite scroll and pull-to-refresh functionality. - Developed `ImageDetail` component for showing detailed information and actions for selected images. - Added `Settings` component for configuring API connection settings. - Integrated Vite for build and development with PWA support. - Implemented caching strategies for API responses and image previews using Workbox.
This commit is contained in:
@@ -30,6 +30,56 @@ local config = require "config"
|
||||
local router = require "router"
|
||||
local api = require "api"
|
||||
|
||||
-- ── Static file server (PWA) ──────────────────────────────────────────────────
|
||||
|
||||
local WEB_BUILD = script_dir .. "/web/build"
|
||||
|
||||
local MIME = {
|
||||
html = "text/html; charset=utf-8",
|
||||
js = "application/javascript; charset=utf-8",
|
||||
mjs = "application/javascript; charset=utf-8",
|
||||
css = "text/css; charset=utf-8",
|
||||
json = "application/json",
|
||||
webmanifest = "application/manifest+json",
|
||||
png = "image/png",
|
||||
jpg = "image/jpeg",
|
||||
jpeg = "image/jpeg",
|
||||
svg = "image/svg+xml",
|
||||
ico = "image/x-icon",
|
||||
woff = "font/woff",
|
||||
woff2 = "font/woff2",
|
||||
txt = "text/plain; charset=utf-8",
|
||||
map = "application/json",
|
||||
}
|
||||
|
||||
-- Read a file from the web build directory. Returns data, content-type or nil.
|
||||
-- Prevents directory traversal by rejecting any path containing "..".
|
||||
local function read_static(req_path)
|
||||
if req_path:find("%.%.", 1, true) then return nil end
|
||||
|
||||
-- Strip leading slash; normalise double-slashes.
|
||||
local rel = req_path:gsub("^/+", ""):gsub("//+", "/")
|
||||
local full = WEB_BUILD .. "/" .. rel
|
||||
|
||||
local f = io.open(full, "rb")
|
||||
if not f then return nil end
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
|
||||
local ext = rel:match("%.([^%.]+)$") or ""
|
||||
local ct = MIME[ext:lower()] or "application/octet-stream"
|
||||
return data, ct
|
||||
end
|
||||
|
||||
-- Cache headers: hashed assets get long TTL; everything else no-store.
|
||||
local function cache_header(path)
|
||||
-- Vite fingerprints assets with a hash in the filename (e.g. index-Bx3Rk.js)
|
||||
if path:match("/assets/") then
|
||||
return "public, max-age=31536000, immutable"
|
||||
end
|
||||
return "no-store"
|
||||
end
|
||||
|
||||
-- ── Register routes ───────────────────────────────────────────────────────────
|
||||
|
||||
router.post("/oauth/token", api.oauth_token)
|
||||
@@ -164,16 +214,32 @@ local function handle_client(client)
|
||||
return
|
||||
end
|
||||
|
||||
-- API / OAuth routes take priority over static files.
|
||||
local handler, path_params = router.match(req.method, req.path)
|
||||
if not handler then
|
||||
write_response(client, 404, json.encode({ error = "not_found", path = req.path }))
|
||||
if handler then
|
||||
req.path_params = path_params or {}
|
||||
local status, body, ct, extra = handler(req)
|
||||
write_response(client, status, body, ct, extra)
|
||||
return
|
||||
end
|
||||
|
||||
req.path_params = path_params or {}
|
||||
-- Static file serving for the PWA (GET/HEAD only).
|
||||
if req.method == "GET" or req.method == "HEAD" then
|
||||
local data, ct = read_static(req.path)
|
||||
|
||||
local status, body, ct, extra = handler(req)
|
||||
write_response(client, status, body, ct, extra)
|
||||
if not data then
|
||||
-- SPA fallback: any unknown path gets index.html so client-side routing works.
|
||||
data, ct = read_static("/index.html")
|
||||
end
|
||||
|
||||
if data then
|
||||
local extra = { ["Cache-Control"] = cache_header(req.path) }
|
||||
write_response(client, 200, req.method == "HEAD" and "" or data, ct, extra)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
write_response(client, 404, json.encode({ error = "not_found", path = req.path }))
|
||||
end
|
||||
|
||||
-- ── Main server loop ──────────────────────────────────────────────────────────
|
||||
@@ -192,8 +258,9 @@ server:settimeout(0) -- non-blocking accept
|
||||
io.write(string.format(
|
||||
"darktable-api: listening on http://%s:%d\n" ..
|
||||
" library : %s\n" ..
|
||||
" cache : %s\n",
|
||||
host, port, config.library_db, config.cache_dir))
|
||||
" cache : %s\n" ..
|
||||
" web app : http://%s:%d/\n",
|
||||
host, port, config.library_db, config.cache_dir, host, port))
|
||||
io.flush()
|
||||
|
||||
while true do
|
||||
|
||||
Reference in New Issue
Block a user