hlassiege/darylpublic Fork
⑂ d2ebfbd
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(web): merge Sourcing into Media with an add-media popup

The Sourcing step duplicated the Media screen. Its YouTube, image search and source capture tabs now live in an "Add media" popup opened from Media, next to a file upload tab. Storyboard B-roll links open that popup on the YouTube tab.
hlassiege committed 2026-10-11T21:55:01+02:00 Browse files
d2ebfbd parent: 3b4d23a
modified web/src/i18n/locales/en/editing.ts +2 -2
@@ -116,7 +116,7 @@ export default {
116116 tabSfx: "Sounds",
117117 tabMusic: "Music",
118118 add: "Place at playhead",
119- empty: "Nothing here. Rushes come from the Rushes step, B-roll from Sourcing; sounds and music from the library.",
119+ empty: "Nothing here. Rushes come from the Rushes step, B-roll from Media; sounds and music from the library.",
120120 more: "… and {n} more: refine the search.",
121121 hint: "↑ ↓ to choose, Enter or double-click to place at the playhead. Drag a file onto the timeline: on the A-roll track to insert it, elsewhere to place it on top.",
122122 },
@@ -218,7 +218,7 @@ export default {
218218 toFind: "To find · the search",
219219 searchPh: "Keywords to search…",
220220 copy: "Copy the search",
221- goAssets: "Search in Assets",
221+ goMedia: "Find in Media",
222222 where: { project: "Project", library: "Library" },
223223 search: "Search: {q}",
224224 missingToast: "{n} B-roll without media: pick one in the Storyboard. | {n} B-roll without media: pick one in the Storyboard.",
@@ -116,7 +116,7 @@ export default {
116 tabSfx: "Sounds",116 tabSfx: "Sounds",
117 tabMusic: "Music",117 tabMusic: "Music",
118 add: "Place at playhead",118 add: "Place at playhead",
119- empty: "Nothing here. Rushes come from the Rushes step, B-roll from Sourcing; sounds and music from the library.",119+ empty: "Nothing here. Rushes come from the Rushes step, B-roll from Media; sounds and music from the library.",
120 more: "… and {n} more: refine the search.",120 more: "… and {n} more: refine the search.",
121 hint: "↑ ↓ to choose, Enter or double-click to place at the playhead. Drag a file onto the timeline: on the A-roll track to insert it, elsewhere to place it on top.",121 hint: "↑ ↓ to choose, Enter or double-click to place at the playhead. Drag a file onto the timeline: on the A-roll track to insert it, elsewhere to place it on top.",
122 },122 },
@@ -218,7 +218,7 @@ export default {
218 toFind: "To find · the search",218 toFind: "To find · the search",
219 searchPh: "Keywords to search…",219 searchPh: "Keywords to search…",
220 copy: "Copy the search",220 copy: "Copy the search",
221- goAssets: "Search in Assets",221+ goMedia: "Find in Media",
222 where: { project: "Project", library: "Library" },222 where: { project: "Project", library: "Library" },
223 search: "Search: {q}",223 search: "Search: {q}",
224 missingToast: "{n} B-roll without media: pick one in the Storyboard. | {n} B-roll without media: pick one in the Storyboard.",224 missingToast: "{n} B-roll without media: pick one in the Storyboard. | {n} B-roll without media: pick one in the Storyboard.",
modified web/src/i18n/locales/en/media.ts +12 -1
@@ -1,8 +1,19 @@
11 export default {
22 title: "Media",
33 uploading: "Uploading · {pct}%",
4- drop: "Drop files here to add them to the project",
54 toLibrary: "→ Library",
65 addedToLibrary: "Added to the library · {category}",
76 filesAdded: "no files added | File added | {n} files added",
7+ add: {
8+ open: "+ Add media",
9+ title: "Add media",
10+ foot: "Everything you add lands straight in Media.",
11+ drop: "Drop files here or click to browse · video, audio, image, PDF",
12+ tabs: {
13+ files: { label: "Files", desc: "Video, audio, image or document from your computer" },
14+ youtube: { label: "YouTube", desc: "A clip or the whole video from a link" },
15+ images: { label: "Image search", desc: "Logos, people, places" },
16+ doc: { label: "Source captures", desc: "Captures of the documents cited in the script" },
17+ },
18+ },
819 };
@@ -1,8 +1,19 @@
1 export default {1 export default {
2 title: "Media",2 title: "Media",
3 uploading: "Uploading · {pct}%",3 uploading: "Uploading · {pct}%",
4- drop: "Drop files here to add them to the project",
5 toLibrary: "→ Library",4 toLibrary: "→ Library",
6 addedToLibrary: "Added to the library · {category}",5 addedToLibrary: "Added to the library · {category}",
7 filesAdded: "no files added | File added | {n} files added",6 filesAdded: "no files added | File added | {n} files added",
7+ add: {
8+ open: "+ Add media",
9+ title: "Add media",
10+ foot: "Everything you add lands straight in Media.",
11+ drop: "Drop files here or click to browse · video, audio, image, PDF",
12+ tabs: {
13+ files: { label: "Files", desc: "Video, audio, image or document from your computer" },
14+ youtube: { label: "YouTube", desc: "A clip or the whole video from a link" },
15+ images: { label: "Image search", desc: "Logos, people, places" },
16+ doc: { label: "Source captures", desc: "Captures of the documents cited in the script" },
17+ },
18+ },
8 };19 };
modified web/src/i18n/locales/en/nav.ts +0 -1
@@ -24,7 +24,6 @@ export default {
2424 words: "no words | {n} word | {n} words",
2525 shots: "{done}/{total} shots",
2626 rushes: "no clips | {n} clip | {n} clips",
27- videos: "no videos | {n} video | {n} videos",
2827 imported: "imported",
2928 roughCut: "rough cut",
3029 shorts: "no shorts | {n} short | {n} shorts",
@@ -24,7 +24,6 @@ export default {
24 words: "no words | {n} word | {n} words",24 words: "no words | {n} word | {n} words",
25 shots: "{done}/{total} shots",25 shots: "{done}/{total} shots",
26 rushes: "no clips | {n} clip | {n} clips",26 rushes: "no clips | {n} clip | {n} clips",
27- videos: "no videos | {n} video | {n} videos",
28 imported: "imported",27 imported: "imported",
29 roughCut: "rough cut",28 roughCut: "rough cut",
30 shorts: "no shorts | {n} short | {n} shorts",29 shorts: "no shorts | {n} short | {n} shorts",
modified web/src/i18n/locales/en/script.ts +1 -1
@@ -95,7 +95,7 @@ export default {
9595 search: "Search files",
9696 pickHint: "Click a file to add it to the shot.",
9797 dragHint: "Drag a file onto the text: it completes the passage’s shot, or creates one on the paragraph.",
98- empty: "No media yet: import clips in Rough cut, gather assets in Sourcing, or drop files in {link}.",
98+ empty: "No media yet: import clips in Rough cut, or add media in {link}.",
9999 mediaLink: "Media",
100100 library: "Library · {label}",
101101 refresh: "Refresh",
@@ -95,7 +95,7 @@ export default {
95 search: "Search files",95 search: "Search files",
96 pickHint: "Click a file to add it to the shot.",96 pickHint: "Click a file to add it to the shot.",
97 dragHint: "Drag a file onto the text: it completes the passage’s shot, or creates one on the paragraph.",97 dragHint: "Drag a file onto the text: it completes the passage’s shot, or creates one on the paragraph.",
98- empty: "No media yet: import clips in Rough cut, gather assets in Sourcing, or drop files in {link}.",98+ empty: "No media yet: import clips in Rough cut, or add media in {link}.",
99 mediaLink: "Media",99 mediaLink: "Media",
100 library: "Library · {label}",100 library: "Library · {label}",
101 refresh: "Refresh",101 refresh: "Refresh",
modified web/src/i18n/locales/en/steps.ts +0 -1
@@ -6,7 +6,6 @@ export default {
66 roast: "Roasting",
77 tournage: "Shoot",
88 derush: "Rough cut",
9- assets: "Sourcing",
109 transcript: "Source video",
1110 storyboard: "Storyboard",
1211 montage: "Editing",
@@ -6,7 +6,6 @@ export default {
6 roast: "Roasting",6 roast: "Roasting",
7 tournage: "Shoot",7 tournage: "Shoot",
8 derush: "Rough cut",8 derush: "Rough cut",
9- assets: "Sourcing",
10 transcript: "Source video",9 transcript: "Source video",
11 storyboard: "Storyboard",10 storyboard: "Storyboard",
12 montage: "Editing",11 montage: "Editing",
modified web/src/i18n/locales/fr/editing.ts +2 -2
@@ -116,7 +116,7 @@ export default {
116116 tabSfx: "Sons",
117117 tabMusic: "Musique",
118118 add: "Poser au curseur",
119- empty: "Rien ici. Les rushes viennent de l’étape Rushes, les B-roll du Sourcing ; sons et musiques de la bibliothèque.",
119+ empty: "Rien ici. Les rushes viennent de l’étape Rushes, les B-roll des Médias ; sons et musiques de la bibliothèque.",
120120 more: "… et {n} autres : affine la recherche.",
121121 hint: "↑ ↓ pour choisir, Entrée ou double-clic pour poser au curseur. Glisse un média sur la timeline : sur la piste A-roll pour l’insérer, ailleurs pour le poser au-dessus.",
122122 },
@@ -218,7 +218,7 @@ export default {
218218 toFind: "À trouver · la recherche",
219219 searchPh: "Mots-clés à chercher…",
220220 copy: "Copier la recherche",
221- goAssets: "Chercher dans Assets",
221+ goMedia: "Chercher dans Médias",
222222 where: { project: "Projet", library: "Bibliothèque" },
223223 search: "Chercher : {q}",
224224 missingToast: "{n} B-roll sans média : choisis-en un au Storyboard. | {n} B-roll sans média : choisis-en un au Storyboard.",
@@ -116,7 +116,7 @@ export default {
116 tabSfx: "Sons",116 tabSfx: "Sons",
117 tabMusic: "Musique",117 tabMusic: "Musique",
118 add: "Poser au curseur",118 add: "Poser au curseur",
119- empty: "Rien ici. Les rushes viennent de l’étape Rushes, les B-roll du Sourcing ; sons et musiques de la bibliothèque.",119+ empty: "Rien ici. Les rushes viennent de l’étape Rushes, les B-roll des Médias ; sons et musiques de la bibliothèque.",
120 more: "… et {n} autres : affine la recherche.",120 more: "… et {n} autres : affine la recherche.",
121 hint: "↑ ↓ pour choisir, Entrée ou double-clic pour poser au curseur. Glisse un média sur la timeline : sur la piste A-roll pour l’insérer, ailleurs pour le poser au-dessus.",121 hint: "↑ ↓ pour choisir, Entrée ou double-clic pour poser au curseur. Glisse un média sur la timeline : sur la piste A-roll pour l’insérer, ailleurs pour le poser au-dessus.",
122 },122 },
@@ -218,7 +218,7 @@ export default {
218 toFind: "À trouver · la recherche",218 toFind: "À trouver · la recherche",
219 searchPh: "Mots-clés à chercher…",219 searchPh: "Mots-clés à chercher…",
220 copy: "Copier la recherche",220 copy: "Copier la recherche",
221- goAssets: "Chercher dans Assets",221+ goMedia: "Chercher dans Médias",
222 where: { project: "Projet", library: "Bibliothèque" },222 where: { project: "Projet", library: "Bibliothèque" },
223 search: "Chercher : {q}",223 search: "Chercher : {q}",
224 missingToast: "{n} B-roll sans média : choisis-en un au Storyboard. | {n} B-roll sans média : choisis-en un au Storyboard.",224 missingToast: "{n} B-roll sans média : choisis-en un au Storyboard. | {n} B-roll sans média : choisis-en un au Storyboard.",
modified web/src/i18n/locales/fr/media.ts +12 -1
@@ -1,8 +1,19 @@
11 export default {
22 title: "Médias",
33 uploading: "Envoi · {pct} %",
4- drop: "Glisse des fichiers ici pour les ajouter au projet",
54 toLibrary: "→ Bibliothèque",
65 addedToLibrary: "Ajouté à la bibliothèque · {category}",
76 filesAdded: "aucun fichier ajouté | Fichier ajouté | {n} fichiers ajoutés",
7+ add: {
8+ open: "+ Ajouter des médias",
9+ title: "Ajouter des médias",
10+ foot: "Les ajouts arrivent directement dans Médias.",
11+ drop: "Glisse des fichiers ici ou clique pour parcourir · vidéo, audio, image, PDF",
12+ tabs: {
13+ files: { label: "Fichiers", desc: "Vidéo, audio, image ou document depuis ton ordinateur" },
14+ youtube: { label: "YouTube", desc: "Extrait ou vidéo entière depuis un lien" },
15+ images: { label: "Recherche d’images", desc: "Logos, personnes, lieux" },
16+ doc: { label: "Captures de sources", desc: "Captures des documents cités dans le script" },
17+ },
18+ },
819 };
@@ -1,8 +1,19 @@
1 export default {1 export default {
2 title: "Médias",2 title: "Médias",
3 uploading: "Envoi · {pct} %",3 uploading: "Envoi · {pct} %",
4- drop: "Glisse des fichiers ici pour les ajouter au projet",
5 toLibrary: "→ Bibliothèque",4 toLibrary: "→ Bibliothèque",
6 addedToLibrary: "Ajouté à la bibliothèque · {category}",5 addedToLibrary: "Ajouté à la bibliothèque · {category}",
7 filesAdded: "aucun fichier ajouté | Fichier ajouté | {n} fichiers ajoutés",6 filesAdded: "aucun fichier ajouté | Fichier ajouté | {n} fichiers ajoutés",
7+ add: {
8+ open: "+ Ajouter des médias",
9+ title: "Ajouter des médias",
10+ foot: "Les ajouts arrivent directement dans Médias.",
11+ drop: "Glisse des fichiers ici ou clique pour parcourir · vidéo, audio, image, PDF",
12+ tabs: {
13+ files: { label: "Fichiers", desc: "Vidéo, audio, image ou document depuis ton ordinateur" },
14+ youtube: { label: "YouTube", desc: "Extrait ou vidéo entière depuis un lien" },
15+ images: { label: "Recherche d’images", desc: "Logos, personnes, lieux" },
16+ doc: { label: "Captures de sources", desc: "Captures des documents cités dans le script" },
17+ },
18+ },
8 };19 };
modified web/src/i18n/locales/fr/nav.ts +0 -1
@@ -24,7 +24,6 @@ export default {
2424 words: "aucun mot | {n} mot | {n} mots",
2525 shots: "{done}/{total} plans",
2626 rushes: "aucun rush | {n} rush | {n} rushes",
27- videos: "aucune vidéo | {n} vidéo | {n} vidéos",
2827 imported: "importée",
2928 roughCut: "dérush",
3029 shorts: "aucun short | {n} short | {n} shorts",
@@ -24,7 +24,6 @@ export default {
24 words: "aucun mot | {n} mot | {n} mots",24 words: "aucun mot | {n} mot | {n} mots",
25 shots: "{done}/{total} plans",25 shots: "{done}/{total} plans",
26 rushes: "aucun rush | {n} rush | {n} rushes",26 rushes: "aucun rush | {n} rush | {n} rushes",
27- videos: "aucune vidéo | {n} vidéo | {n} vidéos",
28 imported: "importée",27 imported: "importée",
29 roughCut: "dérush",28 roughCut: "dérush",
30 shorts: "aucun short | {n} short | {n} shorts",29 shorts: "aucun short | {n} short | {n} shorts",
modified web/src/i18n/locales/fr/script.ts +1 -1
@@ -95,7 +95,7 @@ export default {
9595 search: "Cherche un fichier",
9696 pickHint: "Clique un fichier pour l’ajouter au plan.",
9797 dragHint: "Glisse un fichier sur le texte : il complète le plan du passage, ou en crée un sur le paragraphe.",
98- empty: "Aucun média pour l’instant : importe des rushes au dérush, récupère des médias au sourcing ou dépose des fichiers dans {link}.",
98+ empty: "Aucun média pour l’instant : importe des rushes au dérush, ou ajoute des médias dans {link}.",
9999 mediaLink: "Médias",
100100 library: "Bibliothèque · {label}",
101101 refresh: "Actualiser",
@@ -95,7 +95,7 @@ export default {
95 search: "Cherche un fichier",95 search: "Cherche un fichier",
96 pickHint: "Clique un fichier pour l’ajouter au plan.",96 pickHint: "Clique un fichier pour l’ajouter au plan.",
97 dragHint: "Glisse un fichier sur le texte : il complète le plan du passage, ou en crée un sur le paragraphe.",97 dragHint: "Glisse un fichier sur le texte : il complète le plan du passage, ou en crée un sur le paragraphe.",
98- empty: "Aucun média pour l’instant : importe des rushes au dérush, récupère des médias au sourcing ou dépose des fichiers dans {link}.",98+ empty: "Aucun média pour l’instant : importe des rushes au dérush, ou ajoute des médias dans {link}.",
99 mediaLink: "Médias",99 mediaLink: "Médias",
100 library: "Bibliothèque · {label}",100 library: "Bibliothèque · {label}",
101 refresh: "Actualiser",101 refresh: "Actualiser",
modified web/src/i18n/locales/fr/steps.ts +0 -1
@@ -6,7 +6,6 @@ export default {
66 roast: "Roasting",
77 tournage: "Tournage",
88 derush: "Dérush",
9- assets: "Sourcing",
109 transcript: "Vidéo source",
1110 storyboard: "Storyboard",
1211 montage: "Montage",
@@ -6,7 +6,6 @@ export default {
6 roast: "Roasting",6 roast: "Roasting",
7 tournage: "Tournage",7 tournage: "Tournage",
8 derush: "Dérush",8 derush: "Dérush",
9- assets: "Sourcing",
10 transcript: "Vidéo source",9 transcript: "Vidéo source",
11 storyboard: "Storyboard",10 storyboard: "Storyboard",
12 montage: "Montage",11 montage: "Montage",
modified web/src/project/ProjectPage.vue +0 -1
@@ -15,7 +15,6 @@ const SCREENS: Record<string, ReturnType<typeof defineAsyncComponent>> = {
1515 roast: defineAsyncComponent(() => import("./steps/RoastStep.vue")),
1616 tournage: defineAsyncComponent(() => import("./steps/TournageStep.vue")),
1717 derush: defineAsyncComponent(() => import("./steps/DerushStep.vue")),
18- assets: defineAsyncComponent(() => import("./steps/AssetsStep.vue")),
1918 transcript: defineAsyncComponent(() => import("./steps/SourceStep.vue")),
2019 storyboard: defineAsyncComponent(() => import("./steps/StoryboardStep.vue")),
2120 montage: defineAsyncComponent(() => import("./steps/MontageStep.vue")),
@@ -15,7 +15,6 @@ const SCREENS: Record<string, ReturnType<typeof defineAsyncComponent>> = {
15 roast: defineAsyncComponent(() => import("./steps/RoastStep.vue")),15 roast: defineAsyncComponent(() => import("./steps/RoastStep.vue")),
16 tournage: defineAsyncComponent(() => import("./steps/TournageStep.vue")),16 tournage: defineAsyncComponent(() => import("./steps/TournageStep.vue")),
17 derush: defineAsyncComponent(() => import("./steps/DerushStep.vue")),17 derush: defineAsyncComponent(() => import("./steps/DerushStep.vue")),
18- assets: defineAsyncComponent(() => import("./steps/AssetsStep.vue")),
19 transcript: defineAsyncComponent(() => import("./steps/SourceStep.vue")),18 transcript: defineAsyncComponent(() => import("./steps/SourceStep.vue")),
20 storyboard: defineAsyncComponent(() => import("./steps/StoryboardStep.vue")),19 storyboard: defineAsyncComponent(() => import("./steps/StoryboardStep.vue")),
21 montage: defineAsyncComponent(() => import("./steps/MontageStep.vue")),20 montage: defineAsyncComponent(() => import("./steps/MontageStep.vue")),
modified web/src/project/ResourcesPage.vue +40 -12
@@ -1,14 +1,16 @@
11 <script setup lang="ts">
2-/** Tout ce que contient le dossier du projet, rangé par catégorie ; on peut y déposer des fichiers. */
2+/** Tout ce que contient le dossier du projet, rangé par catégorie ; « Ajouter des médias » (ou un glisser-déposer) l'enrichit. */
33 import { computed, ref, watch } from "vue";
44 import { useI18n } from "vue-i18n";
55 import { useRoute, useRouter } from "vue-router";
66 import MediaLightbox from "../components/MediaLightbox.vue";
77 import Thumb from "../components/Thumb.vue";
88 import { api, post } from "../lib/api";
9+import { usePref } from "../lib/composables";
910 import { duration as fmtDuration } from "../lib/format";
1011 import { toast } from "../lib/toast";
1112 import { droppedFiles, upload } from "../lib/upload";
13+import AddMediaModal, { ADD_TABS, type AddTab } from "./media/AddMediaModal.vue";
1214 import { open, store } from "./store";
1315
1416 interface Item { name: string; rel: string; kind: string; duration: number | null; size: number }
@@ -41,15 +43,36 @@ watch(pid, async (id) => {
4143 }
4244 }, { immediate: true });
4345
46+// --- la popup « Ajouter des médias » ; ?add=youtube l'ouvre sur un onglet (liens du storyboard)
47+const addTab = usePref<AddTab>("media.addTab", "files");
48+const linked = route.query.add as AddTab | undefined;
49+if (linked && ADD_TABS.includes(linked)) addTab.value = linked;
50+const adding = ref(!!linked);
51+
52+function closeAdd() {
53+ adding.value = false;
54+ // la page est rendue par chemin complet : retirer ?add la recharge
55+ if (route.query.add) router.replace({ query: {} });
56+ else load();
57+}
58+
59+// un téléchargement YouTube qui se termine arrive dans la liste
60+const downloaded = computed(() => store.project?.runtime.assets.downloads.filter((d) => d.finished).length ?? 0);
61+watch(downloaded, (n, before) => { if (n > before) load(); });
62+
4463 const total = computed(() => groups.value.reduce((a, g) => a + g.items.length, 0));
4564 const items = computed(() => tab.value === "all"
4665 ? groups.value.flatMap((g) => g.items.map((x) => ({ ...x, k: g.k })))
4766 : (groups.value.find((g) => g.k === tab.value)?.items ?? []).map((x) => ({ ...x, k: x.kind })));
4867
49-async function onDrop(e: DragEvent) {
68+function onDrop(e: DragEvent) {
5069 over.value = false;
70+ if (adding.value) return;
5171 const files = droppedFiles(e);
52- if (!files.length) return;
72+ if (files.length) uploadFiles(files);
73+}
74+
75+async function uploadFiles(files: File[]) {
5376 uploading.value = 0;
5477 try {
5578 groups.value = (await upload<{ groups: Group[] }>(`/api/projects/${pid.value}/resources/upload`, files, "files", {},
@@ -94,18 +117,18 @@ async function toLibrary(item: Item, category: string) {
94117 </script>
95118
96119 <template>
97- <div class="page">
120+ <div class="page" :class="{ over }" @dragover.prevent="over = !adding" @dragleave.self="over = false" @drop.prevent="onDrop">
98121 <div class="titles">
99122 <span class="eyebrow">{{ store.project?.title }}</span>
100123 <span class="h1">{{ t("media.title") }}</span>
101124 </div>
102- <div class="tabs">
103- <button :class="{ on: tab === 'all' }" @click="tab = 'all'">{{ t("common.all") }}<span class="n">{{ total }}</span></button>
104- <button v-for="g in groups" :key="g.k" :class="{ on: tab === g.k }" @click="tab = g.k">{{ g.k }}<span class="n">{{ g.items.length }}</span></button>
105- </div>
106- <div class="drop-zone" :class="{ over }" @dragover.prevent="over = true" @dragleave="over = false" @drop.prevent="onDrop">
107- <template v-if="uploading != null">{{ t("media.uploading", { pct: uploading }) }}</template>
108- <template v-else>{{ t("media.drop") }}</template>
125+ <div class="tab-bar">
126+ <div class="tabs">
127+ <button :class="{ on: tab === 'all' }" @click="tab = 'all'">{{ t("common.all") }}<span class="n">{{ total }}</span></button>
128+ <button v-for="g in groups" :key="g.k" :class="{ on: tab === g.k }" @click="tab = g.k">{{ g.k }}<span class="n">{{ g.items.length }}</span></button>
129+ </div>
130+ <span v-if="uploading != null && !adding" class="mono faint uploading">{{ t("media.uploading", { pct: uploading }) }}</span>
131+ <button class="btn dark add" @click="adding = true">{{ t("media.add.open") }}</button>
109132 </div>
110133 <div class="table">
111134 <div class="row head eyebrow"><span /><span>{{ t("common.name") }}</span><span>{{ t("common.type") }}</span><span>{{ t("common.duration") }}</span><span /></div>
@@ -123,6 +146,7 @@ async function toLibrary(item: Item, category: string) {
123146 </div>
124147 <span v-if="!items.length" class="nothing">{{ t("common.nothingYet") }}</span>
125148 </div>
149+ <AddMediaModal v-if="adding && store.project?.id === pid" v-model:tab="addTab" :uploading="uploading" @files="uploadFiles" @close="closeAdd" />
126150 <MediaLightbox v-model="zoom" :items="zoomable">
127151 <template #actions="{ item }"><button class="link-btn" @click="reveal(item.rel)">{{ t("common.showInExplorer") }}</button></template>
128152 </MediaLightbox>
@@ -133,7 +157,11 @@ async function toLibrary(item: Item, category: string) {
133157 .page { flex: 1; overflow: auto; padding: 32px 40px; display: flex; flex-direction: column; gap: 20px; }
134158 .titles { display: flex; flex-direction: column; gap: 4px; }
135159 .h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.015em; }
136-.drop-zone { padding: 18px; }
160+.page.over { outline: 2px dashed var(--ink); outline-offset: -8px; }
161+.tab-bar { display: flex; align-items: flex-end; gap: 16px; border-bottom: 1px solid var(--line); }
162+.tab-bar .tabs { flex: 1; border-bottom: none; }
163+.uploading { font-size: 12px; margin-bottom: 16px; }
164+.add { margin-bottom: 8px; }
137165 .table { display: flex; flex-direction: column; }
138166 .row {
139167 display: grid; grid-template-columns: 72px minmax(0, 1fr) 160px 90px 130px; gap: 16px; align-items: center;
@@ -1,14 +1,16 @@
1 <script setup lang="ts">1 <script setup lang="ts">
2-/** Tout ce que contient le dossier du projet, rangé par catégorie ; on peut y déposer des fichiers. */2+/** Tout ce que contient le dossier du projet, rangé par catégorie ; « Ajouter des médias » (ou un glisser-déposer) l'enrichit. */
3 import { computed, ref, watch } from "vue";3 import { computed, ref, watch } from "vue";
4 import { useI18n } from "vue-i18n";4 import { useI18n } from "vue-i18n";
5 import { useRoute, useRouter } from "vue-router";5 import { useRoute, useRouter } from "vue-router";
6 import MediaLightbox from "../components/MediaLightbox.vue";6 import MediaLightbox from "../components/MediaLightbox.vue";
7 import Thumb from "../components/Thumb.vue";7 import Thumb from "../components/Thumb.vue";
8 import { api, post } from "../lib/api";8 import { api, post } from "../lib/api";
9+import { usePref } from "../lib/composables";
9 import { duration as fmtDuration } from "../lib/format";10 import { duration as fmtDuration } from "../lib/format";
10 import { toast } from "../lib/toast";11 import { toast } from "../lib/toast";
11 import { droppedFiles, upload } from "../lib/upload";12 import { droppedFiles, upload } from "../lib/upload";
13+import AddMediaModal, { ADD_TABS, type AddTab } from "./media/AddMediaModal.vue";
12 import { open, store } from "./store";14 import { open, store } from "./store";
13 15
14 interface Item { name: string; rel: string; kind: string; duration: number | null; size: number }16 interface Item { name: string; rel: string; kind: string; duration: number | null; size: number }
@@ -41,15 +43,36 @@ watch(pid, async (id) => {
41 }43 }
42 }, { immediate: true });44 }, { immediate: true });
43 45
46+// --- la popup « Ajouter des médias » ; ?add=youtube l'ouvre sur un onglet (liens du storyboard)
47+const addTab = usePref<AddTab>("media.addTab", "files");
48+const linked = route.query.add as AddTab | undefined;
49+if (linked && ADD_TABS.includes(linked)) addTab.value = linked;
50+const adding = ref(!!linked);
51+
52+function closeAdd() {
53+ adding.value = false;
54+ // la page est rendue par chemin complet : retirer ?add la recharge
55+ if (route.query.add) router.replace({ query: {} });
56+ else load();
57+}
58+
59+// un téléchargement YouTube qui se termine arrive dans la liste
60+const downloaded = computed(() => store.project?.runtime.assets.downloads.filter((d) => d.finished).length ?? 0);
61+watch(downloaded, (n, before) => { if (n > before) load(); });
62+
44 const total = computed(() => groups.value.reduce((a, g) => a + g.items.length, 0));63 const total = computed(() => groups.value.reduce((a, g) => a + g.items.length, 0));
45 const items = computed(() => tab.value === "all"64 const items = computed(() => tab.value === "all"
46 ? groups.value.flatMap((g) => g.items.map((x) => ({ ...x, k: g.k })))65 ? groups.value.flatMap((g) => g.items.map((x) => ({ ...x, k: g.k })))
47 : (groups.value.find((g) => g.k === tab.value)?.items ?? []).map((x) => ({ ...x, k: x.kind })));66 : (groups.value.find((g) => g.k === tab.value)?.items ?? []).map((x) => ({ ...x, k: x.kind })));
48 67
49-async function onDrop(e: DragEvent) {68+function onDrop(e: DragEvent) {
50 over.value = false;69 over.value = false;
70+ if (adding.value) return;
51 const files = droppedFiles(e);71 const files = droppedFiles(e);
52- if (!files.length) return;72+ if (files.length) uploadFiles(files);
73+}
74+
75+async function uploadFiles(files: File[]) {
53 uploading.value = 0;76 uploading.value = 0;
54 try {77 try {
55 groups.value = (await upload<{ groups: Group[] }>(`/api/projects/${pid.value}/resources/upload`, files, "files", {},78 groups.value = (await upload<{ groups: Group[] }>(`/api/projects/${pid.value}/resources/upload`, files, "files", {},
@@ -94,18 +117,18 @@ async function toLibrary(item: Item, category: string) {
94 </script>117 </script>
95 118
96 <template>119 <template>
97- <div class="page">120+ <div class="page" :class="{ over }" @dragover.prevent="over = !adding" @dragleave.self="over = false" @drop.prevent="onDrop">
98 <div class="titles">121 <div class="titles">
99 <span class="eyebrow">{{ store.project?.title }}</span>122 <span class="eyebrow">{{ store.project?.title }}</span>
100 <span class="h1">{{ t("media.title") }}</span>123 <span class="h1">{{ t("media.title") }}</span>
101 </div>124 </div>
102- <div class="tabs">125+ <div class="tab-bar">
103- <button :class="{ on: tab === 'all' }" @click="tab = 'all'">{{ t("common.all") }}<span class="n">{{ total }}</span></button>126+ <div class="tabs">
104- <button v-for="g in groups" :key="g.k" :class="{ on: tab === g.k }" @click="tab = g.k">{{ g.k }}<span class="n">{{ g.items.length }}</span></button>127+ <button :class="{ on: tab === 'all' }" @click="tab = 'all'">{{ t("common.all") }}<span class="n">{{ total }}</span></button>
105- </div>128+ <button v-for="g in groups" :key="g.k" :class="{ on: tab === g.k }" @click="tab = g.k">{{ g.k }}<span class="n">{{ g.items.length }}</span></button>
106- <div class="drop-zone" :class="{ over }" @dragover.prevent="over = true" @dragleave="over = false" @drop.prevent="onDrop">129+ </div>
107- <template v-if="uploading != null">{{ t("media.uploading", { pct: uploading }) }}</template>130+ <span v-if="uploading != null && !adding" class="mono faint uploading">{{ t("media.uploading", { pct: uploading }) }}</span>
108- <template v-else>{{ t("media.drop") }}</template>131+ <button class="btn dark add" @click="adding = true">{{ t("media.add.open") }}</button>
109 </div>132 </div>
110 <div class="table">133 <div class="table">
111 <div class="row head eyebrow"><span /><span>{{ t("common.name") }}</span><span>{{ t("common.type") }}</span><span>{{ t("common.duration") }}</span><span /></div>134 <div class="row head eyebrow"><span /><span>{{ t("common.name") }}</span><span>{{ t("common.type") }}</span><span>{{ t("common.duration") }}</span><span /></div>
@@ -123,6 +146,7 @@ async function toLibrary(item: Item, category: string) {
123 </div>146 </div>
124 <span v-if="!items.length" class="nothing">{{ t("common.nothingYet") }}</span>147 <span v-if="!items.length" class="nothing">{{ t("common.nothingYet") }}</span>
125 </div>148 </div>
149+ <AddMediaModal v-if="adding && store.project?.id === pid" v-model:tab="addTab" :uploading="uploading" @files="uploadFiles" @close="closeAdd" />
126 <MediaLightbox v-model="zoom" :items="zoomable">150 <MediaLightbox v-model="zoom" :items="zoomable">
127 <template #actions="{ item }"><button class="link-btn" @click="reveal(item.rel)">{{ t("common.showInExplorer") }}</button></template>151 <template #actions="{ item }"><button class="link-btn" @click="reveal(item.rel)">{{ t("common.showInExplorer") }}</button></template>
128 </MediaLightbox>152 </MediaLightbox>
@@ -133,7 +157,11 @@ async function toLibrary(item: Item, category: string) {
133 .page { flex: 1; overflow: auto; padding: 32px 40px; display: flex; flex-direction: column; gap: 20px; }157 .page { flex: 1; overflow: auto; padding: 32px 40px; display: flex; flex-direction: column; gap: 20px; }
134 .titles { display: flex; flex-direction: column; gap: 4px; }158 .titles { display: flex; flex-direction: column; gap: 4px; }
135 .h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.015em; }159 .h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.015em; }
136-.drop-zone { padding: 18px; }160+.page.over { outline: 2px dashed var(--ink); outline-offset: -8px; }
161+.tab-bar { display: flex; align-items: flex-end; gap: 16px; border-bottom: 1px solid var(--line); }
162+.tab-bar .tabs { flex: 1; border-bottom: none; }
163+.uploading { font-size: 12px; margin-bottom: 16px; }
164+.add { margin-bottom: 8px; }
137 .table { display: flex; flex-direction: column; }165 .table { display: flex; flex-direction: column; }
138 .row {166 .row {
139 display: grid; grid-template-columns: 72px minmax(0, 1fr) 160px 90px 130px; gap: 16px; align-items: center;167 display: grid; grid-template-columns: 72px minmax(0, 1fr) 160px 90px 130px; gap: 16px; align-items: center;
added web/src/project/media/AddMediaModal.vue +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+<script lang="ts">
2+export type AddTab = "files" | "youtube" | "images" | "doc";
3+export const ADD_TABS: readonly AddTab[] = ["files", "youtube", "images", "doc"];
4+</script>
5+
6+<script setup lang="ts">
7+/** « Ajouter des médias » : fichiers de l'ordinateur, extraits YouTube, images trouvées par Claude, captures des sources du script. */
8+import { computed, ref } from "vue";
9+import { useI18n } from "vue-i18n";
10+import Modal from "../../components/Modal.vue";
11+import DocTab from "./DocTab.vue";
12+import ImagesTab from "./ImagesTab.vue";
13+import YoutubeTab from "./YoutubeTab.vue";
14+
15+const tab = defineModel<AddTab>("tab", { required: true });
16+defineProps<{ uploading: number | null }>();
17+const emit = defineEmits<{ close: []; files: [files: File[]] }>();
18+const { t } = useI18n();
19+
20+const title = computed(() => t(`media.add.tabs.${tab.value}.label`));
21+
22+const over = ref(false);
23+const input = ref<HTMLInputElement>();
24+
25+function send(list: FileList | null | undefined) {
26+ const files = Array.from(list ?? []);
27+ if (files.length) emit("files", files);
28+}
29+
30+function onDrop(e: DragEvent) {
31+ over.value = false;
32+ send(e.dataTransfer?.files);
33+}
34+
35+function onPick(e: Event) {
36+ const el = e.target as HTMLInputElement;
37+ send(el.files);
38+ el.value = "";
39+}
40+</script>
41+
42+<template>
43+ <Modal :title="title" @close="emit('close')">
44+ <template #nav>
45+ <span class="nav-title">{{ t("media.add.title") }}</span>
46+ <button v-for="k in ADD_TABS" :key="k" class="src" :class="{ on: k === tab }" @click="tab = k">
47+ <span class="label">{{ t(`media.add.tabs.${k}.label`) }}</span>
48+ <span class="desc">{{ t(`media.add.tabs.${k}.desc`) }}</span>
49+ </button>
50+ <span class="spacer" />
51+ <span class="foot">{{ t("media.add.foot") }}</span>
52+ </template>
53+
54+ <template v-if="tab === 'files'">
55+ <div class="drop-zone files" :class="{ over }" @click="input?.click()"
56+ @dragover.prevent="over = true" @dragleave="over = false" @drop.prevent="onDrop">
57+ <template v-if="uploading != null">{{ t("media.uploading", { pct: uploading }) }}</template>
58+ <template v-else>{{ t("media.add.drop") }}</template>
59+ </div>
60+ <input ref="input" type="file" multiple hidden @change="onPick">
61+ </template>
62+ <YoutubeTab v-else-if="tab === 'youtube'" />
63+ <ImagesTab v-else-if="tab === 'images'" />
64+ <DocTab v-else />
65+ </Modal>
66+</template>
67+
68+<style scoped>
69+.nav-title { font-size: 15px; font-weight: 600; padding: 0 10px 14px; }
70+.src {
71+ display: flex; flex-direction: column; gap: 2px; padding: 9px 10px; border: none; border-radius: 4px;
72+ background: transparent; text-align: left; cursor: pointer;
73+}
74+.src:hover { background: var(--hover); }
75+.src.on { background: #fff; }
76+.src .label { font-size: 13.5px; font-weight: 450; color: var(--ink); }
77+.src.on .label { font-weight: 600; }
78+.src .desc { font-size: 12px; color: var(--muted-2); line-height: 1.35; }
79+.foot { font-size: 12px; color: var(--faint); padding: 0 10px; line-height: 1.4; }
80+.files { padding: 36px 18px; }
81+</style>
new file mode 100644
@@ -0,0 +1,81 @@
1+<script lang="ts">
2+export type AddTab = "files" | "youtube" | "images" | "doc";
3+export const ADD_TABS: readonly AddTab[] = ["files", "youtube", "images", "doc"];
4+</script>
5+
6+<script setup lang="ts">
7+/** « Ajouter des médias » : fichiers de l'ordinateur, extraits YouTube, images trouvées par Claude, captures des sources du script. */
8+import { computed, ref } from "vue";
9+import { useI18n } from "vue-i18n";
10+import Modal from "../../components/Modal.vue";
11+import DocTab from "./DocTab.vue";
12+import ImagesTab from "./ImagesTab.vue";
13+import YoutubeTab from "./YoutubeTab.vue";
14+
15+const tab = defineModel<AddTab>("tab", { required: true });
16+defineProps<{ uploading: number | null }>();
17+const emit = defineEmits<{ close: []; files: [files: File[]] }>();
18+const { t } = useI18n();
19+
20+const title = computed(() => t(`media.add.tabs.${tab.value}.label`));
21+
22+const over = ref(false);
23+const input = ref<HTMLInputElement>();
24+
25+function send(list: FileList | null | undefined) {
26+ const files = Array.from(list ?? []);
27+ if (files.length) emit("files", files);
28+}
29+
30+function onDrop(e: DragEvent) {
31+ over.value = false;
32+ send(e.dataTransfer?.files);
33+}
34+
35+function onPick(e: Event) {
36+ const el = e.target as HTMLInputElement;
37+ send(el.files);
38+ el.value = "";
39+}
40+</script>
41+
42+<template>
43+ <Modal :title="title" @close="emit('close')">
44+ <template #nav>
45+ <span class="nav-title">{{ t("media.add.title") }}</span>
46+ <button v-for="k in ADD_TABS" :key="k" class="src" :class="{ on: k === tab }" @click="tab = k">
47+ <span class="label">{{ t(`media.add.tabs.${k}.label`) }}</span>
48+ <span class="desc">{{ t(`media.add.tabs.${k}.desc`) }}</span>
49+ </button>
50+ <span class="spacer" />
51+ <span class="foot">{{ t("media.add.foot") }}</span>
52+ </template>
53+
54+ <template v-if="tab === 'files'">
55+ <div class="drop-zone files" :class="{ over }" @click="input?.click()"
56+ @dragover.prevent="over = true" @dragleave="over = false" @drop.prevent="onDrop">
57+ <template v-if="uploading != null">{{ t("media.uploading", { pct: uploading }) }}</template>
58+ <template v-else>{{ t("media.add.drop") }}</template>
59+ </div>
60+ <input ref="input" type="file" multiple hidden @change="onPick">
61+ </template>
62+ <YoutubeTab v-else-if="tab === 'youtube'" />
63+ <ImagesTab v-else-if="tab === 'images'" />
64+ <DocTab v-else />
65+ </Modal>
66+</template>
67+
68+<style scoped>
69+.nav-title { font-size: 15px; font-weight: 600; padding: 0 10px 14px; }
70+.src {
71+ display: flex; flex-direction: column; gap: 2px; padding: 9px 10px; border: none; border-radius: 4px;
72+ background: transparent; text-align: left; cursor: pointer;
73+}
74+.src:hover { background: var(--hover); }
75+.src.on { background: #fff; }
76+.src .label { font-size: 13.5px; font-weight: 450; color: var(--ink); }
77+.src.on .label { font-weight: 600; }
78+.src .desc { font-size: 12px; color: var(--muted-2); line-height: 1.35; }
79+.foot { font-size: 12px; color: var(--faint); padding: 0 10px; line-height: 1.4; }
80+.files { padding: 36px 18px; }
81+</style>
renamed web/src/project/media/CheckBox.vue +0 -0
similarity index 100%
rename from web/src/project/steps/assets/CheckBox.vue
rename to web/src/project/media/CheckBox.vue
similarity index 100%
rename from web/src/project/steps/assets/CheckBox.vue
rename to web/src/project/media/CheckBox.vue
renamed web/src/project/media/DestPicker.vue +0 -0
similarity index 100%
rename from web/src/project/steps/assets/DestPicker.vue
rename to web/src/project/media/DestPicker.vue
similarity index 100%
rename from web/src/project/steps/assets/DestPicker.vue
rename to web/src/project/media/DestPicker.vue
renamed web/src/project/media/DocTab.vue +4 -4
similarity index 97%
rename from web/src/project/steps/assets/DocTab.vue
rename to web/src/project/media/DocTab.vue
@@ -6,10 +6,10 @@
66 */
77 import { computed, ref } from "vue";
88 import { useI18n } from "vue-i18n";
9-import { toast } from "../../../lib/toast";
10-import { upload } from "../../../lib/upload";
11-import { act, receive, running, store, update } from "../../store";
12-import type { Project, ResearchSource } from "../../types";
9+import { toast } from "../../lib/toast";
10+import { upload } from "../../lib/upload";
11+import { act, receive, running, store, update } from "../store";
12+import type { Project, ResearchSource } from "../types";
1313 import CheckBox from "./CheckBox.vue";
1414
1515 const { t } = useI18n();
similarity index 97%
rename from web/src/project/steps/assets/DocTab.vue
rename to web/src/project/media/DocTab.vue
@@ -6,10 +6,10 @@
6 */6 */
7 import { computed, ref } from "vue";7 import { computed, ref } from "vue";
8 import { useI18n } from "vue-i18n";8 import { useI18n } from "vue-i18n";
9-import { toast } from "../../../lib/toast";9+import { toast } from "../../lib/toast";
10-import { upload } from "../../../lib/upload";10+import { upload } from "../../lib/upload";
11-import { act, receive, running, store, update } from "../../store";11+import { act, receive, running, store, update } from "../store";
12-import type { Project, ResearchSource } from "../../types";12+import type { Project, ResearchSource } from "../types";
13 import CheckBox from "./CheckBox.vue";13 import CheckBox from "./CheckBox.vue";
14 14
15 const { t } = useI18n();15 const { t } = useI18n();
renamed web/src/project/media/ImagesTab.vue +5 -5
similarity index 97%
rename from web/src/project/steps/assets/ImagesTab.vue
rename to web/src/project/media/ImagesTab.vue
@@ -6,11 +6,11 @@
66 */
77 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
88 import { useI18n } from "vue-i18n";
9-import { api, post } from "../../../lib/api";
10-import { usePref } from "../../../lib/composables";
11-import { toast } from "../../../lib/toast";
12-import { store } from "../../store";
13-import type { ImagesState } from "../../types";
9+import { api, post } from "../../lib/api";
10+import { usePref } from "../../lib/composables";
11+import { toast } from "../../lib/toast";
12+import { store } from "../store";
13+import type { ImagesState } from "../types";
1414 import CheckBox from "./CheckBox.vue";
1515 import DestPicker from "./DestPicker.vue";
1616
similarity index 97%
rename from web/src/project/steps/assets/ImagesTab.vue
rename to web/src/project/media/ImagesTab.vue
@@ -6,11 +6,11 @@
6 */6 */
7 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";7 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
8 import { useI18n } from "vue-i18n";8 import { useI18n } from "vue-i18n";
9-import { api, post } from "../../../lib/api";9+import { api, post } from "../../lib/api";
10-import { usePref } from "../../../lib/composables";10+import { usePref } from "../../lib/composables";
11-import { toast } from "../../../lib/toast";11+import { toast } from "../../lib/toast";
12-import { store } from "../../store";12+import { store } from "../store";
13-import type { ImagesState } from "../../types";13+import type { ImagesState } from "../types";
14 import CheckBox from "./CheckBox.vue";14 import CheckBox from "./CheckBox.vue";
15 import DestPicker from "./DestPicker.vue";15 import DestPicker from "./DestPicker.vue";
16 16
renamed web/src/project/media/YoutubePlayer.vue +0 -0
similarity index 100%
rename from web/src/project/steps/assets/YoutubePlayer.vue
rename to web/src/project/media/YoutubePlayer.vue
similarity index 100%
rename from web/src/project/steps/assets/YoutubePlayer.vue
rename to web/src/project/media/YoutubePlayer.vue
renamed web/src/project/media/YoutubeTab.vue +6 -6
similarity index 98%
rename from web/src/project/steps/assets/YoutubeTab.vue
rename to web/src/project/media/YoutubeTab.vue
@@ -7,13 +7,13 @@
77 */
88 import { computed, onMounted, onUnmounted, ref, watch } from "vue";
99 import { useI18n } from "vue-i18n";
10-import Seg from "../../../components/Seg.vue";
11-import { usePref } from "../../../lib/composables";
12-import { mmss, mss, mssd, parseTc } from "../../../lib/format";
13-import { toast } from "../../../lib/toast";
10+import Seg from "../../components/Seg.vue";
11+import { usePref } from "../../lib/composables";
12+import { mmss, mss, mssd, parseTc } from "../../lib/format";
13+import { toast } from "../../lib/toast";
1414 import YoutubePlayer from "./YoutubePlayer.vue";
15-import { act, store } from "../../store";
16-import type { AssetDownload } from "../../types";
15+import { act, store } from "../store";
16+import type { AssetDownload } from "../types";
1717 import CheckBox from "./CheckBox.vue";
1818 import DestPicker from "./DestPicker.vue";
1919
similarity index 98%
rename from web/src/project/steps/assets/YoutubeTab.vue
rename to web/src/project/media/YoutubeTab.vue
@@ -7,13 +7,13 @@
7 */7 */
8 import { computed, onMounted, onUnmounted, ref, watch } from "vue";8 import { computed, onMounted, onUnmounted, ref, watch } from "vue";
9 import { useI18n } from "vue-i18n";9 import { useI18n } from "vue-i18n";
10-import Seg from "../../../components/Seg.vue";10+import Seg from "../../components/Seg.vue";
11-import { usePref } from "../../../lib/composables";11+import { usePref } from "../../lib/composables";
12-import { mmss, mss, mssd, parseTc } from "../../../lib/format";12+import { mmss, mss, mssd, parseTc } from "../../lib/format";
13-import { toast } from "../../../lib/toast";13+import { toast } from "../../lib/toast";
14 import YoutubePlayer from "./YoutubePlayer.vue";14 import YoutubePlayer from "./YoutubePlayer.vue";
15-import { act, store } from "../../store";15+import { act, store } from "../store";
16-import type { AssetDownload } from "../../types";16+import type { AssetDownload } from "../types";
17 import CheckBox from "./CheckBox.vue";17 import CheckBox from "./CheckBox.vue";
18 import DestPicker from "./DestPicker.vue";18 import DestPicker from "./DestPicker.vue";
19 19
renamed web/src/project/media/categories.ts +1 -1
similarity index 91%
rename from web/src/project/steps/assets/categories.ts
rename to web/src/project/media/categories.ts
@@ -1,5 +1,5 @@
11 /** Les catégories de la bibliothèque (backend projects/library.py) : mêmes clés côté projet (assets/<clé>/). */
2-import { t } from "../../../i18n";
2+import { t } from "../../i18n";
33
44 export const CATEGORY_IDS = ["broll", "musique", "sfx", "images", "logos", "personnes", "lieux"] as const;
55
similarity index 91%
rename from web/src/project/steps/assets/categories.ts
rename to web/src/project/media/categories.ts
@@ -1,5 +1,5 @@
1 /** Les catégories de la bibliothèque (backend projects/library.py) : mêmes clés côté projet (assets/<clé>/). */1 /** Les catégories de la bibliothèque (backend projects/library.py) : mêmes clés côté projet (assets/<clé>/). */
2-import { t } from "../../../i18n";2+import { t } from "../../i18n";
3 3
4 export const CATEGORY_IDS = ["broll", "musique", "sfx", "images", "logos", "personnes", "lieux"] as const;4 export const CATEGORY_IDS = ["broll", "musique", "sfx", "images", "logos", "personnes", "lieux"] as const;
5 5
modified web/src/project/nav.ts +0 -4
@@ -26,10 +26,6 @@ export function stepSub(p: Project, id: string): string {
2626 case "roast": return p.runtime.roast_score != null ? `${p.runtime.roast_score.toLocaleString(locale())}/10` : "";
2727 case "tournage": return p.runtime.plans.total ? t("nav.sub.shots", { done: p.runtime.plans.done, total: p.runtime.plans.total }) : "";
2828 case "derush": return p.rushes.length ? t("nav.sub.rushes", p.rushes.length) : "";
29- case "assets": {
30- const n = p.runtime.assets.downloads.filter((d) => d.status === "done").length;
31- return n ? t("nav.sub.videos", n) : "";
32- }
3329 case "transcript": return p.source ? (p.source.kind === "import" ? t("nav.sub.imported") : t("nav.sub.roughCut")) : "";
3430 case "selection": return p.runtime.shorts.count ? t("nav.sub.shorts", p.runtime.shorts.count) : "";
3531 case "smontage": return p.runtime.shorts.montages ? `${p.runtime.shorts.montages}/${p.runtime.shorts.count}` : "";
@@ -26,10 +26,6 @@ export function stepSub(p: Project, id: string): string {
26 case "roast": return p.runtime.roast_score != null ? `${p.runtime.roast_score.toLocaleString(locale())}/10` : "";26 case "roast": return p.runtime.roast_score != null ? `${p.runtime.roast_score.toLocaleString(locale())}/10` : "";
27 case "tournage": return p.runtime.plans.total ? t("nav.sub.shots", { done: p.runtime.plans.done, total: p.runtime.plans.total }) : "";27 case "tournage": return p.runtime.plans.total ? t("nav.sub.shots", { done: p.runtime.plans.done, total: p.runtime.plans.total }) : "";
28 case "derush": return p.rushes.length ? t("nav.sub.rushes", p.rushes.length) : "";28 case "derush": return p.rushes.length ? t("nav.sub.rushes", p.rushes.length) : "";
29- case "assets": {
30- const n = p.runtime.assets.downloads.filter((d) => d.status === "done").length;
31- return n ? t("nav.sub.videos", n) : "";
32- }
33 case "transcript": return p.source ? (p.source.kind === "import" ? t("nav.sub.imported") : t("nav.sub.roughCut")) : "";29 case "transcript": return p.source ? (p.source.kind === "import" ? t("nav.sub.imported") : t("nav.sub.roughCut")) : "";
34 case "selection": return p.runtime.shorts.count ? t("nav.sub.shorts", p.runtime.shorts.count) : "";30 case "selection": return p.runtime.shorts.count ? t("nav.sub.shorts", p.runtime.shorts.count) : "";
35 case "smontage": return p.runtime.shorts.montages ? `${p.runtime.shorts.montages}/${p.runtime.shorts.count}` : "";31 case "smontage": return p.runtime.shorts.montages ? `${p.runtime.shorts.montages}/${p.runtime.shorts.count}` : "";
modified web/src/project/steps.ts +0 -1
@@ -17,7 +17,6 @@ export const ALL_STEPS: Step[] = [
1717 step("roast", "pre", true),
1818 step("tournage", "tour", true),
1919 step("derush", "post", true),
20- step("assets", "post", true),
2120 step("transcript", "post", true),
2221 step("storyboard", "long", true),
2322 step("montage", "long", true),
@@ -17,7 +17,6 @@ export const ALL_STEPS: Step[] = [
17 step("roast", "pre", true),17 step("roast", "pre", true),
18 step("tournage", "tour", true),18 step("tournage", "tour", true),
19 step("derush", "post", true),19 step("derush", "post", true),
20- step("assets", "post", true),
21 step("transcript", "post", true),20 step("transcript", "post", true),
22 step("storyboard", "long", true),21 step("storyboard", "long", true),
23 step("montage", "long", true),22 step("montage", "long", true),
deleted web/src/project/steps/AssetsStep.vue +0 -38
deleted file mode 100644
@@ -1,38 +0,0 @@
1-<script setup lang="ts">
2-/** Assets : ce qu'il faut pour le montage — extraits YouTube, images trouvées par Claude, captures des sources du script. */
3-import { computed } from "vue";
4-import { useI18n } from "vue-i18n";
5-import { usePref } from "../../lib/composables";
6-import { store } from "../store";
7-import DocTab from "./assets/DocTab.vue";
8-import ImagesTab from "./assets/ImagesTab.vue";
9-import YoutubeTab from "./assets/YoutubeTab.vue";
10-
11-type Tab = "youtube" | "images" | "doc";
12-const tab = usePref<Tab>("assets.tab", "youtube");
13-const project = computed(() => store.project!);
14-const { t } = useI18n();
15-
16-const tabs = computed<[Tab, string, number | null][]>(() => [
17- ["youtube", t("sourcing.tabs.youtube"), project.value.assets.videos.length || null],
18- ["images", t("sourcing.tabs.images"), null],
19- ["doc", t("sourcing.tabs.doc"), project.value.research.length || null],
20-]);
21-</script>
22-
23-<template>
24- <div class="assets">
25- <div class="tabs">
26- <button v-for="[k, label, n] in tabs" :key="k" :class="{ on: k === tab }" @click="tab = k">
27- {{ label }}<span v-if="n" class="n">{{ n }}</span>
28- </button>
29- </div>
30- <YoutubeTab v-if="tab === 'youtube'" />
31- <ImagesTab v-else-if="tab === 'images'" />
32- <DocTab v-else />
33- </div>
34-</template>
35-
36-<style scoped>
37-.assets { padding: 20px 32px 40px; display: flex; flex-direction: column; gap: 22px; }
38-</style>
deleted file mode 100644
@@ -1,38 +0,0 @@
1-<script setup lang="ts">
2-/** Assets : ce qu'il faut pour le montage — extraits YouTube, images trouvées par Claude, captures des sources du script. */
3-import { computed } from "vue";
4-import { useI18n } from "vue-i18n";
5-import { usePref } from "../../lib/composables";
6-import { store } from "../store";
7-import DocTab from "./assets/DocTab.vue";
8-import ImagesTab from "./assets/ImagesTab.vue";
9-import YoutubeTab from "./assets/YoutubeTab.vue";
10-
11-type Tab = "youtube" | "images" | "doc";
12-const tab = usePref<Tab>("assets.tab", "youtube");
13-const project = computed(() => store.project!);
14-const { t } = useI18n();
15-
16-const tabs = computed<[Tab, string, number | null][]>(() => [
17- ["youtube", t("sourcing.tabs.youtube"), project.value.assets.videos.length || null],
18- ["images", t("sourcing.tabs.images"), null],
19- ["doc", t("sourcing.tabs.doc"), project.value.research.length || null],
20-]);
21-</script>
22-
23-<template>
24- <div class="assets">
25- <div class="tabs">
26- <button v-for="[k, label, n] in tabs" :key="k" :class="{ on: k === tab }" @click="tab = k">
27- {{ label }}<span v-if="n" class="n">{{ n }}</span>
28- </button>
29- </div>
30- <YoutubeTab v-if="tab === 'youtube'" />
31- <ImagesTab v-else-if="tab === 'images'" />
32- <DocTab v-else />
33- </div>
34-</template>
35-
36-<style scoped>
37-.assets { padding: 20px 32px 40px; display: flex; flex-direction: column; gap: 22px; }
38-</style>
modified web/src/project/steps/StoryboardStep.vue +1 -1
@@ -438,7 +438,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKey));
438438 @input="change(b, 'search', ($event.target as HTMLInputElement).value)" @blur="SB.flush()">
439439 <span class="find-links">
440440 <button class="link-btn" @click="copySearch(b)">{{ tr("editing.storyboard.broll.copy") }}</button>
441- <RouterLink :to="`/p/${project.id}/assets`">{{ tr("editing.storyboard.broll.goAssets") }}</RouterLink>
441+ <RouterLink :to="`/p/${project.id}/ressources?add=youtube`">{{ tr("editing.storyboard.broll.goMedia") }}</RouterLink>
442442 </span>
443443 </div>
444444 <div v-if="isImage(b.src)">
@@ -438,7 +438,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKey));
438 @input="change(b, 'search', ($event.target as HTMLInputElement).value)" @blur="SB.flush()">438 @input="change(b, 'search', ($event.target as HTMLInputElement).value)" @blur="SB.flush()">
439 <span class="find-links">439 <span class="find-links">
440 <button class="link-btn" @click="copySearch(b)">{{ tr("editing.storyboard.broll.copy") }}</button>440 <button class="link-btn" @click="copySearch(b)">{{ tr("editing.storyboard.broll.copy") }}</button>
441- <RouterLink :to="`/p/${project.id}/assets`">{{ tr("editing.storyboard.broll.goAssets") }}</RouterLink>441+ <RouterLink :to="`/p/${project.id}/ressources?add=youtube`">{{ tr("editing.storyboard.broll.goMedia") }}</RouterLink>
442 </span>442 </span>
443 </div>443 </div>
444 <div v-if="isImage(b.src)">444 <div v-if="isImage(b.src)">
modified web/src/project/steps/montage/Preview.vue +1 -1
@@ -346,7 +346,7 @@ defineExpose({ capture });
346346 </optgroup>
347347 </select>
348348 <button class="btn sm" @click.stop="copySearch(beat)">{{ tr("editing.storyboard.broll.copy") }}</button>
349- <RouterLink class="btn sm" :to="`/p/${route.params.pid}/assets`">{{ tr("editing.storyboard.broll.goAssets") }}</RouterLink>
349+ <RouterLink class="btn sm" :to="`/p/${route.params.pid}/ressources?add=youtube`">{{ tr("editing.storyboard.broll.goMedia") }}</RouterLink>
350350 </span>
351351 </div>
352352 <div v-else-if="beat" class="slide" :class="[beat.type === 'transition' ? 'full' : beat.place, beat.type, { off: !beat.on, img: isImage(beat.src) }]">
@@ -346,7 +346,7 @@ defineExpose({ capture });
346 </optgroup>346 </optgroup>
347 </select>347 </select>
348 <button class="btn sm" @click.stop="copySearch(beat)">{{ tr("editing.storyboard.broll.copy") }}</button>348 <button class="btn sm" @click.stop="copySearch(beat)">{{ tr("editing.storyboard.broll.copy") }}</button>
349- <RouterLink class="btn sm" :to="`/p/${route.params.pid}/assets`">{{ tr("editing.storyboard.broll.goAssets") }}</RouterLink>349+ <RouterLink class="btn sm" :to="`/p/${route.params.pid}/ressources?add=youtube`">{{ tr("editing.storyboard.broll.goMedia") }}</RouterLink>
350 </span>350 </span>
351 </div>351 </div>
352 <div v-else-if="beat" class="slide" :class="[beat.type === 'transition' ? 'full' : beat.place, beat.type, { off: !beat.on, img: isImage(beat.src) }]">352 <div v-else-if="beat" class="slide" :class="[beat.type === 'transition' ? 'full' : beat.place, beat.type, { off: !beat.on, img: isImage(beat.src) }]">