Added media support in RTC

This commit is contained in:
DrMint 2024-04-07 02:19:50 +02:00
parent 9a1cbd28f7
commit 35e2991086
18 changed files with 357 additions and 161 deletions

View File

@ -4,6 +4,9 @@
- [Timeline] inline links to pages not working (timeline 2026/06)
- Save cookies for longer than just the session
- [Image] media page
- [Video] media page
- [Audio] media page
## Mid term

View File

@ -5,7 +5,6 @@ import Topbar from "./components/Topbar/Topbar.astro";
import Footer from "./components/Footer.astro";
import AppLayoutTitle from "./components/AppLayoutTitle.astro";
import type { ComponentProps } from "astro/types";
import type { PayloadImage } from "src/shared/payload/payload-sdk";
interface Props {
openGraph?: ComponentProps<typeof Html>["openGraph"];
@ -17,7 +16,7 @@ interface Props {
illustration?: string;
illustrationSize?: string;
illustrationPosition?: string;
backgroundImage?: PayloadImage | undefined;
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
hideFooterLinks?: boolean;
hideHomeButton?: boolean;
}

View File

@ -3,7 +3,7 @@ import type { PayloadImage } from "src/shared/payload/payload-sdk";
import { getRandomId } from "src/utils/random";
interface Props {
img: PayloadImage;
img: Pick<PayloadImage, "url" | "width" | "height">;
}
const {

View File

@ -50,23 +50,14 @@ const { currentTheme } = Astro.locals;
"light-theme": currentTheme === "light",
"dark-theme": currentTheme === "dark",
"texture-dots": !isIOS,
}}
>
}}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/tippy.css" />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="#fdebd4"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#27231e"
/>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fdebd4" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27231e" />
<link rel="manifest" href="/site.webmanifest" />
{/* Meta & OpenGraph */}
@ -80,10 +71,7 @@ const { currentTheme } = Astro.locals;
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={thumbnail} />
<meta
property="og:type"
content={video ? "video.movie" : audio ? "music.song" : "website"}
/>
<meta property="og:type" content={video ? "video.movie" : audio ? "music.song" : "website"} />
<meta property="og:locale" content={currentLocale} />
<meta property="og:site_name" content={t("global.siteName")} />
@ -133,9 +121,7 @@ const { currentTheme } = Astro.locals;
<AppLayoutSpinner />
</html>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style is:global>
html {
@ -467,8 +453,7 @@ const { currentTheme } = Astro.locals;
backdrop-filter: blur(10px);
transition-duration: 250ms;
transition-property: padding-top, box-shadow, background-color, color,
border-color;
transition-property: padding-top, box-shadow, background-color, color, border-color;
&:hover {
--foreground-color: var(--color-base-1000);
@ -587,19 +572,13 @@ const { currentTheme } = Astro.locals;
}
</style>
{
/* ------------------------------------------- JS --------------------------------------------- */
}
{/* ------------------------------------------- JS --------------------------------------------- */}
<script is:inline>
Array.from(document.querySelectorAll(".when-no-js")).forEach((node) =>
node.remove()
);
Array.from(document.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
document.addEventListener("astro:before-swap", ({ newDocument }) => {
Array.from(newDocument.querySelectorAll(".when-no-js")).forEach((node) =>
node.remove()
);
Array.from(newDocument.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
});
</script>

View File

@ -0,0 +1,53 @@
---
interface Props {
id?: string;
header: number;
}
const { header, id } = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
header === 1 ? (
<h1 id={id}>
<slot />
</h1>
) : header === 2 ? (
<h2 id={id}>
<slot />
</h2>
) : header === 3 ? (
<h3 id={id}>
<slot />
</h3>
) : header === 4 ? (
<h4 id={id}>
<slot />
</h4>
) : header === 5 ? (
<h5 id={id}>
<slot />
</h5>
) : (
<h6 id={id}>
<slot />
</h6>
)
}
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
h1,
h2,
h3,
h4,
h5,
h6 {
display: inline-flex;
place-items: center;
gap: 0.5em;
}
</style>

View File

@ -1,10 +1,10 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectiblePreview } from "src/shared/payload/payload-sdk";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
interface Props {
collectible: EndpointCollectiblePreview;
collectible: EndpointCollectible;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);

View File

@ -1,10 +1,10 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointPagePreview } from "src/shared/payload/payload-sdk";
import type { EndpointPage } from "src/shared/payload/payload-sdk";
interface Props {
page: EndpointPagePreview;
page: EndpointPage;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);

View File

@ -1,4 +1,5 @@
---
import HeaderTitle from "components/HeaderTitle.astro";
import RichText from "components/RichText/RichText.astro";
import type { RichTextSectionBlock } from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
@ -13,34 +14,10 @@ const { node, context } = Astro.props;
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
context.depth < 2 ? (
<h2 id={node.anchorHash}>
<HeaderTitle id={node.anchorHash} header={context.depth + 1}>
<span>{`${node.anchorHash} `}</span>
{node.fields.blockName}
</h2>
) : context.depth === 2 ? (
<h3 id={node.anchorHash}>
<span>{`${node.anchorHash} `}</span>
{node.fields.blockName}
</h3>
) : context.depth === 3 ? (
<h4 id={node.anchorHash}>
<span>{`${node.anchorHash} `}</span>
{node.fields.blockName}
</h4>
) : context.depth === 4 ? (
<h5 id={node.anchorHash}>
<span>{`${node.anchorHash} `}</span>
{node.fields.blockName}
</h5>
) : (
<h6 id={node.anchorHash}>
<span>{`${node.anchorHash} `}</span>
{node.fields.blockName}
</h6>
)
}
</HeaderTitle>
<RichText content={node.fields.content} context={{ ...context, depth: context.depth + 1 }} />
@ -51,6 +28,6 @@ const { node, context } = Astro.props;
color: var(--color-base-650);
font-weight: 500;
font-size: 70%;
margin-right: 0.3em;
place-self: end;
}
</style>

View File

@ -19,9 +19,7 @@ interface Props {
const { node, context } = Astro.props;
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
isUploadNodeImageNode(node) ? (

View File

@ -0,0 +1,38 @@
---
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
interface Props {
url: string;
}
const { url } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div class="button">
<a href={url}>
<div class="pressable-label">
<Icon name="material-symbols:left-click" />
<p>{t("global.openMediaPage")}</p>
</div>
</a>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
.button {
display: flex;
place-items: start;
gap: 0.3em;
font-size: 85%;
& > a > div {
padding: 0.3em 0.6em;
padding-right: 0.8em;
}
}
</style>

View File

@ -1,32 +1,54 @@
---
import { type RichTextUploadAudioNode } from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
import { getI18n } from "src/i18n/i18n";
import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
interface Props {
node: RichTextUploadAudioNode;
context: RichTextContext;
}
const { node } = Astro.props;
const {
node: {
value: { id, url, mimeType, translations },
},
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(translations);
---
{
/* ------------------------------------------- HTML ------------------------------------------- */
}
{/* ------------------------------------------- HTML ------------------------------------------- */}
<audio controls>
<source src={node.value.url} type={node.value.mimeType} />
</audio>
<div>
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:headphones" />
{title}
</HeaderTitle>
{
/* ------------------------------------------- CSS -------------------------------------------- */
}
<audio controls>
<source src={url} type={mimeType} />
</audio>
<OpenMediaPageButton url={`/audios/${id}`} />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
audio {
div {
margin-block: 4em;
& > audio {
margin-top: 0.4em;
margin-bottom: 0.2em;
width: 100%;
margin-block: 3em;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
</style>

View File

@ -1,27 +1,60 @@
---
import { type RichTextUploadImageNode } from "src/shared/payload/payload-sdk";
import type { RichTextContext } from "src/utils/richText";
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
import { getI18n } from "src/i18n/i18n";
import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
interface Props {
node: RichTextUploadImageNode;
context: RichTextContext;
}
const { node } = Astro.props;
const {
node: {
value: { id, url, translations },
},
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title }: { title?: string } =
translations.length > 0 ? getLocalizedMatch(translations) : {};
const mediaPage = `/images/${id}`;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<img src={node.value.url} />
<div>
{
title && (
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:image-outline" />
{title}
</HeaderTitle>
)
}
<a href={mediaPage}><img src={url} /></a>
<OpenMediaPageButton url={mediaPage} />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
img {
div {
margin-block: 4em;
& > a > img {
width: 100%;
height: auto;
margin-block: 3em;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
margin-top: 0.4em;
margin-bottom: 0.2em;
}
}
</style>

View File

@ -1,29 +1,61 @@
---
import { getI18n } from "src/i18n/i18n";
import { type RichTextUploadVideoNode } from "src/shared/payload/payload-sdk";
import { formatLocale } from "src/utils/format";
import type { RichTextContext } from "src/utils/richText";
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
interface Props {
node: RichTextUploadVideoNode;
context: RichTextContext;
}
const { node } = Astro.props;
const {
node: {
value: { id, url, thumbnail, mimeType, subtitles, translations },
},
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<video controls>
<source src={node.value.url} type={node.value.mimeType} />
</video>
<div>
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:movie-outline" />
{title}
</HeaderTitle>
<video controls poster={thumbnail?.url}>
<source src={url} type={mimeType} />
{
subtitles.map(({ language, url }) => (
<track label={formatLocale(language)} src={url} kind="subtitles" srclang={language} />
))
}
</video>
<OpenMediaPageButton url={`/videos/${id}`} />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
video {
div {
margin-block: 4em;
& > video {
margin-top: 0.4em;
margin-bottom: 0.2em;
width: 100%;
height: auto;
margin-block: 3em;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
}
</style>

View File

@ -111,4 +111,5 @@ export type WordingKey =
| "global.sources.typeLabel.folder"
| "global.sources.typeLabel.collectible.range.page"
| "global.sources.typeLabel.collectible.range.timestamp"
| "global.sources.typeLabel.collectible.range.custom";
| "global.sources.typeLabel.collectible.range.custom"
| "global.openMediaPage";

View File

@ -1,11 +1,11 @@
---
import type { EndpointFolderPreview } from "src/shared/payload/payload-sdk";
import type { EndpointFolder } from "src/shared/payload/payload-sdk";
import FolderCard from "./FolderCard.astro";
import { getI18n } from "src/i18n/i18n";
interface Props {
title?: string | undefined;
folders: EndpointFolderPreview[];
folders: EndpointFolder[];
}
const { title, folders } = Astro.props;

View File

@ -17,10 +17,8 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
openGraph={{ title: t("home.title") }}
backgroundImage={{
url: "/img/background-image.webp",
filename: "background-image",
height: 2279,
width: 1920,
mimeType: "image/webp",
}}
hideFooterLinks
hideHomeButton>

View File

@ -21,10 +21,8 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu
<AppLayoutBackgroundImg
img={{
url: "/img/timeline-background.webp",
filename: "timeline-background",
width: 2478,
height: 4110,
mimeType: "image/webp",
}}
/>
<AppLayoutTitle title={t("timeline.title")} />

View File

@ -551,7 +551,6 @@ export interface Audio {
};
[k: string]: unknown;
} | null;
subfile?: string | VideoSubtitle | null;
id?: string | null;
}[];
tags?: (string | Tag)[] | null;
@ -597,19 +596,6 @@ export interface MediaThumbnail {
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos-subtitles".
*/
export interface VideoSubtitle {
id: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos".
@ -617,7 +603,7 @@ export interface VideoSubtitle {
export interface Video {
id: string;
duration: number;
thumbnail: string | MediaThumbnail;
thumbnail?: string | MediaThumbnail | null;
translations: {
language: string | Language;
title: string;
@ -658,6 +644,19 @@ export interface Video {
width?: number | null;
height?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos-subtitles".
*/
export interface VideoSubtitle {
id: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos-channels".
@ -666,7 +665,7 @@ export interface VideosChannel {
id: string;
url: string;
title: string;
subscribers?: number | null;
subscribers: number;
videos?: (string | Video)[] | null;
}
/**
@ -1099,17 +1098,17 @@ export interface RichTextUploadNode extends RichTextNode {
export interface RichTextUploadImageNode extends RichTextUploadNode {
relationTo: Collections.Images;
value: Image;
value: EndpointImage;
}
export interface RichTextUploadVideoNode extends RichTextUploadNode {
relationTo: Collections.Videos;
value: Video;
value: EndpointVideo;
}
export interface RichTextUploadAudioNode extends RichTextUploadNode {
relationTo: Collections.Audios;
value: Audio;
value: EndpointAudio;
}
export interface RichTextTextNode extends RichTextNode {
@ -1179,10 +1178,10 @@ export const isNodeUploadNode = (node: RichTextNode): node is RichTextUploadNode
export const isUploadNodeImageNode = (node: RichTextUploadNode): node is RichTextUploadImageNode =>
node.relationTo === Collections.Images;
export const isUploadNodeVideoNode = (node: RichTextUploadNode): node is RichTextUploadVideoNode =>
export const isUploadNodeVideoNode = (node: RichTextUploadNode): node is RichTextUploadVideoNode =>
node.relationTo === Collections.Videos;
export const isUploadNodeAudioNode = (node: RichTextUploadNode): node is RichTextUploadAudioNode =>
export const isUploadNodeAudioNode = (node: RichTextUploadNode): node is RichTextUploadAudioNode =>
node.relationTo === Collections.Audios;
export const isNodeListNode = (node: RichTextNode): node is RichTextListNode =>
@ -1338,7 +1337,7 @@ const request = async (url: string, init?: RequestInit): Promise<Response> => {
// SDK and Types
export type EndpointFolderPreview = {
export type EndpointFolder = {
slug: string;
icon?: string;
translations: {
@ -1346,33 +1345,42 @@ export type EndpointFolderPreview = {
name: string;
description?: RichTextContent;
}[];
};
export type EndpointFolder = EndpointFolderPreview & {
sections:
| { type: "single"; subfolders: EndpointFolderPreview[] }
| { type: "single"; subfolders: EndpointFolder[] }
| {
type: "multiple";
sections: {
translations: { language: string; name: string }[];
subfolders: EndpointFolderPreview[];
subfolders: EndpointFolder[];
}[];
};
files: (
| {
relationTo: "collectibles";
value: EndpointCollectiblePreview;
relationTo: Collections.Collectibles;
value: EndpointCollectible;
}
| {
relationTo: "pages";
value: EndpointPagePreview;
relationTo: Collections.Pages;
value: EndpointPage;
}
| {
relationTo: Collections.Images;
value: EndpointImage;
}
| {
relationTo: Collections.Audios;
value: EndpointAudio;
}
| {
relationTo: Collections.Videos;
value: EndpointVideo;
}
)[];
parentPages: EndpointSource[];
};
export type EndpointWebsiteConfig = {
homeFolders: (EndpointFolderPreview & {
homeFolders: (EndpointFolder & {
lightThumbnail?: PayloadImage;
darkThumbnail?: PayloadImage;
})[];
@ -1420,23 +1428,18 @@ export type EndpointTagsGroup = {
tags: EndpointTag[];
};
export type EndpointPagePreview = {
export type EndpointPage = {
slug: string;
type: PageType;
thumbnail?: PayloadImage;
authors: EndpointRecorder[];
tagGroups: EndpointTagsGroup[];
backgroundImage?: PayloadImage;
translations: {
language: string;
pretitle?: string;
title: string;
subtitle?: string;
}[];
};
export type EndpointPage = EndpointPagePreview & {
backgroundImage?: PayloadImage;
translations: (EndpointPagePreview["translations"][number] & {
sourceLanguage: string;
summary?: RichTextContent;
content: RichTextContent;
@ -1444,11 +1447,11 @@ export type EndpointPage = EndpointPagePreview & {
translators: EndpointRecorder[];
proofreaders: EndpointRecorder[];
toc: TableOfContentEntry[];
})[];
}[];
parentPages: EndpointSource[];
};
export type EndpointCollectiblePreview = {
export type EndpointCollectible = {
slug: string;
thumbnail?: PayloadImage;
translations: {
@ -1461,9 +1464,6 @@ export type EndpointCollectiblePreview = {
tagGroups: EndpointTagsGroup[];
releaseDate?: string;
languages: string[];
};
export type EndpointCollectible = EndpointCollectiblePreview & {
backgroundImage?: PayloadImage;
nature: CollectibleNature;
gallery: PayloadImage[];
@ -1484,15 +1484,23 @@ export type EndpointCollectible = EndpointCollectiblePreview & {
bindingType?: CollectibleBindingTypes;
pageOrder?: CollectiblePageOrders;
};
subitems: EndpointCollectiblePreview[];
subitems: EndpointCollectible[];
contents: {
content:
| {
relationTo: "pages";
value: EndpointPagePreview;
relationTo: Collections.Pages;
value: EndpointPage;
}
| {
relationTo: "generic-contents";
relationTo: Collections.Audios;
value: EndpointAudio;
}
| {
relationTo: Collections.Videos;
value: EndpointVideo;
}
| {
relationTo: Collections.GenericContents;
value: {
translations: {
language: string;
@ -1557,21 +1565,72 @@ export type EndpointSource =
| { type: "url"; url: string; label: string }
| {
type: "collectible";
collectible: EndpointCollectiblePreview;
collectible: EndpointCollectible;
range?:
| { type: "page"; page: number }
| { type: "timestamp"; timestamp: string }
| { type: "custom"; translations: { language: string; note: string }[] };
}
| { type: "page"; page: EndpointPagePreview }
| { type: "folder"; folder: EndpointFolderPreview };
| { type: "page"; page: EndpointPage }
| { type: "folder"; folder: EndpointFolder };
export type PayloadImage = {
export type EndpointMedia = {
id: string;
url: string;
filename: string;
mimeType: string;
filesize: number;
updatedAt: string;
createdAt: string;
tagGroups: EndpointTagsGroup[];
translations: {
language: string;
title: string;
description?: RichTextContent;
}[];
};
export type EndpointImage = EndpointMedia & {
width: number;
height: number;
};
export type EndpointAudio = EndpointMedia & {
thumbnail?: PayloadImage;
duration: number;
};
export type EndpointVideo = EndpointMedia & {
thumbnail?: PayloadImage;
subtitles: {
language: string;
url: string;
}[];
platform?: {
channel: {
url: string;
title: string;
subscribers: number;
};
views?: number;
likes?: number;
dislikes?: number;
url: string;
publishedDate: string;
};
duration: number;
};
export type PayloadMedia = {
url: string;
mimeType: string;
filename: string;
filesize: number;
};
export type PayloadImage = PayloadMedia & {
width: number;
height: number;
};
export const payload = {
@ -1595,4 +1654,10 @@ export const payload = {
await (await request(payloadApiUrl(Collections.ChronologyEvents, `all`))).json(),
getChronologyEventByID: async (id: string): Promise<EndpointChronologyEvent> =>
await (await request(payloadApiUrl(Collections.ChronologyEvents, `id/${id}`))).json(),
getImageByID: async (id: string): Promise<EndpointImage> =>
await (await request(payloadApiUrl(Collections.Images, `id/${id}`))).json(),
getAudioByID: async (id: string): Promise<EndpointAudio> =>
await (await request(payloadApiUrl(Collections.Audios, `id/${id}`))).json(),
getVideoByID: async (id: string): Promise<EndpointVideo> =>
await (await request(payloadApiUrl(Collections.Videos, `id/${id}`))).json(),
};