This commit is contained in:
parent
11fa3d1558
commit
ad01868a9f
23 changed files with 328 additions and 97 deletions
29
Dockerfile
29
Dockerfile
|
|
@ -1,29 +0,0 @@
|
|||
FROM oven/bun:1 AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Installe les dépendances de développement.
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
# Installe les dépendances de production.
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
# Récupère node_modules et les fichiers du projet.
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules/ node_modules
|
||||
COPY . .
|
||||
# Compile le projet.
|
||||
ENV NODE_ENV=production
|
||||
RUN bun --bun vite build
|
||||
|
||||
# Créé le nécessaire pour Angie, le proxy inversé servant l'application.
|
||||
FROM docker.angie.software/angie:minimal AS release
|
||||
COPY --from=prerelease /usr/src/app/dist/ /usr/share/angie/html/
|
||||
COPY ./docker/default.conf /etc/angie/http.d/default.conf
|
||||
|
||||
# Démarre Angie.
|
||||
EXPOSE 80
|
||||
CMD ["angie", "-g", "daemon off;"]
|
||||
12
bun.lock
12
bun.lock
|
|
@ -213,11 +213,11 @@
|
|||
|
||||
"@effect/experimental": ["@effect/experimental@0.41.4", "", { "dependencies": { "msgpackr": "^1.10.2", "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.77.4", "@effect/platform-node": "^0.73.4", "effect": "^3.13.4", "ioredis": "^5", "lmdb": "^3", "ws": "^8" }, "optionalPeers": ["@effect/platform-node", "ioredis", "lmdb", "ws"] }, "sha512-celrEhl/K2Eis906d3/oOv/3/w+M1jcONvrChG5qQgWYo6CJjSJm4xXopcTbiBhxAhvkd06zABkB3tr/lptUCA=="],
|
||||
|
||||
"@effect/platform": ["@effect/platform@0.77.4", "", { "dependencies": { "find-my-way-ts": "^0.1.5", "multipasta": "^0.2.5" }, "peerDependencies": { "effect": "^3.13.4" } }, "sha512-3jBDn4bAWqRSKARmoMY2NmMxIMM5Py5BqJdUuYJqPyI6jbz7qkOMwgeaF62rWqiqhQLHLTgU4QL/cPwyZl7mVg=="],
|
||||
"@effect/platform": ["@effect/platform@0.77.6", "", { "dependencies": { "find-my-way-ts": "^0.1.5", "multipasta": "^0.2.5" }, "peerDependencies": { "effect": "^3.13.6" } }, "sha512-ghhLNyj/UoQvmp2I29nqngMlAzQB72BhjUKcOA58cUPaUUwNy3K2jmUAzdU6SB3RHIObsX44CM/jXZiYfTv59A=="],
|
||||
|
||||
"@effect/sql": ["@effect/sql@0.30.4", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.25.1", "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.41.4", "@effect/platform": "^0.77.4", "effect": "^3.13.4" } }, "sha512-hyGOZsNRlw09yqBcXCTr1o2+vVIVxEnBMNZzi0ZX5JbS8JBT05O7xAu3OYEzSU6bC5i+uVpJJUgdx2zDlOLkeA=="],
|
||||
|
||||
"@effect/sql-drizzle": ["@effect/sql-drizzle@0.29.4", "", { "peerDependencies": { "@effect/sql": "^0.30.4", "drizzle-orm": "^0.31", "effect": "^3.13.4" } }, "sha512-Hz8RypUu5g/V3DVmD0WLK+Nqy9H/V/GA2ew41sSZhJ0fTmxaCoQ08it23qypeNN66CCJNF5uE9OWnHl10cfKGA=="],
|
||||
"@effect/sql-drizzle": ["@effect/sql-drizzle@0.29.6", "", { "peerDependencies": { "@effect/sql": "^0.30.6", "drizzle-orm": "^0.31", "effect": "^3.13.6" } }, "sha512-ST3HEXQZfp9tUXKOKxpfh9bbTukJsomyXXzZLvPo/KvF8nPpRltovYNT55Dsk+0vN8JyOHri+mX8hKBG5JzXcA=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
|
|
@ -623,7 +623,7 @@
|
|||
|
||||
"easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="],
|
||||
|
||||
"effect": ["effect@3.13.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-XZgCImyBpONuKdsBRk37JfV7242yxTu8r/TcL5ZELIyqRbMYa+Prr86cz1INxXi7iFXfU0havZkCJGyYp1BsiA=="],
|
||||
"effect": ["effect@3.13.6", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-NKmzyIuOb2UuHFPRz9EYScbhMBxXkzjPRuu+4axE+hMk1f0U7TZxzi2CP3TVVxA2kzvh00aBQEbyH7Opq4PnWg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.109", "", {}, "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ=="],
|
||||
|
||||
|
|
@ -649,7 +649,7 @@
|
|||
|
||||
"eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@4.9.0", "", { "dependencies": { "@typescript-eslint/types": "^8.24.0", "@typescript-eslint/utils": "^8.24.0", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-76lDfJnonOcXGW3bEXuqhEGId0LrOlvIE1yLHvK/eKMMPOc0b43KchAIR2Bdbqlg+LPXU5/Q+UzuzkO+cWHT6w=="],
|
||||
|
||||
"eslint-plugin-vue": ["eslint-plugin-vue@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "globals": "^13.24.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-b/Y05HYmnB/32wqVcjxjHZzNpwxj1onBOvqW89W+V+XNG1dRuaFbNd3vT9CLbr2LXjEoq+3vn8DanWf7XU22Ug=="],
|
||||
"eslint-plugin-vue": ["eslint-plugin-vue@10.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" } }, "sha512-XKckedtajqwmaX6u1VnECmZ6xJt+YvlmMzBPZd+/sI3ub2lpYZyFnsyWo7c3nMOQKJQudeyk1lw/JxdgeKT64w=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
|
||||
|
||||
|
|
@ -1037,8 +1037,6 @@
|
|||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="],
|
||||
|
|
@ -1129,8 +1127,6 @@
|
|||
|
||||
"eslint/file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"eslint-plugin-vue/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||
|
||||
"fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
|
|
|||
53
containers/Dockerfile
Normal file
53
containers/Dockerfile
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
FROM oven/bun:1 AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Définis les variables d'environnement.
|
||||
ARG VERSION="0.1"
|
||||
|
||||
# Installe les dépendances de développement.
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
# Installe les dépendances de production.
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
# Récupère node_modules et les fichiers du projet.
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules/ node_modules
|
||||
COPY . .
|
||||
# Compile le projet.
|
||||
ENV NODE_ENV=production
|
||||
RUN bun --bun vite build
|
||||
|
||||
# Créé le nécessaire pour Angie, le proxy inversé servant l'application.
|
||||
FROM docker.angie.software/angie:minimal AS release
|
||||
COPY --from=prerelease /usr/src/app/dist/ /usr/share/angie/html/
|
||||
COPY containers/angie/default.conf /etc/angie/http.d/default.conf
|
||||
|
||||
# Met en place les métadonnées OCI.
|
||||
LABEL org.opencontainers.image.title=journal-media-vue \
|
||||
org.opencontainers.image.description="A modern client-server application for the Soulseek file sharing network" \
|
||||
org.opencontainers.image.authors="gcch" \
|
||||
org.opencontainers.image.vendor="gcch" \
|
||||
org.opencontainers.image.licenses=AGPL-3.0
|
||||
# org.opencontainers.image.url=https://slskd.org \
|
||||
# org.opencontainers.image.source=https://github.com/slskd/slskd \
|
||||
# org.opencontainers.image.documentation=https://github.com/slskd/slskd \
|
||||
# org.opencontainers.image.version=$VERSION \
|
||||
# org.opencontainers.image.revision=$REVISION \
|
||||
# org.opencontainers.image.created=$BUILD_DATE
|
||||
|
||||
# Démarre Angie.
|
||||
EXPOSE 80
|
||||
CMD ["angie", "-g", "daemon off;"]
|
||||
|
||||
# Met en place une vérification périodique de la bonne santé du conteneur.
|
||||
HEALTHCHECK \
|
||||
--interval=60s \
|
||||
--timeout=3s \
|
||||
--start-period=5s \
|
||||
--retries=3 \
|
||||
CMD wget -q -O - http://localhost:80/
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
"quartary",
|
||||
"fieldset",
|
||||
"tabindex",
|
||||
"currentcolor"
|
||||
"currentcolor",
|
||||
"labelledby"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ services:
|
|||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: containers/Dockerfile
|
||||
container_name: journal-media-vue
|
||||
ports:
|
||||
- 127.0.0.1:8080:8
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.77.4",
|
||||
"@effect/sql-drizzle": "^0.29.4",
|
||||
"@effect/platform": "^0.77.6",
|
||||
"@effect/sql-drizzle": "^0.29.6",
|
||||
"@thi.ng/color-palettes": "^1.4.36",
|
||||
"@thi.ng/pixel": "^7.3.19",
|
||||
"@thi.ng/pixel-dither": "^1.1.159",
|
||||
"a11y-dialog": "^8.1.1",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"effect": "^3.13.4",
|
||||
"effect": "^3.13.6",
|
||||
"pinia": "^3.0.1",
|
||||
"sqlocal": "^0.14.0",
|
||||
"vue": "^3.5.13",
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"drizzle-kit": "^0.30.5",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-perfectionist": "^4.9.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"knip": "^5.45.0",
|
||||
|
|
|
|||
|
|
@ -90,12 +90,12 @@
|
|||
tabindex="0" @click="onRowClicked(result[0])" @keypress="onRowClicked(result[0])"
|
||||
>
|
||||
<th class="name" scope="row">
|
||||
{{ result[1].original_result_index }}
|
||||
{{ result[1].originalResultIndex }}
|
||||
</th>
|
||||
<th class="name" scope="row">
|
||||
{{ result[1].original_title }}
|
||||
{{ result[1].originalTitle }}
|
||||
</th>
|
||||
<td class="release-date">{{ result[1].release_date }}</td>
|
||||
<td class="release-date">{{ result[1].releaseDate }}</td>
|
||||
<td class="popularite">{{ result[1].popularity }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import type { MergedTmdbLocalData } from "@/libs/search/schemas";
|
||||
import type { ComputedRef, Ref, ShallowRef } from "vue";
|
||||
|
||||
import ImposterBox from "@/components/dialogs/ImposterBox.vue";
|
||||
import { Images } from "@/services/images.ts";
|
||||
import { RuntimeClient } from "@/services/runtime-client";
|
||||
import { Url } from "@effect/platform";
|
||||
import { Effect } from "effect";
|
||||
import { onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { watchEffect } from "vue";
|
||||
import { useTemplateRef } from "vue";
|
||||
import { Effect, pipe } from "effect";
|
||||
import { isTruthy } from "effect/Predicate";
|
||||
import { onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
// Émissions et props
|
||||
const emit = defineEmits(["dialog-hidden"]);
|
||||
const { entryData } = defineProps<{ entryData: MergedTmdbLocalData }>();
|
||||
|
||||
const ditheredPoster = ref<HTMLCanvasElement>();
|
||||
const imageContainer = useTemplateRef("imageContainer");
|
||||
const imageContainer: Readonly<ShallowRef<HTMLDivElement | null>> = useTemplateRef("imageContainer");
|
||||
const ditheredPoster: Ref<HTMLCanvasElement | undefined> = ref<HTMLCanvasElement>();
|
||||
const isFirstTimeEntryEditing: Ref<boolean> = ref<boolean>(false);
|
||||
|
||||
const closeDialog = () => {
|
||||
const hasEntry: ComputedRef<boolean> = computed(() => isTruthy(entryData.entryId));
|
||||
const hasUniqueOriginalTitle: ComputedRef<boolean> = computed(() =>
|
||||
entryData.originalTitle.toLowerCase() !== entryData.title.toLowerCase()
|
||||
);
|
||||
|
||||
const firstTimeEditingButtonText: ComputedRef<"Ajouter" | "Annuler"> = computed(() =>
|
||||
isFirstTimeEntryEditing.value ? "Annuler" : "Ajouter"
|
||||
);
|
||||
|
||||
// Gestionnaires d'événements
|
||||
const closeDialog = (event?: Event): void => {
|
||||
event?.preventDefault();
|
||||
emit("dialog-hidden");
|
||||
};
|
||||
const toggleFirstTimeEditing = (event?: Event): void => {
|
||||
event?.preventDefault();
|
||||
isFirstTimeEntryEditing.value = !isFirstTimeEntryEditing.value;
|
||||
};
|
||||
|
||||
watchEffect(async () => {
|
||||
// Cycles
|
||||
watch(() => entryData, async (): Promise<void> => {
|
||||
ditheredPoster.value = await RuntimeClient.runPromise(Effect.gen(function*() {
|
||||
if (!entryData.artWorkCoverPath || !imageContainer.value) return undefined;
|
||||
const imageService: Images = yield* Images;
|
||||
const originalUrl = yield* Url.fromString(`https://image.tmdb.org/t/p/w500/${entryData.artWorkCoverPath}`);
|
||||
const originalImage = yield* imageService.imageFromUrl(originalUrl);
|
||||
const ditheredImage = yield* imageService.ditherImage(originalImage, imageContainer.value);
|
||||
if (!entryData.posterUrl) return undefined;
|
||||
console.debug("dithering");
|
||||
|
||||
return ditheredImage;
|
||||
const imageService: Images = yield* Images;
|
||||
|
||||
return yield* pipe(
|
||||
Url.fromString(`https://image.tmdb.org/t/p/w500/${entryData.posterUrl}`),
|
||||
Effect.andThen((url: URL) => imageService.imageFromUrl(url)),
|
||||
Effect.andThen((img: HTMLImageElement) => imageService.ditherImage(img, imageContainer.value ?? undefined)),
|
||||
Effect.andThen(dithered => dithered.canvas),
|
||||
);
|
||||
|
||||
// const base64 = encodeBase64Url(ditheredImage.buffer.data);
|
||||
// console.debug(base64.length);
|
||||
}));
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
console.debug("EditEntryDialog mounted");
|
||||
|
|
@ -43,13 +68,47 @@
|
|||
<template #title>Éditer une entrée</template>
|
||||
|
||||
<template #content>
|
||||
<section aria-labelledby="media-title" class="switcher">
|
||||
<section aria-labelledby="media-title" class="switcher container">
|
||||
<div ref="imageContainer" class="canvas-container"> </div>
|
||||
|
||||
<div class="stack">
|
||||
<h3 id="media-title">{{ entryData.original_title }}</h3>
|
||||
<p class="center">{{ entryData.release_date }} | {{ entryData.original_language }} </p>
|
||||
<h3 id="media-title">{{ entryData.originalTitle }}</h3>
|
||||
<p class="original-metadata">
|
||||
<span v-if="hasUniqueOriginalTitle">{{ entryData.title }} | </span>
|
||||
{{ entryData.releaseDate }} | {{ entryData.originalLanguage }} | {{ entryData.popularity }}
|
||||
</p>
|
||||
|
||||
<p class="overview">{{ entryData.overview }}</p>
|
||||
|
||||
<h3>Journal</h3>
|
||||
<div class="cluster entry-state">
|
||||
<p>
|
||||
<strong>État : </strong>
|
||||
<span v-if="hasEntry"></span>
|
||||
<span v-else>Pas encore dans le journal.</span>
|
||||
</p>
|
||||
|
||||
<button v-if="!hasEntry" :class="{ invert: isFirstTimeEntryEditing }" @click="toggleFirstTimeEditing">
|
||||
{{ firstTimeEditingButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form v-if="hasEntry || isFirstTimeEntryEditing" class="cluster entry-metadata">
|
||||
<div class="field stack">
|
||||
<label for="date-created">Date de création</label>
|
||||
<input id="date-created" type="datetime-local">
|
||||
</div>
|
||||
|
||||
<div class="field stack">
|
||||
<label for="date-created">Date de modification</label>
|
||||
<input id="date-created" type="datetime-local">
|
||||
</div>
|
||||
|
||||
<div class="field stack">
|
||||
<label for="date-created">Date d'obtention</label>
|
||||
<input id="date-created" type="datetime-local">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -72,7 +131,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
inline-size: 85vi;
|
||||
max-inline-size: 74rem;
|
||||
}
|
||||
|
||||
.overview {
|
||||
max-inline-size: 40rem;
|
||||
}
|
||||
|
||||
.entry-metadata {
|
||||
--layout-cluster-gap: var(--s0);
|
||||
margin-block-start: var(--s2);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
4
src/components/loading/LoadingBox.vue
Normal file
4
src/components/loading/LoadingBox.vue
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
CREATE TABLE `diary_entries` (
|
||||
`appreciation` text NOT NULL,
|
||||
`art_work_id` integer NOT NULL,
|
||||
`commentary` text,
|
||||
`date_created` integer NOT NULL,
|
||||
`date_modified` integer NOT NULL,
|
||||
`date_obtained` integer NOT NULL,
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`state_id` integer NOT NULL,
|
||||
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
|
|
@ -59,4 +62,10 @@ CREATE TABLE `media_types` (
|
|||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `media_types_name_unique` ON `media_types` (`name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `media_types_slug_unique` ON `media_types` (`slug`);
|
||||
CREATE UNIQUE INDEX `media_types_slug_unique` ON `media_types` (`slug`);--> statement-breakpoint
|
||||
CREATE TABLE `posters` (
|
||||
`art_work_id` integer NOT NULL,
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`image` blob NOT NULL,
|
||||
FOREIGN KEY (`art_work_id`) REFERENCES `art_works`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
|
|
@ -1,12 +1,19 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "8b217318-7662-4f81-bc55-92d5c6ddf2b2",
|
||||
"id": "01d72a44-6bdd-4f2a-b1dd-34546aa1b734",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"diary_entries": {
|
||||
"name": "diary_entries",
|
||||
"columns": {
|
||||
"appreciation": {
|
||||
"name": "appreciation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"art_work_id": {
|
||||
"name": "art_work_id",
|
||||
"type": "integer",
|
||||
|
|
@ -14,6 +21,13 @@
|
|||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"commentary": {
|
||||
"name": "commentary",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"date_created": {
|
||||
"name": "date_created",
|
||||
"type": "integer",
|
||||
|
|
@ -28,6 +42,13 @@
|
|||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"date_obtained": {
|
||||
"name": "date_obtained",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
|
|
@ -404,6 +425,51 @@
|
|||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"posters": {
|
||||
"name": "posters",
|
||||
"columns": {
|
||||
"art_work_id": {
|
||||
"name": "art_work_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"posters_art_work_id_art_works_id_fk": {
|
||||
"name": "posters_art_work_id_art_works_id_fk",
|
||||
"tableFrom": "posters",
|
||||
"tableTo": "art_works",
|
||||
"columnsFrom": [
|
||||
"art_work_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1740814587298,
|
||||
"tag": "0000_unusual_karen_page",
|
||||
"when": 1741093519331,
|
||||
"tag": "0000_goofy_vanisher",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -20,3 +20,14 @@ export const DIARY_ENTRY_STATES = {
|
|||
/** Un média ayant été regardé au mois une fois. */
|
||||
WATCHED: "watched",
|
||||
} as const;
|
||||
|
||||
export const APPRECIATION_STATES = {
|
||||
/** Oeuvre appréciée. */
|
||||
APPRECIATED: "appreciated",
|
||||
/** Oeuvre non appréciée (détestée). */
|
||||
DISLIKED: "disliked",
|
||||
/** Oeuvre laissant de marbre. */
|
||||
NEUTRAL: "neutral",
|
||||
/** Appréciation inconnue. */
|
||||
UNKNOWN: "unknown",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -5,15 +5,18 @@ import * as t from "drizzle-orm/sqlite-core";
|
|||
import { sqliteTable as table } from "drizzle-orm/sqlite-core";
|
||||
import { Schema } from "effect";
|
||||
|
||||
import { DIARY_ENTRY_STATES } from "./constants";
|
||||
import { APPRECIATION_STATES, DIARY_ENTRY_STATES } from "./constants";
|
||||
import { ArtWorks, Genres } from "./works";
|
||||
|
||||
// Tables
|
||||
|
||||
export const DiaryEntries = table("diary_entries", {
|
||||
appreciation: t.text("appreciation").$type<Values<typeof APPRECIATION_STATES>>().notNull(),
|
||||
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
||||
commentary: t.text("commentary").notNull(),
|
||||
dateCreated: t.integer("date_created", { mode: "timestamp" }).notNull(),
|
||||
dateModified: t.integer("date_modified", { mode: "timestamp" }).notNull(),
|
||||
dateObtained: t.integer("date_obtained", { mode: "timestamp" }).notNull(),
|
||||
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
||||
stateId: t.integer("state_id").references((): AnySQLiteColumn => DiaryEntriesStates.id).notNull(),
|
||||
});
|
||||
|
|
@ -38,9 +41,12 @@ export const Viewings = table("viewings", {
|
|||
// Schémas
|
||||
|
||||
export const DiaryEntrySchema = Schema.Struct({
|
||||
appreciation: Schema.Enums(APPRECIATION_STATES),
|
||||
artWorkId: Schema.NonNegativeInt,
|
||||
commentary: Schema.String.pipe(Schema.optional),
|
||||
dateCreated: Schema.Number,
|
||||
dateModified: Schema.Number,
|
||||
dateObtained: Schema.Number,
|
||||
id: Schema.NonNegativeInt,
|
||||
stateId: Schema.NonNegativeInt,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ export const MediaTypes = table("media_types", {
|
|||
slug: t.text("slug").$type<Values<typeof MEDIA_TYPES>>().notNull().unique(),
|
||||
});
|
||||
|
||||
export const Posters = table("posters", {
|
||||
artWorkId: t.integer("art_work_id").references((): AnySQLiteColumn => ArtWorks.id).notNull(),
|
||||
id: t.integer("id").primaryKey({ autoIncrement: true }),
|
||||
image: t.blob("image", { mode: "buffer" }).notNull(),
|
||||
});
|
||||
|
||||
export const ArtWorks = table("art_works", {
|
||||
coverPath: t.text("cover_path").unique(),
|
||||
dateCreated: t.integer("date_created", { mode: "timestamp" }).notNull(),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const getTmdbSortFunction = (sortData: TmdbSortData) =>
|
|||
|
||||
export const byOriginalIndexAscending = Order.mapInput(
|
||||
Order.number,
|
||||
(data: [number, MergedTmdbLocalData]) => data[1].original_result_index,
|
||||
(data: [number, MergedTmdbLocalData]) => data[1].originalResultIndex,
|
||||
);
|
||||
export const byPopularityAscending = Order.mapInput(
|
||||
Order.number,
|
||||
|
|
@ -55,7 +55,7 @@ export const byPopularityAscending = Order.mapInput(
|
|||
);
|
||||
export const byReleaseDateAscending = Order.mapInput(
|
||||
Order.string,
|
||||
(data: [number, MergedTmdbLocalData]) => data[1].release_date,
|
||||
(data: [number, MergedTmdbLocalData]) => data[1].releaseDate,
|
||||
);
|
||||
export const byTitleAscending = Order.mapInput(
|
||||
Order.string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { MEDIA_TYPES } from "@/db/schemas/constants";
|
||||
import { APPRECIATION_STATES, MEDIA_TYPES } from "@/db/schemas/constants";
|
||||
import { Schema } from "effect";
|
||||
|
||||
export class SearchPageQueryParams extends Schema.Class<SearchPageQueryParams>("SearchPageQueryParams")({
|
||||
|
|
@ -8,20 +8,24 @@ export class SearchPageQueryParams extends Schema.Class<SearchPageQueryParams>("
|
|||
}) {}
|
||||
|
||||
export class MergedTmdbLocalData extends Schema.Class<MergedTmdbLocalData>("MergedTmdbLocalData")({
|
||||
artWorkCoverPath: Schema.Union(Schema.String, Schema.Null),
|
||||
artWorkId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||
artWorkMediumTypeId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||
entryAppreciation: Schema.Enums(APPRECIATION_STATES).pipe(Schema.optional),
|
||||
entryCommentary: Schema.String.pipe(Schema.optional),
|
||||
entryDateCreated: Schema.Date.pipe(Schema.optional),
|
||||
entryDateModified: Schema.Date.pipe(Schema.optional),
|
||||
entryDateObtained: Schema.Date.pipe(Schema.optional),
|
||||
entryId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||
entryStateId: Schema.NonNegativeInt.pipe(Schema.optional),
|
||||
genre_ids: Schema.Array(Schema.NonNegativeInt),
|
||||
original_language: Schema.String,
|
||||
original_result_index: Schema.Int,
|
||||
original_title: Schema.String,
|
||||
genreIds: Schema.Array(Schema.NonNegativeInt),
|
||||
originalLanguage: Schema.String,
|
||||
originalResultIndex: Schema.Int,
|
||||
originalTitle: Schema.String,
|
||||
overview: Schema.String,
|
||||
popularity: Schema.Number,
|
||||
release_date: Schema.String,
|
||||
posterBlob: Schema.Unknown.pipe(Schema.optional),
|
||||
posterUrl: Schema.Union(Schema.String, Schema.Null),
|
||||
releaseDate: Schema.String,
|
||||
title: Schema.String,
|
||||
tmdbId: Schema.NonNegativeInt,
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -153,20 +153,23 @@
|
|||
result.id,
|
||||
yield* Schema.decodeUnknown(MergedTmdbLocalData)(
|
||||
{
|
||||
artWorkCoverPath: result.poster_path,
|
||||
artWorkId: artWork?.id,
|
||||
artWorkMediumTypeId: artWork?.mediumTypeId,
|
||||
entryAppreciation: entry?.appreciation,
|
||||
entryCommentary: entry?.commentary,
|
||||
entryDateCreated: entry?.dateCreated,
|
||||
entryDateModified: entry?.dateModified,
|
||||
entryDateObtained: entry?.dateObtained,
|
||||
entryId: entry?.id,
|
||||
entryStateId: entry?.stateId,
|
||||
genre_ids: result.genre_ids,
|
||||
original_language: result.original_language,
|
||||
original_result_index: index,
|
||||
original_title: result.original_title,
|
||||
genreIds: result.genre_ids,
|
||||
originalLanguage: result.original_language,
|
||||
originalResultIndex: index,
|
||||
originalTitle: result.original_title,
|
||||
overview: result.overview,
|
||||
popularity: result.popularity,
|
||||
release_date: result.release_date,
|
||||
posterUrl: result.poster_path,
|
||||
releaseDate: result.release_date,
|
||||
title: result.title,
|
||||
tmdbId: result.id,
|
||||
} satisfies MergedTmdbLocalData,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { asInt } from "@thi.ng/color-palettes";
|
||||
import { ARGB8888, canvasFromPixelBuffer, defIndexed, imageFromURL, intBufferFromImage } from "@thi.ng/pixel";
|
||||
import {
|
||||
ARGB8888,
|
||||
canvasFromPixelBuffer,
|
||||
defIndexed,
|
||||
imageFromURL,
|
||||
IntBuffer,
|
||||
intBufferFromImage,
|
||||
type IntFormat,
|
||||
} from "@thi.ng/pixel";
|
||||
import { ATKINSON, ditherWith } from "@thi.ng/pixel-dither";
|
||||
import { Data, Effect, pipe } from "effect";
|
||||
|
||||
|
|
@ -10,12 +18,12 @@ export class Images extends Effect.Service<Images>()("Images", {
|
|||
return {
|
||||
ditherImage: (image: HTMLImageElement, parent?: HTMLElement) =>
|
||||
Effect.gen(function*() {
|
||||
const buf = intBufferFromImage(image, ARGB8888).scale(0.8, "cubic");
|
||||
const theme = defIndexed(asInt(["salmon", "black"]));
|
||||
const ditheredBuf = ditherWith(ATKINSON, buf.copy(), {}).as(theme);
|
||||
const buf: IntBuffer = intBufferFromImage(image, ARGB8888).scale(0.8, "cubic");
|
||||
const theme: IntFormat = defIndexed(asInt(["salmon", "black"]));
|
||||
const ditheredBuffer: IntBuffer = ditherWith(ATKINSON, buf.copy(), {}).as(theme);
|
||||
|
||||
const canvas = canvasFromPixelBuffer(ditheredBuf, parent, { pixelated: true });
|
||||
return canvas;
|
||||
const canvas = canvasFromPixelBuffer(ditheredBuffer, parent, { pixelated: true });
|
||||
return { buffer: ditheredBuffer, canvas: canvas };
|
||||
}),
|
||||
imageFromUrl: (url: URL) =>
|
||||
pipe(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SQLocalDrizzle } from "sqlocal/drizzle";
|
||||
|
||||
import v0000 from "@/db/drizzle/0000_unusual_karen_page.sql?raw";
|
||||
import v0000 from "@/db/drizzle/0000_goofy_vanisher.sql?raw";
|
||||
import { Data, Effect } from "effect";
|
||||
|
||||
import { LocalSqlite } from "./db";
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: var(--brkly-font-weight-semibold);
|
||||
}
|
||||
|
||||
.container {
|
||||
--layout-center-max-width: 100%;
|
||||
--layout-center-inline-padding: var(--s1);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@
|
|||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable", "WebWorker"],
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"DOM.AsyncIterable",
|
||||
"WebWorker"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "bundler",
|
||||
|
|
@ -32,8 +38,12 @@
|
|||
"noUncheckedSideEffectImports": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"paths": { "@/*": ["./src/*"] },
|
||||
"plugins": [{ "name": "@vue/typescript-plugin" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{ "name": "@vue/typescript-plugin" }
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
@ -43,10 +53,20 @@
|
|||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"target": "ESNext",
|
||||
"tsBuildInfoFile": ".cache/tsbuildinfo",
|
||||
"useDefineForClassFields": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist/", ".cache"],
|
||||
"include": ["src/router/typed-routes.d.ts", "src/env.d.ts", "**/*", "**/*.vue"]
|
||||
"exclude": [
|
||||
".cache",
|
||||
"dist/",
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/*.vue",
|
||||
"src/vite-env.d.ts",
|
||||
"src/router/typed-routes.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue