[+] A button

This commit is contained in:
2025-11-18 15:46:53 +08:00
parent 5d6500c54b
commit 6c80a76d65
10 changed files with 481 additions and 9 deletions
+10
View File
@@ -0,0 +1,10 @@
<script>
let { icon, children, ...rest } = $props();
</script>
<button class="hbox h-40px rounded-12px px-16px py-10px gap-8px mfg-on-primary-container mbg-inverse-primary m3-font-label-large" {...rest}>
{#if icon}
<span class="size-20px {icon}"></span>
{/if}
{@render children?.()}
</button>
+3
View File
@@ -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() })
+37
View File
@@ -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 }
}
}
+2 -2
View File
@@ -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'
+5 -3
View File
@@ -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 }
}
+34 -1
View File
@@ -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)
</script>
@@ -21,9 +24,39 @@
{/if}
<div>
<TitleHeader title="我的歌单"/>
<TitleHeader title="历史数据"/>
<div class="p-content">
TODO
</div>
</div>
<div>
<TitleHeader title="我的歌单"/>
<div class="p-content hbox gap-8px w-auto overflow-x-auto py-8px">
{#each data.myPlaylists as playlist}
<div class="vbox flex-shrink-0 gap-4px w-96px">
<img src="{playlist.coverImgUrl}" alt="" class="size-96px rounded-16px">
<div class="m3-font-label-large truncate">{playlist.name}</div>
</div>
{/each}
</div>
<div class="p-content">
<Button icon="i-material-symbols:cloud-download-outline">从网易云导入</Button>
</div>
</div>
<div>
<TitleHeader title="推荐歌单"/>
<div class="p-content hbox gap-8px w-auto overflow-x-auto py-8px">
{#each data.recPlaylists as playlist}
<div class="vbox flex-shrink-0 p-8px gap-8px rounded-12px mbg-surface-container-high">
<img src="{playlist.coverImgUrl}" alt="" class="size-116px rounded-8px">
<div>
<div class="m3-font-title-small font-bold truncate">{playlist.name}</div>
<div class="m3-font-body-small truncate">{playlist.creator.nickname} 创建</div>
</div>
</div>
{/each}
</div>
</div>
</div>
+3 -1
View File
@@ -25,4 +25,6 @@ body
align-items: center
.p-content
padding: 0 16px
// padding: 0 16px
padding-left: 16px
padding-right: 16px
+379
View File
@@ -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));
}
@@ -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;
+7
View File
@@ -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,
})],