diff --git a/SHA256SUMS b/SHA256SUMS index 312c6a6..76b52fc 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -32,22 +32,22 @@ b48f3ab6f28404bb34c188067a832384f4ba290e56912461ef3565528aa157d8 docs/PUBLICATI 5eb19e7e4ed1ceddbdf388fd55697e40f1b7ecd9a4fbdf8ab485132cc684dcb2 docs/REAL_DEVICE_TEST.md bfc38b889fa966031e195c3131db1d4a713e0dc35c644e3047c7ed94f2354700 docs/RESEARCH.md fac36f986e1a7a331d3669aa8e2013e6c0ea7a99c16a61ba3ff573e836aa87e1 docs/SYNOLOGY_PHOTOS_API.md -00602929b1a4a2ab5f0e1b5746e2bede71e5568e2c423e11bf44ec9397c8679f docs/WATCH-THUMBNAIL.md +36695835826ce75d416353938eae1c46224f3a34aa57b80b2694443604c0c1a0 docs/WATCH-THUMBNAIL.md f8ef7749c58d4396781bbc21371048f6561e791efbd489570224dc51173bffff docs/brand/README.md f709156217c798f6b6230dae1f341373e0814a67ab90e881401da577f52ec3d3 docs/brand/pigway-avatar.png da6734d8a23305f6254c0936dadff4ec34977b4c934f5067186423c8cfce3fc7 docs/brand/pigway-logo.png 0fa494a3fbd456c754bbea3f7f050be139e32e3b42b09a1ed953094087437210 docs/brand/pigway-logo.svg ac0481196e593ff9cca2e5133ff41fb3614268e6a10728a0deca3ad3bfb80b34 docs/integrations/TICKTAR.md -d81f11dda188117b5dbf84983ca54309f6d09cd81d9a342c446bb46b28241e8e docs/openapi.json +f0ffb49c9832dad780bb9e7917690645076e02f2f89b7aa348bab3a5d792b284 docs/openapi.json 54bf7061e0626900ce35e19e81efc5c32c8fd6a0438bdad68f494e1ce81616d4 docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md a07cbe71571c6e2d5a1b94897bd63321c74675c05d15438c4964eef39db0fd76 docs/research/KOKONNA_CONTENT_SUBSCRIPTIONS.md ec53d587b855be84283c50f8fb45be62a4c1caba78b28d436a8260fd171943a2 docs/research/KOKONNA_LOCAL_CONTROL.md 2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 docs/research/KOKONNA_WIDGET_CATALOG.json e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/api/__init__.py -5df5fa4e4baeaac6a68e5007cf1097131d93ec9659f15e65c239df099eddc8df pigway_frame_flow/api/app.py +2b68a29bc4927700c49de0693d5647134671494a3d54af76df6c0e8f06a9e377 pigway_frame_flow/api/app.py 1efe9b1fc774a97e5be72d81199b1b65b25c258956fdf166aebff732cc820d7c pigway_frame_flow/api/history.py -512fbf679913d29ad8bfbed496faa9a29d0e2385501a81c0ee4a54659b8f270f pigway_frame_flow/api/thumbnails.py +3dda509a34daca61836913976dba35e59c2135c48bf04bc7617e923cc5cffdc1 pigway_frame_flow/api/thumbnails.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/core/__init__.py 0b8402603c2f397f4f42491873b27bf363959194b05a0c67c7b1d49596288b95 pigway_frame_flow/core/accounts.py 818e8fb29d22c13159a6cb22bcbc6b9e211a8a1301588126eb192ac4ed71441b pigway_frame_flow/core/auth.py @@ -58,9 +58,10 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_f 5e7e272e671a6d01d9b677416c77967ca488b4e2aa559f4211cf0de4ba1e88bc pigway_frame_flow/display/recovery.py e1e4ae3ba321ae7282c5a4e061c52f24d2a6a6e3d4637e69e4b1fbd05004f5f6 pigway_frame_flow/display/retention.py a00adacca6b5bbc18c4d15dec6ff3c7bba018f8f8575c69205526228032c39b1 pigway_frame_flow/display/service.py +de6b53b2911837af5e8e4ee3065ae513e4523436ddabde4a11503e432c3c1351 pigway_frame_flow/display/thumbnail_gallery.py 18fc72b7f726c85d8b11bed78d88403c7867b7d71d2580d47afad6fa0242edc2 pigway_frame_flow/display/thumbnail_plans.py 0975b24488be19f24e768c4c76d9f3c0348417c1351c4e410897da00f7be6656 pigway_frame_flow/display/upload_reconciliation.py -9de31405afb22dd5e26ec0cfe023a33c4d5c65b8326c26a8df3378764056023a pigway_frame_flow/display/wrist_thumbnail.py +a521e5d107f456e007050de65cd25e7ed3f269c27360e2bda6f768faa9b28841 pigway_frame_flow/display/wrist_thumbnail.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/drivers/__init__.py 9a8386ef368a6df7fd8cb23bcc442c28e784729a5ce4a78911ad86245d5c5794 pigway_frame_flow/drivers/base.py 54cff8782e21e2ee4b8540259a4868380c58a0aa656c9d23796a049ab96a1b4f pigway_frame_flow/drivers/generic_http.py @@ -126,7 +127,7 @@ f9e777980ea5bb3ea8fb2357f23530802811ea2674d25100af9b728cbc9f83a9 pigway_frame_f 8f37296f52a88c11a000a5a0767310354550da34e1d4c0c7260393ec00cd0d9b pigway_frame_flow/templates/quote.json 82c17f4e7eaac95483592add181f549186dea9d7c0375fa9cad3554e3ccd1794 pigway_frame_flow/templates/system-alert.json d6f705b9d183b57d50759e283db2161f8493c4e3969459e74da635252014c925 pigway_frame_flow/templates/weather.json -62e898e118823965f1ab90e8386a604512a028b1b9e8bf41a6e82062ac1e99e0 pigway_frame_flow/web/app.js +4c97eed75273e9b7579c023dc45177c794fbc4dfcce4fd8308c0849fe00730f6 pigway_frame_flow/web/app.js f332fac63f611572597812a06ae0cc266a59e5677efda0d6e9b6925acf9f9309 pigway_frame_flow/web/apple-touch-icon.png d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_flow/web/extensions.js 94558dbf524a1068e83283eb814c1b1501512d4b7e75e092b52e9aabfea720e7 pigway_frame_flow/web/favicon.ico @@ -139,7 +140,7 @@ f756a8a103cf06abdbd7c2ed6e50da49b0de166b1d7bb98a9b798bc892d07287 pigway_frame_f cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 pigway_frame_flow/web/swagger-LICENSE 050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 pigway_frame_flow/web/swagger-ui-bundle.js 1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 pigway_frame_flow/web/swagger-ui.css -af2d71e493dab0ecc2c0ebf93abde752344a1e54f63dc7b3908bee534b507d5a pigway_frame_flow/web/thumbnails.js +8bfe4c25024860cda7b83d385be9197a1b78ac4fd29ff27954f1f52d1097e346 pigway_frame_flow/web/thumbnails.js 5f392f94636d261ae2982815763b794ae9595a1b9226946b792105634ec3f559 pyproject.toml d34ac82e4c3feacd38546aa2f0a4491ab17ddbf3ad8f489375783166d78923f0 requirements.lock 008af5f4adf616c86e805febc016faad60be0008abf5dddad6e955f1d03606cf scripts/build-verify.sh @@ -184,11 +185,11 @@ d390c1c9a76ddc167a17bfd7afb0bd1a7169809c8a0b30a26a5be4d18c3032de scripts/ui-pro e474740bdb7dbe0617f7c6b170ada918e51f5eed8edbb9c2c47c4590a5dc2359 scripts/verify-photos-api-container.py b7816947e33a77997a4dd7d595ce385081fc578b2821aedb859277c3b490bf07 scripts/verify-photos-api.py d4f04e4c8f51c56aa21389d60115ecd1dda384da444dfd4d40237314fb51e3ea tests/conftest.py -5108885a53feb7119c9b1933432744ef794b0146febef9a33f021e7655198aaa tests/contract.json +c583ae686b2988a31067d9a3af348843a93f04b644e8b01ef6203fd752595383 tests/contract.json cf57d406e1743df4697d5f8341ce3470a963880f7a7d17a5e375eafff625fd65 tests/schema-contract.json 23ffc7413f8388648c9d52d3c8f56e2966b73ca1bdc5e209e6c15469c790edba tests/test_accounts.py e87ce325c8260f96e586b20697ab0bac71058918ba73f7412eb482364f83f73e tests/test_ai_images.py -a6edb781f4d081b2db45ffe2b02eed52a10f9b87a70e1e201b5d2f7c6813b93b tests/test_api.py +d5bf4f0463a9e369557adf2f440ad401815cb8ffea84af49d106e916eee16501 tests/test_api.py 24fca799b0f5e33c800c6310764b18f6b39fc210be769684171388cf14b95746 tests/test_audit_regressions.py 7001a2209473e6f6bf1050d33212985f63f4844defeb5df8e75d9f2a96c0d83c tests/test_auto_collage.py f267ebcdb3cf1fcb01ef9fd9ef7c5c8c4daca11d02543f84bfbe56359f35b78a tests/test_ci_audit.py @@ -232,7 +233,7 @@ ab2cd30163453fe571b55c30cb26dc047a104a536ec01d2c64aa2bc91996ba11 tests/test_sch a7ae1b4a64bfcbcf2b7ff795b30ba91db24ee2ad7544073dca2bf43d021cd266 tests/test_smoke_dependencies.py 2624fb6ca01f32d93499d7d771144dcaa2850e5f77b2ac3b7b566891b5e9d283 tests/test_synology_photos.py ca98469f6a6b8b49c36a00657ea1a81775c3496e1fbdd5e38eb4d5298b5936b4 tests/test_synology_photos_api.py -e623f7c5796586e4ad47ebd38a49f8d03c08d63a46c539fcb3bd16b2182ac0b4 tests/test_thumbnail_plans.py +a83390e9f580bc27a2928eb6f94d7377307ec46ab45ef71bfea1e5378cb7adb9 tests/test_thumbnail_plans.py 33f05bc6279cf3573493335ae5bb1c91130754b927b52aa5f50e47f2d951d2ae tests/test_upload_reconciliation.py bdc3142f9c2291a91a5091b4b98b1addf9569cfb141810024ce6f85309704b9b tests/test_upload_retries.py 8ec55ce5706d791a155f13445fe7e0828278e8ee1fa1b527591fdf24cc5b2e86 tests/test_weather.py diff --git a/docs/WATCH-THUMBNAIL.md b/docs/WATCH-THUMBNAIL.md index 4a058a9..8d688bb 100644 --- a/docs/WATCH-THUMBNAIL.md +++ b/docs/WATCH-THUMBNAIL.md @@ -13,3 +13,9 @@ 方案接口为 `/api/v1/thumbnail-plans` 和 `/{plan_id}`;预览及生成分别使用 `/api/v1/thumbnails/preview` 与 `/generate`。读取和预览需要 `images:read`,写入需要 `admin`。参数由服务端校验,任务提交也校验方案存在性与来源类型。 上传失败不更新小图。辅助小图生成失败保留已有记录并记录日志,不重放已成功的画框上传。最新小图受现有本地清理策略保护,较早小图沿用 cache 清理策略。本阶段未接入 BLE、手表端下载或实机显示验证。 + +## 手表小图图库 + +图库新增“手表 · 小图图库”标签,顶部设备选择器也提供“手表 · 410×502”,选择后进入该图库。手表是独立逻辑设备,传输状态明确为尚未接入,不会把小图发送到现有画框。图库支持分页、预览、下载、删除,并展示尺寸、生成方案和时间。“小图方案”页生成后可直接跳转查看。 + +手动生成与任务生成的小图都按专用元数据归入该图库,从普通本地图库中分离;图像资源继续使用原有鉴权和删除接口。删除最新小图时同时清除对应的最新记录,不影响源照片。历史自动小图按操作回执或最新记录恢复分类,不会根据文件名、尺寸猜测普通图片。旧版手动生成的小图缺少可靠分类信息,仍在原本地图库。 diff --git a/docs/openapi.json b/docs/openapi.json index 343cd1b..0ee9375 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -15844,6 +15844,299 @@ } ] } + }, + "/api/v1/thumbnail-devices": { + "get": { + "summary": "Devices", + "operationId": "devices_api_v1_thumbnail_devices_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "413": { + "description": "Content Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad Gateway", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "AccountSession": [] + } + ] + } + }, + "/api/v1/thumbnails": { + "get": { + "summary": "Gallery", + "operationId": "gallery_api_v1_thumbnails_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "AccountSession": [] + } + ], + "parameters": [ + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Limit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Content Too Large" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unprocessable Content" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Too Many Requests" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Server Error" + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Gateway" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Service Unavailable" + } + } + } } }, "components": { diff --git a/pigway_frame_flow/api/app.py b/pigway_frame_flow/api/app.py index 23d5c51..70405e2 100644 --- a/pigway_frame_flow/api/app.py +++ b/pigway_frame_flow/api/app.py @@ -601,7 +601,8 @@ def create_app(settings=None, start_worker=True): return [ assets.info(r["id"]) for r in db.all( - "SELECT assets.id FROM assets LEFT JOIN asset_metadata ON asset_metadata.asset_id=assets.id WHERE kind!='cache' AND " + "SELECT assets.id FROM assets LEFT JOIN asset_metadata ON asset_metadata.asset_id=assets.id WHERE kind!='cache' " + "AND NOT EXISTS (SELECT 1 FROM asset_sources s WHERE s.asset_id=assets.id AND json_extract(s.payload,'$.type')='thumbnail') AND " "(?='all' OR (?='uploaded' AND kind='images') OR (?='generated' AND kind='generated')) " "AND (?='all' OR COALESCE(origin,'unknown')=?) AND (? IS NULL OR job_id=?) " "ORDER BY created_at DESC LIMIT ? OFFSET ?", @@ -642,6 +643,16 @@ def create_app(settings=None, start_worker=True): db.execute("DELETE FROM asset_metadata WHERE asset_id=?", (asset_id,)) db.execute("DELETE FROM asset_sources WHERE asset_id=?", (asset_id,)) db.execute("DELETE FROM assets WHERE id=?", (asset_id,)) + for row in db.all( + "SELECT key,payload FROM app_config WHERE key LIKE 'wrist-thumbnail:%'" + ): + import json + + if json.loads(row["payload"]).get("asset_id") == asset_id: + db.execute( + "DELETE FROM app_config WHERE key=? AND payload=?", + (row["key"], row["payload"]), + ) return Response(status_code=204) @app.put( @@ -1095,6 +1106,14 @@ def create_app(settings=None, start_worker=True): or bool(settings.api_key) or any(registry.keys), ), + GalleryDestination( + id="watch", + adapter="watch-thumbnails", + name="Watch", + storage="local", + connected=True, + supports_upload=True, + ), GalleryDestination( id="local", adapter="local-assets", diff --git a/pigway_frame_flow/api/thumbnails.py b/pigway_frame_flow/api/thumbnails.py index 14ab82e..7b610ac 100644 --- a/pigway_frame_flow/api/thumbnails.py +++ b/pigway_frame_flow/api/thumbnails.py @@ -1,7 +1,7 @@ import json from uuid import uuid4 -from fastapi import Depends +from fastapi import Depends, Query from fastapi.responses import Response from pydantic import Field @@ -24,6 +24,50 @@ class ThumbnailRequest(StrictModel): def install_thumbnails(app, service): + from pigway_frame_flow.display.thumbnail_gallery import record, recover + + recover(service) + + @app.get( + "/api/v1/thumbnail-devices", dependencies=[Depends(require("images:read"))] + ) + def devices(): + return [ + { + "id": "watch", + "name": "Watch", + "width": 410, + "height": 502, + "transport_connected": False, + "status": "generation_only", + } + ] + + @app.get("/api/v1/thumbnails", dependencies=[Depends(require("images:read"))]) + def gallery(offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200)): + rows = service.db.all( + "SELECT assets.id,asset_sources.payload FROM assets JOIN asset_sources ON asset_sources.asset_id=assets.id " + "WHERE json_extract(asset_sources.payload,'$.type')='thumbnail' " + "ORDER BY assets.created_at DESC,assets.id DESC LIMIT ? OFFSET ?", + (limit, offset), + ) + total = service.db.one( + "SELECT COUNT(*) AS count FROM assets JOIN asset_sources ON asset_sources.asset_id=assets.id " + "WHERE json_extract(asset_sources.payload,'$.type')='thumbnail'" + )["count"] + return { + "items": [ + { + **service.assets.info(row["id"]), + "thumbnail": json.loads(row["payload"]), + } + for row in rows + ], + "total": total, + "offset": offset, + "limit": limit, + } + @app.get("/api/v1/thumbnail-plans", dependencies=[Depends(require("images:read"))]) def plans(): return [ @@ -113,6 +157,18 @@ def install_thumbnails(app, service): @app.post("/api/v1/thumbnails/generate", dependencies=[Depends(require("admin"))]) def generate(request: ThumbnailRequest): - return service.assets.put( + asset = service.assets.put( image(request), request.plan.name + ".jpg", "generated" ) + record( + service, + asset["asset_id"], + { + "width": request.plan.width, + "height": request.plan.height, + "plan": request.plan.model_dump(), + "generated_at": asset["created_at"], + "source": {"source_id": request.source_id, "item_id": request.item_id}, + }, + ) + return asset diff --git a/pigway_frame_flow/display/thumbnail_gallery.py b/pigway_frame_flow/display/thumbnail_gallery.py new file mode 100644 index 0000000..2049e16 --- /dev/null +++ b/pigway_frame_flow/display/thumbnail_gallery.py @@ -0,0 +1,37 @@ +"""Separate watch gallery metadata; image files retain existing asset access rules.""" + +import json + +from pigway_frame_flow.storage.database import encode + +TARGET_ID = "watch" + + +def record(service, asset_id, metadata): + service.db.execute( + "INSERT OR REPLACE INTO asset_sources VALUES(?,?)", + (asset_id, encode({**metadata, "type": "thumbnail", "target_id": TARGET_ID})), + ) + + +def recover(service): + # Earlier automatic thumbnails have authoritative operation receipts. Never + # guess that an unrelated image is a thumbnail from its name or dimensions. + rows = service.db.all("SELECT result FROM operations WHERE result IS NOT NULL") + receipts = [json.loads(row["result"]).get("wrist_thumbnail") for row in rows] + receipts += [ + json.loads(row["payload"]) + for row in service.db.all( + "SELECT payload FROM app_config WHERE key LIKE 'wrist-thumbnail:%'" + ) + ] + for receipt in receipts: + if not isinstance(receipt, dict) or not receipt.get("asset_id"): + continue + asset_id = receipt["asset_id"] + if service.db.one( + "SELECT id FROM assets WHERE id=?", (asset_id,) + ) and not service.db.one( + "SELECT asset_id FROM asset_sources WHERE asset_id=?", (asset_id,) + ): + record(service, asset_id, receipt) diff --git a/pigway_frame_flow/display/wrist_thumbnail.py b/pigway_frame_flow/display/wrist_thumbnail.py index b3faa96..706bf2b 100644 --- a/pigway_frame_flow/display/wrist_thumbnail.py +++ b/pigway_frame_flow/display/wrist_thumbnail.py @@ -54,6 +54,9 @@ def publish(service, device_id, asset_id, data=None, plan_id=None): if service.photo_sources.get(source["source_id"]).kind == "synology-photos" else "source", } + from pigway_frame_flow.display.thumbnail_gallery import record + + record(service, asset["asset_id"], {**result, "source_frame_id": device_id}) service.db.execute( "INSERT OR REPLACE INTO app_config VALUES(?,?)", ("wrist-thumbnail:" + device_id, encode(result)), diff --git a/pigway_frame_flow/web/app.js b/pigway_frame_flow/web/app.js index 9598950..ad0db6e 100644 --- a/pigway_frame_flow/web/app.js +++ b/pigway_frame_flow/web/app.js @@ -48,16 +48,16 @@ function operationResultHTML(o){ } function operationErrorLabel(error){if(!error?.code)return '';const label=t(error.code);return label===error.code?t('error'):label;} function deviceDisplayName(device){const name=device.name||device.id;return device.id==='living-room'&&['living-room','Living room'].includes(name)?t('livingRoom'):name;} -function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).map(([id,icon])=>``).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>``).join('');revealCurrentNavigation();} +function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).map(([id,icon])=>``).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);const watchSelected=state.page==='gallery'&&state.tab==='watch';$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>``).join('')+``;revealCurrentNavigation();} // A destination appears only when its connection and read/write adapter are available. // Future cloud-drive destinations must implement their adapter before registration here. -const galleryDestinationAdapters=new Set(['kokonna','local-assets']); +const galleryDestinationAdapters=new Set(['kokonna','local-assets','watch-thumbnails']); function galleryDestinations(){ const configured=state.settings?.gallery_destinations; const destinations=Array.isArray(configured)?configured:[{id:'cloud',adapter:'kokonna',name:'KoKonna',storage:'cloud',connected:true,supports_upload:true},{id:'local',adapter:'local-assets',name:state.settings?.storage_name||'NAS',storage:'local',connected:true,supports_upload:true}]; return destinations.filter(d=>d.connected&&d.supports_upload&&galleryDestinationAdapters.has(d.adapter)); } -function galleryDestinationName(destination){return `${destination.name} ${t(destination.storage==='local'?'local':'cloudGallery')}`;} +function galleryDestinationName(destination){if(destination.id==='watch')return tr('手表 · 小图图库','Watch · Thumbnail gallery');return `${destination.name} ${t(destination.storage==='local'?'local':'cloudGallery')}`;} function imageLoading(img){ if(img.parentElement?.classList.contains('image-loading'))return img.parentElement; const box=document.createElement('div');box.className='image-loading'; @@ -134,9 +134,10 @@ const views={ async dashboard(epoch){const d=await api('/api/v1/devices/'+state.device);if(epoch!==state.epoch)return;state.devices=state.devices.map(x=>x.id===d.id?{...x,name:d.name}:x);shell();const s=d.slideshow;$('#main').innerHTML=notice()+`${d.power.battery_percent!==null&&d.power.battery_percent<20?`
${esc(d.model)} · ${esc(d.model_code)} · ${d.display.width} × ${d.display.height}
${t('refreshInterval')}
${t('rotationNotice')}
${t('playlists')} →${tr('独立小图设备 · 图片已保存在服务器;设备传输尚未接入。','Separate thumbnail device · Images are saved on the server; device transfer is not connected.')}
${tr('在“小图方案”中生成,或为照片任务勾选生成小图。旧版手动生成的小图仍在原本地图库。','Generate one in Thumbnail plans or enable thumbnails on a photo task. Older manually generated images remain in the original local gallery.')}