mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
feat: background analysis and disclaimer
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
19
src/app.css
19
src/app.css
@@ -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);
|
||||
}
|
||||
|
||||
56
src/lib/components/app/background-analyzer.svelte
Normal file
56
src/lib/components/app/background-analyzer.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
27
src/lib/components/app/disclaimer.svelte
Normal file
27
src/lib/components/app/disclaimer.svelte
Normal 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>
|
||||
@@ -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>
|
||||
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
39
src/lib/components/ui/dialog/dialog-content.svelte
Normal 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>
|
||||
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal 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}
|
||||
/>
|
||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal 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>
|
||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal 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>
|
||||
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal 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}
|
||||
/>
|
||||
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal 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}
|
||||
/>
|
||||
37
src/lib/components/ui/dialog/index.ts
Normal file
37
src/lib/components/ui/dialog/index.ts
Normal 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,
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { ModeWatcher } from "mode-watcher";
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
{@render children()}
|
||||
|
||||
@@ -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">
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user