feat: background analysis and disclaimer

This commit is contained in:
2025-04-18 20:28:05 +02:00
parent 5365ed71d0
commit 3b98f11b70
18 changed files with 327 additions and 42 deletions

View File

@@ -1,17 +1,17 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}

View File

@@ -14,7 +14,7 @@
"format": "prettier --write ."
},
"devDependencies": {
"@lucide/svelte": "^0.488.0",
"@lucide/svelte": "^0.492.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-cloudflare": "^5.0.1",
"@sveltejs/kit": "^2.16.0",
@@ -34,5 +34,8 @@
"vite": "^6.0.0",
"wrangler": "^4.12.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"dependencies": {
"mode-watcher": "0.5.1"
}
}

View File

@@ -32,6 +32,12 @@
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--text-color: rgba(0, 0, 0, 0.85);
--text-shadow: 0 1px 2px rgba(255, 255, 255, 0.2);
--glass-bg: rgba(0, 0, 0, 0.1);
--glass-hover: rgba(0, 0, 0, 0.2);
--glass-border: rgba(0, 0, 0, 0.2);
}
.dark {
@@ -62,14 +68,23 @@
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--text-color: rgba(255, 255, 255, 0.95);
--text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-hover: rgba(255, 255, 255, 0.2);
--glass-border: rgba(255, 255, 255, 0.2);
}
}
@layer base {
* {
@apply border-border;
@apply border-border text-dynamic;
}
body {
@apply bg-background text-foreground m-0 p-0 overflow-hidden;
@apply bg-background text-dynamic m-0 p-0 overflow-hidden;
}
}
.text-dynamic {
color: var(--text-color);
text-shadow: var(--text-shadow);
}

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import { onMount } from 'svelte';
import { setMode } from 'mode-watcher';
export let videoSelector: string = "#bg-video";
export let updateInterval: number = 2000;
console.log('update')
onMount(() => {
const video = document.querySelector(videoSelector) as HTMLVideoElement;
if (!video) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
function updateGlobalTextColor() {
if (video.paused || video.ended) return;
canvas.width = 32;
canvas.height = 32;
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
let totalBrightness = 0;
let pixelCount = 0;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
totalBrightness += brightness;
pixelCount++;
}
const avgBrightness = totalBrightness / pixelCount;
const isDark = avgBrightness < 0.5;
//document.documentElement.setAttribute('data-bg-theme', isDark ? 'dark' : 'light');
setMode(isDark ? 'dark' : 'light');
}
video.addEventListener('loadeddata', updateGlobalTextColor);
const interval = setInterval(updateGlobalTextColor, updateInterval);
return () => {
clearInterval(interval);
video.removeEventListener('loadeddata', updateGlobalTextColor);
};
});
</script>

View File

@@ -1,8 +1,10 @@
<video
src={false ? "https://1230610135274225734.discordsays.com/.proxy/static-assets/scenes/chill-vibes/bedroom/videos/night-rain.mp4" : "https://ch-cdn.srizan.dev/flower-shop-beachside-moewalls-com.mp4"}
src={true ? "https://ch-cdn.srizan.dev/discord-rain.mp4" : "https://ch-cdn.srizan.dev/flower-shop-beachside-moewalls-com.mp4"}
autoplay
loop
muted
playsinline
class="absolute top-0 left-0 w-full h-full object-cover -z-10"
id="bg-video"
crossorigin="anonymous"
></video>

View File

@@ -1,12 +1,17 @@
<script>
import Disclaimer from '@/components/app/disclaimer.svelte';
import MusicPlayer from '@/components/app/now-playing.svelte';
import StationDropdown from '@/components/app/station-dropdown.svelte';
</script>
<div
class="fixed bottom-5 left-2 right-2 z-50 flex items-center p-4 bg-white/10 backdrop-blur-lg rounded-xl shadow-lg"
class="fixed bottom-5 left-2 right-2 z-50 flex flex-col sm:flex-row items-center p-4 bg-white/10 backdrop-blur-lg rounded-xl shadow-lg"
>
<MusicPlayer />
<div class="flex-1"></div>
<StationDropdown />
</div>
<div class="hidden sm:block flex-1"></div>
<div class="flex gap-4 mt-3 sm:mt-0">
<StationDropdown />
<Disclaimer />
</div>
</div>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import Info from '@lucide/svelte/icons/info';
import { Button } from '../ui/button';
</script>
<Dialog.Root>
<Dialog.Trigger><Button size="icon"><Info /></Button></Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Info & disclaimer</Dialog.Title>
</Dialog.Header>
<p>
This project was created as an alternative to Chillhop Radio's web player, by implementing a
better, glassmorphic user interface paired with other utilities like Pomodoro timers and such,
which will be added soon.
</p>
<p>
I do not have the right to the songs played on this radio station, nor do I have any
affiliation with Chillhop Music. This project is open source and not intended for
commercial use. The source code is available on GitHub.
</p>
<p>
🄯 GNU GPL, <a href="https://srizan.dev" class="underline">Sr Izan</a>.
</p>
</Dialog.Content>
</Dialog.Root>

View File

@@ -9,17 +9,18 @@
}
</script>
<img src={state.currentSong?.image} alt="Cover Art" class="size-16 rounded-lg shadow-lg" />
<div class="flex flex-col ml-4">
<h2 class="text-lg font-semibold">{state.currentSong?.title}</h2>
<p class="text-sm">{state.currentSong?.artists}</p>
</div>
<Button size="icon" onclick={togglePlay} class="w-10 h-10 ml-4">
{#if state.isPlaying}
<Pause />
{:else}
<Play />
{/if}
</Button>
<div class="flex items-center w-full sm:w-auto">
<img src={state.currentSong?.image} alt="Cover Art" class="size-16 rounded-lg shadow-lg" />
<div class="flex flex-col ml-4">
<h2 class="text-lg font-semibold">{state.currentSong?.title}</h2>
<p class="text-sm">{state.currentSong?.artists}</p>
</div>
<div class="flex-1"></div>
<Button size="icon" onclick={togglePlay} class="w-10 h-10 md:ml-4">
{#if state.isPlaying}
<Pause />
{:else}
<Play />
{/if}
</Button>
</div>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<DialogPrimitive.Content
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
"backdrop-blur-md text-dynamic border drop-shadow-md shadow-lg",
"bg-[var(--glass-bg)] hover:bg-[var(--glass-hover)] border-[var(--glass-border)]",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none text-dynamic backdrop-blur-sm p-1 shadow-sm bg-[var(--glass-bg)] border-[var(--glass-border)] hover:bg-[var(--glass-hover)]"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
class={cn("text-black/80 text-sm", className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
/>

View File

@@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import '../app.css';
import { ModeWatcher } from "mode-watcher";
let { children } = $props();
</script>
<ModeWatcher />
{@render children()}

View File

@@ -4,10 +4,12 @@
import Daemon from '@/components/app/daemon.svelte';
import Spinner from '@lucide/svelte/icons/loader';
import { state } from '@/state.svelte';
import BackgroundAnalyzer from '@/components/app/background-analyzer.svelte';
</script>
<BgImage />
<Daemon />
<BackgroundAnalyzer videoSelector="#bg-video" updateInterval={2000} />
{#if state.isLoading && !state.hasInteracted}
<div class="flex flex-col h-screen w-full items-center justify-center space-y-2">

View File

@@ -518,10 +518,10 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@lucide/svelte@^0.488.0":
version "0.488.0"
resolved "https://registry.yarnpkg.com/@lucide/svelte/-/svelte-0.488.0.tgz#8f600b12c2e5ff3933ffb22db33df3e50987e740"
integrity sha512-i8TFY+vOVci2J/UhaeF1Yj25NOL8UJ27hf+/CexvfIyLSdgid3zHwh0iVf+DlWpAsXXJl2rQ5Cl5g/qfMbfOHw==
"@lucide/svelte@^0.492.0":
version "0.492.0"
resolved "https://registry.yarnpkg.com/@lucide/svelte/-/svelte-0.492.0.tgz#d37602405680f36bd263b3db6f9311b5887c0bf9"
integrity sha512-67ubMQPVSeoP0hWeNCJNhCB3IgOWXB0Ke7zwLObUidYH/1l0kGtIdN/cP9tJEWOg3QE33R3iqyXSAEKElMyJcQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1364,6 +1364,11 @@ minimatch@^9.0.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
mode-watcher@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mode-watcher/-/mode-watcher-0.5.1.tgz#310e6ac144b3f0b3cfb486e1015d9e746e503377"
integrity sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w==
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"