start video streaming
This commit is contained in:
parent
4b51fbc908
commit
445fde7b6b
15 changed files with 448 additions and 225 deletions
1
bun.lock
1
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/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",
|
||||||
"@solidjs/start": "^1.1.3",
|
"@solidjs/start": "^1.1.3",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solid-primitives/context": "^0.3.0",
|
"@solid-primitives/context": "^0.3.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",
|
||||||
"@solidjs/start": "^1.1.3",
|
"@solidjs/start": "^1.1.3",
|
||||||
|
|
20
src/api/stream/video.ts
Normal file
20
src/api/stream/video.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { json } from "@solidjs/router";
|
||||||
|
import vid from "../../../public/videos/bbb_sunflower_2160p_60fps_normal.mp4";
|
||||||
|
import { APIEvent } from "@solidjs/start/server";
|
||||||
|
|
||||||
|
export const GET = async (event: APIEvent) => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
console.log(event);
|
||||||
|
|
||||||
|
// async function* packetGenerator() {
|
||||||
|
// for (let i = 0; i < 10; i++) {
|
||||||
|
// yield `packet ${i}`;
|
||||||
|
// await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log(vid);
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
};
|
|
@ -10,7 +10,7 @@ export const ListItem: Component<{ entry: Entry }> = (props) => {
|
||||||
<main>
|
<main>
|
||||||
<strong>{props.entry.title}</strong>
|
<strong>{props.entry.title}</strong>
|
||||||
|
|
||||||
<a href={`/content/${props.entry.id}`}>Watch now</a>
|
<a href={`/watch/${props.entry.id}`}>Watch now</a>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { Component, createEffect, createSignal, Index, onMount } from "solid-js";
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
Index,
|
||||||
|
onMount,
|
||||||
|
} from "solid-js";
|
||||||
import type { Entry, Category } from "../content";
|
import type { Entry, Category } from "../content";
|
||||||
import { ListItem } from "./list-item";
|
import { ListItem } from "./list-item";
|
||||||
import { List } from "~/components/list";
|
import { List } from "~/components/list";
|
||||||
|
@ -15,7 +21,11 @@ export const Overview: Component<OverviewProps> = (props) => {
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
new MutationObserver(() => {
|
new MutationObserver(() => {
|
||||||
container()?.querySelector(`.${css.list} > ul > div:nth-child(4) > main > a`)?.focus({ preventScroll: true });
|
container()
|
||||||
|
?.querySelector<HTMLElement>(
|
||||||
|
`.${css.list} > ul > div:nth-child(4) > main > a`,
|
||||||
|
)
|
||||||
|
?.focus({ preventScroll: true });
|
||||||
}).observe(document.body, { subtree: true, childList: true });
|
}).observe(document.body, { subtree: true, childList: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,11 +35,15 @@ export const Overview: Component<OverviewProps> = (props) => {
|
||||||
|
|
||||||
<Index each={props.categories}>
|
<Index each={props.categories}>
|
||||||
{(category) => (
|
{(category) => (
|
||||||
<List class={css.list} label={category().label} items={category().entries}>
|
<List
|
||||||
|
class={css.list}
|
||||||
|
label={category().label}
|
||||||
|
items={category().entries}
|
||||||
|
>
|
||||||
{(entry) => <ListItem entry={entry()} />}
|
{(entry) => <ListItem entry={entry()} />}
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
</Index>
|
</Index>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
1
src/features/player/index.ts
Normal file
1
src/features/player/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Player } from "./player";
|
0
src/features/player/player.module.css
Normal file
0
src/features/player/player.module.css
Normal file
161
src/features/player/player.tsx
Normal file
161
src/features/player/player.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import {
|
||||||
|
createEventListenerMap,
|
||||||
|
makeEventListener,
|
||||||
|
makeEventListenerStack,
|
||||||
|
} from "@solid-primitives/event-listener";
|
||||||
|
import { createAsync, json, query } from "@solidjs/router";
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
} from "solid-js";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
const streamKaas = query(async () => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const stream = new WritableStream();
|
||||||
|
|
||||||
|
async function* packetGenerator() {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
yield `packet ${i}`;
|
||||||
|
await new Promise((res) => setTimeout(res, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const writer = stream.getWriter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writer.ready;
|
||||||
|
|
||||||
|
for await (const packet of packetGenerator()) {
|
||||||
|
writer.write(packet);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
writer.releaseLock();
|
||||||
|
}
|
||||||
|
// response.body.wr
|
||||||
|
})();
|
||||||
|
|
||||||
|
return new Response(packetGenerator());
|
||||||
|
}, "kaas");
|
||||||
|
|
||||||
|
interface PlayerProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
const [video, setVideo] = createSignal<HTMLVideoElement>();
|
||||||
|
const stream = createAsync(async () => {
|
||||||
|
const res = await streamKaas();
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
// const [kaas, { refetch }] = createResource(async () => {
|
||||||
|
// if (isServer) {
|
||||||
|
// return "";
|
||||||
|
// }
|
||||||
|
// const response = await fetch("http://localhost:3000/api/stream/video", {
|
||||||
|
// method: "GET",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// console.log(response.body);
|
||||||
|
|
||||||
|
// for await (const packet of response.body) {
|
||||||
|
// console.log(new TextDecoder().decode(packet));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return "";
|
||||||
|
// });
|
||||||
|
|
||||||
|
// onMount(() => refetch());
|
||||||
|
|
||||||
|
// createEffect(() => console.log(stream()));
|
||||||
|
|
||||||
|
// const progress = createMemo(() => {
|
||||||
|
// const
|
||||||
|
// });
|
||||||
|
|
||||||
|
createEventListenerMap(() => video()!, {
|
||||||
|
durationchange(e) {
|
||||||
|
console.log("durationchange", e);
|
||||||
|
},
|
||||||
|
loadeddata(e) {
|
||||||
|
console.log("loadeddata", e);
|
||||||
|
},
|
||||||
|
loadedmetadata(e) {
|
||||||
|
console.log("loadedmetadata", e);
|
||||||
|
},
|
||||||
|
ratechange(e) {
|
||||||
|
console.log("ratechange", e);
|
||||||
|
},
|
||||||
|
seeked(e) {
|
||||||
|
console.log("seeked", e);
|
||||||
|
},
|
||||||
|
seeking(e) {
|
||||||
|
console.log("seeking", e);
|
||||||
|
},
|
||||||
|
stalled(e) {
|
||||||
|
console.log("stalled", e);
|
||||||
|
},
|
||||||
|
|
||||||
|
play(e) {
|
||||||
|
console.log("play", e);
|
||||||
|
},
|
||||||
|
playing(e) {
|
||||||
|
console.log("playing", e);
|
||||||
|
},
|
||||||
|
pause(e) {
|
||||||
|
console.log("pause", e);
|
||||||
|
},
|
||||||
|
suspend(e) {
|
||||||
|
console.log("suspend", e);
|
||||||
|
},
|
||||||
|
|
||||||
|
volumechange(e) {
|
||||||
|
console.log("volumechange", e);
|
||||||
|
},
|
||||||
|
|
||||||
|
waiting(e) {
|
||||||
|
console.log("waiting", e);
|
||||||
|
},
|
||||||
|
|
||||||
|
progress(e) {
|
||||||
|
console.log(e);
|
||||||
|
},
|
||||||
|
|
||||||
|
timeupdate(e) {
|
||||||
|
console.log("timeupdate", e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
const el = video();
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el[el.paused ? "play" : "pause"]();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>{props.id}</h1>
|
||||||
|
|
||||||
|
<video ref={setVideo} muted preload="metadata">
|
||||||
|
<source src="/videos/bbb_sunflower_2160p_60fps_normal.mp4" />
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<button onclick={toggle}>play/pause</button>
|
||||||
|
|
||||||
|
<progress />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
171
src/features/shell/nav.module.css
Normal file
171
src/features/shell/nav.module.css
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
.nav {
|
||||||
|
grid-area: 2 / 1 / 3 / 2;
|
||||||
|
display: block grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
inline-size: 5em;
|
||||||
|
block-size: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
background: inherit;
|
||||||
|
z-index: 0;
|
||||||
|
transition: z-index 0.3s step-end;
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
position: relative;
|
||||||
|
display: block grid;
|
||||||
|
grid-template-columns: 2.5rem auto;
|
||||||
|
align-content: center;
|
||||||
|
inline-size: 4rem;
|
||||||
|
gap: 1rem;
|
||||||
|
transform-origin: left center;
|
||||||
|
padding: 0;
|
||||||
|
padding-inline-start: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 100%;
|
||||||
|
inset-block: 0;
|
||||||
|
inline-size: 20vw;
|
||||||
|
/* background:
|
||||||
|
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
|
||||||
|
linear-gradient(to right, #0003, transparent); */
|
||||||
|
background-image: linear-gradient(to right, #0003, transparent);
|
||||||
|
mask: radial-gradient(
|
||||||
|
ellipse 20vw 100% at left center,
|
||||||
|
black,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s var(--ease-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
position: relative;
|
||||||
|
grid-column: span 2;
|
||||||
|
display: block grid;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
transform-origin: center left;
|
||||||
|
transition:
|
||||||
|
transform 2s var(--ease-spring-5),
|
||||||
|
opacity 0.3s var(--ease-3);
|
||||||
|
color: var(--stone-4);
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s var(--ease-3);
|
||||||
|
text-shadow: 0 0 1em #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
fill: var(--stone-4);
|
||||||
|
inline-size: 2.5rem;
|
||||||
|
block-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--yellow-4);
|
||||||
|
list-style: disc;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "•";
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
fill: var(--yellow-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus))::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus)) > a {
|
||||||
|
transform: scale(
|
||||||
|
max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index)))))
|
||||||
|
);
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(1)) {
|
||||||
|
--target: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(2)) {
|
||||||
|
--target: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(3)) {
|
||||||
|
--target: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(4)) {
|
||||||
|
--target: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(5)) {
|
||||||
|
--target: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(6)) {
|
||||||
|
--target: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(7)) {
|
||||||
|
--target: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(8)) {
|
||||||
|
--target: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(9)) {
|
||||||
|
--target: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(10)) {
|
||||||
|
--target: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(11)) {
|
||||||
|
--target: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(12)) {
|
||||||
|
--target: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(13)) {
|
||||||
|
--target: 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(14)) {
|
||||||
|
--target: 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:is(:hover, :focus):nth-child(15)) {
|
||||||
|
--target: 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(a:hover, :focus-within) {
|
||||||
|
z-index: 1;
|
||||||
|
transition: z-index 0.3s step-start;
|
||||||
|
}
|
||||||
|
}
|
25
src/features/shell/nav.tsx
Normal file
25
src/features/shell/nav.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
import { AiOutlineHome, AiOutlineStar, AiOutlineSearch } from "solid-icons/ai";
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
import css from "./nav.module.css";
|
||||||
|
|
||||||
|
export const Nav: Component = (props) => {
|
||||||
|
return (
|
||||||
|
<nav class={css.nav}>
|
||||||
|
<ul>
|
||||||
|
<A href="/" activeClass={css.active} end={true}>
|
||||||
|
<AiOutlineHome />
|
||||||
|
<span>Home</span>
|
||||||
|
</A>
|
||||||
|
<A href="/library" activeClass={css.active}>
|
||||||
|
<AiOutlineStar />
|
||||||
|
<span>Library</span>
|
||||||
|
</A>
|
||||||
|
<A href="/search" activeClass={css.active}>
|
||||||
|
<AiOutlineSearch />
|
||||||
|
<span>Search</span>
|
||||||
|
</A>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,27 +3,27 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: auto 1fr / 5em 1fr;
|
grid: auto 1fr / 5em 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'top top'
|
"top top"
|
||||||
'nav content'
|
"nav content";
|
||||||
;
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
background-color: var(--surface-1);
|
background-color: var(--surface-1);
|
||||||
|
|
||||||
/* &:has(.nav a:hover) > .body {
|
|
||||||
filter: blur(3px);
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
grid-area: 2 / 1 / 3 / 3;
|
grid-area: 2 / 1 / 3 / 3;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
background: linear-gradient(180deg, transparent, transparent 90vh, var(--surface-500) 90vh, var(--surface-500));
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
transparent,
|
||||||
|
transparent 90vh,
|
||||||
|
var(--surface-500) 90vh,
|
||||||
|
var(--surface-500)
|
||||||
|
);
|
||||||
overflow: clip auto;
|
overflow: clip auto;
|
||||||
padding-inline-start: 5em;
|
padding-inline-start: 5em;
|
||||||
transition: filter var(--duration-moderate-1) var(--ease-3);
|
transition: filter var(--duration-moderate-1) var(--ease-3);
|
||||||
|
@ -37,177 +37,3 @@
|
||||||
min-block-size: 100%;
|
min-block-size: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.top {
|
|
||||||
grid-area: top;
|
|
||||||
display: block grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
justify-content: end;
|
|
||||||
z-index: 1;
|
|
||||||
background-color: inherit;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
grid-area: nav;
|
|
||||||
display: block grid;
|
|
||||||
grid-auto-flow: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
inline-size: 5em;
|
|
||||||
block-size: 100%;
|
|
||||||
padding: 1em;
|
|
||||||
background: inherit;
|
|
||||||
z-index: 0;
|
|
||||||
transition: z-index .3s step-end;
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
position: relative;
|
|
||||||
display: block grid;
|
|
||||||
grid-template-columns: 2.5rem auto;
|
|
||||||
align-content: center;
|
|
||||||
inline-size: 4rem;
|
|
||||||
gap: 1rem;
|
|
||||||
transform-origin: left center;
|
|
||||||
padding: 0;
|
|
||||||
padding-inline-start: .5rem;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset-inline-start: 100%;
|
|
||||||
inset-block: 0;
|
|
||||||
inline-size: 20vw;
|
|
||||||
/* background:
|
|
||||||
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
|
|
||||||
linear-gradient(to right, #0003, transparent); */
|
|
||||||
background-image: linear-gradient(to right, #0003, transparent);
|
|
||||||
mask: radial-gradient(ellipse 20vw 100% at left center, black, transparent);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .3s var(--ease-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
position: relative;
|
|
||||||
grid-column: span 2;
|
|
||||||
display: block grid;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
align-items: center;
|
|
||||||
text-decoration: none;
|
|
||||||
transform-origin: center left;
|
|
||||||
transition: transform 2s var(--ease-spring-5), opacity 0.3s var(--ease-3);
|
|
||||||
color: var(--stone-4);
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .3s var(--ease-3);
|
|
||||||
text-shadow: 0 0 1em #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > svg {
|
|
||||||
fill: var(--stone-4);
|
|
||||||
inline-size: 2.5rem;
|
|
||||||
block-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: var(--yellow-4);
|
|
||||||
list-style: disc;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '•';
|
|
||||||
position: absolute;
|
|
||||||
inset-inline-start: -1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > svg {
|
|
||||||
fill: var(--yellow-4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus))::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
|
||||||
opacity: .25;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus)) > a {
|
|
||||||
transform: scale(max(1, calc(1.5 - (.2 * abs(var(--target) - var(--sibling-index))))));
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(1)) {
|
|
||||||
--target: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(2)) {
|
|
||||||
--target: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(3)) {
|
|
||||||
--target: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(4)) {
|
|
||||||
--target: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(5)) {
|
|
||||||
--target: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(6)) {
|
|
||||||
--target: 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(7)) {
|
|
||||||
--target: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(8)) {
|
|
||||||
--target: 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(9)) {
|
|
||||||
--target: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(10)) {
|
|
||||||
--target: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(11)) {
|
|
||||||
--target: 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(12)) {
|
|
||||||
--target: 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(13)) {
|
|
||||||
--target: 13;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(14)) {
|
|
||||||
--target: 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(15)) {
|
|
||||||
--target: 15;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:hover, :focus-within) {
|
|
||||||
z-index: 1;
|
|
||||||
transition: z-index .3s step-start;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { A } from "@solidjs/router";
|
import { ParentComponent } from "solid-js";
|
||||||
import {
|
import { Top } from "./top";
|
||||||
AiOutlineHome,
|
import { Nav } from "./nav";
|
||||||
AiOutlineStar,
|
|
||||||
AiOutlineSearch,
|
|
||||||
} from "solid-icons/ai";
|
|
||||||
import { ParentComponent, Component } from "solid-js";
|
|
||||||
import { ColorSchemePicker } from "../theme";
|
|
||||||
import css from "./shell.module.css";
|
import css from "./shell.module.css";
|
||||||
|
|
||||||
export const Shell: ParentComponent = (props) => {
|
export const Shell: ParentComponent = (props) => {
|
||||||
|
@ -20,32 +15,3 @@ export const Shell: ParentComponent = (props) => {
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Top: Component = (props) => {
|
|
||||||
return (
|
|
||||||
<aside class={css.top}>
|
|
||||||
<ColorSchemePicker />
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Nav: Component = (props) => {
|
|
||||||
return (
|
|
||||||
<nav class={css.nav}>
|
|
||||||
<ul>
|
|
||||||
<A href="/" activeClass={css.active} end={true}>
|
|
||||||
<AiOutlineHome />
|
|
||||||
<span>Home</span>
|
|
||||||
</A>
|
|
||||||
<A href="/library" activeClass={css.active}>
|
|
||||||
<AiOutlineStar />
|
|
||||||
<span>Library</span>
|
|
||||||
</A>
|
|
||||||
<A href="/search" activeClass={css.active}>
|
|
||||||
<AiOutlineSearch />
|
|
||||||
<span>Search</span>
|
|
||||||
</A>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
9
src/features/shell/top.module.css
Normal file
9
src/features/shell/top.module.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.top {
|
||||||
|
grid-area: 1 / 1 / 2 / 3;
|
||||||
|
display: block grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
justify-content: end;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: inherit;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
11
src/features/shell/top.tsx
Normal file
11
src/features/shell/top.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
import { ColorSchemePicker } from "../theme";
|
||||||
|
import css from "./top.module.css";
|
||||||
|
|
||||||
|
export const Top: Component = (props) => {
|
||||||
|
return (
|
||||||
|
<aside class={css.top}>
|
||||||
|
<ColorSchemePicker />
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
17
src/routes/(shell)/watch/:item.tsx
Normal file
17
src/routes/(shell)/watch/:item.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Params, useParams } from "@solidjs/router";
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
import { Player } from "~/features/player";
|
||||||
|
|
||||||
|
interface ItemParams extends Params {
|
||||||
|
item: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Item() {
|
||||||
|
const params = useParams<ItemParams>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Player id={params.item} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue