cool beans yo
This commit is contained in:
parent
ce62e92370
commit
f5b2b7aaba
11 changed files with 198 additions and 60 deletions
5
bun.lock
5
bun.lock
|
@ -5,6 +5,7 @@
|
||||||
"name": "streamarr",
|
"name": "streamarr",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solid-primitives/context": "^0.3.0",
|
"@solid-primitives/context": "^0.3.0",
|
||||||
|
"@solid-primitives/deep": "^0.3.1",
|
||||||
"@solid-primitives/event-listener": "^2.4.0",
|
"@solid-primitives/event-listener": "^2.4.0",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
|
@ -313,12 +314,16 @@
|
||||||
|
|
||||||
"@solid-primitives/cursor": ["@solid-primitives/cursor@0.0.115", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-8nEmUN/sacXPChwuJOAi6Yi6VnxthW/Jk8VGvvcF38AenjUvOA6FHI6AkJILuFXjQw1PGxia1YbH/Mn77dPiOA=="],
|
"@solid-primitives/cursor": ["@solid-primitives/cursor@0.0.115", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-8nEmUN/sacXPChwuJOAi6Yi6VnxthW/Jk8VGvvcF38AenjUvOA6FHI6AkJILuFXjQw1PGxia1YbH/Mn77dPiOA=="],
|
||||||
|
|
||||||
|
"@solid-primitives/deep": ["@solid-primitives/deep@0.3.1", "", { "dependencies": { "@solid-primitives/memo": "^1.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-DaJ2iuHmYmHyGhA5EACfqVKXNivGlLA+zYborz1pLzKOpyMZq0LvmNoBgwFwN/ncOq5FhGP0Hvr6U7QP+hmLXw=="],
|
||||||
|
|
||||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||||
|
|
||||||
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-0QX9O3eUaQorNNmXZn8a4efSByayIScVq+iGSwheD7m3SL/ACLM5oZlCNpTPLcemnVVfUPAHFiViEj86XpN5qw=="],
|
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-0QX9O3eUaQorNNmXZn8a4efSByayIScVq+iGSwheD7m3SL/ACLM5oZlCNpTPLcemnVVfUPAHFiViEj86XpN5qw=="],
|
||||||
|
|
||||||
"@solid-primitives/media": ["@solid-primitives/media@2.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7+C3wfbWnGE/WPoNsqcp/EeOP2aNNB92RCpsWhBth8E5lZo/J+rK6jMb7umVsK0zguT8HBpeXp1pFyFbcsHStA=="],
|
"@solid-primitives/media": ["@solid-primitives/media@2.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7+C3wfbWnGE/WPoNsqcp/EeOP2aNNB92RCpsWhBth8E5lZo/J+rK6jMb7umVsK0zguT8HBpeXp1pFyFbcsHStA=="],
|
||||||
|
|
||||||
|
"@solid-primitives/memo": ["@solid-primitives/memo@1.4.1", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-MzNCJNpXidQdLOZUsEkwpuq52uwT8zrFrBxEVMEr9N35yIIvGhjqwrI1M6xzPmJGzuVUe8anCk57q+N5gyRk0Q=="],
|
||||||
|
|
||||||
"@solid-primitives/platform": ["@solid-primitives/platform@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg=="],
|
"@solid-primitives/platform": ["@solid-primitives/platform@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg=="],
|
||||||
|
|
||||||
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solid-primitives/context": "^0.3.0",
|
"@solid-primitives/context": "^0.3.0",
|
||||||
|
"@solid-primitives/deep": "^0.3.1",
|
||||||
"@solid-primitives/event-listener": "^2.4.0",
|
"@solid-primitives/event-listener": "^2.4.0",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
|
|
|
@ -53,6 +53,10 @@
|
||||||
|
|
||||||
z-index: calc(var(--sibling-count) - var(--sibling-index));
|
z-index: calc(var(--sibling-count) - var(--sibling-index));
|
||||||
|
|
||||||
|
&:has(> :hover, > :focus-within) {
|
||||||
|
z-index: calc(var(--sibling-count) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
@supports (animation-timeline: view()) {
|
@supports (animation-timeline: view()) {
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
|
|
@ -1,16 +1,55 @@
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript
|
import type { paths, components } from "./jellyfin.generated"; // generated by openapi-typescript
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { Entry } from "../types";
|
import { Entry } from "../types";
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
'use server';
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
type ItemImageType = "Primary" | "Art" | "Backdrop" | "Banner" | "Logo" | "Thumb" | "Disc" | "Box" | "Screenshot" | "Menu" | "Chapter" | "BoxRear" | "Profile";
|
||||||
|
|
||||||
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
||||||
const client = createClient<paths>({
|
const client = createClient<paths>({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
'Authorization': `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
||||||
|
'Content-Type': 'application/json; profile="CamelCase"',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TEST = query(async () => {
|
||||||
|
const userId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
|
const itemId = "919dfa97e4dad2758a925d056e590a28";
|
||||||
|
const seriesId = "5230ddbcd9400733dc07e5b8cb7a4f49";
|
||||||
|
|
||||||
|
const { data: seriesData } = await client.GET("/UserItems/{itemId}/UserData", {
|
||||||
|
params: {
|
||||||
|
path: { itemId: seriesId },
|
||||||
|
query: { userId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: epData } = await client.GET("/UserItems/{itemId}/UserData", {
|
||||||
|
params: {
|
||||||
|
path: { itemId },
|
||||||
|
query: { userId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(seriesData, epData)
|
||||||
|
}, "jellyfin.TEST");
|
||||||
|
|
||||||
|
export const getCurrentUser = query(async () => {
|
||||||
|
const { data, error, response } = await client.GET("/Users/Public", {
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data, error, response)
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, "jellyfin.getCurrentUser");
|
||||||
|
|
||||||
export const listUsers = query(async () => {
|
export const listUsers = query(async () => {
|
||||||
const { data, error } = await client.GET("/Users", {
|
const { data, error } = await client.GET("/Users", {
|
||||||
params: {},
|
params: {},
|
||||||
|
@ -19,7 +58,7 @@ export const listUsers = query(async () => {
|
||||||
return data ?? [];
|
return data ?? [];
|
||||||
}, "jellyfin.listUsers");
|
}, "jellyfin.listUsers");
|
||||||
|
|
||||||
export const getItem = query(async (userId: string, itemId: string) => {
|
export const getItem = query(async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
||||||
const { data, error } = await client.GET("/Items/{itemId}", {
|
const { data, error } = await client.GET("/Items/{itemId}", {
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
|
@ -41,30 +80,97 @@ export const getItem = query(async (userId: string, itemId: string) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data?.Items ?? [];
|
if (data === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.Id!,
|
||||||
|
title: data.Name!,
|
||||||
|
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||||
|
};
|
||||||
}, "jellyfin.getItem");
|
}, "jellyfin.getItem");
|
||||||
|
|
||||||
export const getContinueWatching = query(
|
export const getItemImage = query(async (itemId: string, imageType: ItemImageType): Promise<any | undefined> => {
|
||||||
async (userId: string): Promise<Entry[]> => {
|
const { data, error } = await client.GET("/Items/{itemId}/Images/{imageType}", {
|
||||||
const { data, error } = await client.GET("/Users/{userId}/Items/Resume", {
|
parseAs: 'blob',
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
userId,
|
itemId,
|
||||||
|
imageType
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
mediaTypes: ["Video"],
|
|
||||||
fields: ["ProviderIds", "Genres"],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = (data?.Items ?? []).map(({ Id, Name }) => ({
|
return data;
|
||||||
id: Id,
|
}, "jellyfin.getItemImage");
|
||||||
title: Name,
|
|
||||||
thumbnail: `${baseUrl}/Items/${Id}/Images/Primary`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return items;
|
export const getItemPlaybackInfo = query(async (userId: string, itemId: string): Promise<any | undefined> => {
|
||||||
|
const { data, error, response } = await client.GET("/Items/{itemId}/PlaybackInfo", {
|
||||||
|
parseAs: 'text',
|
||||||
|
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
itemId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, "jellyfin.getItemPlaybackInfo");
|
||||||
|
|
||||||
|
export const queryItems = query(async () => {
|
||||||
|
const { data, error } = await client.GET("/Items", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
mediaTypes: ["Video"],
|
||||||
|
isUnaired: true,
|
||||||
|
limit: 10,
|
||||||
|
// fields: ["ProviderIds", "Genres"],
|
||||||
|
includeItemTypes: ["Series", "Movie"],
|
||||||
|
recursive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
}, 'jellyfin.queryItems');
|
||||||
|
|
||||||
|
export const getContinueWatching = query(
|
||||||
|
async (userId: string): Promise<Entry[]> => {
|
||||||
|
const { data, error } = await client.GET("/UserItems/Resume", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
userId,
|
||||||
|
mediaTypes: ["Video"],
|
||||||
|
// fields: ["ProviderIds", "Genres"],
|
||||||
|
// includeItemTypes: ["Series", "Movie"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data?.Items) !== true) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueIds = new Set<string>(data.Items.map(item => item.Type === 'Episode' ? item.SeriesId! : 'MOVIE_ID'));
|
||||||
|
const results = await Promise.allSettled(uniqueIds.values().map(id => getItem(userId, id)).toArray());
|
||||||
|
|
||||||
|
assertNoErrors(results);
|
||||||
|
|
||||||
|
return results.filter((result): result is PromiseFulfilledResult<Entry> => result.value !== undefined).map(({ value }) => value);
|
||||||
},
|
},
|
||||||
"jellyfin.continueWatching",
|
"jellyfin.continueWatching",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function assertNoErrors<T>(results: PromiseSettledResult<T>[]): asserts results is PromiseFulfilledResult<T>[] {
|
||||||
|
if (results.some(({ status }) => status !== 'fulfilled')) {
|
||||||
|
throw new Error('one or more promices failed', { cause: results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason) });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
import type { Category, Entry } from "./types";
|
import type { Category, Entry } from "./types";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { entries } from "./data";
|
import { entries } from "./data";
|
||||||
import { getContinueWatching } from "./apis/jellyfin";
|
import { getContinueWatching, getItem, TEST } from "./apis/jellyfin";
|
||||||
|
|
||||||
|
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
|
|
||||||
export const listCategories = query(async (): Promise<Category[]> => {
|
export const listCategories = query(async (): Promise<Category[]> => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
// await TEST()
|
||||||
|
// console.log(await getItemPlaybackInfo(jellyfinUserId, 'a69c0c0ab66177a7adb671f126335d16'));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
|
{ label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
|
||||||
|
@ -69,9 +72,9 @@ export const getEntry = query(
|
||||||
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
return entries.get(id);
|
return getItem(jellyfinUserId, id);
|
||||||
},
|
},
|
||||||
"series.get",
|
"series.get",
|
||||||
);
|
);
|
||||||
|
|
||||||
export { listUsers, getItem, getContinueWatching } from "./apis/jellyfin";
|
export { listUsers, getContinueWatching } from "./apis/jellyfin";
|
||||||
|
|
|
@ -10,7 +10,7 @@ export interface Entry {
|
||||||
summary?: string;
|
summary?: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
sources?: Entry.Source[];
|
sources?: Entry.Source[];
|
||||||
thumbnail?: string;
|
thumbnail?: URL | string;
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
|
||||||
const slug = createMemo(() => createSlug(props.entry));
|
const slug = createMemo(() => createSlug(props.entry));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<figure class={css.listItem}>
|
<figure class={css.listItem} data-id={props.entry.id}>
|
||||||
<img src={props.entry.thumbnail} alt={props.entry.title} />
|
<img src={props.entry.thumbnail ?? ''} alt={props.entry.title} />
|
||||||
|
|
||||||
<figcaption>
|
<figcaption>
|
||||||
<strong>{props.entry.title}</strong>
|
<strong>{props.entry.title}</strong>
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import { Component, createSignal } from "solid-js";
|
import { Component, createEffect, createSignal, Show } from "solid-js";
|
||||||
import css from "./volume.module.css";
|
import css from "./volume.module.css";
|
||||||
|
import { createStore, unwrap } from "solid-js/store";
|
||||||
|
import { trackDeep } from "@solid-primitives/deep";
|
||||||
|
|
||||||
interface VolumeProps {
|
interface VolumeProps {
|
||||||
value: number;
|
value: number;
|
||||||
|
muted?: boolean;
|
||||||
|
onInput?: (next: { volume: number, muted: boolean }) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Volume: Component<VolumeProps> = (props) => {
|
export const Volume: Component<VolumeProps> = (props) => {
|
||||||
const [volume, setVolume] = createSignal(props.value);
|
const [state, setState] = createStore({ volume: props.value, muted: props.muted ?? false });
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.onInput?.(unwrap(trackDeep(state)));
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={css.container}>
|
<div class={css.container}>
|
||||||
<button>mute</button>
|
<button onClick={() => setState('muted', m => !m)}><Show when={state.muted} fallback="mute">unmute</Show></button>
|
||||||
<input type="range" value={volume()} min="0" max="1" step="0.01" />
|
<input type="range" value={state.volume} min="0" max="1" step="0.01" onInput={(e) => setState('volume', e.target.valueAsNumber)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { createAsync, json, query } from "@solidjs/router";
|
||||||
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
||||||
import css from "./player.module.css";
|
import css from "./player.module.css";
|
||||||
import { Volume } from "./controls/volume";
|
import { Volume } from "./controls/volume";
|
||||||
|
import { getEntry } from "../content";
|
||||||
|
|
||||||
const metadata = query(async (id: string) => {
|
const metadata = query(async (id: string) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
@ -42,9 +43,12 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
const [video, setVideo] = createSignal<HTMLVideoElement>(
|
const [video, setVideo] = createSignal<HTMLVideoElement>(
|
||||||
undefined as unknown as HTMLVideoElement,
|
undefined as unknown as HTMLVideoElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const entry = createAsync(() => getEntry(props.id));
|
||||||
|
|
||||||
const data = createAsync(() => metadata(props.id), {
|
const data = createAsync(() => metadata(props.id), {
|
||||||
deferStream: true,
|
deferStream: true,
|
||||||
initialValue: {},
|
initialValue: {} as any,
|
||||||
});
|
});
|
||||||
const captionUrl = createMemo(() => {
|
const captionUrl = createMemo(() => {
|
||||||
const { captions } = data();
|
const { captions } = data();
|
||||||
|
@ -168,7 +172,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<figure class={css.player}>
|
<figure class={css.player}>
|
||||||
<h1>{props.id}</h1>
|
<h1>{entry()?.title}</h1>
|
||||||
|
|
||||||
<video
|
<video
|
||||||
ref={setVideo}
|
ref={setVideo}
|
||||||
|
@ -194,7 +198,10 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<figcaption>
|
<figcaption>
|
||||||
<Volume value={0.5} />
|
<Volume value={video()?.volume ?? 0} muted={video()?.muted ?? false} onInput={({ volume, muted }) => {
|
||||||
|
video().volume = volume;
|
||||||
|
video().muted = muted;
|
||||||
|
}} />
|
||||||
</figcaption>
|
</figcaption>
|
||||||
|
|
||||||
<button onclick={toggle}>play/pause</button>
|
<button onclick={toggle}>play/pause</button>
|
||||||
|
|
|
@ -115,63 +115,63 @@
|
||||||
--sibling-index: 15;
|
--sibling-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(1)) {
|
:has(> :last-child:nth-child(1)) > * {
|
||||||
--sibbling-count: 1;
|
--sibling-count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(2)) {
|
:has(> :last-child:nth-child(2)) > * {
|
||||||
--sibbling-count: 2;
|
--sibling-count: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(3)) {
|
:has(> :last-child:nth-child(3)) > * {
|
||||||
--sibbling-count: 3;
|
--sibling-count: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(4)) {
|
:has(> :last-child:nth-child(4)) > * {
|
||||||
--sibbling-count: 4;
|
--sibling-count: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(5)) {
|
:has(> :last-child:nth-child(5)) > * {
|
||||||
--sibbling-count: 5;
|
--sibling-count: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(6)) {
|
:has(> :last-child:nth-child(6)) > * {
|
||||||
--sibbling-count: 6;
|
--sibling-count: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(7)) {
|
:has(> :last-child:nth-child(7)) > * {
|
||||||
--sibbling-count: 7;
|
--sibling-count: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(8)) {
|
:has(> :last-child:nth-child(8)) > * {
|
||||||
--sibbling-count: 8;
|
--sibling-count: 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(9)) {
|
:has(> :last-child:nth-child(9)) > * {
|
||||||
--sibbling-count: 9;
|
--sibling-count: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(10)) {
|
:has(> :last-child:nth-child(10)) > * {
|
||||||
--sibbling-count: 10;
|
--sibling-count: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(11)) {
|
:has(> :last-child:nth-child(11)) > * {
|
||||||
--sibbling-count: 11;
|
--sibling-count: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(12)) {
|
:has(> :last-child:nth-child(12)) > * {
|
||||||
--sibbling-count: 12;
|
--sibling-count: 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(13)) {
|
:has(> :last-child:nth-child(13)) > * {
|
||||||
--sibbling-count: 13;
|
--sibling-count: 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(14)) {
|
:has(> :last-child:nth-child(14)) > * {
|
||||||
--sibbling-count: 14;
|
--sibling-count: 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(15)) {
|
:has(> :last-child:nth-child(15)) > * {
|
||||||
--sibbling-count: 15;
|
--sibling-count: 15;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
createAsync,
|
||||||
json,
|
json,
|
||||||
Params,
|
Params,
|
||||||
query,
|
query,
|
||||||
|
@ -6,6 +7,7 @@ import {
|
||||||
RouteDefinition,
|
RouteDefinition,
|
||||||
useParams,
|
useParams,
|
||||||
} from "@solidjs/router";
|
} from "@solidjs/router";
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
import { createSlug, getEntry } from "~/features/content";
|
import { createSlug, getEntry } from "~/features/content";
|
||||||
import { Player } from "~/features/player";
|
import { Player } from "~/features/player";
|
||||||
import { toSlug } from "~/utilities";
|
import { toSlug } from "~/utilities";
|
||||||
|
@ -33,6 +35,8 @@ interface ItemParams extends Params {
|
||||||
export const route = {
|
export const route = {
|
||||||
async preload({ params }) {
|
async preload({ params }) {
|
||||||
await healUrl(params.slug);
|
await healUrl(params.slug);
|
||||||
|
|
||||||
|
return getEntry(params.slug.slice(params.slug.lastIndexOf("-") + 1));
|
||||||
},
|
},
|
||||||
} satisfies RouteDefinition;
|
} satisfies RouteDefinition;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue