Merge branch 'main' into tann2019/local-storage

This commit is contained in:
2025-04-25 21:34:53 +02:00
committed by GitHub
7 changed files with 117 additions and 11 deletions

View File

@@ -7,9 +7,9 @@ The ultimate lofi player. Uses music from [Chillhop](https://chillhop.com/).
- [x] Play music
- [x] Change stations
- [x] Change backgrounds
- [ ] Background sounds
- [ ] Pomodoro timers
- [ ] Volume control
- [ ] Background sounds
- [ ] Links to Spotify
- [ ] Alarm(?)
- [ ] Sleep timer

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import AudioLines from '@lucide/svelte/icons/audio-lines';
import { state as appState } from '@/state.svelte';
import { Slider } from '../ui/slider/index.js';
import VolumeX from '@lucide/svelte/icons/volume-x';
import VolumeZero from '@lucide/svelte/icons/volume';
import VolumeOne from '@lucide/svelte/icons/volume-1';
import VolumeTwo from '@lucide/svelte/icons/volume-2';
function sliderChange(name: string, volume: number) {
if (volume === 0) {
delete appState.activeAtmospheres[name];
} else {
appState.activeAtmospheres[name] = volume;
}
}
function getVolumeValue(name: string) {
return appState.activeAtmospheres[name] || 0;
}
console.log(appState.atmospheres)
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'default', size: 'icon' })}>
<AudioLines />
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56 max-h-[50vh] overflow-y-auto">
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Atmospheres</DropdownMenu.GroupHeading>
<DropdownMenu.Separator />
<div class="grid gap-4">
{#each appState.atmospheres as atmosphere}
<div class="p-2 rounded-md hover:bg-white/5 transition-colors">
<div class="flex items-center justify-between mb-1">
<label for={atmosphere.id} class="text-sm font-medium">{atmosphere.name}</label>
<span class="text-xs opacity-70">
{#if getVolumeValue(atmosphere.name) > 0}
{#if getVolumeValue(atmosphere.name) > 0 && getVolumeValue(atmosphere.name) <= 0.4}
<VolumeZero class="inline size-3 mr-1" />
{:else if getVolumeValue(atmosphere.name) > 0.4 && getVolumeValue(atmosphere.name) <= 0.8}
<VolumeOne class="inline size-3 mr-1" />
{:else}
<VolumeTwo class="inline size-3 mr-1" />
{/if}
{Math.round(getVolumeValue(atmosphere.name) * 100)}%
{:else}
<VolumeX class="inline size-3 mr-1" />
Off
{/if}
</span>
</div>
<Slider
type="single"
value={getVolumeValue(atmosphere.name)}
id={atmosphere.id}
max={1}
step={0.01}
onValueChange={(value) => {
sliderChange(atmosphere.name, value);
}}
/>
</div>
{/each}
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,6 +2,7 @@
import BgDropdown from '@/components/app/bg-dropdown.svelte';
import Disclaimer from '@/components/app/disclaimer.svelte';
import MusicPlayer from '@/components/app/now-playing.svelte';
import Sounds from '@/components/app/atmospheres.svelte';
import StationDropdown from '@/components/app/station-dropdown.svelte';
</script>
@@ -14,6 +15,7 @@
<div class="flex gap-4 mt-3 sm:mt-0">
<StationDropdown />
<BgDropdown />
<Sounds />
<Disclaimer />
</div>
</div>

View File

@@ -1,11 +1,13 @@
<script lang="ts">
import { state as appState } from '@/state.svelte';
import { getGeneralData, getStationSongs, setSongTime } from '@/utils';
import { getGeneralData, getStationSongs } from '@/utils';
import { onMount } from 'svelte';
import { useIsMobile } from '@/isMobile.svelte';
// svelte-ignore non_reactive_update
let audioElement: HTMLAudioElement;
let isTransitioning = $state(false);
let isMobile = useIsMobile();
function togglePlayback(play: boolean) {
if (!audioElement) return;
@@ -139,8 +141,6 @@
appState.error = 'No songs available.';
}
setSongTime()
appState.isLoading = false;
if ('mediaSession' in navigator) {
@@ -190,7 +190,6 @@
appState.songQueue = songs;
appState.currentSong = appState.songQueue[0];
appState.duration = appState.currentSong.duration;
setSongTime();
setMediaSession();
} else {
appState.error = 'Failed to load songs.';
@@ -223,3 +222,15 @@
class="hidden"
></audio>
{/if}
{#each Object.entries(appState.activeAtmospheres) as [name, volume]}
<audio
src={isMobile ? appState.atmospheres.find(atm => atm.name === name)?.urlMobile : appState.atmospheres.find(atm => atm.name === name)?.url}
class="hidden"
id={name}
volume={volume}
loop
autoplay
preload="none"
></audio>
{/each}

View File

@@ -17,7 +17,7 @@
<p class="text-sm">{state.currentSong?.artists}</p>
</div>
<div class="flex-1"></div>
<div class="gap-4">
<div class="gap-4 flex">
<Button size="icon" onclick={togglePlay} class="size-10 md:ml-4">
{#if state.isPlaying}
<Pause />
@@ -25,6 +25,8 @@
<Play />
{/if}
</Button>
<Volume />
<div class="hidden sm:block">
<Volume />
</div>
</div>
</div>

View File

@@ -20,10 +20,11 @@
{align}
{sideOffset}
class={cn(
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
"bg-[var(--glass-bg)] transition hover:bg-[var(--glass-hover)] border-[var(--glass-border)]",
className
)}
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none',
'bg-white/10 backdrop-blur-md transition hover:bg-white/15 text-foreground border border-white/20 shadow-lg',
className
)}
{...restProps}
/>
</PopoverPrimitive.Portal>

View File

@@ -0,0 +1,20 @@
import { readable } from 'svelte/store';
import { browser } from '$app/environment';
export function useIsMobile(mobileWidth = 768) {
return readable(false, (set) => {
if (!browser) return;
const checkIsMobile = () => {
const isMobile = window.innerWidth < mobileWidth;
set(isMobile);
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => {
window.removeEventListener('resize', checkIsMobile);
};
});
}