diff --git a/src/components/Button.svelte b/src/components/Button.svelte
new file mode 100644
index 0000000..e0c18e9
--- /dev/null
+++ b/src/components/Button.svelte
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts
index 27018e9..ffbe4b4 100644
--- a/src/lib/server/songs.ts
+++ b/src/lib/server/songs.ts
@@ -46,6 +46,9 @@ const getPlaylistRaw = cached('playlists',
return pl
})
+export const listPlaylists = () => db.collection('playlists').find()
+ .map(it => it.data.playlist).toArray()
+
export const getSongMeta = cached('songs',
async (songId: number) => {
const detail = await ne.song_detail({ ids: songId.toString() })
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
new file mode 100644
index 0000000..38dc587
--- /dev/null
+++ b/src/routes/+layout.server.ts
@@ -0,0 +1,37 @@
+import { createUser, login } from '$lib/server/user'
+import type { LayoutServerLoad } from './$types'
+
+export const load: LayoutServerLoad = async ({ cookies, request }) => {
+ let session = cookies.get('session')
+
+ const registUA = request.headers.get('user-agent') || ''
+
+ if (!session) {
+ // Create new user
+ session = await createUser(registUA)
+ cookies.set('session', session, {
+ path: '/',
+ httpOnly: true,
+ secure: false, // Set to true in production
+ sameSite: 'strict',
+ maxAge: 60 * 60 * 24 * 365 // 1 year
+ })
+ }
+
+ try {
+ const user = structuredClone(await login(session))
+ return { user }
+ } catch (e) {
+ // Invalid session, create new
+ session = await createUser(registUA)
+ cookies.set('session', session, {
+ path: '/',
+ httpOnly: true,
+ secure: false,
+ sameSite: 'strict',
+ maxAge: 60 * 60 * 24 * 365
+ })
+ const user = structuredClone(await login(session))
+ return { user }
+ }
+}
\ No newline at end of file
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 0451abd..7c317d8 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -2,8 +2,8 @@
import favicon from "$lib/assets/favicon.svg"
import "@fontsource/roboto"
import 'virtual:uno.css'
- import "./app.sass"
- import "./material.scss"
+ import "../style/app.sass"
+ import "../style/material.scss"
import '@unocss/reset/normalize.css'
import '@unocss/reset/tailwind-v4.css'
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
index 9406628..70e1a9e 100644
--- a/src/routes/+page.server.ts
+++ b/src/routes/+page.server.ts
@@ -1,8 +1,10 @@
// import { log } from 'console';
-import { getSongMeta, parseBrief } from '../lib/server/songs';
+import { getSongMeta, getSongsFromPlaylist, listPlaylists, parseBrief } from '../lib/server/songs';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
let last = parseBrief(await getSongMeta(25723366))
- return { last }
-}
+ let myPlaylists = await listPlaylists()
+ let recPlaylists = myPlaylists
+ return { last, myPlaylists, recPlaylists }
+}
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 6d6dcdd..50476af 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -3,8 +3,11 @@
import TitleHeader from "../components/TitleHeader.svelte";
import SongInfo from "../components/SongInfo.svelte";
import type { PageProps } from "./$types";
+ import Button from "../components/Button.svelte";
let { data }: PageProps = $props()
+
+ console.log(data.myPlaylists)
@@ -21,9 +24,39 @@
{/if}
+
+
+
+
+ {#each data.myPlaylists as playlist}
+
+

+
{playlist.name}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+ {#each data.recPlaylists as playlist}
+
+

+
+
{playlist.name}
+
{playlist.creator.nickname} 创建
+
+
+ {/each}
+
+
\ No newline at end of file
diff --git a/src/routes/app.sass b/src/style/app.sass
similarity index 85%
rename from src/routes/app.sass
rename to src/style/app.sass
index cae1e56..dba91de 100644
--- a/src/routes/app.sass
+++ b/src/style/app.sass
@@ -25,4 +25,6 @@ body
align-items: center
.p-content
- padding: 0 16px
\ No newline at end of file
+ // padding: 0 16px
+ padding-left: 16px
+ padding-right: 16px
\ No newline at end of file
diff --git a/src/style/m3-styles.css b/src/style/m3-styles.css
new file mode 100644
index 0000000..3b85889
--- /dev/null
+++ b/src/style/m3-styles.css
@@ -0,0 +1,379 @@
+/* Needed for elevation to work */
+:root {
+ --m3-util-elevation-0: none;
+ --m3-util-elevation-1:
+ /* Spot */
+ 0px 1px 2px 0px rgb(var(--m3-scheme-shadow) / 0.3),
+ /* Ambient */ 0px 1px 3px 1px rgb(var(--m3-scheme-shadow) / 0.15);
+ --m3-util-elevation-2:
+ 0px 1px 2px 0px rgb(var(--m3-scheme-shadow) / 0.3),
+ 0px 2px 6px 2px rgb(var(--m3-scheme-shadow) / 0.15);
+ --m3-util-elevation-3:
+ 0px 1px 3px 0px rgb(var(--m3-scheme-shadow) / 0.3),
+ 0px 4px 8px 3px rgb(var(--m3-scheme-shadow) / 0.15);
+ --m3-util-elevation-4:
+ 0px 2px 3px 0px rgb(var(--m3-scheme-shadow) / 0.3),
+ 0px 6px 10px 4px rgb(var(--m3-scheme-shadow) / 0.15);
+ --m3-util-elevation-5:
+ 0px 4px 4px 0px rgb(var(--m3-scheme-shadow) / 0.3),
+ 0px 8px 12px 6px rgb(var(--m3-scheme-shadow) / 0.15);
+
+ /* Sizing */
+ --m3-util-bottom-offset: 0;
+ --m3-util-density: 0;
+ --m3-util-density-term: calc(var(--m3-util-density) * 0.25rem);
+
+ /* Shapes */
+ --m3-util-rounding-none: 0;
+ --m3-util-rounding-extra-small: 4px;
+ --m3-util-rounding-small: 8px;
+ --m3-util-rounding-medium: 12px;
+ --m3-util-rounding-large: 16px;
+ --m3-util-rounding-extra-large: 28px;
+ --m3-util-rounding-full: calc(infinity * 1px);
+ --m3-util-optical-centering-coefficient: 0.11;
+
+ --m3-font: Roboto, system-ui;
+ --m3-font-mono: "Google Sans Code", "Roboto Mono", monospace;
+
+ /* Expressive easing */
+ --m3-util-timing-function-fast-spatial: cubic-bezier(0.42, 1.67, 0.21, 0.9);
+ --m3-util-duration-fast-spatial: 350ms;
+ --m3-util-easing-fast-spatial: var(--m3-util-timing-function-fast-spatial)
+ var(--m3-util-duration-fast-spatial);
+
+ --m3-util-timing-function-spatial: cubic-bezier(0.38, 1.21, 0.22, 1);
+ --m3-util-duration-spatial: 500ms;
+ --m3-util-easing-spatial: var(--m3-util-timing-function-spatial) var(--m3-util-duration-spatial);
+
+ --m3-util-timing-function-slow-spatial: cubic-bezier(0.39, 1.29, 0.35, 0.98);
+ --m3-util-duration-slow-spatial: 650ms;
+ --m3-util-easing-slow-spatial: var(--m3-util-timing-function-slow-spatial)
+ var(--m3-util-duration-slow-spatial);
+
+ --m3-util-timing-function-fast: cubic-bezier(0.31, 0.94, 0.34, 1);
+ --m3-util-duration-fast: 150ms;
+ --m3-util-easing-fast: var(--m3-util-timing-function-fast) var(--m3-util-duration-fast);
+
+ --m3-util-timing-function: cubic-bezier(0.34, 0.8, 0.34, 1);
+ --m3-util-duration: 200ms;
+ --m3-util-easing: var(--m3-util-timing-function) var(--m3-util-duration);
+
+ --m3-util-timing-function-slow: cubic-bezier(0.34, 0.88, 0.34, 1);
+ --m3-util-duration-slow: 300ms;
+ --m3-util-easing-slow: var(--m3-util-timing-function-slow) var(--m3-util-duration-slow);
+
+ --m3-util-timing-function-emphasized: linear(
+ 0,
+ 0.034 6%,
+ 0.073 9%,
+ 0.126 11%,
+ 0.194 13%,
+ 0.273 15%,
+ 0.351 16%,
+ 0.541 18%,
+ 0.652 20%,
+ 0.755 24%,
+ 0.839 30%,
+ 0.91 39%,
+ 0.959 53%,
+ 0.989 72%,
+ 1
+ );
+ --m3-util-timing-function-emphasized-accel: cubic-bezier(0.3, 0, 0.8, 0.15);
+ --m3-util-timing-function-emphasized-decel: cubic-bezier(0.05, 0.7, 0.1, 1);
+
+ /* deprecated */
+ --m3-util-curve: var(--m3-util-timing-function-emphasized);
+ --m3-util-curve-accel: var(--m3-util-timing-function-emphasized-accel);
+ --m3-util-curve-decel: var(--m3-util-timing-function-emphasized-decel);
+
+ accent-color: rgb(var(--m3-scheme-primary));
+}
+.m3-font-display-large,
+.m3-font-display-medium,
+.m3-font-display-small {
+ font-family: var(--m3-font-display, var(--m3-font));
+}
+.m3-font-headline-large,
+.m3-font-headline-medium,
+.m3-font-headline-small {
+ font-family: var(--m3-font-headline, var(--m3-font));
+}
+.m3-font-title-large,
+.m3-font-title-medium,
+.m3-font-title-small {
+ font-family: var(--m3-font-title, var(--m3-font));
+}
+.m3-font-label-large,
+.m3-font-label-medium,
+.m3-font-label-small {
+ font-family: var(--m3-font-label, var(--m3-font));
+}
+.m3-font-body-large,
+.m3-font-body-medium,
+.m3-font-body-small {
+ font-family: var(--m3-font-body, var(--m3-font));
+}
+/* As the largest text on the screen, display styles are reserved for short, important text or numerals. */
+.m3-font-display-large {
+ font-size: var(--m3-font-display-large-size, 3.563rem);
+ line-height: var(--m3-font-display-large-height, 1.123);
+ letter-spacing: var(--m3-font-display-large-tracking, 0);
+ font-weight: var(--m3-font-display-large-weight, 400);
+}
+.m3-font-display-medium {
+ font-size: var(--m3-font-display-medium-size, 2.813rem);
+ line-height: var(--m3-font-display-medium-height, 1.156);
+ letter-spacing: var(--m3-font-display-medium-tracking, 0);
+ font-weight: var(--m3-font-display-medium-weight, 400);
+}
+.m3-font-display-small {
+ font-size: var(--m3-font-display-small-size, 2.25rem);
+ line-height: var(--m3-font-display-small-height, 1.222);
+ letter-spacing: var(--m3-font-display-small-tracking, 0);
+ font-weight: var(--m3-font-display-small-weight, 400);
+}
+/* Headlines are best-suited for short, high-emphasis text on smaller screens. */
+.m3-font-headline-large {
+ font-size: var(--m3-font-headline-large-size, 2rem);
+ line-height: var(--m3-font-headline-large-height, 1.25);
+ letter-spacing: var(--m3-font-headline-large-tracking, 0);
+ font-weight: var(--m3-font-headline-large-weight, 400);
+}
+.m3-font-headline-medium {
+ font-size: var(--m3-font-headline-medium-size, 1.75rem);
+ line-height: var(--m3-font-headline-medium-height, 1.286);
+ letter-spacing: var(--m3-font-headline-medium-tracking, 0);
+ font-weight: var(--m3-font-headline-medium-weight, 400);
+}
+.m3-font-headline-small {
+ font-size: var(--m3-font-headline-small-size, 1.5rem);
+ line-height: var(--m3-font-headline-small-height, 1.333);
+ letter-spacing: var(--m3-font-headline-small-tracking, 0);
+ font-weight: var(--m3-font-headline-small-weight, 400);
+}
+/* Titles are smaller than headline styles, and should be used for medium-emphasis text that remains relatively short. */
+.m3-font-title-large {
+ font-size: var(--m3-font-title-large-size, 1.375rem);
+ line-height: var(--m3-font-title-large-height, 1.273);
+ letter-spacing: var(--m3-font-title-large-tracking, 0);
+ font-weight: var(--m3-font-title-large-weight, 400);
+}
+.m3-font-title-medium {
+ font-size: var(--m3-font-title-medium-size, 1rem);
+ line-height: var(--m3-font-title-medium-height, 1.5);
+ letter-spacing: var(--m3-font-title-medium-tracking, 0);
+ font-weight: var(--m3-font-title-medium-weight, 500);
+}
+.m3-font-title-small {
+ font-size: var(--m3-font-title-small-size, 0.875rem);
+ line-height: var(--m3-font-title-small-height, 1.429);
+ letter-spacing: var(--m3-font-title-small-tracking, 0.006rem);
+ font-weight: var(--m3-font-title-small-weight, 500);
+}
+/* Label styles are smaller, utilitarian styles, used for things like the text inside components
+or for very small text in the content body, such as captions. */
+.m3-font-label-large {
+ font-size: var(--m3-font-label-large-size, 0.875rem);
+ line-height: var(--m3-font-label-large-height, 1.429);
+ letter-spacing: var(--m3-font-label-large-tracking, 0.006rem);
+ font-weight: var(--m3-font-label-large-weight, 500);
+}
+.m3-font-label-medium {
+ font-size: var(--m3-font-label-medium-size, 0.75rem);
+ line-height: var(--m3-font-label-medium-height, 1.333);
+ letter-spacing: var(--m3-font-label-medium-tracking, 0.031rem);
+ font-weight: var(--m3-font-label-medium-weight, 500);
+}
+.m3-font-label-small {
+ font-size: var(--m3-font-label-small-size, 0.688rem);
+ line-height: var(--m3-font-label-small-height, 1.455);
+ letter-spacing: var(--m3-font-label-small-tracking, 0.031rem);
+ font-weight: var(--m3-font-label-small-weight, 500);
+}
+/* Body styles are used for longer passages of text in your app. */
+.m3-font-body-large {
+ font-size: var(--m3-font-body-large-size, 1rem);
+ line-height: var(--m3-font-body-large-height, 1.5);
+ letter-spacing: var(--m3-font-body-large-tracking, 0);
+ font-weight: var(--m3-font-body-large-weight, 400);
+}
+.m3-font-body-medium {
+ font-size: var(--m3-font-body-medium-size, 0.875rem);
+ line-height: var(--m3-font-body-medium-height, 1.429);
+ letter-spacing: var(--m3-font-body-medium-tracking, 0.016rem);
+ font-weight: var(--m3-font-body-medium-weight, 400);
+}
+.m3-font-body-small {
+ font-size: var(--m3-font-body-small-size, 0.75rem);
+ line-height: var(--m3-font-body-small-height, 1.333);
+ letter-spacing: var(--m3-font-body-small-tracking, 0.025rem);
+ font-weight: var(--m3-font-body-small-weight, 400);
+}
+/* Reset a few things */
+@layer base {
+ *,
+ *::before,
+ *::after {
+ box-sizing: inherit;
+ }
+ .m3-container {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+ }
+ .m3-container a,
+ a.m3-container {
+ text-decoration: none;
+ }
+ .m3-container dialog,
+ dialog.m3-container {
+ margin: auto;
+ }
+}
+
+/* Emphasized easing timing function derived from this code:
+const createBezierLUT = (points: number[][], pointCount: number = 100): number[][] => {
+const lut: number[][] = [];
+for (let t = 0; t < 1; t += 1 / pointCount) {
+ const a = (1 - t) * (1 - t) * (1 - t);
+ const b = (1 - t) * (1 - t) * t;
+ const c = (1 - t) * t * t;
+ const d = t * t * t;
+ const x = a * points[0][0] + 3 * b * points[1][0] + 3 * c * points[2][0] + d * points[3][0];
+ const y = a * points[0][1] + 3 * b * points[1][1] + 3 * c * points[2][1] + d * points[3][1];
+ lut.push([x, y]);
+}
+return lut;
+};
+
+const createCSSEaseOptimized = (
+lutOptions: number[][][],
+maxErrorThreshold: number = 0.01
+): string => {
+// Create the full lookup table
+const lut = lutOptions.map((args) => createBezierLUT(args)).flat();
+
+// Find key points using Douglas-Peucker algorithm
+const keyPoints: number[][] = [lut[0], lut[lut.length - 1]];
+const segments: number[][] = [[0, lut.length - 1]];
+
+while (segments.length > 0) {
+ const [startIdx, endIdx] = segments.pop() || [0, 0];
+ let maxError = 0;
+ let maxErrorIdx = -1;
+
+ // Skip if segment is too small
+ if (endIdx - startIdx <= 1) continue;
+
+ const [startX, startY] = lut[startIdx];
+ const [endX, endY] = lut[endIdx];
+
+ // Find point with maximum error
+ for (let i = startIdx + 1; i < endIdx; i++) {
+ const [x, y] = lut[i];
+
+ // Linear interpolation
+ const t = (x - startX) / (endX - startX);
+ const interpolatedY = startY + t * (endY - startY);
+
+ const error = Math.abs(y - interpolatedY);
+
+ if (error > maxError) {
+ maxError = error;
+ maxErrorIdx = i;
+ }
+ }
+
+ // If error exceeds threshold, add point and split segment
+ if (maxError > maxErrorThreshold) {
+ keyPoints.push(lut[maxErrorIdx]);
+ segments.push([startIdx, maxErrorIdx]);
+ segments.push([maxErrorIdx, endIdx]);
+ }
+}
+
+// Sort by x value
+keyPoints.sort((a, b) => a[0] - b[0]);
+
+// Format result using CSS linear() with explicit percentages
+let result = "linear(";
+
+// First point (no percentage for first point)
+result += keyPoints[0][1].toFixed(3);
+
+// Middle points with explicit percentages
+for (let i = 1; i < keyPoints.length - 1; i++) {
+ const [x, y] = keyPoints[i];
+ const percentage = (x * 100).toFixed(0) + "%";
+ result += `, ${y.toFixed(3)} ${percentage}`;
+}
+
+// Last point (no percentage for last point)
+result += `, ${keyPoints[keyPoints.length - 1][1].toFixed(3)}`;
+result += ")";
+
+return result;
+};
+
+// Example usage
+const testInput = [
+[
+ [0, 0],
+ [0.05, 0],
+ [0.133, 0.06],
+ [0.166, 0.4],
+],
+[
+ [0.166, 0.4],
+ [0.208, 0.82],
+ [0.25, 1],
+ [1, 1],
+],
+];
+
+// Run optimized function with a more relaxed threshold for smaller size
+const optimizedSmallResult = createCSSEaseOptimized(testInput, 0.01);
+console.log("Optimized result (relaxed threshold 0.01):");
+console.log(optimizedSmallResult);
+*/
+
+:root {
+ --m3-util-refocus: refocus-outward 0.6s cubic-bezier(0.14, 5.63, 0.4, 0.5) forwards;
+}
+:focus-visible:not(.focus-inset, .focus-none) {
+ animation: var(--m3-util-refocus);
+ outline: none;
+}
+:focus-visible:is(.focus-inset):not(.focus-none) {
+ animation: refocus-inward 0.6s cubic-bezier(0.14, 5.63, 0.4, 0.5) forwards;
+ outline: none;
+}
+:focus-visible.focus-none {
+ outline: none;
+}
+@keyframes refocus-outward {
+ 0% {
+ box-shadow: 0 0 0 0px rgb(var(--m3-scheme-secondary));
+ }
+ 100% {
+ box-shadow: 0 0 0 3px rgb(var(--m3-scheme-secondary));
+ }
+}
+@keyframes refocus-inward {
+ 0% {
+ box-shadow: inset 0 0 0 0px rgb(var(--m3-scheme-secondary));
+ }
+ 100% {
+ box-shadow: inset 0 0 0 3px rgb(var(--m3-scheme-secondary));
+ }
+}
+
+[placeholder]::placeholder {
+ color: rgb(var(--m3-scheme-on-background) / 0.5);
+ opacity: 1;
+}
+::selection {
+ background-color: rgb(var(--m3-scheme-tertiary-container));
+ color: rgb(var(--m3-scheme-on-tertiary-container));
+}
diff --git a/src/routes/material.scss b/src/style/material.scss
similarity index 98%
rename from src/routes/material.scss
rename to src/style/material.scss
index 2c9f377..23868a1 100644
--- a/src/routes/material.scss
+++ b/src/style/material.scss
@@ -1,5 +1,4 @@
-@import "m3-svelte/misc/styles.css";
-@import "m3-svelte/misc/recommended-styles.css";
+@import "m3-styles.css";
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
diff --git a/uno.config.ts b/uno.config.ts
index 8cf26ea..bf47b21 100644
--- a/uno.config.ts
+++ b/uno.config.ts
@@ -97,6 +97,13 @@ export default defineConfig({
},
}),
rules: [
+ // Material 3 scheme helpers for background/foreground utilities
+ [/^mbg-([a-zA-Z0-9-]+)$/i, ([, token]) => ({
+ 'background-color': `rgb(var(--m3-scheme-${token}) / 1)`,
+ })],
+ [/^mfg-([a-zA-Z0-9-]+)$/i, ([, token]) => ({
+ color: `rgb(var(--m3-scheme-${token}) / 1)`,
+ })],
[/^dir-([a-zA-Z0-9-]+)$/, ([, dir]) => ({
direction: dir,
})],