diff --git a/pigway_frame_flow/api/app.py b/pigway_frame_flow/api/app.py index dd3739c..062fe0b 100644 --- a/pigway_frame_flow/api/app.py +++ b/pigway_frame_flow/api/app.py @@ -3,7 +3,7 @@ import os import re from contextlib import asynccontextmanager from pathlib import Path -from typing import Any +from typing import Any, Literal from uuid import uuid4 from fastapi import Depends, FastAPI, File, Form, Header, Query, Request, UploadFile @@ -479,12 +479,17 @@ def create_app(settings=None, start_worker=True): tags=["Images"], dependencies=[Depends(require("images:read"))], ) - def list_assets(limit: int = Query(default=100, ge=1, le=200)): + def list_assets( + limit: int = Query(default=100, ge=1, le=200), + source: Literal["all", "uploaded", "generated"] = "all", + ): return [ assets.info(r["id"]) for r in db.all( - "SELECT id FROM assets WHERE kind!='cache' ORDER BY created_at DESC LIMIT ?", - (limit,), + "SELECT id FROM assets WHERE kind!='cache' AND " + "(?='all' OR (?='uploaded' AND kind='images') OR (?='generated' AND kind='generated')) " + "ORDER BY created_at DESC LIMIT ?", + (source, source, source, limit), ) ] diff --git a/pigway_frame_flow/models.py b/pigway_frame_flow/models.py index 6ed9ef6..d74836e 100644 --- a/pigway_frame_flow/models.py +++ b/pigway_frame_flow/models.py @@ -291,6 +291,7 @@ class Readiness(BaseModel): class Asset(BaseModel): + source: Literal["uploaded", "generated"] = "uploaded" asset_id: str name: str url: str diff --git a/pigway_frame_flow/renderers/images.py b/pigway_frame_flow/renderers/images.py index 1d22a95..ebcd093 100644 --- a/pigway_frame_flow/renderers/images.py +++ b/pigway_frame_flow/renderers/images.py @@ -93,6 +93,7 @@ class Assets: raise FlowError(E.NOT_FOUND, "Asset not found", 404) return { "asset_id": row["id"], + "source": "generated" if row["kind"] == "generated" else "uploaded", "name": row["name"], "url": f"/api/v1/assets/{row['id']}", "created_at": row["created_at"], diff --git a/pigway_frame_flow/web/app.js b/pigway_frame_flow/web/app.js index b1ac5b3..6a4faea 100644 --- a/pigway_frame_flow/web/app.js +++ b/pigway_frame_flow/web/app.js @@ -43,12 +43,12 @@ function detail(label,value){return `
${t(label)}${es function liveWriteAllowed(){return state.settings?.mock||state.settings?.write_enabled;} function guardDeviceButtons(){if(!liveWriteAllowed())document.querySelectorAll('[data-show],'+(state.tab==='cloud'?'.image-card .danger,':'')+'#show-name,#display-template,#display-upload,#restore').forEach(b=>{b.disabled=true;b.title=t('readOnlyWarning');});} async function templateEditor(selected){ - const raw=$('#template-data');const assets=await api('/api/v1/assets');let values=JSON.parse(raw.value); + const raw=$('#template-data');const assets=await api('/api/v1/assets?source=uploaded&limit=200');let values=JSON.parse(raw.value); const panel=document.createElement('div');panel.id='template-fields';raw.before(panel); const labels={title:state.lang==='zh'?'标题':'Title',message:state.lang==='zh'?'正文':'Message',quote:state.lang==='zh'?'一句话':'Quote',author:state.lang==='zh'?'作者':'Author',events:t('eventsHelp'),date:t('date'),weekday:state.lang==='zh'?'星期(留空自动)':'Weekday (blank for automatic)',weather:state.lang==='zh'?'天气数据(JSON,可留空使用 Provider)':'Weather JSON (blank uses provider)'}; const fields=[...new Set(['title',...selected.fields.filter(k=>Object.hasOwn(selected.defaults,k)),...(['daily-dashboard','calendar'].includes(selected.id)?['events','date','weekday']:selected.id==='daily-poetry'?['date']:[]),...(selected.id==='weather'||selected.id==='daily-dashboard'?['weather']:[]),...(['photo-text','daily-dashboard'].includes(selected.id)?['photo_id']:[])])]; function invalidate(){state.templatePreview=null;$('#display-template').disabled=true;$('#template-preview').className='empty';$('#template-preview').textContent=t('previewHint');} - function build(){panel.innerHTML=`

${t('formEditor')}

`+fields.map(k=>k==='photo_id'?`
`:`
`).join('');panel.querySelectorAll('[data-field]').forEach(el=>el.oninput=()=>{try{const k=el.dataset.field;let v=el.value;if(k==='events')v=v.split('\n').filter(x=>x.trim());if(['weather','forecast'].includes(k))v=v.trim()?JSON.parse(v):null;if(v===null||v==='')delete values[k];else values[k]=v;raw.value=JSON.stringify(values,null,2);el.setCustomValidity('');invalidate();}catch{el.setCustomValidity(t('INVALID_REQUEST'));el.reportValidity();}});} + function build(){panel.innerHTML=`

${t('formEditor')}

`+fields.map(k=>k==='photo_id'?`

${state.lang==='zh'?'只列出上传的图片,不包含模板生成图。没有照片时,请使用下方文件选择器上传。':'Uploaded images only; generated templates are excluded. Upload a photo below if needed.'}

`:`
`).join('');panel.querySelectorAll('[data-field]').forEach(el=>el.oninput=()=>{try{const k=el.dataset.field;let v=el.value;if(k==='events')v=v.split('\n').filter(x=>x.trim());if(['weather','forecast'].includes(k))v=v.trim()?JSON.parse(v):null;if(v===null||v==='')delete values[k];else values[k]=v;raw.value=JSON.stringify(values,null,2);el.setCustomValidity('');invalidate();}catch{el.setCustomValidity(t('INVALID_REQUEST'));el.reportValidity();}});} build();const advanced=document.createElement('details');advanced.innerHTML=`${t('advanced')}`;raw.before(advanced);advanced.append(raw);raw.oninput=()=>{invalidate();try{values=JSON.parse(raw.value);if(!values||typeof values!=='object'||Array.isArray(values))throw Error();raw.setCustomValidity('');build();}catch{raw.setCustomValidity(t('INVALID_REQUEST'));}}; const reset=document.createElement('button');reset.id='reset-template';reset.textContent=t('resetTemplate');$('#save-template').after(reset);bind('reset-template',()=>confirmDialog(t('resetConfirm'),async()=>{await api('/api/v1/templates/'+selected.id+'/config',{method:'DELETE'});await navigate();})); const save=$('#save-template');const oldSave=save.onclick;save.onclick=async e=>{if([...panel.querySelectorAll('textarea'),raw].some(x=>!x.checkValidity())){toast(t('INVALID_REQUEST'));return;}await oldSave(e);}; diff --git a/tests/test_photo_sources.py b/tests/test_photo_sources.py new file mode 100644 index 0000000..00a2ad0 --- /dev/null +++ b/tests/test_photo_sources.py @@ -0,0 +1,19 @@ +def test_uploaded_photos_exclude_generated_templates_before_limit(client, service): + photo = service.assets.put(b"fixture", "family.jpg", "images") + for _ in range(5): + service.assets.put(b"fixture", "quote.jpg", "generated") + response = client.get("/api/v1/assets?source=uploaded&limit=1") + assert response.status_code == 200 + assert response.json()[0]["asset_id"] == photo["asset_id"] + assert response.json()[0]["source"] == "uploaded" + generated = client.get("/api/v1/assets?source=generated").json() + assert len(generated) == 5 and all(a["source"] == "generated" for a in generated) + assert len(client.get("/api/v1/assets").json()) == 6 + + +def test_filename_does_not_determine_photo_source(client, service): + service.assets.put(b"fixture", "weather.jpg", "images") + assert ( + client.get("/api/v1/assets?source=uploaded").json()[0]["name"] == "weather.jpg" + ) + assert client.get("/api/v1/assets?source=invalid").status_code == 422