Compare commits
2 commits
4b8ae99a73
...
fe98476154
Author | SHA1 | Date | |
---|---|---|---|
fe98476154 | |||
818900edc1 |
14 changed files with 316 additions and 75 deletions
|
@ -8,7 +8,7 @@
|
|||
%sveltekit.head%
|
||||
</head>
|
||||
<title>Tiraya</title>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="tap">
|
||||
<noscript>Please enable JavaScript to run the Tiraya application</noscript>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("sum test", () => {
|
||||
it("adds 1 + 2 to equal 3", () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@
|
|||
} from "$lib/util/types";
|
||||
import { DBLCLICK_MS, LIST_PAGENAV_N } from "$lib/util/constants";
|
||||
import {
|
||||
arrMoveMulti2,
|
||||
arrMoveMulti,
|
||||
clamp,
|
||||
dateToNumber,
|
||||
findParentWithAttr,
|
||||
|
@ -598,24 +598,10 @@
|
|||
function doMove(data: MoveData) {
|
||||
dispatch("moveTracks", data);
|
||||
|
||||
// let os = 0;
|
||||
// for (let i = 0; i < data.positions.length; i++) {
|
||||
// if (data.positions[i] < data.target) {
|
||||
// os--;
|
||||
// }
|
||||
// }
|
||||
let offset = arrMoveMulti(tracks, data.positions, data.target);
|
||||
tracks = tracks;
|
||||
|
||||
// for (let i = 0; i < data.positions.length; i++) {
|
||||
// selection[data.target + i + os] = true;
|
||||
// }
|
||||
// lastSelection = Math.max(
|
||||
// data.target + data.positions.length + os - 1,
|
||||
// tracks.length - 1
|
||||
// );
|
||||
let res = arrMoveMulti2(tracks, data.positions, data.target);
|
||||
tracks = res.array;
|
||||
|
||||
let toff = data.target + res.offset;
|
||||
let toff = data.target + offset;
|
||||
selection = {};
|
||||
selectRange([toff, toff + data.positions.length - 1]);
|
||||
scrollTo(data.target);
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import NavbarMobileItem from "./NavbarMobileItem.svelte";
|
||||
</script>
|
||||
|
||||
<div class="btm-nav btm-nav-sm bg-base-300 z-50">
|
||||
<div class="btm-nav btm-nav-sm text-xs bg-base-300 z-50">
|
||||
<NavbarMobileItem
|
||||
title={$LL.home()}
|
||||
icon={mdiHomeOutline}
|
||||
|
|
59
src/lib/components/nav/NavbarMobileLandscape.svelte
Normal file
59
src/lib/components/nav/NavbarMobileLandscape.svelte
Normal file
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import {
|
||||
mdiAccountMusic,
|
||||
mdiAccountMusicOutline,
|
||||
mdiAlbum,
|
||||
mdiHome,
|
||||
mdiHomeOutline,
|
||||
mdiPlaylistMusic,
|
||||
mdiPlaylistMusicOutline,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { iconAlbumOutline, iconSearch, iconSearchFilled } from "$lib/util/icons";
|
||||
|
||||
import NavbarItemLarge from "./NavbarItemLarge.svelte";
|
||||
import PlayerbarMobileLandscape from "$lib/components/player/PlayerbarMobileLandscape.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<ul class="menu menu-compact flex flex-col p-1">
|
||||
<NavbarItemLarge
|
||||
title={$LL.home()}
|
||||
icon={mdiHomeOutline}
|
||||
iconActive={mdiHome}
|
||||
href="/"
|
||||
/>
|
||||
<NavbarItemLarge
|
||||
title={$LL.search()}
|
||||
icon={iconSearch}
|
||||
iconActive={iconSearchFilled}
|
||||
href="/search"
|
||||
/>
|
||||
<NavbarItemLarge
|
||||
title={$LL.artists()}
|
||||
icon={mdiAccountMusicOutline}
|
||||
iconActive={mdiAccountMusic}
|
||||
href="/artist"
|
||||
/>
|
||||
<NavbarItemLarge
|
||||
title={$LL.albums()}
|
||||
icon={iconAlbumOutline}
|
||||
iconActive={mdiAlbum}
|
||||
href="/album"
|
||||
/>
|
||||
<!-- <NavbarItemLarge
|
||||
title={$LL.tracks()}
|
||||
icon={mdiMusicNoteOutline}
|
||||
iconActive={mdiMusicNote}
|
||||
href="/tracks"
|
||||
/> -->
|
||||
<NavbarItemLarge
|
||||
title={$LL.playlists()}
|
||||
icon={mdiPlaylistMusicOutline}
|
||||
iconActive={mdiPlaylistMusic}
|
||||
href="/playlist"
|
||||
/>
|
||||
</ul>
|
||||
<PlayerbarMobileLandscape />
|
||||
</div>
|
|
@ -49,8 +49,8 @@
|
|||
<div
|
||||
class="flex items-center justify-start"
|
||||
aria-label={$LL.now_playing({
|
||||
title: "Across the Sea",
|
||||
artist: "Leaves' Eyes",
|
||||
title: track.name,
|
||||
artist: track.artists[0].name,
|
||||
})}
|
||||
>
|
||||
<CurrentCover />
|
||||
|
|
|
@ -29,9 +29,6 @@
|
|||
// Enable color transition after first image load
|
||||
let colorTrans = false;
|
||||
|
||||
let buttonsElm: Element;
|
||||
let seekbarElm: Element;
|
||||
|
||||
function onImageLoad(e: Event) {
|
||||
fastAverageColor(e.target as HTMLImageElement, (res) => {
|
||||
imgColor = res;
|
||||
|
@ -75,7 +72,7 @@
|
|||
class="shadow rounded-md z-50"
|
||||
class:transition-colors={colorTrans}
|
||||
style={`background-color: ${bgColorHex}`}
|
||||
aria-label={$LL.now_playing({ title: "Across the Sea", artist: "Leaves' Eyes" })}
|
||||
aria-label={$LL.now_playing({ title: track.name, artist: track.artists[0].name })}
|
||||
>
|
||||
<div class="inner">
|
||||
<button class="relative" aria-label={$LL.fullscreen_player()}>
|
||||
|
@ -107,7 +104,7 @@
|
|||
<TrackNameMobile slot="next" track={tracks[ti + 1]} />
|
||||
</Swiper>
|
||||
|
||||
<div class="controls flex items-center" bind:this={buttonsElm}>
|
||||
<div class="controls flex items-center">
|
||||
<LikeButton />
|
||||
{#if !$mainPhone}
|
||||
<IconButton
|
||||
|
@ -136,7 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seekbar" bind:this={seekbarElm}>
|
||||
<div class="seekbar">
|
||||
<Slider ariaLabel={$LL.seek_bar()} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
147
src/lib/components/player/PlayerbarMobileLandscape.svelte
Normal file
147
src/lib/components/player/PlayerbarMobileLandscape.svelte
Normal file
|
@ -0,0 +1,147 @@
|
|||
<script lang="ts">
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import { mdiFullscreen, mdiPause, mdiPlay } from "@mdi/js";
|
||||
|
||||
import { Direction, type Color, type Track } from "$lib/util/types";
|
||||
import { colorToHex, correctBgColor, fastAverageColor } from "$lib/util/colors";
|
||||
import { textColor } from "$lib/stores/theme";
|
||||
import { COLOR_B3, MIN_CONTRAST_BODY } from "$lib/util/constants";
|
||||
import { tracks } from "$lib/util/testdata";
|
||||
import { TRACK_SKIP, hub, registerHandler } from "$lib/util/events";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import IconToggle from "$lib/components/ui/IconToggle.svelte";
|
||||
import LikeButton from "$lib/components/ui/LikeButton.svelte";
|
||||
import Slider from "$lib/components/ui/Slider.svelte";
|
||||
import Swiper from "$lib/components/ui/Swiper.svelte";
|
||||
import TrackNameMobile from "./TrackNameMobile.svelte";
|
||||
|
||||
let imgColor: Color | null;
|
||||
let bgColorHex: string;
|
||||
// Enable color transition after first image load
|
||||
let colorTrans = false;
|
||||
|
||||
function onImageLoad(e: Event) {
|
||||
fastAverageColor(e.target as HTMLImageElement, (res) => {
|
||||
imgColor = res;
|
||||
});
|
||||
}
|
||||
|
||||
$: if (imgColor) {
|
||||
const corrected = correctBgColor($textColor, imgColor, MIN_CONTRAST_BODY);
|
||||
bgColorHex = colorToHex(corrected);
|
||||
window.setTimeout(() => (colorTrans = true), 300);
|
||||
} else if (!bgColorHex) {
|
||||
bgColorHex = COLOR_B3;
|
||||
}
|
||||
|
||||
// Test code to simulate player behavior
|
||||
let ti = 1;
|
||||
let track: Track;
|
||||
$: track = tracks[ti];
|
||||
|
||||
function nextTrack() {
|
||||
ti++;
|
||||
if (ti >= tracks.length) ti = 0;
|
||||
}
|
||||
|
||||
function prevTrack() {
|
||||
ti--;
|
||||
if (ti < 0) ti = tracks.length - 1;
|
||||
}
|
||||
|
||||
registerHandler(TRACK_SKIP, (direction) => {
|
||||
if (direction === Direction.Forward) {
|
||||
nextTrack();
|
||||
} else if (direction == Direction.Backward) {
|
||||
prevTrack();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="playerbar-mobile-landscape"
|
||||
class="m-2 shadow rounded-md relative"
|
||||
class:transition-colors={colorTrans}
|
||||
style={`background-color: ${bgColorHex}`}
|
||||
aria-label={$LL.now_playing({ title: track.name, artist: track.artists[0].name })}
|
||||
>
|
||||
<div class="inner">
|
||||
<Swiper
|
||||
cls="h-14 mx-2"
|
||||
hasPrev={ti > 0}
|
||||
hasNext={ti + 1 < tracks.length}
|
||||
on:swiped={(e) => hub.emit(TRACK_SKIP, e.detail)}
|
||||
>
|
||||
<TrackNameMobile slot="prev" track={tracks[ti - 1]} />
|
||||
<TrackNameMobile slot="curr" {track} />
|
||||
<TrackNameMobile slot="next" track={tracks[ti + 1]} />
|
||||
</Swiper>
|
||||
|
||||
<div class="seekbar">
|
||||
<Slider ariaLabel={$LL.seek_bar()} marginX={false} />
|
||||
</div>
|
||||
|
||||
<button class="relative" aria-label={$LL.fullscreen_player()}>
|
||||
<div
|
||||
class="absolute w-full h-full flex items-center justify-center bg-base-100/50
|
||||
transition-opacity opacity-0 hover:opacity-100"
|
||||
>
|
||||
<Icon path={mdiFullscreen} size={2} />
|
||||
</div>
|
||||
<img
|
||||
class="h-full rounded-bl-md"
|
||||
src={track.album.imageUrl}
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
height="54"
|
||||
width="54"
|
||||
draggable="false"
|
||||
on:load={onImageLoad}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div class="controls flex items-center">
|
||||
<LikeButton />
|
||||
<IconToggle
|
||||
iconOn={mdiPause}
|
||||
iconOff={mdiPlay}
|
||||
size="md"
|
||||
iColorOn=""
|
||||
ariaLabelOff={$LL.pause()}
|
||||
ariaLabelOn={$LL.play()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
#playerbar-mobile-landscape {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template:
|
||||
"trackname trackname" 1fr
|
||||
"img controls" / 54px 1fr;
|
||||
gap: 0 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
grid-area: img;
|
||||
}
|
||||
|
||||
.controls {
|
||||
grid-area: controls;
|
||||
}
|
||||
|
||||
.seekbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 3rem;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,7 @@
|
|||
export let max = 100;
|
||||
export let value = 0;
|
||||
export let ariaLabel: string | undefined = undefined;
|
||||
export let marginX = true;
|
||||
|
||||
/**
|
||||
* Value to be displayed next to the slider
|
||||
|
@ -141,7 +142,7 @@
|
|||
on:mouseup={onDragEnd}
|
||||
on:resize={resizeSlider}
|
||||
/>
|
||||
<div class="flex-1 mx-2">
|
||||
<div class="flex-1" class:mx-2={marginX}>
|
||||
<div
|
||||
class="slider-wrapper"
|
||||
class:holding
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let hasNext = true;
|
||||
/** Bind to get the current state */
|
||||
export let swiping = false;
|
||||
export let cls = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -44,7 +45,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="carousel"
|
||||
class="carousel {cls}"
|
||||
bind:clientWidth={width}
|
||||
use:swipeable={{ thresholdProvider: () => width / 3 }}
|
||||
on:swipeStart={onSwipeStart}
|
||||
|
|
62
src/lib/util/functions.test.ts
Normal file
62
src/lib/util/functions.test.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { test, expect } from "vitest";
|
||||
import { arrMoveMulti } from "./functions";
|
||||
|
||||
const ARR = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
test.each([
|
||||
{
|
||||
name: "single fwd",
|
||||
positions: [1],
|
||||
target: 3,
|
||||
result: [0, 2, 1, 3, 4, 5, 6, 7, 8, 9],
|
||||
offset: -1,
|
||||
},
|
||||
{
|
||||
name: "range fwd",
|
||||
positions: [1, 2, 3],
|
||||
target: 7,
|
||||
result: [0, 4, 5, 6, 1, 2, 3, 7, 8, 9],
|
||||
offset: -3,
|
||||
},
|
||||
{
|
||||
name: "range+single fwd",
|
||||
positions: [1, 2, 3, 7],
|
||||
target: 9,
|
||||
result: [0, 4, 5, 6, 8, 1, 2, 3, 7, 9],
|
||||
offset: -4,
|
||||
},
|
||||
{
|
||||
name: "single bck",
|
||||
positions: [8],
|
||||
target: 1,
|
||||
result: [0, 8, 1, 2, 3, 4, 5, 6, 7, 9],
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
name: "range bck",
|
||||
positions: [6, 7, 8],
|
||||
target: 1,
|
||||
result: [0, 6, 7, 8, 1, 2, 3, 4, 5, 9],
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
name: "range+single bck",
|
||||
positions: [8, 3, 6, 7],
|
||||
target: 1,
|
||||
result: [0, 3, 6, 7, 8, 1, 2, 4, 5, 9],
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
name: "range inside", // no items get moved
|
||||
positions: [2, 3, 4, 5],
|
||||
target: 4,
|
||||
result: ARR,
|
||||
offset: -2,
|
||||
},
|
||||
])("arrMoveMulti $name", ({ positions, target, result, offset }) => {
|
||||
const arr = [...ARR];
|
||||
|
||||
const o = arrMoveMulti(arr, positions, target);
|
||||
|
||||
expect(arr).toStrictEqual(result);
|
||||
expect(o).toBe(offset);
|
||||
});
|
|
@ -236,8 +236,12 @@ export function arrMove<T>(arr: T[], oldIndex: number, newIndex: number) {
|
|||
arr.splice(newIndex > oldIndex ? newIndex - 1 : newIndex, 0, ...items);
|
||||
}
|
||||
|
||||
export function arrMoveMulti<T>(arr: T[], positions: number[], target: number) {
|
||||
positions.sort();
|
||||
/** Move a list of items in an array to the target position.
|
||||
*
|
||||
* Returns the offset of the move operation
|
||||
* (the number of positions the items were shifted to the right) */
|
||||
export function arrMoveMulti<T>(arr: T[], positions: number[], target: number): number {
|
||||
positions.sort((a, b) => a - b);
|
||||
const toInsert = [];
|
||||
let offset = 0;
|
||||
|
||||
|
@ -247,33 +251,8 @@ export function arrMoveMulti<T>(arr: T[], positions: number[], target: number) {
|
|||
}
|
||||
toInsert.reverse();
|
||||
arr.splice(target + offset, 0, ...toInsert);
|
||||
}
|
||||
|
||||
export function arrMoveMulti2<T>(
|
||||
arr: T[],
|
||||
positions: number[],
|
||||
target: number
|
||||
): { array: T[]; offset: number } {
|
||||
const a1 = [],
|
||||
spliced = [];
|
||||
const pset = new Set(positions);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (let i = positions.length - 1; i >= 0; i--) {
|
||||
if (target > positions[i]) offset--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (pset.has(i)) {
|
||||
spliced.push(arr[i]);
|
||||
} else {
|
||||
a1.push(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
a1.splice(target + offset, 0, ...spliced);
|
||||
return { array: a1, offset };
|
||||
return offset;
|
||||
}
|
||||
|
||||
export function setCookie(cName: string, cValue: string, expDays: number) {
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
headerColor,
|
||||
} from "$lib/stores/layout";
|
||||
import { clamp, getEventCoords } from "$lib/util/functions";
|
||||
import { WIDTH_SMALL } from "$lib/util/constants";
|
||||
import { WIDTH_PHONE, WIDTH_SMALL } from "$lib/util/constants";
|
||||
|
||||
import Menubar from "$lib/components/nav/Navbar.svelte";
|
||||
import Navbar from "$lib/components/nav/Navbar.svelte";
|
||||
import Playerbar from "$lib/components/player/Playerbar.svelte";
|
||||
import DummyText from "$lib/components/ui/DummyText.svelte";
|
||||
import NavbarMobile from "$lib/components/nav/NavbarMobile.svelte";
|
||||
import NavbarMobileLandscape from "$lib/components/nav/NavbarMobileLandscape.svelte";
|
||||
import PlayerbarMobile from "$lib/components/player/PlayerbarMobile.svelte";
|
||||
import FixedHeader from "$lib/components/header/FixedHeader.svelte";
|
||||
|
||||
|
@ -53,8 +54,10 @@
|
|||
let queuebarElm: HTMLElement;
|
||||
let queuebarResizing = false;
|
||||
|
||||
let mobileLandscape = false;
|
||||
$: mobileLandscape = $screenHeight < WIDTH_PHONE && $screenWidth > WIDTH_PHONE;
|
||||
let showNavbar = false;
|
||||
$: showNavbar = $screenWidth >= WIDTH_SMALL;
|
||||
$: showNavbar = $screenWidth >= WIDTH_SMALL && !mobileLandscape;
|
||||
let showQueue = false;
|
||||
$: showQueue = showNavbar && $clientState.showQueue;
|
||||
|
||||
|
@ -168,7 +171,8 @@
|
|||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id="app"
|
||||
class:mobile={!showNavbar}
|
||||
class:mobile={!showNavbar && !mobileLandscape}
|
||||
class:mobile-landscape={mobileLandscape}
|
||||
bind:clientWidth={$screenWidth}
|
||||
bind:clientHeight={$screenHeight}
|
||||
on:dragend={onDragEnd}
|
||||
|
@ -186,7 +190,11 @@
|
|||
on:touchstart|passive={initResizeNavbarTouch}
|
||||
class:active={navbarResizing}
|
||||
/>
|
||||
<Menubar />
|
||||
<Navbar />
|
||||
</nav>
|
||||
{:else if mobileLandscape}
|
||||
<nav class="sidebar">
|
||||
<NavbarMobileLandscape />
|
||||
</nav>
|
||||
{/if}
|
||||
{#if showQueue}
|
||||
|
@ -214,15 +222,15 @@
|
|||
<div class="h-12" />
|
||||
{/if}
|
||||
<slot />
|
||||
{#if !showNavbar}
|
||||
<!-- Spacer to ensure enough bottom space to scroll beyond floating playerbara -->
|
||||
{#if !showNavbar && !mobileLandscape}
|
||||
<!-- Spacer to ensure enough bottom space to scroll beyond floating playerbar -->
|
||||
<div class="h-24" />
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{#if showNavbar}
|
||||
{#if showNavbar && !mobileLandscape}
|
||||
<Playerbar />
|
||||
{:else}
|
||||
{:else if !mobileLandscape}
|
||||
<PlayerbarMobile />
|
||||
<NavbarMobile />
|
||||
{/if}
|
||||
|
@ -242,6 +250,10 @@
|
|||
"main-content" calc(100vh - 3rem)
|
||||
"player" 3rem / 1fr;
|
||||
}
|
||||
|
||||
&.mobile-landscape {
|
||||
grid-template: "main-content" 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
#main-content {
|
||||
|
@ -253,6 +265,10 @@
|
|||
);
|
||||
}
|
||||
|
||||
.mobile-landscape > #main-content {
|
||||
grid-template: "sidebar main" 1fr/10rem 1fr;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import type { PageData } from "./$types";
|
||||
|
||||
import { searchTerm } from "$lib/stores/layout";
|
||||
import { userLink, arrMoveMulti, arrMoveMulti2 } from "$lib/util/functions";
|
||||
import { userLink } from "$lib/util/functions";
|
||||
import { ListView, type MoveData } from "$lib/util/types";
|
||||
import { testPlaylist } from "$lib/util/testdata";
|
||||
// import testPlaylist from "$lib/util/testdata/playlist_5K_conv.json";
|
||||
|
|
Loading…
Reference in a new issue