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