2025-03-14
Some checks failed
ci/woodpecker/push/publish_instable Pipeline failed

This commit is contained in:
gcch 2025-03-13 16:32:23 +01:00
commit 5688e10d0d
42 changed files with 635 additions and 295 deletions

View file

@ -1,13 +1,218 @@
<script setup lang="ts">
import { useEntryStore } from "@/stores/entry";
import { onMounted } from "vue";
import type { MovieMainCrew } from "@/libs/apis/tmdb/schemas/credits.ts";
import type { TmdbGenreName } from "@/libs/apis/tmdb/schemas/genres.ts";
import type { ArtWorkTitle } from "@/libs/apis/tmdb/schemas/works.ts";
import { getMainMovieCrew } from "@/libs/apis/tmdb/crew.ts";
import { TmdbGenre } from "@/libs/apis/tmdb/schemas/genres.ts";
import { generateDitheredBufferFromUrl } from "@/libs/utils/images.ts";
import { convertToMillions } from "@/libs/utils/numbers.ts";
import { PrettyLogger } from "@/services/logger.ts";
import { RuntimeClient } from "@/services/runtime-client.ts";
import { useEntryStore } from "@/stores/entry.ts";
import { canvasFromPixelBuffer } from "@thi.ng/pixel";
import { Array as Arr, Effect, pipe, String as Str } from "effect";
import { ref, type Ref, type ShallowRef, useTemplateRef, watch } from "vue";
import { useRouter } from "vue-router";
// Types
interface AddEntryDisplayData {
budget: string;
genres: TmdbGenreName[];
hasUniqueOriginalTitle: boolean;
mainCrew: MovieMainCrew;
originalLanguage: string;
originalTitle: ArtWorkTitle;
overview: string;
popularity: number;
releaseDate: string;
revenue: string;
runtime: string;
tagline: string;
title: ArtWorkTitle;
}
// Magasins et routeur
const router = useRouter();
const entryStore = useEntryStore();
onMounted(() => {
console.debug(entryStore.entry);
console.debug(entryStore.tmdbData);
});
// Données
/** Ensemble des données affichées à l'Utilisateur au sein */
const displayData: Ref<AddEntryDisplayData | undefined> = ref<AddEntryDisplayData | undefined>();
/** Conteneur du canvas du poster de l'oeuvre courante avec tramage. */
const imageContainer: Readonly<ShallowRef<HTMLDivElement | null>> = useTemplateRef("imageContainer");
/** Canvas dans lequel est inséré le poster de l'oeuvre courante avec tramage. */
const ditheredPoster: Ref<HTMLCanvasElement | undefined> = ref<HTMLCanvasElement>();
// Cycles
watch(() => entryStore.diaryEntry, async (): Promise<void> => {
if (!entryStore.diaryEntry || !entryStore.tmdbData) {
console.error("AddEntryPage - Pas d'entrée dans le magasin !");
await router.push("/");
return;
}
// Récupère les données à afficher.
displayData.value = {
budget: entryStore.tmdbData.budget ? `${String(convertToMillions(entryStore.tmdbData.budget))} M$` : "?",
genres: Arr.map(entryStore.tmdbData.genres, (genre: TmdbGenre) => genre.name),
hasUniqueOriginalTitle:
Str.toLowerCase(entryStore.diaryEntry.originalTitle) !== Str.toLowerCase(entryStore.diaryEntry.title),
mainCrew: getMainMovieCrew(entryStore.tmdbData.credits.crew),
originalLanguage: entryStore.tmdbData.original_language,
originalTitle: entryStore.diaryEntry.originalTitle,
overview: entryStore.diaryEntry.overview,
popularity: entryStore.diaryEntry.popularity,
releaseDate: entryStore.diaryEntry.releaseDate,
revenue: entryStore.tmdbData.revenue ? `${String(convertToMillions(entryStore.tmdbData.revenue))} M$` : "?",
runtime: `${String(entryStore.tmdbData.runtime)} min`,
tagline: entryStore.tmdbData.tagline,
title: entryStore.diaryEntry.title,
};
// Applique le tramage à l'affiche de l'oeuvre.
await RuntimeClient.runPromise(Effect.gen(function*() {
if (!entryStore.diaryEntry?.posterUrl) return;
yield* pipe(
generateDitheredBufferFromUrl(`https://image.tmdb.org/t/p/w500${entryStore.diaryEntry.posterUrl}`),
Effect.tapError(Effect.logError),
Effect.tap(ditheredBuffer => {
const canvas = canvasFromPixelBuffer(ditheredBuffer, imageContainer.value, { pixelated: true });
ditheredPoster.value = canvas;
}),
Effect.provide(PrettyLogger),
);
}));
}, { immediate: true });
</script>
<template></template>
<template>
<div class="stack">
<h2>Ajouter une entrée</h2>
<aside class="go-back">
<a class="link-with-iconography" href="javascript:void(0)" @click="router.back">
<span class="iconography"></span>
<span class="text">Retour à la recherche</span></a>
</aside>
<div class="container switcher">
<div ref="imageContainer" class="canvas-container"></div>
<div class="stack">
<h3>{{ entryStore.diaryEntry?.originalTitle }}</h3>
<div class="original-metadata">
<span v-if="displayData?.hasUniqueOriginalTitle">{{ displayData?.title }} | </span>
{{ displayData?.releaseDate }} | {{ displayData?.runtime }} | {{ displayData?.originalLanguage }} | {{
displayData?.popularity
}} | {{ displayData?.budget }} -> {{ displayData?.revenue }}
</div>
<div class="main-movie-crew cluster">
<div class="department">
<strong>RÉALISATION </strong>
<ul>
<li v-for="director of displayData?.mainCrew.directors" :key="director.id">{{ director.name }}</li>
<li v-if="displayData?.mainCrew.directors.length === 0">/</li>
</ul>
</div>
<div class="department">
<strong>ÉCRITURE </strong>
<ul>
<li v-for="writer of displayData?.mainCrew.writers" :key="writer.id">{{ writer.name }}</li>
<li v-if="displayData?.mainCrew.writers.length === 0">/</li>
</ul>
</div>
<div class="department">
<strong>MONTAGE </strong>
<ul>
<li v-for="editor of displayData?.mainCrew.editors" :key="editor.id">{{ editor.name }}</li>
<li v-if="displayData?.mainCrew.editors.length === 0">/</li>
</ul>
</div>
</div>
<hr>
<p v-if="displayData?.tagline" class="tagline">{{ displayData?.tagline }}</p>
<p class="overview">{{ displayData?.overview }}</p>
<ul class="genres">
<strong>Genres</strong>
<li v-for="genre of displayData?.genres" :key="Str.toLowerCase(genre)">{{ genre }}</li>
</ul>
<hr>
</div>
</div>
</div>
</template>
<style scoped lang="css">
.go-back {
margin-block-end: var(--s1);
margin-inline-start: calc(var(--s1) * -1);
}
.container {
--threshold: 51rem;
gap: var(--s2);
max-inline-size: 70rem;
}
.canvas-container {
aspect-ratio: 0.6;
width: 400px;
max-width: 400px;
height: 600px;
max-height: 600px;
border: 4px double var(--root-text-color);
background: var(--bg25-tertiary);
> * {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.main-movie-crew {
--layout-cluster-gap: 2ch;
align-items: start;
}
hr {
block-size: 1px;
background: var(--root-text-color);
}
.overview {
hyphens: auto;
text-align: justify;
}
.tagline {
text-transform: uppercase;
}
.genres {
display: inline-flex;
strong {
margin-inline-end: 1ch;
text-transform: uppercase;
}
li + li {
padding-inline-start: 1ch;
&::before {
content: "·";
padding-inline-end: 1ch;
}
}
}
</style>