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.
d2ebfbd parent: 3b4d23a modified
web/src/i18n/locales/en/editing.ts +2 -2 | @@ -116,7 +116,7 @@ export default { | ||
| 116 | 116 | tabSfx: "Sounds", |
| 117 | 117 | tabMusic: "Music", |
| 118 | 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 | 120 | more: "… and {n} more: refine the search.", |
| 121 | 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 | 218 | toFind: "To find · the search", |
| 219 | 219 | searchPh: "Keywords to search…", |
| 220 | 220 | copy: "Copy the search", |
| 221 | - goAssets: "Search in Assets", | |
| 221 | + goMedia: "Find in Media", | |
| 222 | 222 | where: { project: "Project", library: "Library" }, |
| 223 | 223 | search: "Search: {q}", |
| 224 | 224 | 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 @@ | ||
| 1 | 1 | export default { |
| 2 | 2 | title: "Media", |
| 3 | 3 | uploading: "Uploading · {pct}%", |
| 4 | - drop: "Drop files here to add them to the project", | |
| 5 | 4 | toLibrary: "→ Library", |
| 6 | 5 | addedToLibrary: "Added to the library · {category}", |
| 7 | 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 | }; |
| @@ -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/script.ts +1 -1 | @@ -95,7 +95,7 @@ export default { | ||
| 95 | 95 | search: "Search files", |
| 96 | 96 | pickHint: "Click a file to add it to the shot.", |
| 97 | 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 | 99 | mediaLink: "Media", |
| 100 | 100 | library: "Library · {label}", |
| 101 | 101 | 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 { | ||
| 6 | 6 | roast: "Roasting", |
| 7 | 7 | tournage: "Shoot", |
| 8 | 8 | derush: "Rough cut", |
| 9 | - assets: "Sourcing", | |
| 10 | 9 | transcript: "Source video", |
| 11 | 10 | storyboard: "Storyboard", |
| 12 | 11 | 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 { | ||
| 116 | 116 | tabSfx: "Sons", |
| 117 | 117 | tabMusic: "Musique", |
| 118 | 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 | 120 | more: "… et {n} autres : affine la recherche.", |
| 121 | 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 | 218 | toFind: "À trouver · la recherche", |
| 219 | 219 | searchPh: "Mots-clés à chercher…", |
| 220 | 220 | copy: "Copier la recherche", |
| 221 | - goAssets: "Chercher dans Assets", | |
| 221 | + goMedia: "Chercher dans Médias", | |
| 222 | 222 | where: { project: "Projet", library: "Bibliothèque" }, |
| 223 | 223 | search: "Chercher : {q}", |
| 224 | 224 | 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 @@ | ||
| 1 | 1 | export default { |
| 2 | 2 | title: "Médias", |
| 3 | 3 | uploading: "Envoi · {pct} %", |
| 4 | - drop: "Glisse des fichiers ici pour les ajouter au projet", | |
| 5 | 4 | toLibrary: "→ Bibliothèque", |
| 6 | 5 | addedToLibrary: "Ajouté à la bibliothèque · {category}", |
| 7 | 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 | }; |
| @@ -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/script.ts +1 -1 | @@ -95,7 +95,7 @@ export default { | ||
| 95 | 95 | search: "Cherche un fichier", |
| 96 | 96 | pickHint: "Clique un fichier pour l’ajouter au plan.", |
| 97 | 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 | 99 | mediaLink: "Médias", |
| 100 | 100 | library: "Bibliothèque · {label}", |
| 101 | 101 | 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 { | ||
| 6 | 6 | roast: "Roasting", |
| 7 | 7 | tournage: "Tournage", |
| 8 | 8 | derush: "Dérush", |
| 9 | - assets: "Sourcing", | |
| 10 | 9 | transcript: "Vidéo source", |
| 11 | 10 | storyboard: "Storyboard", |
| 12 | 11 | 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>> = { | ||
| 15 | 15 | roast: defineAsyncComponent(() => import("./steps/RoastStep.vue")), |
| 16 | 16 | tournage: defineAsyncComponent(() => import("./steps/TournageStep.vue")), |
| 17 | 17 | derush: defineAsyncComponent(() => import("./steps/DerushStep.vue")), |
| 18 | - assets: defineAsyncComponent(() => import("./steps/AssetsStep.vue")), | |
| 19 | 18 | transcript: defineAsyncComponent(() => import("./steps/SourceStep.vue")), |
| 20 | 19 | storyboard: defineAsyncComponent(() => import("./steps/StoryboardStep.vue")), |
| 21 | 20 | 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 @@ | ||
| 1 | 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 | 3 | import { computed, ref, watch } from "vue"; |
| 4 | 4 | import { useI18n } from "vue-i18n"; |
| 5 | 5 | import { useRoute, useRouter } from "vue-router"; |
| 6 | 6 | import MediaLightbox from "../components/MediaLightbox.vue"; |
| 7 | 7 | import Thumb from "../components/Thumb.vue"; |
| 8 | 8 | import { api, post } from "../lib/api"; |
| 9 | +import { usePref } from "../lib/composables"; | |
| 9 | 10 | import { duration as fmtDuration } from "../lib/format"; |
| 10 | 11 | import { toast } from "../lib/toast"; |
| 11 | 12 | import { droppedFiles, upload } from "../lib/upload"; |
| 13 | +import AddMediaModal, { ADD_TABS, type AddTab } from "./media/AddMediaModal.vue"; | |
| 12 | 14 | import { open, store } from "./store"; |
| 13 | 15 | |
| 14 | 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 | 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 | 63 | const total = computed(() => groups.value.reduce((a, g) => a + g.items.length, 0)); |
| 45 | 64 | const items = computed(() => tab.value === "all" |
| 46 | 65 | ? groups.value.flatMap((g) => g.items.map((x) => ({ ...x, k: g.k }))) |
| 47 | 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 | 69 | over.value = false; |
| 70 | + if (adding.value) return; | |
| 51 | 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 | 76 | uploading.value = 0; |
| 54 | 77 | try { |
| 55 | 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 | 117 | </script> |
| 95 | 118 | |
| 96 | 119 | <template> |
| 97 | - <div class="page"> | |
| 120 | + <div class="page" :class="{ over }" @dragover.prevent="over = !adding" @dragleave.self="over = false" @drop.prevent="onDrop"> | |
| 98 | 121 | <div class="titles"> |
| 99 | 122 | <span class="eyebrow">{{ store.project?.title }}</span> |
| 100 | 123 | <span class="h1">{{ t("media.title") }}</span> |
| 101 | 124 | </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> | |
| 109 | 132 | </div> |
| 110 | 133 | <div class="table"> |
| 111 | 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 | 146 | </div> |
| 124 | 147 | <span v-if="!items.length" class="nothing">{{ t("common.nothingYet") }}</span> |
| 125 | 148 | </div> |
| 149 | + <AddMediaModal v-if="adding && store.project?.id === pid" v-model:tab="addTab" :uploading="uploading" @files="uploadFiles" @close="closeAdd" /> | |
| 126 | 150 | <MediaLightbox v-model="zoom" :items="zoomable"> |
| 127 | 151 | <template #actions="{ item }"><button class="link-btn" @click="reveal(item.rel)">{{ t("common.showInExplorer") }}</button></template> |
| 128 | 152 | </MediaLightbox> |
| @@ -133,7 +157,11 @@ async function toLibrary(item: Item, category: string) { | ||
| 133 | 157 | .page { flex: 1; overflow: auto; padding: 32px 40px; display: flex; flex-direction: column; gap: 20px; } |
| 134 | 158 | .titles { display: flex; flex-direction: column; gap: 4px; } |
| 135 | 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 | 165 | .table { display: flex; flex-direction: column; } |
| 138 | 166 | .row { |
| 139 | 167 | 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 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | import { computed, ref } from "vue"; |
| 8 | 8 | 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"; | |
| 13 | 13 | import CheckBox from "./CheckBox.vue"; |
| 14 | 14 | |
| 15 | 15 | 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 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; |
| 8 | 8 | 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"; | |
| 14 | 14 | import CheckBox from "./CheckBox.vue"; |
| 15 | 15 | import DestPicker from "./DestPicker.vue"; |
| 16 | 16 | |
| 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 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | import { computed, onMounted, onUnmounted, ref, watch } from "vue"; |
| 9 | 9 | 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"; | |
| 14 | 14 | 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"; | |
| 17 | 17 | import CheckBox from "./CheckBox.vue"; |
| 18 | 18 | import DestPicker from "./DestPicker.vue"; |
| 19 | 19 | |
| 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 @@ | ||
| 1 | 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 | 4 | export const CATEGORY_IDS = ["broll", "musique", "sfx", "images", "logos", "personnes", "lieux"] as const; |
| 5 | 5 | |
| 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/steps.ts +0 -1 | @@ -17,7 +17,6 @@ export const ALL_STEPS: Step[] = [ | ||
| 17 | 17 | step("roast", "pre", true), |
| 18 | 18 | step("tournage", "tour", true), |
| 19 | 19 | step("derush", "post", true), |
| 20 | - step("assets", "post", true), | |
| 21 | 20 | step("transcript", "post", true), |
| 22 | 21 | step("storyboard", "long", true), |
| 23 | 22 | 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)); | ||
| 438 | 438 | @input="change(b, 'search', ($event.target as HTMLInputElement).value)" @blur="SB.flush()"> |
| 439 | 439 | <span class="find-links"> |
| 440 | 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 | 442 | </span> |
| 443 | 443 | </div> |
| 444 | 444 | <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 }); | ||
| 346 | 346 | </optgroup> |
| 347 | 347 | </select> |
| 348 | 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 | 350 | </span> |
| 351 | 351 | </div> |
| 352 | 352 | <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) }]"> |