From a1479f239e69a0f7f25519ba84f055177b3c59ff Mon Sep 17 00:00:00 2001 From: anyone Date: Tue, 30 Jun 2026 14:58:45 +0800 Subject: [PATCH 01/38] remove heat reduction --- apps/cli/internal/scheduler/scheduler.go | 13 ----- apps/cli/internal/scheduler/scheduler_test.go | 53 ------------------- 2 files changed, 66 deletions(-) diff --git a/apps/cli/internal/scheduler/scheduler.go b/apps/cli/internal/scheduler/scheduler.go index 40854c1af..35e572a8b 100644 --- a/apps/cli/internal/scheduler/scheduler.go +++ b/apps/cli/internal/scheduler/scheduler.go @@ -40,7 +40,6 @@ func Start() { {"remove_unlinked_asset", removeUnlinkedAsset}, {"clean_music_transcode_cache", cleanMusicTranscodeCache}, {"pretranscode_music", pretranscodeMusic}, - {"decrease_music_heat", decreaseMusicHeat}, {"remove_outdated_shared_invitation", removeOutdatedSharedInvitation}, {"remove_outdated_auth_session", removeOutdatedAuthSession}, {"clean_outdated_file", cleanOutdatedFile}, @@ -411,18 +410,6 @@ func pretranscodeMusic() (schedulerJobResult, error) { }, errors.Join(errs...) } -func decreaseMusicHeat() (schedulerJobResult, error) { - updated, err := execRowsAffected( - `UPDATE music - SET heat = CASE WHEN heat > 0 THEN heat - 1 ELSE 0 END - WHERE heat != 0`, - ) - return schedulerJobResult{ - Summary: fmt.Sprintf("decreased heat for %d music rows", updated), - Metrics: map[string]int64{"updated_music_heat_rows": updated}, - }, err -} - // removeOutdatedSharedInvitation removes unanswered shared musicbill invitations older than 3 days. func removeOutdatedSharedInvitation() (schedulerJobResult, error) { threshold := time.Now().Add(-3 * 24 * time.Hour).UnixMilli() diff --git a/apps/cli/internal/scheduler/scheduler_test.go b/apps/cli/internal/scheduler/scheduler_test.go index cd7b34a8c..b81e6d20f 100644 --- a/apps/cli/internal/scheduler/scheduler_test.go +++ b/apps/cli/internal/scheduler/scheduler_test.go @@ -443,59 +443,6 @@ func TestRemoveUnlinkedAssetDeletesUnreferencedFiles(t *testing.T) { } } -func TestDecreaseMusicHeatDecreasesDailyWithoutGoingBelowZero(t *testing.T) { - if err := store.ResetForTests(); err != nil { - t.Fatalf("reset store: %v", err) - } - t.Cleanup(func() { - if err := store.ResetForTests(); err != nil { - t.Fatalf("cleanup store: %v", err) - } - }) - - config.Set(config.Config{ - Mode: config.ModeProduction, - Data: t.TempDir(), - Port: 8000, - }) - - if err := store.Initialize(); err != nil { - t.Fatalf("initialize store: %v", err) - } - - if _, err := store.DB().Exec( - `INSERT INTO music (id,type,name,asset,heat,createTimestamp) VALUES - ('HEAT02',1,'two','two.mp3',2,0), - ('HEAT01',1,'one','one.mp3',1,0), - ('HEAT00',1,'zero','zero.mp3',0,0)`, - ); err != nil { - t.Fatalf("insert music: %v", err) - } - - result, err := decreaseMusicHeat() - if err != nil { - t.Fatalf("decrease music heat: %v", err) - } - if result.Metrics["updated_music_heat_rows"] != 2 { - t.Fatalf("updated rows = %d", result.Metrics["updated_music_heat_rows"]) - } - - expected := map[string]int64{ - "HEAT02": 1, - "HEAT01": 0, - "HEAT00": 0, - } - for id, want := range expected { - var got int64 - if err := store.DB().QueryRow(`SELECT heat FROM music WHERE id=?`, id).Scan(&got); err != nil { - t.Fatalf("query %s heat: %v", id, err) - } - if got != want { - t.Fatalf("%s heat = %d, want %d", id, got, want) - } - } -} - func writeMusicAssetForCleanTest(t *testing.T, filename string) { t.Helper() From d4328a5e5e26bab964ce633b304824fa32f0440a Mon Sep 17 00:00:00 2001 From: anyone Date: Tue, 30 Jun 2026 19:44:49 +0800 Subject: [PATCH 02/38] hover not on mobile --- apps/pwa/src/components/button/index.tsx | 27 +++++++------- apps/pwa/src/components/dialog/index.tsx | 7 ++-- apps/pwa/src/components/drawer/index.tsx | 9 +++-- apps/pwa/src/components/image_frame/index.tsx | 11 +++--- apps/pwa/src/components/pagination/index.tsx | 15 ++++---- apps/pwa/src/components/slider/index.tsx | 21 ++++++----- apps/pwa/src/components/tabs.tsx | 19 ++++++---- apps/pwa/src/style/hover.ts | 36 +++++++++++++++++++ 8 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 apps/pwa/src/style/hover.ts diff --git a/apps/pwa/src/components/button/index.tsx b/apps/pwa/src/components/button/index.tsx index 249319e3f..858302fd0 100644 --- a/apps/pwa/src/components/button/index.tsx +++ b/apps/pwa/src/components/button/index.tsx @@ -6,6 +6,7 @@ import { } from 'react'; import styled, { css } from 'styled-components'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import { CSS_VAR } from '../theme'; import Spinner from '../spinner'; @@ -77,18 +78,20 @@ const makeVariant = ( box-shadow: 0 ${({ $offset }) => $offset}px 0 ${shadow}; - &:not(:disabled):hover { - ${({ $disableHoverLift, $offset }) => - $disableHoverLift - ? css` - transform: none; - box-shadow: 0 ${$offset}px 0 ${shadow}; - ` - : css` - transform: translateY(-2px); - box-shadow: 0 ${$offset + 2}px 0 ${shadow}; - `} - } + ${hover(css<{ $disableHoverLift: boolean; $offset: number }>` + &:not(:disabled):hover { + ${({ $disableHoverLift, $offset }) => + $disableHoverLift + ? css` + transform: none; + box-shadow: 0 ${$offset}px 0 ${shadow}; + ` + : css` + transform: translateY(-2px); + box-shadow: 0 ${$offset + 2}px 0 ${shadow}; + `} + } + `)} &:not(:disabled):active { transform: translateY(${({ $offset }) => $offset}px); diff --git a/apps/pwa/src/components/dialog/index.tsx b/apps/pwa/src/components/dialog/index.tsx index 20d52b2f0..7b3fcbea6 100644 --- a/apps/pwa/src/components/dialog/index.tsx +++ b/apps/pwa/src/components/dialog/index.tsx @@ -34,8 +34,9 @@ import { useState, } from 'react'; import * as RadixDialog from '@radix-ui/react-dialog'; -import styled, { keyframes } from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import { useTheme, CSS_VAR } from '../theme'; import { t } from '@/i18n'; import { @@ -241,7 +242,9 @@ const CloseButton = styled(RadixDialog.Close)` cursor: pointer; transition: color 120ms, background 120ms; - &:hover { color: rgb(60 60 60); background: rgb(240 240 240); } + ${hover(css` + &:hover { color: rgb(60 60 60); background: rgb(240 240 240); } + `)} &:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; diff --git a/apps/pwa/src/components/drawer/index.tsx b/apps/pwa/src/components/drawer/index.tsx index fcb2b658a..5b12196e1 100644 --- a/apps/pwa/src/components/drawer/index.tsx +++ b/apps/pwa/src/components/drawer/index.tsx @@ -36,6 +36,7 @@ import * as RadixDialog from '@radix-ui/react-dialog'; import styled, { css, keyframes } from 'styled-components'; import { useTheme, CSS_VAR } from '../theme'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import { t } from '@/i18n'; import { DialogTitleRegistryContext, @@ -178,9 +179,11 @@ const CloseButton = styled(RadixDialog.Close)` box-shadow 150ms ease-out, filter 120ms; - &:hover { - filter: brightness(1.06); - } + ${hover(css` + &:hover { + filter: brightness(1.06); + } + `)} &:active { transform: translateY(3px); diff --git a/apps/pwa/src/components/image_frame/index.tsx b/apps/pwa/src/components/image_frame/index.tsx index d65c94153..8e0afce3d 100644 --- a/apps/pwa/src/components/image_frame/index.tsx +++ b/apps/pwa/src/components/image_frame/index.tsx @@ -1,5 +1,6 @@ import { HTMLAttributes } from 'react'; import styled, { css } from 'styled-components'; +import hover from '@/style/hover'; import Cover, { Shape } from '@/components/cover'; const FACE = '#ffffff'; @@ -62,10 +63,12 @@ const Root = styled.div<{ user-select: none; -webkit-tap-highlight-color: transparent; - &:hover { - transform: translateY(-2px); - box-shadow: 0 ${$shadowOffset + 2}px 0 ${$shadowColor}; - } + ${hover(css` + &:hover { + transform: translateY(-2px); + box-shadow: 0 ${$shadowOffset + 2}px 0 ${$shadowColor}; + } + `)} &:active { transform: translateY(${$shadowOffset}px); diff --git a/apps/pwa/src/components/pagination/index.tsx b/apps/pwa/src/components/pagination/index.tsx index 8f139baec..c455bbbf1 100644 --- a/apps/pwa/src/components/pagination/index.tsx +++ b/apps/pwa/src/components/pagination/index.tsx @@ -2,6 +2,7 @@ import { HtmlHTMLAttributes } from 'react'; import styled, { css } from 'styled-components'; import Button, { Size } from '@/components/button'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import usePagination from './use_pagination'; import { t } from '@/i18n'; import { ChevronLeft, ChevronRight, MoreHorizontal } from '@/components/icon'; @@ -38,12 +39,14 @@ const PageButton = styled(Button)<{ $selected: boolean; $size: Size }>` transform 150ms ease-out, filter 120ms ease-out; - &:not(:disabled):hover { - background: #fff; - border-color: ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - box-shadow: 0 ${SHADOW_OFFSET[$size] + 2}px 0 - ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } + ${hover(css` + &:not(:disabled):hover { + background: #fff; + border-color: ${CSSVariable.COLOR_NEUTRAL_SHADOW}; + box-shadow: 0 ${SHADOW_OFFSET[$size] + 2}px 0 + ${CSSVariable.COLOR_NEUTRAL_SHADOW}; + } + `)} &:not(:disabled):active { transition: diff --git a/apps/pwa/src/components/slider/index.tsx b/apps/pwa/src/components/slider/index.tsx index b49fc2151..e1e5c72d5 100644 --- a/apps/pwa/src/components/slider/index.tsx +++ b/apps/pwa/src/components/slider/index.tsx @@ -1,8 +1,8 @@ import * as Radix from '@radix-ui/react-slider'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; +import hover, { FINE_POINTER_MEDIA } from '@/style/hover'; import classnames from 'classnames'; import { ComponentPropsWithoutRef } from 'react'; -import { IS_TOUCHABLE } from '@/constants/browser'; import { CSS_VAR } from '../theme'; export type SliderEdge = 'rounded' | 'square'; @@ -121,9 +121,11 @@ const StyledRoot = styled(Radix.Root)` &.square ${StyledTrack} { border-radius: 4px; } /* hover:轨道略亮,同 Button hover 行为 */ - &:not([data-disabled]):hover ${StyledTrack} { - filter: brightness(1.06); - } + ${hover(css` + &:not([data-disabled]):hover ${StyledTrack} { + filter: brightness(1.06); + } + `)} /* disabled */ &[data-disabled] { @@ -131,9 +133,11 @@ const StyledRoot = styled(Radix.Root)` opacity: 0.5; } - /* 非触摸设备:默认隐藏拇指 */ - &.untouchable:not(.always-show-thumb) ${StyledThumb} { - display: none; + /* 精确指针设备 (鼠标等):默认隐藏拇指, 仅留轨道, 反馈交给 hover 提亮 */ + @media ${FINE_POINTER_MEDIA} { + &:not(.always-show-thumb) ${StyledThumb} { + display: none; + } } `; @@ -178,7 +182,6 @@ function Slider({ className={classnames( edge, { - untouchable: !IS_TOUCHABLE, 'always-show-thumb': alwaysShowThumb, }, className, diff --git a/apps/pwa/src/components/tabs.tsx b/apps/pwa/src/components/tabs.tsx index d90f86798..b3c6bba2d 100644 --- a/apps/pwa/src/components/tabs.tsx +++ b/apps/pwa/src/components/tabs.tsx @@ -8,6 +8,7 @@ import { import { animated, useSpring } from '@react-spring/web'; import styled, { css } from 'styled-components'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import { CSS_VAR } from './theme'; const PRIMARY = `var(${CSS_VAR.colorPrimary})`; @@ -114,9 +115,11 @@ const TabButton = styled.button<{ $active: boolean }>` pointer-events: none; } - &:not(:disabled):hover { - filter: brightness(1.04); - } + ${hover(css` + &:not(:disabled):hover { + filter: brightness(1.04); + } + `)} &:disabled { cursor: not-allowed; @@ -137,11 +140,13 @@ const TabButton = styled.button<{ $active: boolean }>` ${({ $active }) => !$active && css` - &:not(:disabled):hover { - &::before { - opacity: 1; + ${hover(css` + &:not(:disabled):hover { + &::before { + opacity: 1; + } } - } + `)} `} `; diff --git a/apps/pwa/src/style/hover.ts b/apps/pwa/src/style/hover.ts new file mode 100644 index 000000000..8ea9d1022 --- /dev/null +++ b/apps/pwa/src/style/hover.ts @@ -0,0 +1,36 @@ +import { + css, + DefaultTheme, + FlattenInterpolation, + ThemeProps, +} from 'styled-components'; + +/** + * 精确指针且支持真实悬停的设备 (鼠标等), 用于区隔触摸设备. + * 作为「悬停态」与「仅触摸设备才需要的交互件」共用的判定条件. + */ +export const FINE_POINTER_MEDIA = '(hover: hover) and (pointer: fine)'; + +/** + * 仅在支持真实悬停的设备 (鼠标等精确指针) 上应用样式, + * 规避触摸设备点击后 hover 状态粘滞的问题. + * + * 选择器由调用方书写, 以兼容后代/嵌套等复杂形态; 若内部样式依赖 + * styled 组件的 props, 通过 `css` 传入以保留类型推断: + * ```ts + * ${hover(css` + * &:not(:disabled):hover { + * filter: brightness(1.04); + * } + * `)} + * ``` + */ +const hover =

>( + style: FlattenInterpolation & P>, +) => css` + @media ${FINE_POINTER_MEDIA} { + ${style} + } +`; + +export default hover; From 0a7bf8b2878446decd631f1a756331c558084e37 Mon Sep 17 00:00:00 2001 From: anyone Date: Tue, 30 Jun 2026 19:48:40 +0800 Subject: [PATCH 03/38] remove slider thumb --- apps/pwa/src/components/slider/index.tsx | 60 ++----------------- .../src/components/slider/slider.stories.tsx | 7 +-- .../floating_music_player.tsx | 1 - .../pages/player/controller/progress_bar.tsx | 1 - .../lyric_panel/controller/progress_bar.tsx | 1 - 5 files changed, 7 insertions(+), 63 deletions(-) diff --git a/apps/pwa/src/components/slider/index.tsx b/apps/pwa/src/components/slider/index.tsx index e1e5c72d5..c04b48b1e 100644 --- a/apps/pwa/src/components/slider/index.tsx +++ b/apps/pwa/src/components/slider/index.tsx @@ -1,6 +1,6 @@ import * as Radix from '@radix-ui/react-slider'; import styled, { css } from 'styled-components'; -import hover, { FINE_POINTER_MEDIA } from '@/style/hover'; +import hover from '@/style/hover'; import classnames from 'classnames'; import { ComponentPropsWithoutRef } from 'react'; import { CSS_VAR } from '../theme'; @@ -11,7 +11,6 @@ const PRIMARY = `var(${CSS_VAR.colorPrimary})`; const PRIMARY_SHADOW = `var(${CSS_VAR.colorPrimaryShadow})`; const TRACK_H = 12; // px — 轨道高度,与 Button 同等视觉分量 -const THUMB_SZ = 26; // px — 拇指直径 const OFFSET = 4; // px — 阴影偏移,对应 Button md 的 4px const BORDER = 2; // px — 描边宽度,与 Button 一致 @@ -63,45 +62,14 @@ const StyledTrack = styled(Radix.Track)` // ─── thumb ──────────────────────────────────────────────────────────────────── // -// 白底圆钮,与 Button secondary 变体同款 3D 公式: -// 正常 — 白底 + 主色描边 + 底部硬阴影 -// 按下 — translateY(offset) + 阴影归零,60ms ease-in -// 释放 — 150ms ease-out 弹回 +// 拇指视觉已移除:仅保留一个不可见的 Radix.Thumb,供拖拽 / 键盘 / 取值使用 +// (轨道点击与拖拽不依赖拇指可见性) const StyledThumb = styled(Radix.Thumb)` display: block; - width: ${THUMB_SZ}px; - height: ${THUMB_SZ}px; - border-radius: 50%; - background: #fff; - border: ${BORDER}px solid ${PRIMARY_SHADOW}; - box-shadow: 0 ${OFFSET}px 0 ${PRIMARY_SHADOW}; + width: 0; + height: 0; outline: none; - cursor: grab; - z-index: 1; - will-change: transform, box-shadow; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out; - - &:active { - cursor: grabbing; - transform: translateY(${OFFSET}px); - box-shadow: none; - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in; - } - - &:focus-visible { - box-shadow: - 0 ${OFFSET}px 0 ${PRIMARY_SHADOW}, - 0 0 0 3px color-mix(in srgb, ${PRIMARY} 40%, transparent); - } - - &[data-disabled] { - cursor: not-allowed; - } `; // ─── root ───────────────────────────────────────────────────────────────────── @@ -132,13 +100,6 @@ const StyledRoot = styled(Radix.Root)` cursor: not-allowed; opacity: 0.5; } - - /* 精确指针设备 (鼠标等):默认隐藏拇指, 仅留轨道, 反馈交给 hover 提亮 */ - @media ${FINE_POINTER_MEDIA} { - &:not(.always-show-thumb) ${StyledThumb} { - display: none; - } - } `; // ─── component ──────────────────────────────────────────────────────────────── @@ -153,8 +114,6 @@ export interface SliderProps onChange?: (value: number) => void; onCommit?: (value: number) => void; edge?: SliderEdge; - /** 在非触摸设备上也显示拇指 */ - alwaysShowThumb?: boolean; /** 副轨道值(0–1),用于缓冲进度等场景 */ secondValue?: number; } @@ -165,7 +124,6 @@ function Slider({ onChange, onCommit, edge = 'rounded', - alwaysShowThumb = false, secondValue, className, ...props @@ -179,13 +137,7 @@ function Slider({ value={[value]} onValueChange={([v]) => onChange?.(v)} onValueCommit={([v]) => onCommit?.(v)} - className={classnames( - edge, - { - 'always-show-thumb': alwaysShowThumb, - }, - className, - )} + className={classnames(edge, className)} > {secondValue !== undefined && ( diff --git a/apps/pwa/src/components/slider/slider.stories.tsx b/apps/pwa/src/components/slider/slider.stories.tsx index 72d2826ed..c225fa67f 100644 --- a/apps/pwa/src/components/slider/slider.stories.tsx +++ b/apps/pwa/src/components/slider/slider.stories.tsx @@ -35,11 +35,6 @@ const meta = { control: { type: 'range', min: 0, max: 1, step: 0.01 }, description: 'Secondary track value (0–1), e.g. buffer progress', }, - alwaysShowThumb: { - control: 'boolean', - description: 'Show thumb on non-touch devices', - table: { defaultValue: { summary: 'false' } }, - }, disabled: { control: 'boolean', description: 'Disabled state', @@ -133,7 +128,7 @@ export const Scenarios: Story = {

Volume
- +
diff --git a/apps/pwa/src/pages/admin/music_management/floating_music_player.tsx b/apps/pwa/src/pages/admin/music_management/floating_music_player.tsx index d9041ee7f..4fc3bbbcd 100644 --- a/apps/pwa/src/pages/admin/music_management/floating_music_player.tsx +++ b/apps/pwa/src/pages/admin/music_management/floating_music_player.tsx @@ -823,7 +823,6 @@ function FloatingMusicPlayer({ value={progressValue} onChange={onSeek} disabled={!canSeek} - alwaysShowThumb aria-label={t('music_playback_progress')} /> diff --git a/apps/pwa/src/pages/player/controller/progress_bar.tsx b/apps/pwa/src/pages/player/controller/progress_bar.tsx index c15ced516..4aa8dd8a1 100644 --- a/apps/pwa/src/pages/player/controller/progress_bar.tsx +++ b/apps/pwa/src/pages/player/controller/progress_bar.tsx @@ -29,7 +29,6 @@ function Progress({ onChange={onChange} onCommit={onCommit} secondValue={bufferedPercent} - alwaysShowThumb /> ); } diff --git a/apps/pwa/src/pages/player/lyric_panel/controller/progress_bar.tsx b/apps/pwa/src/pages/player/lyric_panel/controller/progress_bar.tsx index 52b15c4e7..6359e51c0 100644 --- a/apps/pwa/src/pages/player/lyric_panel/controller/progress_bar.tsx +++ b/apps/pwa/src/pages/player/lyric_panel/controller/progress_bar.tsx @@ -62,7 +62,6 @@ function Wrapper({ onCommit={onTimeCommit} className="slider" secondValue={bufferedPercent} - alwaysShowThumb />
{formatSecond(duration)}
From ba5806c1c211e78bf0ac1074a5857be21a4ef353 Mon Sep 17 00:00:00 2001 From: mebtte Date: Tue, 30 Jun 2026 20:58:27 +0800 Subject: [PATCH 04/38] fix pagination --- apps/pwa/src/components/pagination/index.tsx | 33 +------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/apps/pwa/src/components/pagination/index.tsx b/apps/pwa/src/components/pagination/index.tsx index c455bbbf1..5c626e1cf 100644 --- a/apps/pwa/src/components/pagination/index.tsx +++ b/apps/pwa/src/components/pagination/index.tsx @@ -1,14 +1,11 @@ import { HtmlHTMLAttributes } from 'react'; import styled, { css } from 'styled-components'; import Button, { Size } from '@/components/button'; -import { CSSVariable } from '@/global_style'; -import hover from '@/style/hover'; import usePagination from './use_pagination'; import { t } from '@/i18n'; import { ChevronLeft, ChevronRight, MoreHorizontal } from '@/components/icon'; const GAP_MAP: Record = { sm: 5, md: 6, lg: 8 }; -const SHADOW_OFFSET: Record = { sm: 3, md: 4, lg: 5 }; const PAGINATION_SIZE: Size = 'sm'; const ELLIPSIS_SIZE: Record = { sm: { box: 34, icon: 18 }, @@ -21,40 +18,13 @@ const Style = styled.div<{ $gap: number }>` align-items: center; gap: ${({ $gap }) => $gap}px; `; -const PageButton = styled(Button)<{ $selected: boolean; $size: Size }>` +const PageButton = styled(Button)<{ $size: Size }>` min-width: ${({ $size }) => ELLIPSIS_SIZE[$size].box}px; line-height: 1; > .btn-label { line-height: 1; } - - ${({ $selected, $size }) => - !$selected && - css` - transition: - background 150ms ease-out, - border-color 150ms ease-out, - box-shadow 150ms ease-out, - transform 150ms ease-out, - filter 120ms ease-out; - - ${hover(css` - &:not(:disabled):hover { - background: #fff; - border-color: ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - box-shadow: 0 ${SHADOW_OFFSET[$size] + 2}px 0 - ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } - `)} - - &:not(:disabled):active { - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in, - filter 60ms ease-in; - } - `} `; const Ellipsis = styled.span<{ $size: Size }>` display: inline-flex; @@ -149,7 +119,6 @@ function Pagination({ Date: Tue, 30 Jun 2026 21:34:39 +0800 Subject: [PATCH 05/38] fix artist drawer --- .../music/components/performer/index.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/pwa/src/features/music/components/performer/index.tsx b/apps/pwa/src/features/music/components/performer/index.tsx index fb1828a88..25763bc35 100644 --- a/apps/pwa/src/features/music/components/performer/index.tsx +++ b/apps/pwa/src/features/music/components/performer/index.tsx @@ -12,16 +12,22 @@ export interface ArtistProps { performer: ArtistValue; } -const Style = styled.span` +const Style = styled.span<{ $clickable: boolean }>` user-select: none; > .name { - cursor: pointer; transition: color 120ms ease-out; - &:hover { - color: ${CSSVariable.TEXT_COLOR_PRIMARY}; - } + ${({ $clickable }) => + $clickable + ? ` + cursor: pointer; + + &:hover { + color: ${CSSVariable.TEXT_COLOR_PRIMARY}; + } + ` + : ''} } > .divider { @@ -36,13 +42,20 @@ const Style = styled.span` `; function Performer({ onOpen, performer }: ArtistProps) { + const clickable = !!onOpen; + return ( - ); } diff --git a/apps/pwa/src/components/tabs.stories.tsx b/apps/pwa/src/components/tabs.stories.tsx index 16139ad13..d221d564d 100644 --- a/apps/pwa/src/components/tabs.stories.tsx +++ b/apps/pwa/src/components/tabs.stories.tsx @@ -22,7 +22,7 @@ const meta = { docs: { description: { component: - 'Duolingo-style tabs with an animated active block. Panels stay mounted so each tab keeps local state while switching.', + 'Tabs with an animated active block. Panels stay mounted so each tab keeps local state while switching.', }, }, }, diff --git a/apps/pwa/src/components/tag/index.tsx b/apps/pwa/src/components/tag/index.tsx index 025095721..c2a4efc18 100644 --- a/apps/pwa/src/components/tag/index.tsx +++ b/apps/pwa/src/components/tag/index.tsx @@ -77,11 +77,6 @@ export interface TagProps extends HTMLAttributes { icon?: ReactNode; } -/** - * 胶囊徽章 (Tag) - * 与 Button 共用 Duolingo 式视觉公式: 纯色填充 + 同色硬阴影, - * 用于状态/角色/类型等只读标签. - */ function Tag( { variant = 'primary', diff --git a/apps/pwa/src/components/tag/tag.stories.tsx b/apps/pwa/src/components/tag/tag.stories.tsx index 75fb211eb..f4aa67f11 100644 --- a/apps/pwa/src/components/tag/tag.stories.tsx +++ b/apps/pwa/src/components/tag/tag.stories.tsx @@ -11,7 +11,7 @@ const meta = { docs: { description: { component: - 'Duolingo-style pill badge with the same hard-shadow formula as `Button`. Use it for read-only status, role, or type tags (e.g. "current device", "admin"). Supports 3 variants (`primary`, `neutral`, `danger`), 2 sizes, and an optional leading icon. Primary variant follows the per-server custom theme color.', + 'Pill badge with the same hard-shadow formula as `Button`. Use it for read-only status, role, or type tags (e.g. "current device", "admin"). Supports 3 variants (`primary`, `neutral`, `danger`), 2 sizes, and an optional leading icon. Primary variant follows the per-server custom theme color.', }, }, }, diff --git a/apps/pwa/src/components/tooltip/index.tsx b/apps/pwa/src/components/tooltip/index.tsx index 29feede5b..5b853ac5b 100644 --- a/apps/pwa/src/components/tooltip/index.tsx +++ b/apps/pwa/src/components/tooltip/index.tsx @@ -1,5 +1,5 @@ /** - * Tooltip — 基于 @tippyjs/react 封装的 Duolingo 风格提示气泡 + * Tooltip — 基于 @tippyjs/react 封装的提示气泡 * * 交互策略: * - 鼠标 hover: 延迟 300ms 显示, 离开立即消失 @@ -28,7 +28,7 @@ import styled from 'styled-components'; const FONT = `'Nunito', 'Varela Round', system-ui, sans-serif`; const Bubble = styled(animated.div)` - /* Duolingo 风格: 浅灰底 + 中性硬阴影, 比纯白柔和又能保留立体落差 */ + max-width: 240px; padding: 6px 10px; diff --git a/apps/pwa/src/components/tooltip/tooltip.stories.tsx b/apps/pwa/src/components/tooltip/tooltip.stories.tsx index 0b97cc575..3e70a93b4 100644 --- a/apps/pwa/src/components/tooltip/tooltip.stories.tsx +++ b/apps/pwa/src/components/tooltip/tooltip.stories.tsx @@ -12,7 +12,7 @@ const meta = { docs: { description: { component: - 'Duolingo-style tooltip. Mouse hover shows the tooltip after a short delay; touch long press shows it after 500ms by default. Short taps on touch devices keep the tooltip hidden and pass the click through to the child element.', + 'Tooltip. Mouse hover shows the tooltip after a short delay; touch long press shows it after 500ms by default. Short taps on touch devices keep the tooltip hidden and pass the click through to the child element.', }, }, }, diff --git a/apps/pwa/src/features/player/drawers/music_drawer/lyric.tsx b/apps/pwa/src/features/player/drawers/music_drawer/lyric.tsx index f9e4948a5..987a2f8dd 100644 --- a/apps/pwa/src/features/player/drawers/music_drawer/lyric.tsx +++ b/apps/pwa/src/features/player/drawers/music_drawer/lyric.tsx @@ -44,7 +44,7 @@ const Line = styled.div` font-weight: 600; color: rgb(120 120 120); `; -// Duolingo 风格小按钮: 纯色填充 + 同色硬阴影(无 blur), 按下 translateY 抹平阴影 + const DOWNLOAD_BTN_OFFSET = 2; const DownloadButton = styled.button` position: absolute; diff --git a/apps/pwa/src/pages/player/music_drawer/lyric.tsx b/apps/pwa/src/pages/player/music_drawer/lyric.tsx index bb1f30d56..c8a5a9b3c 100644 --- a/apps/pwa/src/pages/player/music_drawer/lyric.tsx +++ b/apps/pwa/src/pages/player/music_drawer/lyric.tsx @@ -44,7 +44,7 @@ const Line = styled.div` font-weight: 600; color: rgb(120 120 120); `; -// Duolingo 风格小按钮: 纯色填充 + 同色硬阴影(无 blur), 按下 translateY 抹平阴影 + const DOWNLOAD_BTN_OFFSET = 2; const DownloadButton = styled.button` position: absolute; diff --git a/apps/pwa/src/style/tokens_style.ts b/apps/pwa/src/style/tokens_style.ts index f87490699..4ba7d422a 100644 --- a/apps/pwa/src/style/tokens_style.ts +++ b/apps/pwa/src/style/tokens_style.ts @@ -68,25 +68,24 @@ const CSS_VARIABLE_MAP_VALUE: Record = { [CSSVariable.BORDER_RADIUS_NORMAL]: '4px', }; -const DUOLINGO_FONT_FAMILY = +const FONT_FAMILY = "'Nunito', 'Varela Round', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"; export const TokensStyle = createGlobalStyle` html { ${Object.keys(CSS_VARIABLE_MAP_VALUE) - .map( - (variable) => - `${variable.match(/^var\((.+)\)$/)![1]}: ${ - CSS_VARIABLE_MAP_VALUE[variable] - };`, - ) - .join('\n')} + .map( + (variable) => + `${variable.match(/^var\((.+)\)$/)![1]}: ${CSS_VARIABLE_MAP_VALUE[variable] + };`, + ) + .join('\n')} accent-color: ${CSSVariable.COLOR_PRIMARY}; } body { - font-family: ${DUOLINGO_FONT_FAMILY}; + font-family: ${FONT_FAMILY}; } input, diff --git a/docs/ui_designment/index.md b/docs/ui_designment/index.md index 50c64f05f..d543d5868 100644 --- a/docs/ui_designment/index.md +++ b/docs/ui_designment/index.md @@ -1,6 +1,6 @@ # UI Designment -`cicada` prefers comic styles and currently mostly likes `duolingo`. +`cicada` prefers comic styles. ## Rules From 61caa9eab49d88a5d192a4b47f5e204e41e89de4 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 16:43:03 +0800 Subject: [PATCH 08/38] improve tab styles --- apps/pwa/src/components/tabs.tsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/pwa/src/components/tabs.tsx b/apps/pwa/src/components/tabs.tsx index b3c6bba2d..b4c1d1056 100644 --- a/apps/pwa/src/components/tabs.tsx +++ b/apps/pwa/src/components/tabs.tsx @@ -14,6 +14,7 @@ import { CSS_VAR } from './theme'; const PRIMARY = `var(${CSS_VAR.colorPrimary})`; const PRIMARY_SHADOW = `var(${CSS_VAR.colorPrimaryShadow})`; const HOVER_SHADOW = CSSVariable.COLOR_DISABLED_SHADOW; +const TAB_HEIGHT = 34; export type TabItem = { tab: TabType; @@ -39,7 +40,6 @@ const TabListRoot = styled.div` background: #fff; border: 2px solid ${CSSVariable.COLOR_BORDER}; border-radius: 16px; - box-shadow: 0 4px 0 ${CSSVariable.COLOR_SURFACE_SHADOW}; `; const ActiveBlock = styled.div<{ @@ -62,7 +62,7 @@ const ActiveBlock = styled.div<{ $widthOffset}px ); background: ${PRIMARY}; - border: 2px solid ${PRIMARY_SHADOW}; + box-shadow: inset 0 0 0 2px ${PRIMARY_SHADOW}; border-radius: 12px; transition: left 220ms cubic-bezier(0.2, 0.8, 0.2, 1); pointer-events: none; @@ -74,7 +74,7 @@ const TabButton = styled.button<{ $active: boolean }>` isolation: isolate; flex: 1 1 0; min-width: 0; - height: 34px; + height: ${TAB_HEIGHT}px; padding: 0 12px; display: flex; @@ -84,6 +84,8 @@ const TabButton = styled.button<{ $active: boolean }>` border: 0; border-radius: 12px; background: transparent; + appearance: none; + -webkit-appearance: none; color: ${CSSVariable.TEXT_COLOR_PRIMARY}; cursor: pointer; user-select: none; @@ -92,11 +94,9 @@ const TabButton = styled.button<{ $active: boolean }>` font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; font-weight: 800; + line-height: 1; letter-spacing: 0; text-transform: capitalize; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; transition: color 150ms ease-out, @@ -150,6 +150,20 @@ const TabButton = styled.button<{ $active: boolean }>` `} `; +const TabButtonLabel = styled.span` + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + max-width: 100%; + height: 100%; + padding-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1; +`; + const PanelsRoot = styled.div` position: relative; width: 100%; @@ -216,7 +230,7 @@ export function TabList({ } }} > - {label} + {label} ); })} From d4f8c9ae70e58de80282ab660bdc55f00af299b6 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 16:51:37 +0800 Subject: [PATCH 09/38] fix exploration on safari --- .../pages/player/pages/exploration/cover.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/pwa/src/pages/player/pages/exploration/cover.tsx b/apps/pwa/src/pages/player/pages/exploration/cover.tsx index 398752f5e..4fe3d42b5 100644 --- a/apps/pwa/src/pages/player/pages/exploration/cover.tsx +++ b/apps/pwa/src/pages/player/pages/exploration/cover.tsx @@ -11,6 +11,7 @@ const Style = styled.div<{ $variant: Variant; }>` position: relative; + isolation: isolate; min-width: 0; padding: ${({ $variant }) => { if ($variant === 'profile') { @@ -46,7 +47,14 @@ const Style = styled.div<{ border-radius: 50%; background: ${({ $accent }) => $accent}; box-shadow: 0 2px 0 ${({ $shadow }) => $shadow}; - z-index: 2; + z-index: 4; + pointer-events: none; + transform: translateZ(0); + } + + > * { + position: relative; + z-index: 1; } > .info { @@ -191,6 +199,7 @@ const CassetteScene = styled.div<{ }>` position: relative; aspect-ratio: 1; + isolation: isolate; overflow: hidden; border: 3px solid ${({ $shadow }) => $shadow}; @@ -201,16 +210,17 @@ const CassetteScene = styled.div<{ content: ''; position: absolute; inset: 0; - z-index: 1; - background: - linear-gradient(180deg, transparent 0 48%, rgb(0 0 0 / 0.08) 100%), - radial-gradient(circle at 20% 16%, rgb(255 255 255 / 0.42) 0 14px, transparent 15px); + z-index: 2; + background: linear-gradient(180deg, transparent 0 48%, rgb(0 0 0 / 0.08) 100%); pointer-events: none; + transform: translateZ(0); } > .artwork-frame { position: absolute; inset: 0; + z-index: 0; + contain: paint; overflow: hidden; } @@ -228,7 +238,7 @@ const CassetteScene = styled.div<{ left: 7%; right: 7%; bottom: 7%; - z-index: 2; + z-index: 3; height: 38%; padding: 7% 12% 6%; From ac1fbbd46cde6d500d20808964bbc887db3bdea2 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 16:53:08 +0800 Subject: [PATCH 10/38] fix tab style --- apps/pwa/src/components/tabs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/pwa/src/components/tabs.tsx b/apps/pwa/src/components/tabs.tsx index b4c1d1056..9cccb3608 100644 --- a/apps/pwa/src/components/tabs.tsx +++ b/apps/pwa/src/components/tabs.tsx @@ -157,7 +157,6 @@ const TabButtonLabel = styled.span` min-width: 0; max-width: 100%; height: 100%; - padding-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; From 0ff9a128efc70ac265f124b08ad7aa4addae9785 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:03:46 +0800 Subject: [PATCH 11/38] remove background --- apps/pwa/src/pages/admin/dashboard/index.tsx | 1 - apps/pwa/src/pages/admin/index.tsx | 1 - apps/pwa/src/pages/login/manage_page.tsx | 1 - apps/pwa/src/pages/player/pages/artist/index.tsx | 1 - apps/pwa/src/pages/player/pages/exploration/index.tsx | 3 --- apps/pwa/src/pages/player/pages/music/index.tsx | 1 - apps/pwa/src/pages/player/pages/musicbill/musicbill.tsx | 4 ---- apps/pwa/src/pages/player/pages/offline_cache/index.tsx | 4 ---- .../pages/player/pages/public_musicbill_collection/index.tsx | 4 ---- .../pages/player/public_musicbill_collection_drawer/index.tsx | 4 ---- apps/pwa/src/pages/radio/page.tsx | 1 - 11 files changed, 25 deletions(-) diff --git a/apps/pwa/src/pages/admin/dashboard/index.tsx b/apps/pwa/src/pages/admin/dashboard/index.tsx index 3599e83be..b48c6341a 100644 --- a/apps/pwa/src/pages/admin/dashboard/index.tsx +++ b/apps/pwa/src/pages/admin/dashboard/index.tsx @@ -62,7 +62,6 @@ interface Data { const Page = styled.div` height: 100%; padding: 20px; - background: rgb(247 247 247); overflow: auto; font-family: ${FONT}; ${autoScrollbar} diff --git a/apps/pwa/src/pages/admin/index.tsx b/apps/pwa/src/pages/admin/index.tsx index a57d71a77..5e2a70131 100644 --- a/apps/pwa/src/pages/admin/index.tsx +++ b/apps/pwa/src/pages/admin/index.tsx @@ -91,7 +91,6 @@ const Page = styled.div` position: fixed; inset: 0; display: flex; - background: rgb(247 247 247); overflow: hidden; `; diff --git a/apps/pwa/src/pages/login/manage_page.tsx b/apps/pwa/src/pages/login/manage_page.tsx index 4e609dec9..3cd8b4e9b 100644 --- a/apps/pwa/src/pages/login/manage_page.tsx +++ b/apps/pwa/src/pages/login/manage_page.tsx @@ -16,7 +16,6 @@ const Wrapper = styled.div` position: absolute; inset: 0; z-index: 10; - background: rgb(248 248 248); display: flex; flex-direction: column; animation: ${slideIn} 300ms cubic-bezier(0.16, 1, 0.3, 1); diff --git a/apps/pwa/src/pages/player/pages/artist/index.tsx b/apps/pwa/src/pages/player/pages/artist/index.tsx index e62995534..89c9e1722 100644 --- a/apps/pwa/src/pages/player/pages/artist/index.tsx +++ b/apps/pwa/src/pages/player/pages/artist/index.tsx @@ -10,7 +10,6 @@ const Style = styled(Page)` left: 0; width: 100%; height: 100%; - background: #fff; `; function Wrapper() { diff --git a/apps/pwa/src/pages/player/pages/exploration/index.tsx b/apps/pwa/src/pages/player/pages/exploration/index.tsx index b40134c98..e2f9a5101 100644 --- a/apps/pwa/src/pages/player/pages/exploration/index.tsx +++ b/apps/pwa/src/pages/player/pages/exploration/index.tsx @@ -59,9 +59,6 @@ type ExplorationMode = 'recommendation' | 'search'; const Root = styled(Page)` position: relative; overflow: hidden; - background: - linear-gradient(180deg, rgb(247 253 248) 0, rgb(248 249 250) 310px), - rgb(248 249 250); > .search-toolbar { z-index: 2; diff --git a/apps/pwa/src/pages/player/pages/music/index.tsx b/apps/pwa/src/pages/player/pages/music/index.tsx index df793792f..c8aa4076b 100644 --- a/apps/pwa/src/pages/player/pages/music/index.tsx +++ b/apps/pwa/src/pages/player/pages/music/index.tsx @@ -10,7 +10,6 @@ const Style = styled(Page)` left: 0; width: 100%; height: 100%; - background: #fff; `; function Wrapper() { diff --git a/apps/pwa/src/pages/player/pages/musicbill/musicbill.tsx b/apps/pwa/src/pages/player/pages/musicbill/musicbill.tsx index 85faaa882..2dd6674a3 100644 --- a/apps/pwa/src/pages/player/pages/musicbill/musicbill.tsx +++ b/apps/pwa/src/pages/player/pages/musicbill/musicbill.tsx @@ -19,15 +19,11 @@ const Style = styled(Page)` left: 0; width: 100%; height: 100%; - background: rgb(248 249 250); > .scrollable { height: 100%; overflow: auto; padding: ${PAGE_HORIZONTAL_PADDING}; - background: - linear-gradient(180deg, rgb(247 253 248) 0, rgb(248 249 250) 300px), - rgb(248 249 250); ${autoScrollbar} } `; diff --git a/apps/pwa/src/pages/player/pages/offline_cache/index.tsx b/apps/pwa/src/pages/player/pages/offline_cache/index.tsx index bc3d47e84..9ba0fdc8e 100644 --- a/apps/pwa/src/pages/player/pages/offline_cache/index.tsx +++ b/apps/pwa/src/pages/player/pages/offline_cache/index.tsx @@ -55,15 +55,11 @@ const Style = styled(Page)` left: 0; width: 100%; height: 100%; - background: rgb(248 249 250); > .scrollable { height: 100%; overflow: auto; padding: ${PAGE_HORIZONTAL_PADDING}; - background: - linear-gradient(180deg, rgb(247 253 248) 0, rgb(248 249 250) 300px), - rgb(248 249 250); ${autoScrollbar} } `; diff --git a/apps/pwa/src/pages/player/pages/public_musicbill_collection/index.tsx b/apps/pwa/src/pages/player/pages/public_musicbill_collection/index.tsx index c6b99b57f..087b55411 100644 --- a/apps/pwa/src/pages/player/pages/public_musicbill_collection/index.tsx +++ b/apps/pwa/src/pages/player/pages/public_musicbill_collection/index.tsx @@ -7,10 +7,6 @@ const Style = styled(Page)` display: flex; flex-direction: column; - - background: - linear-gradient(180deg, rgb(247 253 248) 0, rgb(248 249 250) 280px), - rgb(248 249 250); `; function PublicMusicbillCollection() { diff --git a/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx b/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx index 9fe745cb8..94abd9a46 100644 --- a/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx +++ b/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx @@ -18,10 +18,6 @@ const Shell = styled.div` display: flex; flex-direction: column; - - background: - linear-gradient(180deg, rgb(247 253 248) 0, rgb(248 249 250) 240px), - rgb(248 249 250); `; const Header = styled(DrawerHeader)` diff --git a/apps/pwa/src/pages/radio/page.tsx b/apps/pwa/src/pages/radio/page.tsx index c9b9b3b11..2c0e55bc8 100644 --- a/apps/pwa/src/pages/radio/page.tsx +++ b/apps/pwa/src/pages/radio/page.tsx @@ -19,7 +19,6 @@ const Style = styled.div` position: relative; width: 100%; height: 100%; - background-color: #fff; overflow: hidden; `; const Bottom = styled.div` From 8a1562864ca1e823fd888046629676d77004a06a Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:08:46 +0800 Subject: [PATCH 12/38] remove languageselect from storybook --- .../language/language_select.stories.tsx | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 apps/pwa/src/features/language/language_select.stories.tsx diff --git a/apps/pwa/src/features/language/language_select.stories.tsx b/apps/pwa/src/features/language/language_select.stories.tsx deleted file mode 100644 index 299ddeed2..000000000 --- a/apps/pwa/src/features/language/language_select.stories.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import LanguageSelect from './language_select'; - -const meta = { - title: 'Form/LanguageSelect', - component: LanguageSelect, - tags: ['autodocs'], - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Language switcher backed by the global setting store. Selecting a new language updates the store and reloads the page so translations re-resolve. Note: in Storybook this *will* reload the preview iframe — that is the production behavior, not a bug.', - }, - }, - }, - decorators: [ - (Story) => ( -
- -
- ), - ], - argTypes: { - label: { control: 'text' }, - size: { control: 'select', options: ['sm', 'md', 'lg'] }, - disabled: { control: 'boolean' }, - confirmBeforeReload: { - control: 'boolean', - description: 'Open a confirmation dialog before applying & reloading.', - }, - }, - args: { - label: 'Language', - confirmBeforeReload: false, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; - -export const WithConfirm: Story = { - name: 'Confirm before reload', - args: { confirmBeforeReload: true }, -}; - -export const Disabled: Story = { - args: { disabled: true }, -}; From 36e5cac1dbd29f2588d58c7f56857655d724202d Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:19:14 +0800 Subject: [PATCH 13/38] add storybook to cli --- Makefile | 21 +++++--- apps/cli/.gitignore | 1 + apps/cli/cmd/start.go | 2 + apps/cli/storybook/storybook_dev.go | 9 ++++ apps/cli/storybook/storybook_prod.go | 43 +++++++++++++++ apps/cli/storybook/storybook_prod_test.go | 65 +++++++++++++++++++++++ apps/pwa/.gitignore | 1 + readme.md | 4 -- 8 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 apps/cli/storybook/storybook_dev.go create mode 100644 apps/cli/storybook/storybook_prod.go create mode 100644 apps/cli/storybook/storybook_prod_test.go diff --git a/Makefile b/Makefile index d4b732e47..f79c8bc34 100644 --- a/Makefile +++ b/Makefile @@ -15,16 +15,25 @@ define stage_ffmpeg_bundle node scripts/prepare_ffmpeg_bundle.mjs --target $(1) --version "$(FFMPEG_VERSION)" $(if $($(2)),--archive "$($(2))") $(if $($(3)),--sha256 "$($(3))") endef -.PHONY: pwa release docker clean ffmpeg-bundles ffmpeg-bundle-darwin-arm64 ffmpeg-bundle-windows-amd64 ffmpeg-bundle-windows-arm64 ffmpeg-bundle-linux-amd64 ffmpeg-bundle-linux-arm64 +.PHONY: pwa storybook release docker clean ffmpeg-bundles ffmpeg-bundle-darwin-arm64 ffmpeg-bundle-windows-amd64 ffmpeg-bundle-windows-arm64 ffmpeg-bundle-linux-amd64 ffmpeg-bundle-linux-arm64 + +## 安装前端依赖 (供 pwa 与 storybook 构建复用, 依赖 lockfile 变更时才重装) +apps/pwa/node_modules/.package-lock.json: apps/pwa/package-lock.json + npm ci --prefix apps/pwa ## 构建 PWA 并嵌入 CLI -pwa: +pwa: apps/pwa/node_modules/.package-lock.json node scripts/build_version.mjs validate "$(VERSION)" - npm ci --prefix apps/pwa CICADA_VERSION=$(VERSION) npm run build --prefix apps/pwa rm -rf $(CLI_DIR)/pwa/dist cp -R apps/pwa/dist $(CLI_DIR)/pwa/dist +## 构建 Storybook 并嵌入 CLI (托管于 /storybook) +storybook: apps/pwa/node_modules/.package-lock.json + npm run build-storybook --prefix apps/pwa + rm -rf $(CLI_DIR)/storybook/static + cp -R apps/pwa/storybook-static $(CLI_DIR)/storybook/static + ffmpeg-bundle-darwin-arm64: $(call stage_ffmpeg_bundle,darwin-arm64,FFMPEG_ARCHIVE_DARWIN_ARM64,FFMPEG_SHA256_DARWIN_ARM64) @@ -48,7 +57,7 @@ ffmpeg-bundles: \ ffmpeg-bundle-linux-arm64 ## 全平台构建发布包 (默认目标) -release: pwa ffmpeg-bundles +release: pwa storybook ffmpeg-bundles rm -rf $(BUILD_DIR) mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)/darwin-arm64 @@ -74,7 +83,7 @@ release: pwa ffmpeg-bundles $(BUILD_DIR)/linux-arm64 ## 构建 Linux 多架构二进制 (供 Docker buildx 使用, 不压缩) -docker: pwa ffmpeg-bundle-linux-amd64 ffmpeg-bundle-linux-arm64 +docker: pwa storybook ffmpeg-bundle-linux-amd64 ffmpeg-bundle-linux-arm64 rm -rf $(BUILD_DIR) mkdir -p $(BUILD_DIR)/linux-amd64 $(call build_cli,linux,amd64,$(BUILD_DIR)/linux-amd64/cicada) @@ -84,4 +93,4 @@ docker: pwa ffmpeg-bundle-linux-amd64 ffmpeg-bundle-linux-arm64 ## 清理构建产物 clean: - rm -rf $(BUILD_DIR) apps/cli/pwa/dist apps/pwa/dist apps/cli/internal/ffmpeg/generated apps/cli/internal/ffmpeg/zz_bundle_*.go + rm -rf $(BUILD_DIR) apps/cli/pwa/dist apps/pwa/dist apps/cli/storybook/static apps/pwa/storybook-static apps/cli/internal/ffmpeg/generated apps/cli/internal/ffmpeg/zz_bundle_*.go diff --git a/apps/cli/.gitignore b/apps/cli/.gitignore index 22c045e55..8835322e3 100644 --- a/apps/cli/.gitignore +++ b/apps/cli/.gitignore @@ -1,2 +1,3 @@ tmp/ pwa/dist/ +storybook/static/ diff --git a/apps/cli/cmd/start.go b/apps/cli/cmd/start.go index 7a4790280..b2428c79a 100644 --- a/apps/cli/cmd/start.go +++ b/apps/cli/cmd/start.go @@ -7,6 +7,7 @@ import ( "cicada/internal/server" "cicada/internal/store" "cicada/pwa" + "cicada/storybook" "fmt" "log" "net/http" @@ -116,6 +117,7 @@ func runStart(cmd *cobra.Command, args []string) error { r := server.NewServer() pwa.Register(r) + storybook.Register(r) addr := fmt.Sprintf(":%d", cfg.Port) log.Printf("cicada listening on %s", addr) diff --git a/apps/cli/storybook/storybook_dev.go b/apps/cli/storybook/storybook_dev.go new file mode 100644 index 000000000..bb68ff9f9 --- /dev/null +++ b/apps/cli/storybook/storybook_dev.go @@ -0,0 +1,9 @@ +//go:build !prod + +package storybook + +import "github.com/gin-gonic/gin" + +// Register is a no-op in development mode. Run Storybook with its own dev +// server (`npm run storybook --prefix apps/pwa`) instead. +func Register(r *gin.Engine) {} diff --git a/apps/cli/storybook/storybook_prod.go b/apps/cli/storybook/storybook_prod.go new file mode 100644 index 000000000..04511c8bf --- /dev/null +++ b/apps/cli/storybook/storybook_prod.go @@ -0,0 +1,43 @@ +//go:build prod + +package storybook + +import ( + "embed" + "io/fs" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +//go:embed all:static +var storybookFS embed.FS + +// Register mounts the embedded Storybook static build under `/storybook`. +// Storybook 的静态产物使用相对路径引用资源,因此在子路径下托管时必须带尾斜杠, +// 否则 `index.html` 里的相对资源会解析到根路径。这里把 `/storybook` 重定向到 +// `/storybook/`,其余文件交给基于 embed FS 的 FileServer 处理。 +func Register(r *gin.Engine) { + sub, err := fs.Sub(storybookFS, "static") + if err != nil { + panic(err) + } + fileServer := http.StripPrefix("/storybook/", http.FileServer(http.FS(sub))) + + r.GET("/storybook", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/storybook/") + }) + r.GET("/storybook/*filepath", func(c *gin.Context) { + filepath := strings.TrimPrefix(c.Param("filepath"), "/") + // 根路径交给 FileServer 返回 index.html;其余仅当真实文件存在时才服务, + // 未声明的路径一律 404,避免误把任意路径当作 index.html 返回。 + if filepath != "" { + if info, err := fs.Stat(sub, filepath); err != nil || info.IsDir() { + c.AbortWithStatus(http.StatusNotFound) + return + } + } + fileServer.ServeHTTP(c.Writer, c.Request) + }) +} diff --git a/apps/cli/storybook/storybook_prod_test.go b/apps/cli/storybook/storybook_prod_test.go new file mode 100644 index 000000000..4908d263b --- /dev/null +++ b/apps/cli/storybook/storybook_prod_test.go @@ -0,0 +1,65 @@ +//go:build prod + +package storybook + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" +) + +func newEngine() *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + Register(r) + return r +} + +func TestRedirectToTrailingSlash(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/storybook", nil) + newEngine().ServeHTTP(w, req) + + if w.Code != http.StatusMovedPermanently { + t.Fatalf("want 301, got %d", w.Code) + } + if loc := w.Header().Get("Location"); loc != "/storybook/" { + t.Fatalf("want redirect to /storybook/, got %q", loc) + } +} + +func TestServesIndex(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/storybook/", nil) + newEngine().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("want 200 for index, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), " Date: Wed, 1 Jul 2026 17:27:16 +0800 Subject: [PATCH 14/38] improve version display --- apps/pwa/src/pages/login/index.tsx | 34 ++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/pwa/src/pages/login/index.tsx b/apps/pwa/src/pages/login/index.tsx index 86dc7abd0..02e608643 100644 --- a/apps/pwa/src/pages/login/index.tsx +++ b/apps/pwa/src/pages/login/index.tsx @@ -48,9 +48,22 @@ const VersionFooter = styled.div` flex-shrink: 0; width: 100%; margin-top: 16px; + display: grid; + grid-template-columns: auto auto; + column-gap: 8px; + row-gap: 2px; + justify-content: center; + align-items: baseline; font-size: ${CSSVariable.TEXT_SIZE_SMALL}; color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - text-align: center; +`; +const VersionLabel = styled.div` + text-align: right; +`; +const VersionValue = styled.div` + text-align: left; + font-variant-numeric: tabular-nums; + color: ${CSSVariable.TEXT_COLOR_PRIMARY}; `; const CENTER_TRANSFORM = 'translateX(0)'; @@ -157,20 +170,19 @@ function Login() { - {/* 第二步时已选定服务端, 将版本号拆成 server 和 pwa 各一行展示 */} + {/* label 右对齐 / value 左对齐的两列 grid, 冒号靠列对齐替代, 数字用 tabular-nums 等宽 */} {step === Step.SECOND && selectedServer ? ( <> -
- {t('server_version')}: {selectedServer.version} -
-
- {t('pwa_version')}: {definition.VERSION} -
+ {t('server_version')} + {selectedServer.version} + {t('pwa_version')} + {definition.VERSION} ) : ( -
- {t('pwa_version')}: {definition.VERSION} -
+ <> + {t('pwa_version')} + {definition.VERSION} + )}
From be609c00cd70511ac28433106f2676822857bed8 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:39:16 +0800 Subject: [PATCH 15/38] reduce same components --- .../player/pages/user_manage/user_list/user.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/pwa/src/pages/player/pages/user_manage/user_list/user.tsx b/apps/pwa/src/pages/player/pages/user_manage/user_list/user.tsx index a34223f4e..a51d871f3 100644 --- a/apps/pwa/src/pages/player/pages/user_manage/user_list/user.tsx +++ b/apps/pwa/src/pages/player/pages/user_manage/user_list/user.tsx @@ -4,6 +4,7 @@ import ellipsis from '@/style/ellipsis'; import styled from 'styled-components'; import Cover from '@/components/cover'; import Button from '@/components/button'; +import Tag from '@/components/tag'; import getResizedImage from '@/server/asset/get_resized_image'; import { t } from '@/i18n'; import capitalize from '@/style/capitalize'; @@ -28,15 +29,8 @@ const Style = styled.div` > .admin { position: absolute; - top: 0; - left: 0; - - background-color: ${CSSVariable.COLOR_PRIMARY}; - color: #fff; - font-size: ${CSSVariable.TEXT_SIZE_SMALL}; - padding: 2px 5px; - border-radius: ${CSSVariable.BORDER_RADIUS_NORMAL} 0 - ${CSSVariable.BORDER_RADIUS_NORMAL} 0; + top: 5px; + left: 5px; } } @@ -96,7 +90,7 @@ function User({ user, width }: { user: UserType; width: string }) { src={getResizedImage({ url: user.avatar, size: ITEM_MIN_WIDTH * 2 })} size="100%" /> - {user.admin ?
{t('admin')}
: null} + {user.admin ? {t('admin')} : null}
From 31a2a3f0e55db41be656943fd51d8fd23972652a Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:43:00 +0800 Subject: [PATCH 16/38] improve list view --- .../src/pages/admin/artist_management/index.tsx | 15 --------------- .../pages/admin/music_management/music_list.tsx | 15 --------------- .../pwa/src/pages/admin/user_management/index.tsx | 15 --------------- 3 files changed, 45 deletions(-) diff --git a/apps/pwa/src/pages/admin/artist_management/index.tsx b/apps/pwa/src/pages/admin/artist_management/index.tsx index 32f8599f0..9f8395dc0 100644 --- a/apps/pwa/src/pages/admin/artist_management/index.tsx +++ b/apps/pwa/src/pages/admin/artist_management/index.tsx @@ -242,9 +242,6 @@ const Td = styled.td` font-weight: 700; letter-spacing: 0; vertical-align: middle; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out; &:first-child { border-left: 2px solid ${CSSVariable.COLOR_BORDER}; @@ -261,18 +258,6 @@ const Td = styled.td` -6px 0 0 #fff, 0 3px 0 ${ROW_SHADOW}; } - - tbody tr:hover & { - z-index: 2; - transform: translateY(-2px); - box-shadow: 0 5px 0 ${ROW_SHADOW}; - } - - tbody tr:hover &:last-child { - box-shadow: - -6px 0 0 #fff, - 0 5px 0 ${ROW_SHADOW}; - } `; const Mono = styled.span` diff --git a/apps/pwa/src/pages/admin/music_management/music_list.tsx b/apps/pwa/src/pages/admin/music_management/music_list.tsx index 19de2cc1a..6c39e6fe9 100644 --- a/apps/pwa/src/pages/admin/music_management/music_list.tsx +++ b/apps/pwa/src/pages/admin/music_management/music_list.tsx @@ -316,9 +316,6 @@ const Td = styled.td` font-weight: 700; letter-spacing: 0; vertical-align: middle; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out; &:first-child { border-left: 2px solid ${CSSVariable.COLOR_BORDER}; @@ -335,18 +332,6 @@ const Td = styled.td` -6px 0 0 #fff, 0 3px 0 ${ROW_SHADOW}; } - - tbody tr:hover & { - z-index: 2; - transform: translateY(-2px); - box-shadow: 0 5px 0 ${ROW_SHADOW}; - } - - tbody tr:hover &:last-child { - box-shadow: - -6px 0 0 #fff, - 0 5px 0 ${ROW_SHADOW}; - } `; const Mono = styled.span` diff --git a/apps/pwa/src/pages/admin/user_management/index.tsx b/apps/pwa/src/pages/admin/user_management/index.tsx index 4a0711e99..ef32aeaed 100644 --- a/apps/pwa/src/pages/admin/user_management/index.tsx +++ b/apps/pwa/src/pages/admin/user_management/index.tsx @@ -257,9 +257,6 @@ const Td = styled.td` font-weight: 700; letter-spacing: 0; vertical-align: middle; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out; &:first-child { border-left: 2px solid ${CSSVariable.COLOR_BORDER}; @@ -276,18 +273,6 @@ const Td = styled.td` -6px 0 0 #fff, 0 3px 0 ${ROW_SHADOW}; } - - tbody tr:hover & { - z-index: 2; - transform: translateY(-2px); - box-shadow: 0 5px 0 ${ROW_SHADOW}; - } - - tbody tr:hover &:last-child { - box-shadow: - -6px 0 0 #fff, - 0 5px 0 ${ROW_SHADOW}; - } `; const UserName = styled.div` From d3be924fbcf797e3891e6f763ac747c5da40cb1e Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:48:19 +0800 Subject: [PATCH 17/38] improve search words --- apps/pwa/src/i18n/en.ts | 2 +- apps/pwa/src/i18n/zh_hans.ts | 2 +- apps/pwa/src/pages/admin/music_management/music_list.tsx | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/pwa/src/i18n/en.ts b/apps/pwa/src/i18n/en.ts index 8c809c0f3..86b6ec387 100644 --- a/apps/pwa/src/i18n/en.ts +++ b/apps/pwa/src/i18n/en.ts @@ -108,7 +108,7 @@ export default { aliases: 'aliases', search_keywords: 'search keywords', search_keywords_placeholder: - 'Pinyin, initials, nicknames, misspellings, translations. Only used for search.', + 'Search keywords, only used for search matching, not displayed, one per line.', no_musicbill: 'no musicbill', no_data: 'no data', no_suitable_music_play_record: 'no suitable music play record', diff --git a/apps/pwa/src/i18n/zh_hans.ts b/apps/pwa/src/i18n/zh_hans.ts index d14632b4d..e19a3bd91 100644 --- a/apps/pwa/src/i18n/zh_hans.ts +++ b/apps/pwa/src/i18n/zh_hans.ts @@ -106,7 +106,7 @@ const zhCN: { alias: '别名', aliases: '别名', search_keywords: '搜索词', - search_keywords_placeholder: '拼音、首字母、昵称、错拼、译名等,仅用于搜索', + search_keywords_placeholder: '搜索关键词,仅用于搜索匹配,不展示,一行一个', no_musicbill: '暂无乐单', no_data: '暂无数据', no_suitable_music_play_record: '暂无相关的音乐播放记录', diff --git a/apps/pwa/src/pages/admin/music_management/music_list.tsx b/apps/pwa/src/pages/admin/music_management/music_list.tsx index 6c39e6fe9..8c7bf0cca 100644 --- a/apps/pwa/src/pages/admin/music_management/music_list.tsx +++ b/apps/pwa/src/pages/admin/music_management/music_list.tsx @@ -1130,9 +1130,7 @@ function MusicList({ ))} - ) : ( - {t('unknown')} - )} + ) : null} {music.type === MusicType.SONG && music.lyricists.length ? ( From a5777dbad56b9ed7849c503d5a0aad0feb09babf Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 17:53:58 +0800 Subject: [PATCH 18/38] improve search --- apps/cli/internal/store/lyric_test.go | 59 +++++++++++++++++++++++++++ apps/cli/internal/store/music_test.go | 51 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/apps/cli/internal/store/lyric_test.go b/apps/cli/internal/store/lyric_test.go index c3ac9ba6e..23d5d63c5 100644 --- a/apps/cli/internal/store/lyric_test.go +++ b/apps/cli/internal/store/lyric_test.go @@ -134,3 +134,62 @@ func TestSearchMusicIDsByLyricRanksMatchesDeterministically(t *testing.T) { } } } + +func TestSearchMusicIDsByLyricOrdersSameRankByHeatThenCreateTimestamp(t *testing.T) { + if err := ResetForTests(); err != nil { + t.Fatalf("reset store: %v", err) + } + t.Cleanup(func() { + if err := ResetForTests(); err != nil { + t.Fatalf("cleanup store: %v", err) + } + }) + + config.Set(config.Config{ + Mode: config.ModeProduction, + Data: t.TempDir(), + Port: 8000, + }) + if err := Initialize(); err != nil { + t.Fatalf("initialize store: %v", err) + } + + now := time.Now().UnixMilli() + if _, err := DB().Exec( + `INSERT INTO music (id,type,name,asset,heat,createTimestamp) VALUES + ('SONG11', ?, 'Alpha', 'alpha.mp3', 10, ?), + ('SONG12', ?, 'Beta', 'beta.mp3', 10, ?), + ('SONG13', ?, 'Charlie', 'charlie.mp3', 5, ?), + ('SONG14', ?, 'Delta', 'delta.mp3', 5, ?)`, + int(MusicTypeSong), now-400, + int(MusicTypeSong), now-300, + int(MusicTypeSong), now-100, + int(MusicTypeSong), now-200, + ); err != nil { + t.Fatalf("insert music: %v", err) + } + if _, err := DB().Exec( + `INSERT INTO lyric (musicId,lrc,lrcContent) VALUES + ('SONG11','[00:00.00]hello alpha','hello alpha'), + ('SONG12','[00:00.00]hello beta','hello beta'), + ('SONG13','[00:00.00]hello charlie','hello charlie'), + ('SONG14','[00:00.00]hello delta','hello delta')`, + ); err != nil { + t.Fatalf("insert lyrics: %v", err) + } + + total, ids, err := SearchMusicIDsByLyric("hello", 1, 10) + if err != nil { + t.Fatalf("search lyrics: %v", err) + } + if total != 4 || len(ids) != 4 { + t.Fatalf("unexpected lyric search result: total=%d ids=%v", total, ids) + } + + want := []string{"SONG12", "SONG11", "SONG13", "SONG14"} + for i := range want { + if ids[i] != want[i] { + t.Fatalf("unexpected order: got %v want %v", ids, want) + } + } +} diff --git a/apps/cli/internal/store/music_test.go b/apps/cli/internal/store/music_test.go index 5b3deee25..a0dcc9d4c 100644 --- a/apps/cli/internal/store/music_test.go +++ b/apps/cli/internal/store/music_test.go @@ -235,6 +235,57 @@ func TestSearchMusicRanksNameMatchesAndEscapesWildcards(t *testing.T) { } } +func TestSearchMusicOrdersSameRankByHeatThenCreateTimestamp(t *testing.T) { + if err := ResetForTests(); err != nil { + t.Fatalf("reset store: %v", err) + } + t.Cleanup(func() { + if err := ResetForTests(); err != nil { + t.Fatalf("cleanup store: %v", err) + } + }) + + config.Set(config.Config{ + Mode: config.ModeProduction, + Data: t.TempDir(), + Port: 8000, + }) + if err := Initialize(); err != nil { + t.Fatalf("initialize store: %v", err) + } + + now := time.Now().UnixMilli() + if _, err := DB().Exec( + `INSERT INTO music (id,type,name,aliases,asset,heat,createTimestamp) VALUES + ('MUS211', ?, 'Love Alpha', '', 'alpha.mp3', 10, ?), + ('MUS212', ?, 'Love Beta', '', 'beta.mp3', 10, ?), + ('MUS213', ?, 'Love Charlie', '', 'charlie.mp3', 5, ?), + ('MUS214', ?, 'Love Delta', '', 'delta.mp3', 5, ?)`, + int(MusicTypeSong), now-400, + int(MusicTypeSong), now-300, + int(MusicTypeSong), now-100, + int(MusicTypeSong), now-200, + ); err != nil { + t.Fatalf("insert music: %v", err) + } + + total, musics, err := SearchMusic("Love", 1, 10) + if err != nil { + t.Fatalf("search music: %v", err) + } + if total != 4 || len(musics) != 4 { + t.Fatalf("unexpected search result: total=%d musics=%+v", total, musics) + } + + got := []string{musics[0].ID, musics[1].ID, musics[2].ID, musics[3].ID} + want := []string{"MUS212", "MUS211", "MUS213", "MUS214"} + for i := range want { + if got[i] != want[i] { + t.Fatalf("unexpected order: got %v want %v", got, want) + } + } +} + func TestGetMusicsByIDsPreservesInputOrder(t *testing.T) { if err := ResetForTests(); err != nil { t.Fatalf("reset store: %v", err) From 68c75989c5adfec52b299d1b447bcafe3dde4308 Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 19:08:47 +0800 Subject: [PATCH 19/38] improve admin sidebar --- .../app_drawer/app_drawer.stories.tsx | 2 - .../src/components/drawer/drawer.stories.tsx | 33 +----- apps/pwa/src/components/drawer/index.tsx | 59 +-------- .../drawers/artist_drawer/artist_drawer.tsx | 1 - .../drawers/music_drawer/music_drawer.tsx | 1 - .../musicbill_drawer/musicbill_drawer.tsx | 1 - .../drawers/user_drawer/user_drawer.tsx | 1 - .../admin/components/artist_edit/drawer.tsx | 1 - .../admin/components/user_edit/drawer.tsx | 1 - apps/pwa/src/pages/admin/index.tsx | 112 +++++++++--------- .../music_management/music_edit_drawer.tsx | 1 - .../player/artist_drawer/artist_drawer.tsx | 1 - .../player/authorized_device_drawer/index.tsx | 1 - .../player/music_drawer/music_drawer.tsx | 1 - .../index.tsx | 1 - .../src/pages/player/sidebar/mini_mode.tsx | 1 - 16 files changed, 59 insertions(+), 159 deletions(-) diff --git a/apps/pwa/src/components/app_drawer/app_drawer.stories.tsx b/apps/pwa/src/components/app_drawer/app_drawer.stories.tsx index 97d14252f..9f79b42ed 100644 --- a/apps/pwa/src/components/app_drawer/app_drawer.stories.tsx +++ b/apps/pwa/src/components/app_drawer/app_drawer.stories.tsx @@ -15,11 +15,9 @@ const meta = { control: 'select', options: ['compact', 'medium', 'wide'], }, - showClose: { control: 'boolean' }, }, args: { width: 'medium', - showClose: true, open: false, onClose: () => {}, children: null, diff --git a/apps/pwa/src/components/drawer/drawer.stories.tsx b/apps/pwa/src/components/drawer/drawer.stories.tsx index b1d0672d9..442f684c3 100644 --- a/apps/pwa/src/components/drawer/drawer.stories.tsx +++ b/apps/pwa/src/components/drawer/drawer.stories.tsx @@ -33,11 +33,6 @@ const meta = { description: 'Which edge the drawer slides from.', table: { defaultValue: { summary: 'right' } }, }, - showClose: { - control: 'boolean', - description: 'Show the close button.', - table: { defaultValue: { summary: 'false' } }, - }, }, } satisfies Meta; @@ -50,13 +45,11 @@ function DrawerDemo({ side = 'right', title = 'Drawer', description, - showClose = false, longContent = false, }: { side?: 'left' | 'right' | 'bottom'; title?: string; description?: string; - showClose?: boolean; longContent?: boolean; }) { const [open, setOpen] = useState(false); @@ -64,7 +57,7 @@ function DrawerDemo({ <> - + {title} {description && {description}} @@ -162,30 +155,6 @@ export const LongContent: Story = { ), }; -export const NoCloseButton: Story = { - name: 'No close button', - render: () => ( - - ), -}; - -export const WithCloseButton: Story = { - name: 'With close button', - render: () => ( - - ), -}; - export const AllSides: Story = { name: 'All sides', render: () => ( diff --git a/apps/pwa/src/components/drawer/index.tsx b/apps/pwa/src/components/drawer/index.tsx index 5b12196e1..a09b933a3 100644 --- a/apps/pwa/src/components/drawer/index.tsx +++ b/apps/pwa/src/components/drawer/index.tsx @@ -36,7 +36,6 @@ import * as RadixDialog from '@radix-ui/react-dialog'; import styled, { css, keyframes } from 'styled-components'; import { useTheme, CSS_VAR } from '../theme'; import { CSSVariable } from '@/global_style'; -import hover from '@/style/hover'; import { t } from '@/i18n'; import { DialogTitleRegistryContext, @@ -46,7 +45,6 @@ import { useRegisterDialogTitle, visuallyHiddenStyle, } from '../dialog_a11y'; -import { Close } from '@/components/icon'; // ─── Tokens ─────────────────────────────────────────────────────────────────── @@ -153,55 +151,8 @@ const Panel = styled.div<{ $side: DrawerSide }>` ${({ $side }) => SIDE_MAP[$side]} `; -// ─── Close button ───────────────────────────────────────────────────────────── - -const CloseButton = styled(RadixDialog.Close)` - position: absolute; - top: 12px; - right: 12px; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - padding: 0; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 10px; - background: #fff; - box-shadow: 0 3px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - color: rgb(88 88 88); - cursor: pointer; - flex-shrink: 0; - font-size: 20px; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out, - filter 120ms; - - ${hover(css` - &:hover { - filter: brightness(1.06); - } - `)} - - &:active { - transform: translateY(3px); - box-shadow: none; - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in, - filter 60ms; - } - - &:focus-visible { - outline: 3px solid ${CSSVariable.COLOR_CONTROL_NEUTRAL}; - outline-offset: 2px; - } -`; - // ─── Scroll area ────────────────────────────────────────────────────────────── -// Wraps all slotted children so they can scroll while the close button stays fixed. +// Wraps all slotted children so they can scroll within the panel. const ScrollArea = styled.div` flex: 1; @@ -220,8 +171,6 @@ export interface DrawerContentProps extends ComponentPropsWithoutRef { /** Which edge the drawer slides from. Default: 'right'. */ side?: DrawerSide; - /** Show the close button. Default: false. */ - showClose?: boolean; /** Screen reader title used when no DrawerTitle is rendered. */ accessibleTitle?: ReactNode; /** @@ -240,7 +189,6 @@ export const DrawerContent = forwardRef< >(({ children, side = 'right', - showClose = false, zIndex = DEFAULT_Z_INDEX, style, accessibleTitle, @@ -278,11 +226,6 @@ export const DrawerContent = forwardRef< {fallbackTitle} - {showClose && ( - - - - )} {children} diff --git a/apps/pwa/src/features/player/drawers/artist_drawer/artist_drawer.tsx b/apps/pwa/src/features/player/drawers/artist_drawer/artist_drawer.tsx index 80d9b1a8f..c0dd2ecc4 100644 --- a/apps/pwa/src/features/player/drawers/artist_drawer/artist_drawer.tsx +++ b/apps/pwa/src/features/player/drawers/artist_drawer/artist_drawer.tsx @@ -18,7 +18,6 @@ function ArtistDrawer({ onClose={onClose} width="wide" zIndex={zIndex} - showClose={false} > diff --git a/apps/pwa/src/features/player/drawers/music_drawer/music_drawer.tsx b/apps/pwa/src/features/player/drawers/music_drawer/music_drawer.tsx index 0335c1b88..75cc0b32e 100644 --- a/apps/pwa/src/features/player/drawers/music_drawer/music_drawer.tsx +++ b/apps/pwa/src/features/player/drawers/music_drawer/music_drawer.tsx @@ -18,7 +18,6 @@ function MusicDrawer({ onClose={onClose} width="medium" zIndex={zIndex} - showClose={false} > diff --git a/apps/pwa/src/features/player/drawers/musicbill_drawer/musicbill_drawer.tsx b/apps/pwa/src/features/player/drawers/musicbill_drawer/musicbill_drawer.tsx index f1132fc5a..631709f05 100644 --- a/apps/pwa/src/features/player/drawers/musicbill_drawer/musicbill_drawer.tsx +++ b/apps/pwa/src/features/player/drawers/musicbill_drawer/musicbill_drawer.tsx @@ -235,7 +235,6 @@ function Wrapper({ open={open} onClose={onClose} width="wide" - showClose={false} zIndex={zIndex} > {transitions((style, d) => { diff --git a/apps/pwa/src/features/player/drawers/user_drawer/user_drawer.tsx b/apps/pwa/src/features/player/drawers/user_drawer/user_drawer.tsx index ebaf3d75f..537facb76 100644 --- a/apps/pwa/src/features/player/drawers/user_drawer/user_drawer.tsx +++ b/apps/pwa/src/features/player/drawers/user_drawer/user_drawer.tsx @@ -244,7 +244,6 @@ function Wrapper({ open={open} onClose={onClose} width="min(80%, 340px)" - showClose={false} zIndex={zIndex} > {transitions((style, d) => { diff --git a/apps/pwa/src/pages/admin/components/artist_edit/drawer.tsx b/apps/pwa/src/pages/admin/components/artist_edit/drawer.tsx index 57eb74dd3..a1119ac18 100644 --- a/apps/pwa/src/pages/admin/components/artist_edit/drawer.tsx +++ b/apps/pwa/src/pages/admin/components/artist_edit/drawer.tsx @@ -85,7 +85,6 @@ function ArtistEditDrawer({ style={{ maxWidth: `calc(100vw - ${DRAWER_NARROW_SCREEN_GUTTER}px)`, }} - showClose={false} onOpenAutoFocus={(event) => event.preventDefault()} > event.preventDefault()} > diff --git a/apps/pwa/src/pages/admin/index.tsx b/apps/pwa/src/pages/admin/index.tsx index 5e2a70131..e07b01214 100644 --- a/apps/pwa/src/pages/admin/index.tsx +++ b/apps/pwa/src/pages/admin/index.tsx @@ -1,4 +1,4 @@ -import { KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { CSSProperties, KeyboardEvent, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom'; import withLogin from '@/platform/with_login'; @@ -12,6 +12,7 @@ import Avatar from '@/components/avatar'; import Button from '@/components/button'; import AppExtraInfo from '@/components/app_extra_info'; import Tooltip from '@/components/tooltip'; +import { Drawer, DrawerContent } from '@/components/drawer'; import getResizedImage from '@/server/asset/get_resized_image'; import autoScrollbar from '@/style/auto_scrollbar'; import { CSS_VAR } from '@/components/theme'; @@ -42,8 +43,6 @@ import { const SIDEBAR_WIDTH = 240; const HEADER_HEIGHT = 72; const MOBILE_BREAKPOINT = 760; -const MOBILE_OVERLAY_Z_INDEX = 20; -const MOBILE_SIDEBAR_Z_INDEX = 30; const AVATAR_SIZE = 36; const PRIMARY = `var(${CSS_VAR.colorPrimary})`; const PRIMARY_SHADOW = `var(${CSS_VAR.colorPrimaryShadow})`; @@ -94,7 +93,9 @@ const Page = styled.div` overflow: hidden; `; -const Sidebar = styled.aside<{ $open: boolean }>` +// Desktop-only persistent rail. Narrow screens render the same menu inside the +// shared instead of transforming this element off-screen. +const Sidebar = styled.aside` width: ${SIDEBAR_WIDTH}px; flex-shrink: 0; display: flex; @@ -104,15 +105,14 @@ const Sidebar = styled.aside<{ $open: boolean }>` box-shadow: 4px 0 0 color-mix(in srgb, ${SURFACE_SHADOW} 35%, transparent); z-index: 3; +`; - @media (max-width: ${MOBILE_BREAKPOINT}px) { - position: fixed; - inset: 0 auto 0 0; - /* Keep the mobile drawer above the fixed admin header while it is open. */ - z-index: ${MOBILE_SIDEBAR_Z_INDEX}; - transform: translateX(${({ $open }) => ($open ? '0' : '-100%')}); - transition: transform 180ms ease; - } +// Fills the Drawer's scroll area so the menu list flexes and the info card stays +// pinned to the bottom, mirroring the desktop sidebar's column layout. +const DrawerSidebarInner = styled.div` + display: flex; + flex-direction: column; + min-height: 100%; `; const SidebarHeader = styled.div` @@ -285,20 +285,6 @@ const MenuLink = styled(NavLink)` } `; -const Overlay = styled.button<{ $open: boolean }>` - display: none; - - @media (max-width: ${MOBILE_BREAKPOINT}px) { - position: fixed; - inset: 0; - display: ${({ $open }) => ($open ? 'block' : 'none')}; - z-index: ${MOBILE_OVERLAY_Z_INDEX}; - border: none; - padding: 0; - background: rgb(0 0 0 / 0.26); - } -`; - const Main = styled.main` flex: 1; min-width: 0; @@ -746,6 +732,39 @@ function AdminPage() { url: user.avatar, size: AVATAR_SIZE * 2, }); + // Shared menu body — reused by the desktop rail and the mobile drawer. On + // desktop we pass the titlebar-aware top padding; inside the drawer the panel + // already reserves the safe-area/titlebar inset, so no extra padding is added. + const renderMenu = (headerStyle?: CSSProperties) => ( + <> + + + + {capitalize(t('cicada'))} + + + + + {ADMIN_MENU_ITEMS.map(({ path, label, Icon }) => ( + + + {capitalize(t(label))} + + ))} + + + + ); + const toggleUserMenu = () => setUserMenuOpen((open) => !open); const onAvatarKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { @@ -761,35 +780,18 @@ function AdminPage() { - - - - - {capitalize(t('cicada'))} - - - - - {ADMIN_MENU_ITEMS.map(({ path, label, Icon }) => ( - - - {capitalize(t(label))} - - ))} - - - - setSidebarOpen(false)} - aria-label={t('close_menu_overlay')} - /> + {isMobileLayout ? ( + + + {renderMenu()} + + + ) : ( + {renderMenu({ paddingTop: sidebarTopPadding })} + )}
event.preventDefault()} > {loading ? ( diff --git a/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx b/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx index 80d9b1a8f..c0dd2ecc4 100644 --- a/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx +++ b/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx @@ -18,7 +18,6 @@ function ArtistDrawer({ onClose={onClose} width="wide" zIndex={zIndex} - showClose={false} > diff --git a/apps/pwa/src/pages/player/authorized_device_drawer/index.tsx b/apps/pwa/src/pages/player/authorized_device_drawer/index.tsx index 501f5cade..77352cc66 100644 --- a/apps/pwa/src/pages/player/authorized_device_drawer/index.tsx +++ b/apps/pwa/src/pages/player/authorized_device_drawer/index.tsx @@ -43,7 +43,6 @@ function AuthorizedDeviceDrawer() { !v && onClose()}> diff --git a/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx b/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx index 94abd9a46..2b233f7b2 100644 --- a/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx +++ b/apps/pwa/src/pages/player/public_musicbill_collection_drawer/index.tsx @@ -66,7 +66,6 @@ function PublicMusicbillCollectionDrawer() { !v && setOpen(false)}> !v && onClose()}> event.preventDefault()} From 82e247b327c4cd29ddd704066ce245f1dd6c95eb Mon Sep 17 00:00:00 2001 From: anyone Date: Wed, 1 Jul 2026 19:21:04 +0800 Subject: [PATCH 20/38] improve avatar --- apps/pwa/src/components/avatar/index.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/pwa/src/components/avatar/index.tsx b/apps/pwa/src/components/avatar/index.tsx index 5eb2a9fe5..e2acb9df3 100644 --- a/apps/pwa/src/components/avatar/index.tsx +++ b/apps/pwa/src/components/avatar/index.tsx @@ -1,11 +1,15 @@ import { HTMLAttributes } from 'react'; import { CSSVariable } from '@/global_style'; -import ImageFrame, { getImageFrameRadius } from '@/components/image_frame'; +import ImageFrame from '@/components/image_frame'; +import { CONTROL_SIZE } from '@/components/control_style'; import { CSS_VAR } from '../theme'; const PRIMARY = `var(${CSS_VAR.colorPrimary})`; const PRIMARY_SHADOW = `var(${CSS_VAR.colorPrimaryShadow})`; const NEUTRAL_SHADOW = CSSVariable.COLOR_NEUTRAL_SHADOW; +const DEFAULT_RADIUS = CONTROL_SIZE.lg.radius; +const SM_RADIUS_MAX = CONTROL_SIZE.sm.height; +const MD_RADIUS_MAX = CONTROL_SIZE.md.height; function getBorderWidth() { return 2; @@ -25,6 +29,22 @@ function getShadowOffset(size: number | string) { return 4; } +function getAvatarRadius(size: number | string) { + if (typeof size !== 'number') { + return DEFAULT_RADIUS; + } + + if (size <= SM_RADIUS_MAX) { + return CONTROL_SIZE.sm.radius; + } + + if (size <= MD_RADIUS_MAX) { + return CONTROL_SIZE.md.radius; + } + + return CONTROL_SIZE.lg.radius; +} + export interface AvatarProps extends HTMLAttributes { src: string; size?: number | string; @@ -39,7 +59,7 @@ function Avatar({ ...props }: AvatarProps) { const borderWidth = getBorderWidth(); - const radius = getImageFrameRadius(size); + const radius = getAvatarRadius(size); return ( Date: Thu, 2 Jul 2026 17:29:45 +0800 Subject: [PATCH 21/38] improve musicbill-music drawer --- .../musicbill_music_drawer/musicbill.tsx | 19 +++++++++++-------- .../drawers/musicbill_music_drawer/top.tsx | 18 +++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/pwa/src/features/player/drawers/musicbill_music_drawer/musicbill.tsx b/apps/pwa/src/features/player/drawers/musicbill_music_drawer/musicbill.tsx index 95a95a4d1..4d0bc360b 100644 --- a/apps/pwa/src/features/player/drawers/musicbill_music_drawer/musicbill.tsx +++ b/apps/pwa/src/features/player/drawers/musicbill_music_drawer/musicbill.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { CSSVariable } from '@/global_style'; +import hover from '@/style/hover'; import { CSS_VAR } from '@/components/theme'; import { Refresh, Check } from '@/components/icon'; import { RequestStatus } from '@/constants'; @@ -52,20 +53,22 @@ const Style = styled.div<{ $public: boolean; $selected: boolean }>` box-shadow 150ms ease-out, border-color 150ms ease-out, background 150ms ease-out, - color 150ms ease-out, - filter 120ms ease-out; + color 150ms ease-out; - &:hover { - filter: brightness(1.04); - } + ${hover(css<{ $selected: boolean }>` + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 0 + ${({ $selected }) => ($selected ? PRIMARY_SHADOW : NEUTRAL_SHADOW)}; + } + `)} &:active { transform: translateY(4px); box-shadow: none; transition: transform 60ms ease-in, - box-shadow 60ms ease-in, - filter 60ms ease-in; + box-shadow 60ms ease-in; } > .icon { diff --git a/apps/pwa/src/features/player/drawers/musicbill_music_drawer/top.tsx b/apps/pwa/src/features/player/drawers/musicbill_music_drawer/top.tsx index f2c6ac5e7..a99db2470 100644 --- a/apps/pwa/src/features/player/drawers/musicbill_music_drawer/top.tsx +++ b/apps/pwa/src/features/player/drawers/musicbill_music_drawer/top.tsx @@ -1,12 +1,11 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { CSSVariable } from '@/global_style'; -import { CSS_VAR } from '@/components/theme'; import getResizedImage from '@/server/asset/get_resized_image'; +import hover from '@/style/hover'; import { Music } from '../../constants'; import MusicInfo from '../../components/music_info'; import { FLOATING_GAP } from './constants'; -const PRIMARY = `var(${CSS_VAR.colorPrimary})`; const NEUTRAL_SHADOW = CSSVariable.COLOR_NEUTRAL_SHADOW; const Style = styled.div` @@ -32,8 +31,7 @@ const Style = styled.div` transition: transform 150ms ease-out, box-shadow 150ms ease-out, - border-color 150ms ease-out, - filter 120ms ease-out; + border-color 150ms ease-out; > :first-child { flex: 0 0 auto; @@ -74,10 +72,12 @@ const Style = styled.div` } } - &:hover { - background: #fff; - border-color: ${PRIMARY}; - } + ${hover(css` + &:hover { + transform: translateY(-2px); + box-shadow: 0 6px 0 ${NEUTRAL_SHADOW}; + } + `)} &:active { transform: translateY(4px); From 2415205fbf2a3afa7f94ca286c34c49d03e19e68 Mon Sep 17 00:00:00 2001 From: anyone Date: Thu, 2 Jul 2026 19:16:22 +0800 Subject: [PATCH 22/38] remove repeated instance --- .../player/artist_drawer/artist_drawer.tsx | 27 -- .../pages/player/artist_drawer/constants.ts | 3 - .../pages/player/artist_drawer/content.tsx | 1 - .../src/pages/player/artist_drawer/index.tsx | 1 - .../src/pages/player/artist_drawer/info.tsx | 261 ------------------ .../pages/player/artist_drawer/music_list.tsx | 35 --- .../pages/player/artist_drawer/toolbar.tsx | 117 -------- .../pages/player/artist_drawer/use_data.ts | 69 ----- .../pages/player/artist_drawer/use_open.ts | 45 --- apps/pwa/src/pages/player/index.tsx | 10 +- .../pages/player/music_drawer/constants.ts | 51 ---- .../src/pages/player/music_drawer/content.tsx | 1 - .../src/pages/player/music_drawer/index.tsx | 1 - .../src/pages/player/music_drawer/info.tsx | 122 -------- .../src/pages/player/music_drawer/lyric.tsx | 166 ----------- .../player/music_drawer/music_drawer.tsx | 27 -- .../music_drawer/performer_list/index.tsx | 52 ---- .../music_drawer/performer_list/performer.tsx | 64 ----- .../related_public_musicbill_list.tsx | 258 ----------------- .../player/music_drawer/sub_music_list.tsx | 101 ------- .../src/pages/player/music_drawer/toolbar.tsx | 173 ------------ .../src/pages/player/music_drawer/use_data.ts | 80 ------ .../src/pages/player/music_drawer/use_open.ts | 47 ---- .../player/musicbill_drawer/constants.ts | 1 - .../player/musicbill_drawer/eventemitter.ts | 2 - .../pages/player/musicbill_drawer/index.tsx | 1 - .../pages/player/musicbill_drawer/info.tsx | 1 - .../player/musicbill_drawer/music_list.tsx | 1 - .../musicbill_drawer/musicbill_drawer.tsx | 1 - .../pages/player/musicbill_drawer/toolbar.tsx | 1 - .../pages/player/musicbill_drawer/use_data.ts | 1 - .../musicbill_music_drawer/constants.ts | 1 - .../player/musicbill_music_drawer/index.tsx | 1 - .../musicbill_music_drawer/musicbill.tsx | 233 ---------------- .../musicbill_music_drawer/musicbill_list.tsx | 64 ----- .../musicbill_music_drawer.tsx | 74 ----- .../player/musicbill_music_drawer/top.tsx | 111 -------- .../src/pages/player/pages/artist/index.tsx | 2 +- .../src/pages/player/pages/music/index.tsx | 2 +- .../src/pages/player/user_drawer/constants.ts | 1 - .../src/pages/player/user_drawer/index.tsx | 1 - .../pwa/src/pages/player/user_drawer/info.tsx | 1 - .../pages/player/user_drawer/mini_info.tsx | 64 ----- .../player/user_drawer/musicbill_list.tsx | 1 - .../src/pages/player/user_drawer/use_data.ts | 1 - .../pages/player/user_drawer/user_drawer.tsx | 1 - 46 files changed, 7 insertions(+), 2272 deletions(-) delete mode 100644 apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/constants.ts delete mode 100644 apps/pwa/src/pages/player/artist_drawer/content.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/index.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/info.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/music_list.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/toolbar.tsx delete mode 100644 apps/pwa/src/pages/player/artist_drawer/use_data.ts delete mode 100644 apps/pwa/src/pages/player/artist_drawer/use_open.ts delete mode 100644 apps/pwa/src/pages/player/music_drawer/constants.ts delete mode 100644 apps/pwa/src/pages/player/music_drawer/content.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/index.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/info.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/lyric.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/music_drawer.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/performer_list/index.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/performer_list/performer.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/related_public_musicbill_list.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/sub_music_list.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/toolbar.tsx delete mode 100644 apps/pwa/src/pages/player/music_drawer/use_data.ts delete mode 100644 apps/pwa/src/pages/player/music_drawer/use_open.ts delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/constants.ts delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/eventemitter.ts delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/index.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/info.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/music_list.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/musicbill_drawer.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/toolbar.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_drawer/use_data.ts delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/constants.ts delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/index.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/musicbill.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_list.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_music_drawer.tsx delete mode 100644 apps/pwa/src/pages/player/musicbill_music_drawer/top.tsx delete mode 100644 apps/pwa/src/pages/player/user_drawer/constants.ts delete mode 100644 apps/pwa/src/pages/player/user_drawer/index.tsx delete mode 100644 apps/pwa/src/pages/player/user_drawer/info.tsx delete mode 100644 apps/pwa/src/pages/player/user_drawer/mini_info.tsx delete mode 100644 apps/pwa/src/pages/player/user_drawer/musicbill_list.tsx delete mode 100644 apps/pwa/src/pages/player/user_drawer/use_data.ts delete mode 100644 apps/pwa/src/pages/player/user_drawer/user_drawer.tsx diff --git a/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx b/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx deleted file mode 100644 index c0dd2ecc4..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/artist_drawer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import AppDrawer from '@/components/app_drawer'; -import ArtistContent from './content'; - -function ArtistDrawer({ - open, - onClose, - id, - zIndex, -}: { - open: boolean; - onClose: () => void; - id: string; - zIndex: number; -}) { - return ( - - - - ); -} - -export default ArtistDrawer; diff --git a/apps/pwa/src/pages/player/artist_drawer/constants.ts b/apps/pwa/src/pages/player/artist_drawer/constants.ts deleted file mode 100644 index def31a03f..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -import getArtist from '@/server/api/get_artist'; - -export type Artist = AsyncReturnType; diff --git a/apps/pwa/src/pages/player/artist_drawer/content.tsx b/apps/pwa/src/pages/player/artist_drawer/content.tsx deleted file mode 100644 index 20362ce1f..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/content.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/artist_drawer/content'; diff --git a/apps/pwa/src/pages/player/artist_drawer/index.tsx b/apps/pwa/src/pages/player/artist_drawer/index.tsx deleted file mode 100644 index 764c387da..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/artist_drawer'; diff --git a/apps/pwa/src/pages/player/artist_drawer/info.tsx b/apps/pwa/src/pages/player/artist_drawer/info.tsx deleted file mode 100644 index 17f779851..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/info.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { CSSVariable } from '@/global_style'; -import styled, { css } from 'styled-components'; -import { useEffect, useState, type Ref } from 'react'; -import Cover, { Shape } from '@/components/cover'; -import ImageViewer, { type ImageViewerPhoto } from '@/components/image_viewer'; -import { t } from '@/i18n'; -import { Artist } from './constants'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; - -const Style = styled.div` - background: #fff; - font-size: 0; -`; -const Identity = styled.section<{ - $integrated: boolean; -}>` - padding: ${({ $integrated }) => - $integrated ? 0 : `24px ${PAGE_HORIZONTAL_PADDING} 12px`}; - background: ${({ $integrated }) => ($integrated ? 'transparent' : '#fff')}; - - ${({ $integrated }) => - $integrated - ? css` - > .name { - -webkit-text-stroke: 0.35px rgb(255 255 255 / 0.9); - text-shadow: - 0 1px 0 rgb(255 255 255 / 0.95), - 0 0 10px rgb(255 255 255 / 0.9); - } - - > .aliases { - color: rgb(68 68 68); - text-shadow: - 0 1px 0 rgb(255 255 255 / 0.92), - 0 0 8px rgb(255 255 255 / 0.86); - } - ` - : null} - - > .name { - margin: 0; - - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: 28px; - font-weight: 800; - line-height: 1.15; - letter-spacing: 0; - color: rgb(50 50 50); - overflow-wrap: anywhere; - } - - > .aliases { - margin: 8px 0 0; - - display: flex; - flex-direction: column; - gap: 3px; - - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 600; - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - line-height: 1.3; - overflow-wrap: anywhere; - } -`; -const Main = styled.div` - position: relative; - overflow: hidden; - - > .photo { - display: block; - width: 100%; - border: none; - padding: 0; - background: transparent; - cursor: zoom-in; - -webkit-tap-highlight-color: transparent; - - &:focus-visible { - outline: 2px solid ${CSSVariable.COLOR_PRIMARY}; - outline-offset: -2px; - } - } -`; -const PhotoOverlay = styled.div<{ $hasThumbnails: boolean }>` - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 1; - padding: ${({ $hasThumbnails }) => - $hasThumbnails - ? `108px ${PAGE_HORIZONTAL_PADDING} 14px` - : `108px ${PAGE_HORIZONTAL_PADDING} 18px`}; - box-sizing: border-box; - background: linear-gradient( - to bottom, - rgb(255 255 255 / 0) 0%, - rgb(255 255 255 / 0.08) 16%, - rgb(255 255 255 / 0.24) 32%, - rgb(255 255 255 / 0.48) 52%, - rgb(255 255 255 / 0.72) 70%, - rgb(255 255 255 / 0.9) 86%, - #fff 100% - ); -`; -const ThumbnailRow = styled.div<{ $integrated: boolean }>` - margin-top: ${({ $integrated }) => ($integrated ? '12px' : 0)}; - padding: ${({ $integrated }) => - $integrated ? '0 0 4px' : `0 ${PAGE_HORIZONTAL_PADDING} 18px`}; - box-sizing: border-box; - - display: flex; - gap: 8px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scroll-snap-type: x proximity; - - &::-webkit-scrollbar { - display: none; - } -`; -const ThumbnailItem = styled.div<{ selected: boolean }>` - flex: 0 0 auto; - width: 42px; - height: 42px; - box-sizing: border-box; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 12px; - overflow: hidden; - background: #fff; - box-shadow: 0 3px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - cursor: pointer; - scroll-snap-align: start; - transition: - transform 140ms ease, - border-color 140ms ease, - box-shadow 140ms ease; - - > * { - width: 100%; - border-radius: 9px; - } - - ${({ selected }) => - selected - ? css` - border-color: ${CSSVariable.COLOR_PRIMARY_ACTIVE}; - box-shadow: 0 3px 0 ${CSSVariable.COLOR_PRIMARY_ACTIVE}; - transform: translateY(-1px); - ` - : css` - &:active { - transform: translateY(2px); - box-shadow: 0 1px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } - `} -`; - -function Info({ - artist, - identityRef, -}: { - artist: Artist; - identityRef?: Ref; -}) { - const { photos } = artist; - const [selectedId, setSelectedId] = useState( - () => photos[0]?.id, - ); - const [viewerPhoto, setViewerPhoto] = useState(null); - - useEffect(() => { - if (!photos.length) { - setSelectedId(undefined); - return; - } - setSelectedId((prev) => - prev && photos.some((p) => p.id === prev) ? prev : photos[0].id, - ); - }, [photos]); - - const selected = photos.find((p) => p.id === selectedId) ?? photos[0]; - const showThumbnails = photos.length > 1; - const photoLabel = selected?.description || artist.name; - const identity = ( - -

{artist.name}

- {artist.aliases.length ? ( -
- {artist.aliases.map((alias, index) => ( -
- {alias} -
- ))} -
- ) : null} -
- ); - const thumbnails = showThumbnails ? ( - - {photos.map((photo) => ( - setSelectedId(photo.id)} - aria-label={photo.description || artist.name} - > - - - ))} - - ) : null; - - return ( - - ); -} - -export default Info; diff --git a/apps/pwa/src/pages/player/artist_drawer/music_list.tsx b/apps/pwa/src/pages/player/artist_drawer/music_list.tsx deleted file mode 100644 index b13bb1ed2..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/music_list.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { CSSProperties, memo, useContext } from 'react'; -import Empty from '@/components/empty'; -import styled from 'styled-components'; -import { MusicWithArtistAliases } from '../constants'; -import Music from '../components/music'; -import Context from '../context'; -import { t } from '@/i18n'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; - -const Root = styled.div` - padding: 12px ${PAGE_HORIZONTAL_PADDING} 16px; -`; -const emptyStyle: CSSProperties = { - padding: '50px 0', -}; - -function MusicList({ musicList }: { musicList: MusicWithArtistAliases[] }) { - const { playqueue, currentPlayqueuePosition } = useContext(Context); - return musicList.length ? ( - - {musicList.map((music, index) => ( - - ))} - - ) : ( - - ); -} - -export default memo(MusicList); diff --git a/apps/pwa/src/pages/player/artist_drawer/toolbar.tsx b/apps/pwa/src/pages/player/artist_drawer/toolbar.tsx deleted file mode 100644 index 0d902f412..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/toolbar.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import styled from 'styled-components'; -import Button from '@/components/button'; -import { Tooltip } from '@/components'; -import { PlaylistAdd, Edit } from '@/components/icon'; -import { CSSVariable } from '@/global_style'; -import notice from '@/utils/notice'; -import { t } from '@/i18n'; -import { useUser } from '@/global_states/server'; -import { useSetting } from '@/global_states/setting'; -import { ROOT_PATH, ADMIN_PATH } from '@/constants/route'; -import { Artist } from './constants'; -import { CONTROLLER_FLOATING_RESERVED_HEIGHT } from '../constants'; -import { MusicWithArtistAliases } from '../constants'; -import addMusicListToPlaylist from '../add_to_playlist'; - -const Style = styled.div<{ $floatingControllerOffset: boolean }>` - z-index: 1; - - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: ${({ $floatingControllerOffset }) => - $floatingControllerOffset - ? CONTROLLER_FLOATING_RESERVED_HEIGHT - : 'calc(14px + env(safe-area-inset-bottom, 0))'}; - max-width: calc(100% - 32px); - padding: 8px 12px; - box-sizing: border-box; - - display: flex; - align-items: center; - gap: 8px; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 16px; - box-shadow: - 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}, - 0 10px 24px rgb(0 0 0 / 0.1); - - > .left { - min-width: 0; - - display: flex; - align-items: center; - gap: 8px; - } -`; - -const getArtistMusicList = (artist: Artist): MusicWithArtistAliases[] => - Array.from( - new Map( - [ - ...artist.performerMusicList, - ...artist.lyricistMusicList, - ...artist.composerMusicList, - ].map((music) => [music.id, music]), - ).values(), - ); - -function Toolbar({ - artist, - floatingControllerOffset = false, -}: { - artist: Artist; - floatingControllerOffset?: boolean; -}) { - const user = useUser(); - const adminQuickEdit = useSetting((s) => s.adminQuickEdit); - const musicList = getArtistMusicList(artist); - const hasMusic = musicList.length > 0; - // 仅在 admin 且开启「管理员快捷编辑」开关时展示编辑入口 - const showAdminEdit = !!user?.admin && adminQuickEdit; - return ( - - ); -} - -export default Toolbar; diff --git a/apps/pwa/src/pages/player/artist_drawer/use_data.ts b/apps/pwa/src/pages/player/artist_drawer/use_data.ts deleted file mode 100644 index 8a5d7c544..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/use_data.ts +++ /dev/null @@ -1,69 +0,0 @@ -import logger from '@/utils/logger'; -import getArtist from '@/server/api/get_artist'; -import { useCallback, useEffect, useState } from 'react'; -import { Artist } from './constants'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; - -type Data = - | { - error: null; - loading: true; - value: null; - } - | { - error: Error; - loading: false; - value: null; - } - | { - error: null; - loading: false; - value: Artist; - }; -const dataLoading: Data = { - error: null, - loading: true, - value: null, -}; - -export default (artistId: string) => { - const [data, setData] = useState(dataLoading); - const getData = useCallback(async () => { - setData(dataLoading); - try { - const artist = await getArtist(artistId); - setData({ - error: null, - loading: false, - value: artist, - }); - } catch (error) { - logger.error(error, 'Fail to get artist'); - setData({ - error, - loading: false, - value: null, - }); - } - }, [artistId]); - - useEffect(() => { - getData(); - }, [getData]); - - useEffect(() => { - const unlistenArtistUpdated = playerEventemitter.listen( - PlayerEventType.ARTIST_UPDATED, - (payload) => { - if (payload.id === artistId) { - getData(); - } - }, - ); - return unlistenArtistUpdated; - }, [getData, artistId]); - - return { data, reload: getData }; -}; diff --git a/apps/pwa/src/pages/player/artist_drawer/use_open.ts b/apps/pwa/src/pages/player/artist_drawer/use_open.ts deleted file mode 100644 index 6a3f49590..000000000 --- a/apps/pwa/src/pages/player/artist_drawer/use_open.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import useNavigate from '@/utils/use_navigate'; -import { Query } from '@/constants'; -import useQuery from '@/utils/use_query'; -import e, { EventType } from '../eventemitter'; - -export default () => { - const navigate = useNavigate(); - const onClose = useCallback( - () => - navigate({ - query: { - [Query.ARTIST_DRAWER_ID]: '', - }, - }), - [navigate], - ); - const { artist_drawer_id: urlId } = useQuery(); - const [id, setId] = useState(urlId); - - useEffect(() => { - setId((i) => urlId || i); - }, [urlId]); - - useEffect(() => { - const unlistenOpen = e.listen(EventType.OPEN_ARTIST_DRAWER, (data) => - window.setTimeout( - () => - navigate({ - query: { - [Query.ARTIST_DRAWER_ID]: data.id, - }, - }), - 0, - ), - ); - return unlistenOpen; - }, [navigate]); - - return { - id, - open: !!urlId, - onClose, - }; -}; diff --git a/apps/pwa/src/pages/player/index.tsx b/apps/pwa/src/pages/player/index.tsx index e2026e621..91240293a 100644 --- a/apps/pwa/src/pages/player/index.tsx +++ b/apps/pwa/src/pages/player/index.tsx @@ -14,18 +14,18 @@ import usePlayqueue from './use_playqueue'; import context from './context'; import useAudio from './use_audio'; import useMediaSession from './use_media_session'; -import MusicDrawer from './music_drawer'; +import MusicDrawer from '@/features/player/drawers/music_drawer'; import PlaylistPlayqueueDrawer from './playlist_playqueue_drawer'; -import MusicbillMusicDrawer from './musicbill_music_drawer'; +import MusicbillMusicDrawer from '@/features/player/drawers/musicbill_music_drawer'; import SortMusicbillDrawer from './sort_musicbilll_drawer'; import MusicbillSharedUserDrawer from './musicbill_shared_user_drawer'; import MusicbillFollowedArtistDrawer from './musicbill_followed_artist_drawer'; import SharedMusicbillInvitationDrawer from './pages/shared_musicbill_invitation'; import { QueueMusic } from './constants'; import LyricPanel from './lyric_panel'; -import ArtistDrawer from './artist_drawer'; -import UserDrawer from './user_drawer'; -import MusicbillDrawer from './musicbill_drawer'; +import ArtistDrawer from '@/features/player/drawers/artist_drawer'; +import UserDrawer from '@/features/player/drawers/user_drawer'; +import MusicbillDrawer from '@/features/player/drawers/musicbill_drawer'; import AuthorizedDeviceDrawer from './authorized_device_drawer'; import useLyricPanelOpen from './use_lyric_panel_open'; import useSearchHotkey from './use_search_hotkey'; diff --git a/apps/pwa/src/pages/player/music_drawer/constants.ts b/apps/pwa/src/pages/player/music_drawer/constants.ts deleted file mode 100644 index fbd318e05..000000000 --- a/apps/pwa/src/pages/player/music_drawer/constants.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Music, ArtistWithAliases } from '../constants'; - -export interface Lyric { - id: number; - lrc: string; -} - -export interface CreateUser { - id: string; - nickname: string; -} - -export interface ArtistPhoto { - id: string; - asset: string; - thumbnail?: string; - description: string; -} - -export interface ArtistDetail extends ArtistWithAliases { - avatar: string; - photos: ArtistPhoto[]; -} - -export interface RelatedPublicMusicbill { - id: string; - name: string; - cover: string; - musicCount: number; - user: CreateUser & { - avatar: string; - }; -} - -export interface MusicDetail extends Music { - heat: number; - lyrics: Lyric[]; - createTime: string; - forkFromList: Omit[]; - forkList: Omit[]; - performers: ArtistDetail[]; - lyricists: ArtistDetail[]; - composers: ArtistDetail[]; - year: number | null; - musicbillCount: number; - assetSize: number; - assetDurationMs: number; - assetCodec: string; - assetBitRate: number; - relatedPublicMusicbillList: RelatedPublicMusicbill[]; -} diff --git a/apps/pwa/src/pages/player/music_drawer/content.tsx b/apps/pwa/src/pages/player/music_drawer/content.tsx deleted file mode 100644 index a3d30ed3e..000000000 --- a/apps/pwa/src/pages/player/music_drawer/content.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/music_drawer/content'; diff --git a/apps/pwa/src/pages/player/music_drawer/index.tsx b/apps/pwa/src/pages/player/music_drawer/index.tsx deleted file mode 100644 index 1383863b5..000000000 --- a/apps/pwa/src/pages/player/music_drawer/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/music_drawer'; diff --git a/apps/pwa/src/pages/player/music_drawer/info.tsx b/apps/pwa/src/pages/player/music_drawer/info.tsx deleted file mode 100644 index 6117ffa88..000000000 --- a/apps/pwa/src/pages/player/music_drawer/info.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { type Ref } from 'react'; -import { CSSVariable } from '@/global_style'; -import styled from 'styled-components'; -import { MusicDetail } from './constants'; -import { t } from '@/i18n'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; - -const Style = styled.section<{ $showTitle: boolean }>` - padding: ${({ $showTitle }) => - $showTitle - ? `22px ${PAGE_HORIZONTAL_PADDING} 0` - : `18px ${PAGE_HORIZONTAL_PADDING} 0`}; - - > .headline { - margin-bottom: 16px; - - > .name { - margin: 0; - - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: 28px; - font-weight: 800; - line-height: 1.15; - letter-spacing: 0; - color: rgb(50 50 50); - overflow-wrap: anywhere; - } - - > .aliases { - margin: 8px 0 0; - - display: flex; - flex-direction: column; - gap: 3px; - - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 600; - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - line-height: 1.3; - } - - > .meta { - margin-top: 10px; - - color: rgb(145 145 145); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: 13px; - font-weight: 800; - line-height: 1.35; - overflow-wrap: anywhere; - } - } -`; - -const formatDurationMs = (durationMs: number) => { - const totalSeconds = Math.round(durationMs / 1000); - const minute = Math.floor(totalSeconds / 60); - const second = totalSeconds % 60; - return `${minute > 9 ? minute : `0${minute}`}:${ - second > 9 ? second : `0${second}` - }`; -}; -const formatFileSize = (size: number) => { - if (size < 1024) { - return `${size}B`; - } - if (size < 1024 * 1024) { - return `${Math.round(size / 1024)}KB`; - } - return `${(size / 1024 / 1024).toFixed(2)}MB`; -}; -const formatBitRate = (bitRate: number) => `${Math.round(bitRate / 1000)}kbps`; - -function MusicMetaLine({ music }: { music: MusicDetail }) { - const metaList = [ - music.assetDurationMs ? formatDurationMs(music.assetDurationMs) : '', - music.year ? `${music.year}` : '', - t('heat', music.heat.toString()), - t('musicbill_count', music.musicbillCount.toString()), - music.assetCodec ? music.assetCodec.toUpperCase() : '', - music.assetBitRate ? formatBitRate(music.assetBitRate) : '', - music.assetSize ? formatFileSize(music.assetSize) : '', - ].filter(Boolean); - - return metaList.length ? ( -
{metaList.join(' · ')}
- ) : null; -} - -function Info({ - music, - showTitle = true, - titleRef, -}: { - music: MusicDetail; - showTitle?: boolean; - titleRef?: Ref; -}) { - if (!showTitle) { - return null; - } - - return ( - - ); -} - -export default Info; diff --git a/apps/pwa/src/pages/player/music_drawer/lyric.tsx b/apps/pwa/src/pages/player/music_drawer/lyric.tsx deleted file mode 100644 index c8a5a9b3c..000000000 --- a/apps/pwa/src/pages/player/music_drawer/lyric.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { MusicType } from '@/constants/music'; -import { CSSVariable } from '@/global_style'; -import { MultipleLrc } from 'react-lrc'; -import styled from 'styled-components'; -import { saveAs } from 'file-saver'; -import { t } from '@/i18n'; -import capitalize from '@/style/capitalize'; -import formatMusicFilename from '@/utils/format_music_filename'; -import { useUser } from '@/global_states/server'; -import { useSetting } from '@/global_states/setting'; -import { MusicDetail } from './constants'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; -import { FileDownload } from '@/components/icon'; - -const Style = styled.section` - margin: 22px ${PAGE_HORIZONTAL_PADDING} 4px; - - > .label { - margin-bottom: 10px; - - color: rgb(75 75 75); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 800; - line-height: 1.2; - ${capitalize} - } - - > .content { - position: relative; - padding: 8px 0 12px; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 14px; - box-shadow: 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } -`; -const Line = styled.div` - margin: 10px 16px; - - line-height: 1.4; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 600; - color: rgb(120 120 120); -`; - -const DOWNLOAD_BTN_OFFSET = 2; -const DownloadButton = styled.button` - position: absolute; - right: 10px; - bottom: ${10 + DOWNLOAD_BTN_OFFSET}px; - - width: 22px; - height: 22px; - padding: 0; - margin: 0; - - display: inline-flex; - align-items: center; - justify-content: center; - - color: rgb(120 120 120); - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 8px; - box-shadow: 0 ${DOWNLOAD_BTN_OFFSET}px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - font-size: 13px; - line-height: 1; - cursor: pointer; - user-select: none; - -webkit-tap-highlight-color: transparent; - will-change: transform, box-shadow; - appearance: none; - -webkit-appearance: none; - - opacity: 0.4; - transition: - opacity 160ms ease, - transform 150ms ease-out, - box-shadow 150ms ease-out, - filter 120ms; - - &:hover, - &:focus-visible { - opacity: 1; - } - - &:hover { - filter: brightness(1.04); - } - - &:active { - transform: translateY(${DOWNLOAD_BTN_OFFSET}px); - box-shadow: none; - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in, - filter 60ms; - } - - &:focus-visible { - outline: 2px solid ${CSSVariable.COLOR_CONTROL_NEUTRAL}; - outline-offset: 2px; - } -`; - -function Lyric({ music }: { music: MusicDetail }) { - const user = useUser(); - const adminQuickEdit = useSetting((s) => s.adminQuickEdit); - - // 乐曲或没有歌词的歌曲, 不展示歌词模块 - if (music.type !== MusicType.SONG || music.lyrics.length === 0) { - return null; - } - - // 下载按钮归入「管理员快捷编辑」开关 - const downloadable = !!user?.admin && adminQuickEdit; - - const downloadLyrics = () => { - const performerNames = music.performers.map((s) => s.name); - // 单条歌词不加 (n) 后缀, 多条则按 1..N 顺序追加 - const multiple = music.lyrics.length > 1; - music.lyrics.forEach((lyric, i) => { - const filename = formatMusicFilename({ - name: music.name, - performerNames, - ext: 'lrc', - index: multiple ? i + 1 : undefined, - }); - saveAs( - new Blob([lyric.lrc], { type: 'text/plain;charset=utf-8' }), - filename, - ); - }); - }; - - return ( - - ); -} - -export default Lyric; diff --git a/apps/pwa/src/pages/player/music_drawer/music_drawer.tsx b/apps/pwa/src/pages/player/music_drawer/music_drawer.tsx deleted file mode 100644 index 75cc0b32e..000000000 --- a/apps/pwa/src/pages/player/music_drawer/music_drawer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import AppDrawer from '@/components/app_drawer'; -import MusicContent from './content'; - -function MusicDrawer({ - id, - open, - onClose, - zIndex, -}: { - id: string; - open: boolean; - onClose: () => void; - zIndex: number; -}) { - return ( - - - - ); -} - -export default MusicDrawer; diff --git a/apps/pwa/src/pages/player/music_drawer/performer_list/index.tsx b/apps/pwa/src/pages/player/music_drawer/performer_list/index.tsx deleted file mode 100644 index 829ce1816..000000000 --- a/apps/pwa/src/pages/player/music_drawer/performer_list/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import styled from 'styled-components'; -import { CSSVariable } from '@/global_style'; -import capitalize from '@/style/capitalize'; -import { ArtistDetail } from '../constants'; -import Performer from './performer'; -import { PAGE_HORIZONTAL_PADDING } from '../../pages/page'; - -const Style = styled.div` - margin: 20px ${PAGE_HORIZONTAL_PADDING}; - - > .label { - margin-bottom: 10px; - - color: rgb(75 75 75); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 800; - line-height: 1.2; - ${capitalize} - } - - > .list { - display: flex; - flex-direction: column; - gap: 10px; - } -`; - -function ArtistList({ - label, - artistList, -}: { - label: string; - artistList: ArtistDetail[]; -}) { - if (!artistList.length) { - return null; - } - - return ( - - ); -} - -export default ArtistList; diff --git a/apps/pwa/src/pages/player/music_drawer/performer_list/performer.tsx b/apps/pwa/src/pages/player/music_drawer/performer_list/performer.tsx deleted file mode 100644 index b0930dbd1..000000000 --- a/apps/pwa/src/pages/player/music_drawer/performer_list/performer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import Avatar from '@/components/avatar'; -import { CSSVariable } from '@/global_style'; -import styled from 'styled-components'; -import ellipsis from '@/style/ellipsis'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { ArtistDetail } from '../constants'; -import e, { EventType } from '../../eventemitter'; - -const AVATAR_SIZE = 32; -const Style = styled.div` - display: flex; - align-items: center; - gap: 10px; - - min-height: 56px; - padding: 8px 12px 12px; - - transition: - transform 120ms ease-out, - box-shadow 120ms ease-out, - filter 120ms ease-out; - cursor: pointer; - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 14px; - box-shadow: 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - - > .name { - flex: 1; - min-width: 0; - - ${ellipsis} - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_LARGE}; - font-weight: 800; - color: rgb(75 75 75); - } - - &:hover { - transform: translateY(-2px); - box-shadow: 0 6px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } - - &:active { - transform: translateY(4px); - box-shadow: none; - } -`; - -function Performer({ performer }: { performer: ArtistDetail }) { - return ( - - ); -} - -export default Performer; diff --git a/apps/pwa/src/pages/player/music_drawer/related_public_musicbill_list.tsx b/apps/pwa/src/pages/player/music_drawer/related_public_musicbill_list.tsx deleted file mode 100644 index 702c6bab4..000000000 --- a/apps/pwa/src/pages/player/music_drawer/related_public_musicbill_list.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useMemo, type KeyboardEvent } from 'react'; -import styled from 'styled-components'; -import Cover, { Shape } from '@/components/cover'; -import { CSSVariable } from '@/global_style'; -import ellipsis from '@/style/ellipsis'; -import capitalize from '@/style/capitalize'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { t } from '@/i18n'; -import { RelatedPublicMusicbill } from './constants'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; - -const RELATED_PUBLIC_MUSICBILL_LIMIT = 5; -const COVER_IMAGE_SIZE = 72; - -const Style = styled.section` - margin: 22px ${PAGE_HORIZONTAL_PADDING} 4px; - - > .label { - margin-bottom: 10px; - - color: rgb(75 75 75); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 800; - line-height: 1.2; - ${capitalize} - } - - > .list { - display: flex; - flex-direction: column; - gap: 10px; - } -`; -const Item = styled.div` - min-height: 72px; - padding: 8px 10px 12px; - box-sizing: border-box; - - display: flex; - align-items: center; - gap: 12px; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 14px; - box-shadow: 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - cursor: pointer; - user-select: none; - transition: - transform 120ms ease-out, - box-shadow 120ms ease-out, - filter 120ms ease-out; - - > .cover-frame { - flex: 0 0 58px; - width: 58px; - height: 58px; - padding: 2px; - box-sizing: border-box; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 10px; - box-shadow: 0 2px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } - - > .cover-frame > .cover { - width: 100%; - height: 100%; - border-radius: 6px; - } - - > .info { - flex: 1; - min-width: 0; - } - - > .info > .name { - color: rgb(55 55 55); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_LARGE}; - font-weight: 800; - line-height: 1.25; - ${ellipsis} - } - - > .info > .owner { - margin-top: 3px; - - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_SMALL}; - font-weight: 800; - line-height: 1.3; - ${ellipsis} - } - - > .info > .owner > button { - padding: 0; - border: 0; - background: transparent; - color: inherit; - font: inherit; - cursor: pointer; - - &:hover { - color: ${CSSVariable.TEXT_COLOR_PRIMARY}; - } - } - - > .count { - flex: 0 0 48px; - min-width: 48px; - padding-left: 10px; - box-sizing: border-box; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - border-left: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - line-height: 1.05; - } - - > .count > .value { - color: ${CSSVariable.TEXT_COLOR_PRIMARY}; - font-size: ${CSSVariable.TEXT_SIZE_LARGE}; - font-weight: 900; - } - - > .count > .label { - margin-top: 3px; - - font-size: ${CSSVariable.TEXT_SIZE_SMALL}; - font-weight: 800; - text-transform: capitalize; - } - - &:hover { - background: #fff; - } - - &:active { - background: #fff; - transform: translateY(4px); - box-shadow: none; - } - - &:focus-visible { - outline: 3px solid rgb(255 184 28); - outline-offset: 3px; - } -`; - -function sampleMusicbillList(musicbillList: RelatedPublicMusicbill[]) { - if (musicbillList.length <= RELATED_PUBLIC_MUSICBILL_LIMIT) { - return musicbillList; - } - - // 后端按 5 个随机抽样返回;这里兜底处理超量数据, 保证 drawer 展示数量稳定。 - const pool = [...musicbillList]; - for (let i = 0; i < RELATED_PUBLIC_MUSICBILL_LIMIT; i += 1) { - const randomIndex = i + Math.floor(Math.random() * (pool.length - i)); - [pool[i], pool[randomIndex]] = [pool[randomIndex], pool[i]]; - } - return pool.slice(0, RELATED_PUBLIC_MUSICBILL_LIMIT); -} - -const openMusicbillDrawer = (id: string) => - playerEventemitter.emit(PlayerEventType.OPEN_MUSICBILL_DRAWER, { id }); - -const openUserDrawer = (id: string) => - playerEventemitter.emit(PlayerEventType.OPEN_USER_DRAWER, { id }); - -function RelatedPublicMusicbillList({ - musicbillList, -}: { - musicbillList: RelatedPublicMusicbill[]; -}) { - const sampledMusicbillList = useMemo( - () => sampleMusicbillList(musicbillList), - [musicbillList], - ); - - if (!sampledMusicbillList.length) { - return null; - } - - const handleItemKeyDown = ( - event: KeyboardEvent, - id: string, - ) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - openMusicbillDrawer(id); - } - }; - - return ( - - ); -} - -export default RelatedPublicMusicbillList; diff --git a/apps/pwa/src/pages/player/music_drawer/sub_music_list.tsx b/apps/pwa/src/pages/player/music_drawer/sub_music_list.tsx deleted file mode 100644 index 6acd8335b..000000000 --- a/apps/pwa/src/pages/player/music_drawer/sub_music_list.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { CSSVariable } from '@/global_style'; -import styled from 'styled-components'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { Music } from '../constants'; -import MusicInfo from '../components/music_info'; -import capitalize from '@/style/capitalize'; -import { PAGE_HORIZONTAL_PADDING } from '../pages/page'; - -const Style = styled.div` - margin: 22px ${PAGE_HORIZONTAL_PADDING}; - - > .label { - margin-bottom: 10px; - - color: rgb(75 75 75); - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 800; - line-height: 1.2; - ${capitalize} - } - - > .list { - display: flex; - flex-direction: column; - gap: 10px; - - > .item { - min-height: 64px; - padding: 8px 10px 12px; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 14px; - box-shadow: 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - transition: - transform 120ms ease-out, - box-shadow 120ms ease-out; - - &:hover { - background: #fff; - } - - &:active { - background: #fff; - transform: translateY(4px); - box-shadow: none; - } - - > div:first-child { - box-sizing: border-box; - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 10px; - box-shadow: 0 2px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - } - - > .info { - > .name { - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-weight: 800; - color: rgb(55 55 55); - } - - > .performers { - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-weight: 700; - } - } - } - } -`; - -function SubMusicList({ - musicList, - label, -}: { - musicList: Omit[]; - label: string; -}) { - return ( - - ); -} - -export default SubMusicList; diff --git a/apps/pwa/src/pages/player/music_drawer/toolbar.tsx b/apps/pwa/src/pages/player/music_drawer/toolbar.tsx deleted file mode 100644 index c0bc8d74e..000000000 --- a/apps/pwa/src/pages/player/music_drawer/toolbar.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import styled from 'styled-components'; -import { useContext } from 'react'; -import Button from '@/components/button'; -import { Tooltip } from '@/components'; -import { CSSVariable } from '@/global_style'; -import { - FileDownload, - PlaylistAdd, - PostAdd, - QueueInsert, - PlayArrow, - Edit, -} from '@/components/icon'; -import { useUser } from '@/global_states/server'; -import { useSetting } from '@/global_states/setting'; -import { ADMIN_PATH, ROOT_PATH } from '@/constants/route'; -import { MusicDetail } from './constants'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; -import { CONTROLLER_FLOATING_RESERVED_HEIGHT } from '../constants'; -import addMusicListToPlaylist from '../add_to_playlist'; -import { t } from '@/i18n'; -import Context from '../context'; -import downloadOriginalMusicFile from '@/features/player/download_music'; - -const Style = styled.div<{ $floatingControllerOffset: boolean }>` - z-index: 1; - - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: ${({ $floatingControllerOffset }) => - $floatingControllerOffset - ? CONTROLLER_FLOATING_RESERVED_HEIGHT - : 'calc(14px + env(safe-area-inset-bottom, 0))'}; - max-width: calc(100% - 32px); - padding: 8px 12px; - box-sizing: border-box; - - display: flex; - align-items: center; - gap: 8px; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_NEUTRAL_SHADOW}; - border-radius: 16px; - box-shadow: - 0 4px 0 ${CSSVariable.COLOR_NEUTRAL_SHADOW}, - 0 10px 24px rgb(0 0 0 / 0.1); - - > .left { - min-width: 0; - - display: flex; - align-items: center; - gap: 8px; - } -`; - -function Toolbar({ - music, - floatingControllerOffset = false, -}: { - music: MusicDetail; - floatingControllerOffset?: boolean; -}) { - const user = useUser(); - const adminQuickEdit = useSetting((s) => s.adminQuickEdit); - const { downloadEnabled } = useContext(Context); - // 编辑按钮: 管理员开启「管理员快捷编辑」时才出现, 点击跳转到管理页并自动打开该音乐的编辑 drawer - const showAdminEdit = !!user?.admin && adminQuickEdit; - return ( - - ); -} - -export default Toolbar; diff --git a/apps/pwa/src/pages/player/music_drawer/use_data.ts b/apps/pwa/src/pages/player/music_drawer/use_data.ts deleted file mode 100644 index 549ef0adb..000000000 --- a/apps/pwa/src/pages/player/music_drawer/use_data.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import getMusicRequest from '@/server/api/get_music'; -import { MusicType } from '@/constants/music'; -import getLyricList from '@/server/api/get_lyric_list'; -import day from '@/utils/day'; -import { MusicDetail, Lyric } from './constants'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; - -interface Data { - error: Error | null; - loading: boolean; - music: MusicDetail | null; -} -const dataLoading: Data = { - error: null, - loading: true, - music: null, -}; - -async function loadMusicDetail(id: string): Promise { - const music = await getMusicRequest({ id }); - let lyrics: Lyric[] = []; - if (music.type === MusicType.SONG) { - lyrics = await getLyricList({ musicId: music.id }); - } - - return { - ...music, - lyrics, - createTime: day(music.createTimestamp).format('YYYY-MM-DD'), - heat: music.heat, - }; -} - -export default (id: string) => { - const [data, setData] = useState(dataLoading); - - const getMusic = useCallback(async () => { - setData(dataLoading); - try { - const music = await loadMusicDetail(id); - setData({ - error: null, - loading: false, - music, - }); - } catch (error) { - setData({ - error, - loading: false, - music: null, - }); - } - }, [id]); - - useEffect(() => { - getMusic(); - }, [getMusic]); - - useEffect(() => { - const unlistenArtistUpdated = playerEventemitter.listen( - PlayerEventType.ARTIST_UPDATED, - (payload) => { - const artists = [ - ...(data.music?.performers ?? []), - ...(data.music?.lyricists ?? []), - ...(data.music?.composers ?? []), - ]; - if (artists.find((artist) => artist.id === payload.id)) { - getMusic(); - } - }, - ); - return unlistenArtistUpdated; - }, [data.music, getMusic]); - - return { data, reload: getMusic }; -}; diff --git a/apps/pwa/src/pages/player/music_drawer/use_open.ts b/apps/pwa/src/pages/player/music_drawer/use_open.ts deleted file mode 100644 index 47e8986b4..000000000 --- a/apps/pwa/src/pages/player/music_drawer/use_open.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import useNavigate from '@/utils/use_navigate'; -import { Query } from '@/constants'; -import useQuery from '@/utils/use_query'; -import eventemitter, { EventType } from '../eventemitter'; - -export default () => { - const navigate = useNavigate(); - const onClose = useCallback( - () => - navigate({ - query: { - [Query.MUSIC_DRAWER_ID]: '', - }, - }), - [navigate], - ); - const { music_drawer_id: urlId } = useQuery(); - const [id, setId] = useState(urlId); - - useEffect(() => { - setId((i) => urlId || i); - }, [urlId]); - - useEffect(() => { - const unlistenOpenMusicDrawer = eventemitter.listen( - EventType.OPEN_MUSIC_DRAWER, - (data) => - window.setTimeout( - () => - navigate({ - query: { - [Query.MUSIC_DRAWER_ID]: data.id, - }, - }), - 0, - ), - ); - return unlistenOpenMusicDrawer; - }, [navigate]); - - return { - open: !!urlId, - onClose, - id, - }; -}; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/constants.ts b/apps/pwa/src/pages/player/musicbill_drawer/constants.ts deleted file mode 100644 index 96e1d21ff..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@/features/player/drawers/musicbill_drawer/constants'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/eventemitter.ts b/apps/pwa/src/pages/player/musicbill_drawer/eventemitter.ts deleted file mode 100644 index 1a73fb806..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/eventemitter.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/eventemitter'; -export * from '@/features/player/drawers/musicbill_drawer/eventemitter'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/index.tsx b/apps/pwa/src/pages/player/musicbill_drawer/index.tsx deleted file mode 100644 index 527906848..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/info.tsx b/apps/pwa/src/pages/player/musicbill_drawer/info.tsx deleted file mode 100644 index 5d5f31697..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/info.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/info'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/music_list.tsx b/apps/pwa/src/pages/player/musicbill_drawer/music_list.tsx deleted file mode 100644 index fe57685ba..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/music_list.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/music_list'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/musicbill_drawer.tsx b/apps/pwa/src/pages/player/musicbill_drawer/musicbill_drawer.tsx deleted file mode 100644 index 8a74445f6..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/musicbill_drawer.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/musicbill_drawer'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/toolbar.tsx b/apps/pwa/src/pages/player/musicbill_drawer/toolbar.tsx deleted file mode 100644 index 1dd5362ff..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/toolbar.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/toolbar'; diff --git a/apps/pwa/src/pages/player/musicbill_drawer/use_data.ts b/apps/pwa/src/pages/player/musicbill_drawer/use_data.ts deleted file mode 100644 index 4750164ec..000000000 --- a/apps/pwa/src/pages/player/musicbill_drawer/use_data.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_drawer/use_data'; diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/constants.ts b/apps/pwa/src/pages/player/musicbill_music_drawer/constants.ts deleted file mode 100644 index 6581ac5d6..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const FLOATING_GAP = 12; diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/index.tsx b/apps/pwa/src/pages/player/musicbill_music_drawer/index.tsx deleted file mode 100644 index dd2ea3682..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/musicbill_music_drawer'; diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill.tsx b/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill.tsx deleted file mode 100644 index 24024e7b9..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { memo } from 'react'; -import styled from 'styled-components'; -import { CSSVariable } from '@/global_style'; -import { CSS_VAR } from '@/components/theme'; -import { Refresh, Check } from '@/components/icon'; -import { RequestStatus } from '@/constants'; -import Spinner from '@/components/spinner'; -import ellipsis from '@/style/ellipsis'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { - MusicWithArtistAliases, - Musicbill as MusicbillType, -} from '../constants'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; -import MusicbillCover from '../components/musicbill_cover'; - -const ICON_SIZE = 24; -const PRIMARY = `var(${CSS_VAR.colorPrimary})`; -const PRIMARY_SHADOW = `var(${CSS_VAR.colorPrimaryShadow})`; -const PUBLIC = '#63d1fa'; -const PUBLIC_SHADOW = 'rgb(72 179 220)'; -const NEUTRAL_SHADOW = CSSVariable.COLOR_NEUTRAL_SHADOW; -const CONTROL_NEUTRAL = CSSVariable.COLOR_NEUTRAL_SHADOW; -const COVER_SIZE = 28; -const COVER_INNER_SIZE = COVER_SIZE - 4; -const CHECKBOX_SIZE = 24; - -const Style = styled.div<{ $public: boolean; $selected: boolean }>` - min-height: 48px; - padding: 10px; - margin: 0 12px 10px; - - display: flex; - align-items: center; - gap: 10px; - - cursor: pointer; - color: ${CSSVariable.TEXT_COLOR_PRIMARY}; - background: ${({ $selected }) => ($selected ? PRIMARY : '#fff')}; - border: 2px solid - ${({ $selected }) => - $selected ? PRIMARY_SHADOW : CSSVariable.COLOR_BORDER}; - border-radius: 14px; - box-shadow: 0 4px 0 - ${({ $selected }) => ($selected ? PRIMARY_SHADOW : NEUTRAL_SHADOW)}; - user-select: none; - -webkit-tap-highlight-color: transparent; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out, - border-color 150ms ease-out, - background 150ms ease-out, - color 150ms ease-out, - filter 120ms ease-out; - - &:hover { - filter: brightness(1.04); - } - - &:active { - transform: translateY(4px); - box-shadow: none; - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in, - filter 60ms ease-in; - } - - > .icon { - width: ${ICON_SIZE}px; - height: ${ICON_SIZE}px; - flex: 0 0 auto; - color: ${({ $selected }) => - $selected ? '#fff' : CSSVariable.TEXT_COLOR_SECONDARY}; - } - - > .cover { - flex: 0 0 auto; - width: ${COVER_SIZE}px; - height: ${COVER_SIZE}px; - box-sizing: border-box; - - display: flex; - align-items: center; - justify-content: center; - - overflow: hidden; - background: #fff; - border: 2px solid - ${({ $public, $selected }) => - $public - ? PUBLIC - : $selected - ? '#fff' - : CSSVariable.COLOR_BORDER}; - border-radius: 9px; - box-shadow: 0 3px 0 - ${({ $public, $selected }) => - $public - ? PUBLIC_SHADOW - : $selected - ? 'rgb(255 255 255 / 0.42)' - : NEUTRAL_SHADOW}; - } - - > .name { - flex: 1; - min-width: 0; - - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: ${CSSVariable.TEXT_SIZE_NORMAL}; - font-weight: 800; - letter-spacing: 0; - color: ${({ $selected }) => - $selected ? '#fff' : CSSVariable.TEXT_COLOR_PRIMARY}; - ${ellipsis} - } -`; -const Checkbox = styled.span<{ $checked: boolean }>` - flex: 0 0 auto; - width: ${CHECKBOX_SIZE}px; - height: ${CHECKBOX_SIZE}px; - box-sizing: border-box; - - display: flex; - align-items: center; - justify-content: center; - - color: #fff; - background: ${({ $checked }) => ($checked ? PRIMARY : '#fff')}; - border: 2px solid - ${({ $checked }) => ($checked ? PRIMARY_SHADOW : CONTROL_NEUTRAL)}; - border-radius: 8px; - box-shadow: 0 3px 0 - ${({ $checked }) => ($checked ? PRIMARY_SHADOW : CONTROL_NEUTRAL)}; - transition: - background 150ms ease-out, - border-color 150ms ease-out, - box-shadow 150ms ease-out, - transform 150ms ease-out; - - > svg { - width: 16px; - height: 16px; - opacity: 1; - transform: scale(${({ $checked }) => ($checked ? 1 : 0.72)}); - transition: - opacity 120ms ease-out, - transform 150ms cubic-bezier(0.34, 1.56, 0.64, 1); - } - - > .empty-check { - opacity: 0; - } - - > .refresh-icon { - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - transform: scale(1); - } -`; - -function Musicbill({ - musicbill, - music, -}: { - musicbill: MusicbillType; - music: MusicWithArtistAliases; -}) { - const { id, status, musicList } = musicbill; - const selected = - status === RequestStatus.SUCCESS && - musicList.some((m) => m.id === music.id); - - return ( - - ); -} - -export default memo(Musicbill); diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_list.tsx b/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_list.tsx deleted file mode 100644 index 97d20b1bb..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_list.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import Spinner from '@/components/spinner'; -import { flexCenter } from '@/style/flexbox'; -import { memo, useContext } from 'react'; -import styled from 'styled-components'; -import ErrorCard from '@/components/error_card'; -import { RequestStatus } from '@/constants'; -import Empty from '@/components/empty'; -import { t } from '@/i18n'; -import { MusicWithArtistAliases } from '../constants'; -import Context from '../context'; -import playerEventemitter, { - EventType as PlayerEventType, -} from '../eventemitter'; -import { FLOATING_GAP } from './constants'; -import Musicbill from './musicbill'; - -const reloadMusicbillList = () => - playerEventemitter.emit(PlayerEventType.RELOAD_MUSICBILL_LIST, { - silence: false, - }); -const Style = styled.div` - padding: ${FLOATING_GAP}px 0 max(env(safe-area-inset-bottom, 0) + 80px) 0; -`; -const StatusContainer = styled.div` - ${flexCenter} - padding: 30px 0; -`; - -function MusicbillList({ music }: { music: MusicWithArtistAliases }) { - const { getMusicbillListStatus, musicbillList } = useContext(Context); - if (getMusicbillListStatus === RequestStatus.SUCCESS) { - if (musicbillList.length) { - return ( - - ); - } - return ( - - - - ); - } - if (getMusicbillListStatus === RequestStatus.ERROR) { - return ( - - - - ); - } - return ( - - - - ); -} - -export default memo(MusicbillList); diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_music_drawer.tsx b/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_music_drawer.tsx deleted file mode 100644 index d411b0aab..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/musicbill_music_drawer.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { memo } from 'react'; -import { Drawer, DrawerContent } from '@/components'; -import Button from '@/components/button'; -import { AddBox } from '@/components/icon'; -import styled from 'styled-components'; -import autoScrollbar from '@/style/auto_scrollbar'; -import { MusicWithArtistAliases } from '../constants'; -import { t } from '@/i18n'; -import { openCreateMusicbillDialog } from '../utils'; -import Top from './top'; -import MusicbillList from './musicbill_list'; - -const Content = styled.div` - position: relative; - height: 100%; - - display: flex; - flex-direction: column; - overflow: hidden; -`; -const ScrollContent = styled.div` - flex: 1; - min-height: 0; - - overflow: auto; - ${autoScrollbar} -`; -const FloatingCreateButton = styled(Button)` - position: absolute; - right: 16px; - bottom: max(env(safe-area-inset-bottom, 0) + 16px, 16px); - z-index: 2; -`; - -function MusicbillMusicDrawer({ - open, - onClose, - music, - zIndex, -}: { - open: boolean; - onClose: () => void; - music: MusicWithArtistAliases; - zIndex: number; -}) { - return ( - !v && onClose()}> - - - - - - - - - - - - - ); -} - -export default memo(MusicbillMusicDrawer); diff --git a/apps/pwa/src/pages/player/musicbill_music_drawer/top.tsx b/apps/pwa/src/pages/player/musicbill_music_drawer/top.tsx deleted file mode 100644 index d6bbe2f3e..000000000 --- a/apps/pwa/src/pages/player/musicbill_music_drawer/top.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import styled from 'styled-components'; -import { CSSVariable } from '@/global_style'; -import { CSS_VAR } from '@/components/theme'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { Music } from '../constants'; -import MusicInfo from '../components/music_info'; -import { FLOATING_GAP } from './constants'; - -const PRIMARY = `var(${CSS_VAR.colorPrimary})`; -const NEUTRAL_SHADOW = CSSVariable.COLOR_NEUTRAL_SHADOW; - -const Style = styled.div` - z-index: 1; - - position: sticky; - top: 0; - - display: flex; - flex-direction: column; - gap: 12px; - pointer-events: none; - - > .music-info { - /* 比下方乐单条目更窄, 悬浮在上方时通过左右缩进与乐单区分, 避免视觉融合 */ - margin: 0 24px; - - pointer-events: auto; - background: #fff; - border: 2px solid ${CSSVariable.COLOR_BORDER}; - border-radius: 14px; - box-shadow: 0 4px 0 ${NEUTRAL_SHADOW}; - transition: - transform 150ms ease-out, - box-shadow 150ms ease-out, - border-color 150ms ease-out, - filter 120ms ease-out; - - > :first-child { - flex: 0 0 auto; - box-sizing: border-box; - overflow: hidden; - - background: #fff; - border: 2px solid ${CSSVariable.COLOR_BORDER}; - border-radius: 10px; - box-shadow: 0 3px 0 ${NEUTRAL_SHADOW}; - } - - > .info { - display: flex; - flex-direction: column; - gap: 3px; - } - - > .info > .name { - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: 15px; - font-weight: 900; - letter-spacing: 0; - line-height: 1.2; - color: rgb(50 50 50); - } - - > .info > .performers { - font-family: 'Nunito', 'Varela Round', system-ui, sans-serif; - font-size: 12px; - font-weight: 700; - letter-spacing: 0; - line-height: 1.25; - color: ${CSSVariable.TEXT_COLOR_SECONDARY}; - - .name { - font-weight: 800; - } - } - - &:hover { - background: #fff; - border-color: ${PRIMARY}; - } - - &:active { - transform: translateY(4px); - box-shadow: none; - transition: - transform 60ms ease-in, - box-shadow 60ms ease-in; - } - } -`; - -function Top({ music }: { music: Music }) { - return ( - - ); -} - -export default Top; diff --git a/apps/pwa/src/pages/player/pages/artist/index.tsx b/apps/pwa/src/pages/player/pages/artist/index.tsx index 89c9e1722..02b65ef18 100644 --- a/apps/pwa/src/pages/player/pages/artist/index.tsx +++ b/apps/pwa/src/pages/player/pages/artist/index.tsx @@ -1,7 +1,7 @@ import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import Page from '../page'; -import ArtistContent from '../../artist_drawer/content'; +import ArtistContent from '@/features/player/drawers/artist_drawer/content'; const Style = styled(Page)` z-index: 1; diff --git a/apps/pwa/src/pages/player/pages/music/index.tsx b/apps/pwa/src/pages/player/pages/music/index.tsx index c8aa4076b..9a5539e51 100644 --- a/apps/pwa/src/pages/player/pages/music/index.tsx +++ b/apps/pwa/src/pages/player/pages/music/index.tsx @@ -1,7 +1,7 @@ import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import Page from '../page'; -import MusicContent from '../../music_drawer/content'; +import MusicContent from '@/features/player/drawers/music_drawer/content'; const Style = styled(Page)` z-index: 1; diff --git a/apps/pwa/src/pages/player/user_drawer/constants.ts b/apps/pwa/src/pages/player/user_drawer/constants.ts deleted file mode 100644 index 6b6239f8d..000000000 --- a/apps/pwa/src/pages/player/user_drawer/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@/features/player/drawers/user_drawer/constants'; diff --git a/apps/pwa/src/pages/player/user_drawer/index.tsx b/apps/pwa/src/pages/player/user_drawer/index.tsx deleted file mode 100644 index 663fec976..000000000 --- a/apps/pwa/src/pages/player/user_drawer/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/user_drawer'; diff --git a/apps/pwa/src/pages/player/user_drawer/info.tsx b/apps/pwa/src/pages/player/user_drawer/info.tsx deleted file mode 100644 index 3da272e48..000000000 --- a/apps/pwa/src/pages/player/user_drawer/info.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/user_drawer/info'; diff --git a/apps/pwa/src/pages/player/user_drawer/mini_info.tsx b/apps/pwa/src/pages/player/user_drawer/mini_info.tsx deleted file mode 100644 index 0a68c8c60..000000000 --- a/apps/pwa/src/pages/player/user_drawer/mini_info.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import useTitlebarArea from '@/utils/use_titlebar_area_rect'; -import styled from 'styled-components'; -import Cover, { Shape } from '@/components/cover'; -import { CSSVariable } from '@/global_style'; -import ellipsis from '@/style/ellipsis'; -import { useEffect, useState } from 'react'; -import getResizedImage from '@/server/asset/get_resized_image'; -import { MINI_INFO_HEIGHT, UserDetail } from './constants'; -import { t } from '@/i18n'; - -const AVATAR_SIZE = 28; -const Style = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: ${MINI_INFO_HEIGHT}px; - - background-color: #fff; - - display: flex; - align-items: center; - gap: 10px; - - > .name { - flex: 1; - min-width: 0; - - ${ellipsis} - font-size: 18px; - font-weight: bold; - color: ${CSSVariable.TEXT_COLOR_PRIMARY}; - } -`; - -function MiniInfo({ user }: { user: UserDetail }) { - const [paddingRight, setPaddingRight] = useState(0); - const { right } = useTitlebarArea(); - - useEffect(() => { - setPaddingRight(right ? window.innerWidth - right : 0); - }, [right]); - - return ( - - ); -} - -export default MiniInfo; diff --git a/apps/pwa/src/pages/player/user_drawer/musicbill_list.tsx b/apps/pwa/src/pages/player/user_drawer/musicbill_list.tsx deleted file mode 100644 index 33d2bbfb4..000000000 --- a/apps/pwa/src/pages/player/user_drawer/musicbill_list.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/user_drawer/musicbill_list'; diff --git a/apps/pwa/src/pages/player/user_drawer/use_data.ts b/apps/pwa/src/pages/player/user_drawer/use_data.ts deleted file mode 100644 index 8d383d4d7..000000000 --- a/apps/pwa/src/pages/player/user_drawer/use_data.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/user_drawer/use_data'; diff --git a/apps/pwa/src/pages/player/user_drawer/user_drawer.tsx b/apps/pwa/src/pages/player/user_drawer/user_drawer.tsx deleted file mode 100644 index b10fc8597..000000000 --- a/apps/pwa/src/pages/player/user_drawer/user_drawer.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@/features/player/drawers/user_drawer/user_drawer'; From 1726206176e777cc29cbdaf62098fa0bf77546d5 Mon Sep 17 00:00:00 2001 From: anyone Date: Thu, 2 Jul 2026 19:27:00 +0800 Subject: [PATCH 23/38] fix sidebar style --- apps/pwa/src/pages/player/sidebar/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/pwa/src/pages/player/sidebar/index.tsx b/apps/pwa/src/pages/player/sidebar/index.tsx index 6c9a4cb38..8039ccec2 100644 --- a/apps/pwa/src/pages/player/sidebar/index.tsx +++ b/apps/pwa/src/pages/player/sidebar/index.tsx @@ -12,8 +12,6 @@ const Placeholder = styled.div` border-right: 2px solid ${CSSVariable.COLOR_BORDER}; background: #fff; - box-shadow: 4px 0 0 - color-mix(in srgb, ${CSSVariable.COLOR_SURFACE_SHADOW} 35%, transparent); overflow: auto; ${autoScrollbar} `; From caff99a7dcbf2424b7f78911f606d041eb0907b4 Mon Sep 17 00:00:00 2001 From: anyone Date: Thu, 2 Jul 2026 19:31:01 +0800 Subject: [PATCH 24/38] improve words --- apps/pwa/src/i18n/en.ts | 2 +- apps/pwa/src/i18n/zh_hans.ts | 2 +- apps/pwa/src/pages/player/sidebar/musicbill_list/top.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/pwa/src/i18n/en.ts b/apps/pwa/src/i18n/en.ts index 86b6ec387..118b8e8f3 100644 --- a/apps/pwa/src/i18n/en.ts +++ b/apps/pwa/src/i18n/en.ts @@ -282,7 +282,7 @@ export default { delete_origin_question: 'are you sure to delete this server address and users within?', sort_musicbill: 'sort musicbill', - reload_musicbill_list: 'refresh', + reload_musicbill_list: 'reload musicbill list', collect_musicbill: 'collect musicbill', uncollect_musicbill: 'uncollect musicbill', make_public_musicbill_question: 'make this musicbill public?', diff --git a/apps/pwa/src/i18n/zh_hans.ts b/apps/pwa/src/i18n/zh_hans.ts index e19a3bd91..d278123cd 100644 --- a/apps/pwa/src/i18n/zh_hans.ts +++ b/apps/pwa/src/i18n/zh_hans.ts @@ -274,7 +274,7 @@ const zhCN: { manage_origins: '服务器地址管理', delete_origin_question: '确定删除该服务器地址以及已登录用户吗?', sort_musicbill: '乐单排序', - reload_musicbill_list: '刷新', + reload_musicbill_list: '重新加载乐单', collect_musicbill: '收藏乐单', uncollect_musicbill: '取消收藏乐单', make_public_musicbill_question: '公开此乐单?', diff --git a/apps/pwa/src/pages/player/sidebar/musicbill_list/top.tsx b/apps/pwa/src/pages/player/sidebar/musicbill_list/top.tsx index edf1103e1..07debfaea 100644 --- a/apps/pwa/src/pages/player/sidebar/musicbill_list/top.tsx +++ b/apps/pwa/src/pages/player/sidebar/musicbill_list/top.tsx @@ -60,7 +60,7 @@ function Top() { return ( ); } diff --git a/apps/pwa/src/features/player/constants.ts b/apps/pwa/src/features/player/constants.ts index 7165665b4..fc365b3b9 100644 --- a/apps/pwa/src/features/player/constants.ts +++ b/apps/pwa/src/features/player/constants.ts @@ -101,6 +101,7 @@ export interface Musicbill { id: string; name: string; cover: string; + coverThumbnail?: string; createTimestamp: number; public: boolean; owner: MusicbillUser; diff --git a/apps/pwa/src/features/player/drawers/music_drawer/constants.ts b/apps/pwa/src/features/player/drawers/music_drawer/constants.ts index b73dcbe4b..0cb650bd0 100644 --- a/apps/pwa/src/features/player/drawers/music_drawer/constants.ts +++ b/apps/pwa/src/features/player/drawers/music_drawer/constants.ts @@ -26,6 +26,7 @@ export interface RelatedPublicMusicbill { id: string; name: string; cover: string; + coverThumbnail?: string; musicCount: number; user: CreateUser & { avatar: string; diff --git a/apps/pwa/src/features/player/drawers/music_drawer/related_public_musicbill_list.tsx b/apps/pwa/src/features/player/drawers/music_drawer/related_public_musicbill_list.tsx index ad381de2c..f1523c34d 100644 --- a/apps/pwa/src/features/player/drawers/music_drawer/related_public_musicbill_list.tsx +++ b/apps/pwa/src/features/player/drawers/music_drawer/related_public_musicbill_list.tsx @@ -224,6 +224,7 @@ function RelatedPublicMusicbillList({ url: musicbill.cover, size: Math.ceil(COVER_IMAGE_SIZE * window.devicePixelRatio), })} + placeholderSrc={musicbill.coverThumbnail} size="100%" />
diff --git a/apps/pwa/src/features/player/drawers/musicbill_drawer/info.tsx b/apps/pwa/src/features/player/drawers/musicbill_drawer/info.tsx index 3c100f237..95a55cc7b 100644 --- a/apps/pwa/src/features/player/drawers/musicbill_drawer/info.tsx +++ b/apps/pwa/src/features/player/drawers/musicbill_drawer/info.tsx @@ -118,11 +118,16 @@ function Info({ musicbill: Musicbill; identityRef?: Ref; }) { - const { name, cover, user } = musicbill; + const { name, cover, coverThumbnail, user } = musicbill; return ( diff --git a/apps/pwa/src/pages/player/sort_musicbilll_drawer/constant.ts b/apps/pwa/src/pages/player/sort_musicbilll_drawer/constant.ts index c6755b992..268cfd8f3 100644 --- a/apps/pwa/src/pages/player/sort_musicbilll_drawer/constant.ts +++ b/apps/pwa/src/pages/player/sort_musicbilll_drawer/constant.ts @@ -2,5 +2,6 @@ export interface LocalMusicbill { id: string; name: string; cover: string; + coverThumbnail?: string; public: boolean; } diff --git a/apps/pwa/src/pages/player/sort_musicbilll_drawer/musicbill.tsx b/apps/pwa/src/pages/player/sort_musicbilll_drawer/musicbill.tsx index 44f8989a8..231ae8a13 100644 --- a/apps/pwa/src/pages/player/sort_musicbilll_drawer/musicbill.tsx +++ b/apps/pwa/src/pages/player/sort_musicbilll_drawer/musicbill.tsx @@ -123,6 +123,7 @@ function Musicbill({ musicbill }: { musicbill: LocalMusicbill }) { $public={musicbill.public} size={COVER_SIZE} src={getResizedImage({ url: musicbill.cover, size: COVER_SIZE * 2 })} + placeholderSrc={musicbill.coverThumbnail} />
{musicbill.name}
diff --git a/apps/pwa/src/pages/player/sort_musicbilll_drawer/sort_musicbill_drawer.tsx b/apps/pwa/src/pages/player/sort_musicbilll_drawer/sort_musicbill_drawer.tsx index a98603613..a161d9927 100644 --- a/apps/pwa/src/pages/player/sort_musicbilll_drawer/sort_musicbill_drawer.tsx +++ b/apps/pwa/src/pages/player/sort_musicbilll_drawer/sort_musicbill_drawer.tsx @@ -82,6 +82,7 @@ const drawerStyle = { const toLocalMusicbill = (musicbill: MusicbillType): LocalMusicbill => ({ id: musicbill.id, cover: musicbill.cover, + coverThumbnail: musicbill.coverThumbnail, name: musicbill.name, public: musicbill.public, }); diff --git a/apps/pwa/src/server/api/admin_get_music.ts b/apps/pwa/src/server/api/admin_get_music.ts index 407f1f5a5..1abe80bdf 100644 --- a/apps/pwa/src/server/api/admin_get_music.ts +++ b/apps/pwa/src/server/api/admin_get_music.ts @@ -45,6 +45,7 @@ type Response = Omit & { id: string; name: string; cover: string; + coverThumbnail?: string; musicCount: number; user: { id: string; @@ -147,6 +148,7 @@ async function adminGetMusic({ ).map((musicbill) => ({ ...musicbill, cover: prefixServerOrigin(musicbill.cover), + coverThumbnail: prefixServerOrigin(musicbill.coverThumbnail ?? ''), user: { ...musicbill.user, avatar: prefixServerOrigin(musicbill.user.avatar), diff --git a/apps/pwa/src/server/api/get_exploration.ts b/apps/pwa/src/server/api/get_exploration.ts index d362420b9..67e5babaa 100644 --- a/apps/pwa/src/server/api/get_exploration.ts +++ b/apps/pwa/src/server/api/get_exploration.ts @@ -20,6 +20,7 @@ type PublicMusicbillItem = { id: string; name: string; cover: string; + coverThumbnail?: string; user: { id: string; nickname: string }; }; type Response = { @@ -47,6 +48,7 @@ const normalizeArtist = (artist: ArtistItem) => ({ const normalizeMusicbill = (mb: PublicMusicbillItem) => ({ ...mb, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), }); /** diff --git a/apps/pwa/src/server/api/get_music.ts b/apps/pwa/src/server/api/get_music.ts index 40f87a7c2..83f708527 100644 --- a/apps/pwa/src/server/api/get_music.ts +++ b/apps/pwa/src/server/api/get_music.ts @@ -45,6 +45,7 @@ type Response = Omit & { id: string; name: string; cover: string; + coverThumbnail?: string; musicCount: number; user: { id: string; @@ -185,6 +186,7 @@ async function getMusic({ ).map((mb) => ({ ...mb, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), user: { ...mb.user, avatar: prefixServerOrigin(mb.user.avatar), diff --git a/apps/pwa/src/server/api/get_musicbill.ts b/apps/pwa/src/server/api/get_musicbill.ts index 650d3ecb8..7266af694 100644 --- a/apps/pwa/src/server/api/get_musicbill.ts +++ b/apps/pwa/src/server/api/get_musicbill.ts @@ -11,6 +11,7 @@ interface User { interface Response { id: string; cover: string; + coverThumbnail?: string; name: string; public: boolean; createTimestamp: number; @@ -60,6 +61,7 @@ async function getMusicbill(id: string) { return { ...musicbill, cover: prefixServerOrigin(musicbill.cover), + coverThumbnail: prefixServerOrigin(musicbill.coverThumbnail ?? ''), owner: { ...musicbill.owner, avatar: prefixServerOrigin(musicbill.owner.avatar), diff --git a/apps/pwa/src/server/api/get_musicbill_list.ts b/apps/pwa/src/server/api/get_musicbill_list.ts index 2be8d926a..eac0d4f04 100644 --- a/apps/pwa/src/server/api/get_musicbill_list.ts +++ b/apps/pwa/src/server/api/get_musicbill_list.ts @@ -10,6 +10,7 @@ interface User { type Response = { id: string; cover: string; + coverThumbnail?: string; name: string; public: boolean; createTimestamp: number; @@ -27,6 +28,7 @@ async function getMusicbillList() { return musicbillList.map((mb) => ({ ...mb, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), owner: { ...mb.owner, avatar: prefixServerOrigin(mb.owner.avatar), diff --git a/apps/pwa/src/server/api/get_public_musicbill.ts b/apps/pwa/src/server/api/get_public_musicbill.ts index a18368f1e..b31e0993e 100644 --- a/apps/pwa/src/server/api/get_public_musicbill.ts +++ b/apps/pwa/src/server/api/get_public_musicbill.ts @@ -5,6 +5,7 @@ import { request } from '..'; type Response = { id: string; cover: string; + coverThumbnail?: string; name: string; public: boolean; user: { @@ -53,6 +54,7 @@ async function getPublicMusicbill(id: string) { return { ...musicbill, cover: prefixServerOrigin(musicbill.cover), + coverThumbnail: prefixServerOrigin(musicbill.coverThumbnail ?? ''), musicList: musicbill.musicList.map((m) => ({ ...m, cover: prefixServerOrigin(m.cover), diff --git a/apps/pwa/src/server/api/get_public_musicbill_collection_list.ts b/apps/pwa/src/server/api/get_public_musicbill_collection_list.ts index 2b654e85b..21103a82d 100644 --- a/apps/pwa/src/server/api/get_public_musicbill_collection_list.ts +++ b/apps/pwa/src/server/api/get_public_musicbill_collection_list.ts @@ -7,6 +7,7 @@ type Response = { id: string; name: string; cover: string; + coverThumbnail?: string; musicCount: number; user: { id: string; nickname: string }; }[]; @@ -39,6 +40,7 @@ async function getSelfMusicbillCollectionList({ ...mb, musicCount: mb.musicCount ?? 0, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), })), }; } diff --git a/apps/pwa/src/server/api/get_user.ts b/apps/pwa/src/server/api/get_user.ts index 4949f6d0b..1bc732145 100644 --- a/apps/pwa/src/server/api/get_user.ts +++ b/apps/pwa/src/server/api/get_user.ts @@ -10,6 +10,7 @@ interface Response { musicbillList: { id: string; cover: string; + coverThumbnail?: string; name: string; musicCount: number; }[]; @@ -31,6 +32,7 @@ async function getUser(id: string) { musicbillList: user.musicbillList.map((mb) => ({ ...mb, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), })), }; } diff --git a/apps/pwa/src/server/api/search_public_musicbill.ts b/apps/pwa/src/server/api/search_public_musicbill.ts index c7ebe1901..dcf8bc864 100644 --- a/apps/pwa/src/server/api/search_public_musicbill.ts +++ b/apps/pwa/src/server/api/search_public_musicbill.ts @@ -7,6 +7,7 @@ type Response = { id: string; name: string; cover: string; + coverThumbnail?: string; musicCount: number; collectionCount: number; user: { @@ -49,6 +50,7 @@ async function searchPublicMusicbill({ musicCount: mb.musicCount ?? 0, collectionCount: mb.collectionCount ?? 0, cover: prefixServerOrigin(mb.cover), + coverThumbnail: prefixServerOrigin(mb.coverThumbnail ?? ''), user: { ...mb.user, avatar: prefixServerOrigin(mb.user.avatar),