mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
Merge branch 'main' into tann2019/local-storage
This commit is contained in:
@@ -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
|
||||
70
src/lib/components/app/atmospheres.svelte
Normal file
70
src/lib/components/app/atmospheres.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
20
src/lib/isMobile.svelte.ts
Normal file
20
src/lib/isMobile.svelte.ts
Normal 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user