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

This commit is contained in:
gcch 2025-03-05 09:10:16 +01:00
commit ad01868a9f
23 changed files with 328 additions and 97 deletions

View file

@ -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;"]

View file

@ -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
View 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/

View file

@ -14,6 +14,7 @@
"quartary",
"fieldset",
"tabindex",
"currentcolor"
"currentcolor",
"labelledby"
]
}

View file

@ -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

View file

@ -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",

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,4 @@
<script lang="ts">
</script>
<template></template>

View file

@ -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
);

View file

@ -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": {},

View file

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1740814587298,
"tag": "0000_unusual_karen_page",
"when": 1741093519331,
"tag": "0000_goofy_vanisher",
"breakpoints": true
}
]

View file

@ -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;

View file

@ -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,
});

View file

@ -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(),

View file

@ -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,

View file

@ -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,
}) {}

View file

@ -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,

View file

@ -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(

View file

@ -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";

View file

@ -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);

View file

@ -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"
]
}