hlassiege/darylpublic Fork
⑂ 456658e
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(vignette): pick reference thumbnails from the project's media

The reference picker only listed the shared library, so thumbnails
downloaded into the project never showed up. It now also lists the
project's images (assets and added files), referenced as projet/<rel>
and resolved against the project folder when generating.
hlassiege committed 2026-10-13T14:46:58+02:00 Browse files
456658e parent: baa3967
modified backend/studio/modules/projects/thumbs.py +18 -12
@@ -3,8 +3,8 @@ L'étape Vignette côté API : les compositions proposées par Claude, la géné
33
44 Chaque variante (project.thumbs, éditée par l'interface) est associée à l'un des titres, et a sa
55 description, le texte que le modèle écrit dans l'image (vide : aucun), ses portraits et ses
6-vignettes de style, choisis dans la bibliothèque (toutes catégories). Une génération lance
7-un job par modèle (`<pid>-thumb-<variante>-<modèle>`), les modèles tournent en parallèle ; une retouche
6+vignettes de style, choisis dans la bibliothèque (toutes catégories) ou les médias du projet.
7+Une génération lance un job par modèle (`<pid>-thumb-<variante>-<modèle>`), les modèles tournent en parallèle ; une retouche
88 (`start_edit`) fait de même en partant d'une image de la galerie et d'une consigne. Les images produites
99 vont dans vignettes/ ; leur liste, project.thumb_images (variante -> images), n'est écrite que
1010 par l'API (`sync`, `remove_image`) : l'interface, qui renvoie project.thumbs en entier, ne peut
@@ -43,11 +43,17 @@ def ready() -> bool:
4343 return bool(fal.key())
4444
4545
46-def _ref_path(rel: str) -> Path:
47- """Une image de la bibliothèque, quelle que soit sa catégorie (le sélecteur les propose toutes)."""
48- path = (library.LIBRARY_DIR / rel).resolve()
49- if library.LIBRARY_DIR.resolve() not in path.parents or not path.is_file():
50- raise ValueError(f"image introuvable dans la bibliothèque : {rel}")
46+# une référence prise dans les médias du projet plutôt que dans la bibliothèque
47+PROJECT_PREFIX = "projet/"
48+
49+
50+def _ref_path(project: dict, rel: str) -> Path:
51+ """Une image de la bibliothèque, quelle que soit sa catégorie (le sélecteur les propose toutes), ou
52+ « projet/<rel> » : une image des médias du projet."""
53+ root = service.project_dir(project["id"]) if rel.startswith(PROJECT_PREFIX) else library.LIBRARY_DIR
54+ path = (root / rel.removeprefix(PROJECT_PREFIX)).resolve()
55+ if root.resolve() not in path.parents or not path.is_file():
56+ raise ValueError(f"image de référence introuvable : {rel}")
5157 return path
5258
5359
@@ -126,8 +132,8 @@ def _launch(project: dict, tid: str, models: list[str], prompt: str, refs: list[
126132 def start(project: dict, tid: str, models: list[str]) -> None:
127133 """Génère des vignettes neuves pour la variante `tid`, à partir de sa description et de ses références."""
128134 thumb = _thumb(project, tid)
129- portraits = [_ref_path(r) for r in thumb.get("portraits") or []]
130- styles = [_ref_path(r) for r in thumb.get("styles") or []]
135+ portraits = [_ref_path(project, r) for r in thumb.get("portraits") or []]
136+ styles = [_ref_path(project, r) for r in thumb.get("styles") or []]
131137 if not portraits and not styles:
132138 raise ValueError("choisis au moins un portrait ou une vignette de référence")
133139 prompt = build_prompt(thumb, video_title(project, thumb), len(portraits), len(styles))
@@ -144,7 +150,7 @@ def start_edit(project: dict, tid: str, file: str, instruction: str, models: lis
144150 base = service.project_dir(project["id"]) / service.VIGNETTES_DIR / file
145151 if not base.is_file():
146152 raise ValueError(f"fichier introuvable : {file}")
147- portraits = [_ref_path(r) for r in thumb.get("portraits") or []][:MAX_REFS - 1]
153+ portraits = [_ref_path(project, r) for r in thumb.get("portraits") or []][:MAX_REFS - 1]
148154 _launch(project, tid, models, build_edit_prompt(instruction, len(portraits)), [base, *portraits],
149155 source=file, edit=instruction.strip())
150156
@@ -158,8 +164,8 @@ def ideas_payload(project: dict, tid: str, note: str) -> dict:
158164 angle = project["angles"][sel] if sel is not None and 0 <= sel < len(project["angles"]) else None
159165 return {"thumb": tid, "note": note, "angle": angle, "current": thumb, "variants": project["thumbs"],
160166 "titles": [t["t"] for t in project["titles"] if t.get("pick")], "title": video_title(project, thumb),
161- "portraits": [str(_ref_path(r)) for r in thumb.get("portraits") or []],
162- "styles": [str(_ref_path(r)) for r in thumb.get("styles") or []]}
167+ "portraits": [str(_ref_path(project, r)) for r in thumb.get("portraits") or []],
168+ "styles": [str(_ref_path(project, r)) for r in thumb.get("styles") or []]}
163169
164170
165171 def apply_ideas(project: dict, result: dict, tid: str) -> None:
@@ -3,8 +3,8 @@ L'étape Vignette côté API : les compositions proposées par Claude, la géné
3 3
4 Chaque variante (project.thumbs, éditée par l'interface) est associée à l'un des titres, et a sa4 Chaque variante (project.thumbs, éditée par l'interface) est associée à l'un des titres, et a sa
5 description, le texte que le modèle écrit dans l'image (vide : aucun), ses portraits et ses5 description, le texte que le modèle écrit dans l'image (vide : aucun), ses portraits et ses
6-vignettes de style, choisis dans la bibliothèque (toutes catégories). Une génération lance6+vignettes de style, choisis dans la bibliothèque (toutes catégories) ou les médias du projet.
7-un job par modèle (`<pid>-thumb-<variante>-<modèle>`), les modèles tournent en parallèle ; une retouche7+Une génération lance un job par modèle (`<pid>-thumb-<variante>-<modèle>`), les modèles tournent en parallèle ; une retouche
8 (`start_edit`) fait de même en partant d'une image de la galerie et d'une consigne. Les images produites8 (`start_edit`) fait de même en partant d'une image de la galerie et d'une consigne. Les images produites
9 vont dans vignettes/ ; leur liste, project.thumb_images (variante -> images), n'est écrite que9 vont dans vignettes/ ; leur liste, project.thumb_images (variante -> images), n'est écrite que
10 par l'API (`sync`, `remove_image`) : l'interface, qui renvoie project.thumbs en entier, ne peut10 par l'API (`sync`, `remove_image`) : l'interface, qui renvoie project.thumbs en entier, ne peut
@@ -43,11 +43,17 @@ def ready() -> bool:
43 return bool(fal.key())43 return bool(fal.key())
44 44
45 45
46-def _ref_path(rel: str) -> Path:46+# une référence prise dans les médias du projet plutôt que dans la bibliothèque
47- """Une image de la bibliothèque, quelle que soit sa catégorie (le sélecteur les propose toutes)."""47+PROJECT_PREFIX = "projet/"
48- path = (library.LIBRARY_DIR / rel).resolve()48+
49- if library.LIBRARY_DIR.resolve() not in path.parents or not path.is_file():49+
50- raise ValueError(f"image introuvable dans la bibliothèque : {rel}")50+def _ref_path(project: dict, rel: str) -> Path:
51+ """Une image de la bibliothèque, quelle que soit sa catégorie (le sélecteur les propose toutes), ou
52+ « projet/<rel> » : une image des médias du projet."""
53+ root = service.project_dir(project["id"]) if rel.startswith(PROJECT_PREFIX) else library.LIBRARY_DIR
54+ path = (root / rel.removeprefix(PROJECT_PREFIX)).resolve()
55+ if root.resolve() not in path.parents or not path.is_file():
56+ raise ValueError(f"image de référence introuvable : {rel}")
51 return path57 return path
52 58
53 59
@@ -126,8 +132,8 @@ def _launch(project: dict, tid: str, models: list[str], prompt: str, refs: list[
126 def start(project: dict, tid: str, models: list[str]) -> None:132 def start(project: dict, tid: str, models: list[str]) -> None:
127 """Génère des vignettes neuves pour la variante `tid`, à partir de sa description et de ses références."""133 """Génère des vignettes neuves pour la variante `tid`, à partir de sa description et de ses références."""
128 thumb = _thumb(project, tid)134 thumb = _thumb(project, tid)
129- portraits = [_ref_path(r) for r in thumb.get("portraits") or []]135+ portraits = [_ref_path(project, r) for r in thumb.get("portraits") or []]
130- styles = [_ref_path(r) for r in thumb.get("styles") or []]136+ styles = [_ref_path(project, r) for r in thumb.get("styles") or []]
131 if not portraits and not styles:137 if not portraits and not styles:
132 raise ValueError("choisis au moins un portrait ou une vignette de référence")138 raise ValueError("choisis au moins un portrait ou une vignette de référence")
133 prompt = build_prompt(thumb, video_title(project, thumb), len(portraits), len(styles))139 prompt = build_prompt(thumb, video_title(project, thumb), len(portraits), len(styles))
@@ -144,7 +150,7 @@ def start_edit(project: dict, tid: str, file: str, instruction: str, models: lis
144 base = service.project_dir(project["id"]) / service.VIGNETTES_DIR / file150 base = service.project_dir(project["id"]) / service.VIGNETTES_DIR / file
145 if not base.is_file():151 if not base.is_file():
146 raise ValueError(f"fichier introuvable : {file}")152 raise ValueError(f"fichier introuvable : {file}")
147- portraits = [_ref_path(r) for r in thumb.get("portraits") or []][:MAX_REFS - 1]153+ portraits = [_ref_path(project, r) for r in thumb.get("portraits") or []][:MAX_REFS - 1]
148 _launch(project, tid, models, build_edit_prompt(instruction, len(portraits)), [base, *portraits],154 _launch(project, tid, models, build_edit_prompt(instruction, len(portraits)), [base, *portraits],
149 source=file, edit=instruction.strip())155 source=file, edit=instruction.strip())
150 156
@@ -158,8 +164,8 @@ def ideas_payload(project: dict, tid: str, note: str) -> dict:
158 angle = project["angles"][sel] if sel is not None and 0 <= sel < len(project["angles"]) else None164 angle = project["angles"][sel] if sel is not None and 0 <= sel < len(project["angles"]) else None
159 return {"thumb": tid, "note": note, "angle": angle, "current": thumb, "variants": project["thumbs"],165 return {"thumb": tid, "note": note, "angle": angle, "current": thumb, "variants": project["thumbs"],
160 "titles": [t["t"] for t in project["titles"] if t.get("pick")], "title": video_title(project, thumb),166 "titles": [t["t"] for t in project["titles"] if t.get("pick")], "title": video_title(project, thumb),
161- "portraits": [str(_ref_path(r)) for r in thumb.get("portraits") or []],167+ "portraits": [str(_ref_path(project, r)) for r in thumb.get("portraits") or []],
162- "styles": [str(_ref_path(r)) for r in thumb.get("styles") or []]}168+ "styles": [str(_ref_path(project, r)) for r in thumb.get("styles") or []]}
163 169
164 170
165 def apply_ideas(project: dict, result: dict, tid: str) -> None:171 def apply_ideas(project: dict, result: dict, tid: str) -> None:
modified web/src/i18n/locales/en/thumbnail.ts +1 -0
@@ -72,6 +72,7 @@ export default {
7272 max: "{n} images max",
7373 picked: "Library · none selected | Library · {n} selected | Library · {n} selected",
7474 search: "Search (name, project)…",
75+ project: "This project",
7576 all: "All · {n}",
7677 noMatch: "No matching images.",
7778 emptyCategory: "No images in this category yet.",
@@ -72,6 +72,7 @@ export default {
72 max: "{n} images max",72 max: "{n} images max",
73 picked: "Library · none selected | Library · {n} selected | Library · {n} selected",73 picked: "Library · none selected | Library · {n} selected | Library · {n} selected",
74 search: "Search (name, project)…",74 search: "Search (name, project)…",
75+ project: "This project",
75 all: "All · {n}",76 all: "All · {n}",
76 noMatch: "No matching images.",77 noMatch: "No matching images.",
77 emptyCategory: "No images in this category yet.",78 emptyCategory: "No images in this category yet.",
modified web/src/i18n/locales/fr/thumbnail.ts +1 -0
@@ -72,6 +72,7 @@ export default {
7272 max: "{n} images au plus",
7373 picked: "Bibliothèque · {n} choisie | Bibliothèque · {n} choisie | Bibliothèque · {n} choisies",
7474 search: "Rechercher (nom, projet)…",
75+ project: "Ce projet",
7576 all: "Toutes · {n}",
7677 noMatch: "Aucune image ne correspond.",
7778 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",
@@ -72,6 +72,7 @@ export default {
72 max: "{n} images au plus",72 max: "{n} images au plus",
73 picked: "Bibliothèque · {n} choisie | Bibliothèque · {n} choisie | Bibliothèque · {n} choisies",73 picked: "Bibliothèque · {n} choisie | Bibliothèque · {n} choisie | Bibliothèque · {n} choisies",
74 search: "Rechercher (nom, projet)…",74 search: "Rechercher (nom, projet)…",
75+ project: "Ce projet",
75 all: "Toutes · {n}",76 all: "Toutes · {n}",
76 noMatch: "Aucune image ne correspond.",77 noMatch: "Aucune image ne correspond.",
77 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",78 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",
modified web/src/project/steps/VignetteStep.vue +3 -3
@@ -16,6 +16,7 @@ import { act, failed, running, store, update } from "../store";
1616 import type { Thumb, ThumbIdea, ThumbImage } from "../types";
1717 import LibraryPicker from "./vignette/LibraryPicker.vue";
1818 import VariantTile from "./vignette/VariantTile.vue";
19+import { refUrl } from "./vignette/refs";
1920
2021 // le plafond d'images de référence de fal (backend projects/thumbs.py)
2122 const MAX_REFS = 10;
@@ -246,7 +247,6 @@ const PICKERS = {
246247 portraits: { category: "portraits", title: "thumbnail.myPortraits" },
247248 styles: { category: "vignettes", title: "thumbnail.styleRefs" },
248249 } as const;
249-const libUrl = (rel: string) => `/api/projects/library/file/${rel.split("/").map(encodeURIComponent).join("/")}`;
250250 const pickerMax = computed(() => {
251251 const t = current.value;
252252 if (!t || !picking.value) return MAX_REFS;
@@ -328,7 +328,7 @@ const big = computed(() => feed.value === "desktop");
328328 </div>
329329 <div class="chips">
330330 <div v-for="rel in current[kind] ?? []" :key="rel" class="chip">
331- <img :src="libUrl(rel)" alt="" :title="rel.split('/').pop()">
331+ <img :src="refUrl(project.id, rel)" alt="" :title="rel.split('/').pop()">
332332 <button class="x-btn" :title="tr('common.remove')" @click="unref(kind, rel)">×</button>
333333 </div>
334334 <span v-if="!(current[kind] ?? []).length" class="faint none">
@@ -409,7 +409,7 @@ const big = computed(() => feed.value === "desktop");
409409 </section>
410410
411411 <LibraryPicker
412- v-if="picking && current" :category="PICKERS[picking].category" :title="tr(PICKERS[picking].title)"
412+ v-if="picking && current" :pid="project.id" :category="PICKERS[picking].category" :title="tr(PICKERS[picking].title)"
413413 :selected="current[picking] ?? []" :max="pickerMax" @done="picked" @cancel="picking = null" />
414414
415415 <template v-if="ideasOpen && current">
@@ -16,6 +16,7 @@ import { act, failed, running, store, update } from "../store";
16 import type { Thumb, ThumbIdea, ThumbImage } from "../types";16 import type { Thumb, ThumbIdea, ThumbImage } from "../types";
17 import LibraryPicker from "./vignette/LibraryPicker.vue";17 import LibraryPicker from "./vignette/LibraryPicker.vue";
18 import VariantTile from "./vignette/VariantTile.vue";18 import VariantTile from "./vignette/VariantTile.vue";
19+import { refUrl } from "./vignette/refs";
19 20
20 // le plafond d'images de référence de fal (backend projects/thumbs.py)21 // le plafond d'images de référence de fal (backend projects/thumbs.py)
21 const MAX_REFS = 10;22 const MAX_REFS = 10;
@@ -246,7 +247,6 @@ const PICKERS = {
246 portraits: { category: "portraits", title: "thumbnail.myPortraits" },247 portraits: { category: "portraits", title: "thumbnail.myPortraits" },
247 styles: { category: "vignettes", title: "thumbnail.styleRefs" },248 styles: { category: "vignettes", title: "thumbnail.styleRefs" },
248 } as const;249 } as const;
249-const libUrl = (rel: string) => `/api/projects/library/file/${rel.split("/").map(encodeURIComponent).join("/")}`;
250 const pickerMax = computed(() => {250 const pickerMax = computed(() => {
251 const t = current.value;251 const t = current.value;
252 if (!t || !picking.value) return MAX_REFS;252 if (!t || !picking.value) return MAX_REFS;
@@ -328,7 +328,7 @@ const big = computed(() => feed.value === "desktop");
328 </div>328 </div>
329 <div class="chips">329 <div class="chips">
330 <div v-for="rel in current[kind] ?? []" :key="rel" class="chip">330 <div v-for="rel in current[kind] ?? []" :key="rel" class="chip">
331- <img :src="libUrl(rel)" alt="" :title="rel.split('/').pop()">331+ <img :src="refUrl(project.id, rel)" alt="" :title="rel.split('/').pop()">
332 <button class="x-btn" :title="tr('common.remove')" @click="unref(kind, rel)">×</button>332 <button class="x-btn" :title="tr('common.remove')" @click="unref(kind, rel)">×</button>
333 </div>333 </div>
334 <span v-if="!(current[kind] ?? []).length" class="faint none">334 <span v-if="!(current[kind] ?? []).length" class="faint none">
@@ -409,7 +409,7 @@ const big = computed(() => feed.value === "desktop");
409 </section>409 </section>
410 410
411 <LibraryPicker411 <LibraryPicker
412- v-if="picking && current" :category="PICKERS[picking].category" :title="tr(PICKERS[picking].title)"412+ v-if="picking && current" :pid="project.id" :category="PICKERS[picking].category" :title="tr(PICKERS[picking].title)"
413 :selected="current[picking] ?? []" :max="pickerMax" @done="picked" @cancel="picking = null" />413 :selected="current[picking] ?? []" :max="pickerMax" @done="picked" @cancel="picking = null" />
414 414
415 <template v-if="ideasOpen && current">415 <template v-if="ideasOpen && current">
modified web/src/project/steps/vignette/LibraryPicker.vue +19 -8
@@ -1,21 +1,24 @@
11 <script setup lang="ts">
22 /**
3- * Choix d'images d'une catégorie de la bibliothèque (plusieurs à la fois) ; on peut y glisser de
4- * nouvelles images, rangées dans la catégorie et cochées d'emblée.
3+ * Choix d'images d'une catégorie de la bibliothèque (plusieurs à la fois), ou parmi les images du projet
4+ * (assets et fichiers ajoutés) ; on peut y glisser de nouvelles images, rangées dans la catégorie et
5+ * cochées d'emblée.
56 */
67 import { computed, onMounted, ref } from "vue";
78 import { useI18n } from "vue-i18n";
89 import { api } from "../../../lib/api";
910 import { toast } from "../../../lib/toast";
1011 import { droppedFiles, upload } from "../../../lib/upload";
12+import { PROJECT_PREFIX, refUrl } from "./refs";
1113
1214 interface Item { name: string; rel: string; category: string; type: string; project: string | null; kind: string }
1315
14-const props = defineProps<{ category: string; title: string; selected: string[]; max: number }>();
16+const props = defineProps<{ pid: string; category: string; title: string; selected: string[]; max: number }>();
1517 const emit = defineEmits<{ done: [rels: string[]]; cancel: [] }>();
1618
1719 const { t } = useI18n();
1820 const items = ref<Item[]>([]);
21+const own = ref<Item[]>([]);
1922 const picked = ref<string[]>([...props.selected]);
2023 const over = ref(false);
2124 const uploading = ref<number | null>(null);
@@ -23,7 +26,7 @@ const uploading = ref<number | null>(null);
2326 const filter = ref<string>(props.category);
2427 const query = ref("");
2528
26-const images = computed(() => items.value.filter((x) => x.kind === "Images"));
29+const images = computed(() => [...own.value, ...items.value].filter((x) => x.kind === "Images"));
2730 const categories = computed(() => {
2831 const counts = new Map<string, { type: string; n: number }>();
2932 for (const x of images.value) {
@@ -38,8 +41,18 @@ const shown = computed(() => {
3841 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));
3942 });
4043
44+// les médias du projet, comme le panneau Médias du script
45+const PROJECT_GROUPS = ["Assets", "Fichiers ajoutés"];
46+
4147 onMounted(async () => {
42- items.value = (await api<{ items: Item[] }>("/api/projects/library")).items;
48+ const [lib, res] = await Promise.all([
49+ api<{ items: Item[] }>("/api/projects/library"),
50+ api<{ groups: { k: string; items: { name: string; rel: string; kind: string }[] }[] }>(`/api/projects/${props.pid}/resources`),
51+ ]);
52+ items.value = lib.items;
53+ own.value = res.groups.filter((g) => PROJECT_GROUPS.includes(g.k)).flatMap((g) => g.items).map((x) => ({
54+ ...x, rel: `${PROJECT_PREFIX}${x.rel}`, category: PROJECT_PREFIX, type: t("thumbnail.picker.project"), project: null,
55+ }));
4356 if (!images.value.some((x) => x.category === props.category)) filter.value = "all";
4457 });
4558
@@ -68,8 +81,6 @@ async function onDrop(e: DragEvent) {
6881 uploading.value = null;
6982 }
7083 }
71-
72-const src = (rel: string) => `/api/projects/library/file/${rel.split("/").map(encodeURIComponent).join("/")}`;
7384 </script>
7485
7586 <template>
@@ -92,7 +103,7 @@ const src = (rel: string) => `/api/projects/library/file/${rel.split("/").map(en
92103 </div>
93104 <div class="grid scroll">
94105 <button v-for="x in shown" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">
95- <img :src="src(x.rel)" alt="">
106+ <img :src="refUrl(pid, x.rel)" alt="">
96107 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>
97108 </button>
98109 <span v-if="!shown.length" class="nothing">
@@ -1,21 +1,24 @@
1 <script setup lang="ts">1 <script setup lang="ts">
2 /**2 /**
3- * Choix d'images d'une catégorie de la bibliothèque (plusieurs à la fois) ; on peut y glisser de3+ * Choix d'images d'une catégorie de la bibliothèque (plusieurs à la fois), ou parmi les images du projet
4- * nouvelles images, rangées dans la catégorie et cochées d'emblée.4+ * (assets et fichiers ajoutés) ; on peut y glisser de nouvelles images, rangées dans la catégorie et
5+ * cochées d'emblée.
5 */6 */
6 import { computed, onMounted, ref } from "vue";7 import { computed, onMounted, ref } from "vue";
7 import { useI18n } from "vue-i18n";8 import { useI18n } from "vue-i18n";
8 import { api } from "../../../lib/api";9 import { api } from "../../../lib/api";
9 import { toast } from "../../../lib/toast";10 import { toast } from "../../../lib/toast";
10 import { droppedFiles, upload } from "../../../lib/upload";11 import { droppedFiles, upload } from "../../../lib/upload";
12+import { PROJECT_PREFIX, refUrl } from "./refs";
11 13
12 interface Item { name: string; rel: string; category: string; type: string; project: string | null; kind: string }14 interface Item { name: string; rel: string; category: string; type: string; project: string | null; kind: string }
13 15
14-const props = defineProps<{ category: string; title: string; selected: string[]; max: number }>();16+const props = defineProps<{ pid: string; category: string; title: string; selected: string[]; max: number }>();
15 const emit = defineEmits<{ done: [rels: string[]]; cancel: [] }>();17 const emit = defineEmits<{ done: [rels: string[]]; cancel: [] }>();
16 18
17 const { t } = useI18n();19 const { t } = useI18n();
18 const items = ref<Item[]>([]);20 const items = ref<Item[]>([]);
21+const own = ref<Item[]>([]);
19 const picked = ref<string[]>([...props.selected]);22 const picked = ref<string[]>([...props.selected]);
20 const over = ref(false);23 const over = ref(false);
21 const uploading = ref<number | null>(null);24 const uploading = ref<number | null>(null);
@@ -23,7 +26,7 @@ const uploading = ref<number | null>(null);
23 const filter = ref<string>(props.category);26 const filter = ref<string>(props.category);
24 const query = ref("");27 const query = ref("");
25 28
26-const images = computed(() => items.value.filter((x) => x.kind === "Images"));29+const images = computed(() => [...own.value, ...items.value].filter((x) => x.kind === "Images"));
27 const categories = computed(() => {30 const categories = computed(() => {
28 const counts = new Map<string, { type: string; n: number }>();31 const counts = new Map<string, { type: string; n: number }>();
29 for (const x of images.value) {32 for (const x of images.value) {
@@ -38,8 +41,18 @@ const shown = computed(() => {
38 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));41 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));
39 });42 });
40 43
44+// les médias du projet, comme le panneau Médias du script
45+const PROJECT_GROUPS = ["Assets", "Fichiers ajoutés"];
46+
41 onMounted(async () => {47 onMounted(async () => {
42- items.value = (await api<{ items: Item[] }>("/api/projects/library")).items;48+ const [lib, res] = await Promise.all([
49+ api<{ items: Item[] }>("/api/projects/library"),
50+ api<{ groups: { k: string; items: { name: string; rel: string; kind: string }[] }[] }>(`/api/projects/${props.pid}/resources`),
51+ ]);
52+ items.value = lib.items;
53+ own.value = res.groups.filter((g) => PROJECT_GROUPS.includes(g.k)).flatMap((g) => g.items).map((x) => ({
54+ ...x, rel: `${PROJECT_PREFIX}${x.rel}`, category: PROJECT_PREFIX, type: t("thumbnail.picker.project"), project: null,
55+ }));
43 if (!images.value.some((x) => x.category === props.category)) filter.value = "all";56 if (!images.value.some((x) => x.category === props.category)) filter.value = "all";
44 });57 });
45 58
@@ -68,8 +81,6 @@ async function onDrop(e: DragEvent) {
68 uploading.value = null;81 uploading.value = null;
69 }82 }
70 }83 }
71-
72-const src = (rel: string) => `/api/projects/library/file/${rel.split("/").map(encodeURIComponent).join("/")}`;
73 </script>84 </script>
74 85
75 <template>86 <template>
@@ -92,7 +103,7 @@ const src = (rel: string) => `/api/projects/library/file/${rel.split("/").map(en
92 </div>103 </div>
93 <div class="grid scroll">104 <div class="grid scroll">
94 <button v-for="x in shown" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">105 <button v-for="x in shown" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">
95- <img :src="src(x.rel)" alt="">106+ <img :src="refUrl(pid, x.rel)" alt="">
96 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>107 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>
97 </button>108 </button>
98 <span v-if="!shown.length" class="nothing">109 <span v-if="!shown.length" class="nothing">
added web/src/project/steps/vignette/refs.ts +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+/** Une image de référence : un chemin de la bibliothèque, ou « projet/<rel> » pour un média du projet (backend thumbs.py). */
2+export const PROJECT_PREFIX = "projet/";
3+
4+const enc = (rel: string) => rel.split("/").map(encodeURIComponent).join("/");
5+
6+export const refUrl = (pid: string, rel: string) => rel.startsWith(PROJECT_PREFIX)
7+ ? `/api/projects/${pid}/files/${enc(rel.slice(PROJECT_PREFIX.length))}`
8+ : `/api/projects/library/file/${enc(rel)}`;
new file mode 100644
@@ -0,0 +1,8 @@
1+/** Une image de référence : un chemin de la bibliothèque, ou « projet/<rel> » pour un média du projet (backend thumbs.py). */
2+export const PROJECT_PREFIX = "projet/";
3+
4+const enc = (rel: string) => rel.split("/").map(encodeURIComponent).join("/");
5+
6+export const refUrl = (pid: string, rel: string) => rel.startsWith(PROJECT_PREFIX)
7+ ? `/api/projects/${pid}/files/${enc(rel.slice(PROJECT_PREFIX.length))}`
8+ : `/api/projects/library/file/${enc(rel)}`;