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}
- +
TODO
+ +
+ +
+ {#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, })],