Skip to content

feat: add lyrics and song sharing functionality#149

Merged
roitium merged 12 commits into
devfrom
feat/lyrics-sharing
Jan 23, 2026
Merged

feat: add lyrics and song sharing functionality#149
roitium merged 12 commits into
devfrom
feat/lyrics-sharing

Conversation

@roitium
Copy link
Copy Markdown
Collaborator

@roitium roitium commented Jan 23, 2026

  • Add LyricsSelectionModal and SongShareModal for sharing
  • Add sharing menu items to PlayerFunctionalMenu
  • Add react-native-view-shot dependency for screenshot support

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加歌词选择与分享,最多支持选择 5 行并导出/预览图片
    • 新增歌曲分享卡片,生成含二维码的分享图片
    • 播放器菜单新增“分享歌词”和“分享歌曲”选项
  • 错误修复

    • 加强播放列表元数据校验,避免缺失数据导致崩溃
    • 优化模态框渲染与底板行为,改进提示样式与导出流程
  • 版本更新

    • 版本号升级至 2.1.10,加入图片捕获支持用于分享功能

✏️ Tip: You can customize this high-level summary in your review settings.

- Add LyricsSelectionModal and SongShareModal for sharing
- Add sharing menu items to PlayerFunctionalMenu
- Add react-native-view-shot dependency for screenshot support
@roitium roitium added the enhancement New feature or request label Jan 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 23, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

本 PR 将版本从 2.1.9 升级到 2.1.10,添加 react-native-view-shot 依赖,新增歌词/歌曲分享功能(模态框 + 分享卡片 + 截图/保存/分享流程),并对模态注册、菜单集成、队列模态与 B 站元数据处理做出若干防御性与渲染优化。

Changes

内聚组 / 文件(s) 变更摘要
包管理与版本
\package.json``
版本从 2.1.9 -> 2.1.10(versionCode 102 -> 103);新增依赖 react-native-view-shot@^4.0.3.
模态类型与注册
\src/types/navigation.ts`, `src/components/ModalRegistry.tsx``
新增 ModalKeys LyricsSelectionSongShare,延迟加载并注册 LyricsSelectionModalSongShareModal.
歌词分享模态与卡片
\src/components/modals/player/LyricsSelectionModal.tsx`, `src/features/player/components/sharing/LyricsShareCard.tsx``
新增歌词选择模态(多选最多5行、预览/保存/分享、使用 view-shot 捕获)与可捕获的歌词分享卡片组件(专辑封面、歌词、二维码、主题色提取)。
歌曲分享模态与卡片
\src/components/modals/player/SongShareModal.tsx`, `src/features/player/components/sharing/SongShareCard.tsx``
新增歌曲分享模态(延迟生成预览、保存/分享、权限与重试处理)及歌曲分享卡片(封面、信息、二维码、主题色提取、ViewShot)。
菜单集成
\src/features/player/components/PlayerFunctionalMenu.tsx``
在播放器功能菜单新增“分享歌词”和“分享歌曲”两项,点击分别打开对应模态。
队列模态优化
\src/components/modals/PlayerQueueModal.tsx``
Backdrop 仅在可见时渲染;将 animationConfigs 合并到主 BottomSheet props(去重/集中配置)。
播放器页面微调
\src/app/player.tsx``
对某处 useEffect 添加 // @ts-expect-error`` 注释并显式列出依赖数组。
Toast 配置微调
\src/components/toast/ToastConfig.tsx``
移除显式 height: 'auto'(保留 minHeight: 60);收紧 fontWeight 的类型断言。
B 站元数据防御
\src/lib/facades/bilibili.ts`, `src/lib/facades/sync.ts``
为 favorite 类型的 playlist metadata 添加 null/空检查,遇空返回明确错误以避免异常。

Sequence Diagram(s)

sequenceDiagram
    participant User as 用户
    participant Menu as 功能菜单
    participant Modal as LyricsSelectionModal / SongShareModal
    participant Card as LyricsShareCard / SongShareCard
    participant ViewShot as react-native-view-shot
    participant OS as 系统分享/媒体库 (Expo)
    participant Toast as 通知

    User->>Menu: 点击“分享歌词”/“分享歌曲”
    Menu->>Modal: 打开对应模态
    Modal->>Modal: 获取歌词/Track 数据
    User->>Modal: 选择/确认内容(或等待自动生成)
    Modal->>Card: 渲染隐藏的分享卡片(颜色提取、布局)
    Modal->>ViewShot: 捕获卡片并返回图片 URI
    ViewShot-->>Modal: 返回预览 URI
    User->>Modal: 选择“分享”或“保存”
    alt 分享
        Modal->>OS: 调用分享 API
        OS-->>Toast: 返回结果
    else 保存
        Modal->>OS: 检查/请求媒体库权限
        OS-->>Modal: 权限结果
        Modal->>OS: 保存图片到相册
        OS-->>Toast: 返回结果
    end
    Toast-->>User: 显示成功/失败提示
    Modal->>Modal: 关闭模态
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: add lyrics and song sharing functionality #149: 大量相同功能位置变更(歌词/歌曲分享模态、分享卡片、view-shot 引入),与本 PR 在文件/逻辑上高度重叠。
  • feat: add some sentry spans #83: 在相同 facade 路径上对 bilibili 相关操作做改动(Sentry spans 与错误处理),与本 PR 的元数据防御检查存在代码级关联。
  • feat: lyrics style #70: 修改过 PlayerFunctionalMenu 的签名/内部结构,可能与本 PR 在添加菜单项处产生冲突或需要同步调整。

Poem

🐰 我是爱写代码的兔子,
歌词与封面在夜里绽放,
截图一按,卡片成像,
分享与保存,轻轻敲响,
版本微升,欢跃成行。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确地概括了拉取请求的主要变更——添加歌词和歌曲分享功能,与摘要中的所有核心文件更改相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

roitium and others added 9 commits January 23, 2026 11:13
- Add null check for metadata in fetchRemotePlaylistMetadata
- Add null check for bilibiliFavoriteListMetadata.info in sync
- Replace optional chaining with non-null assertions where appropriate
- Clean up toast config style and add const assertion
- Add ts-expect-error comment for legacy streamer option
@roitium roitium marked this pull request as ready for review January 23, 2026 04:56
@safedep
Copy link
Copy Markdown

safedep Bot commented Jan 23, 2026

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

Package Details
Package Malware Vulnerability Risky License Report
icon base64-arraybuffer @ 1.0.2
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon css-line-break @ 2.1.0
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon html2canvas @ 1.4.1
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon react-native-view-shot @ 4.0.3
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon text-segmentation @ 1.0.3
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗
icon utrie @ 1.0.2
pnpm-lock.yaml
ok icon
ok icon
ok icon
🔗

This report is generated by SafeDep Github App

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/player.tsx (1)

171-179: useEffect 缺少依赖数组,导致每次渲染都会执行

此 useEffect 没有依赖数组,会在每次渲染时执行。由于 playerBackgroundStylesetSettings 不会频繁变化,建议添加依赖数组以避免不必要的执行和潜在的 toast 重复显示。

🔧 建议的修复方案
 useEffect(() => {
   // `@ts-expect-error` -- 虽然我们项目内已经移除了 streamer 选项,但部分存量用户可能还在这个选项,需要帮他回退
   if (playerBackgroundStyle === 'streamer') {
     toast.show(
       '因为会对性能造成较大影响,并且也不好看,所以我们移除了流光效果,已为您回退到渐变模式',
     )
     setSettings({ playerBackgroundStyle: 'gradient' })
   }
-})
+}, [playerBackgroundStyle, setSettings])
🤖 Fix all issues with AI agents
In `@src/features/player/components/sharing/SongShareCard.tsx`:
- Around line 40-48: The fileName passed to ViewShot uses track.uniqueKey (in
SongShareCard) which contains characters like ":" that are invalid in filenames;
compute a sanitizedUniqueKey (e.g., by replacing non-alphanumeric characters
with "_" or using a safe encoding like encodeURIComponent/base64) and use that
in the fileName option instead of track.uniqueKey; update the ViewShot props
(options.fileName) and the place where viewShotRef/fileName is constructed so
all references use sanitizedUniqueKey to avoid file-save failures.
🧹 Nitpick comments (8)
src/components/modals/player/SongShareModal.tsx (2)

48-54: 缺少 useEffect 依赖项。

generatePreview 在 effect 中被调用但未包含在依赖数组中。虽然当前实现由于 [] 依赖只运行一次可能不会出问题,但这违反了 React hooks 的规则,可能在未来重构时导致 bug。

♻️ 建议修复
+	const generatePreviewRef = useRef(generatePreview)
+	generatePreviewRef.current = generatePreview
+
 	// 打开时自动生成预览
 	useEffect(() => {
 		const timer = setTimeout(() => {
-			void generatePreview()
+			void generatePreviewRef.current()
 		}, 300) // 延迟确保组件已渲染
 		return () => clearTimeout(timer)
 	}, [])

或者将 generatePreview 包装在 useCallback 中并添加到依赖数组。


120-157: 考虑在没有当前曲目时提供更明确的提示。

currentTracknull 时,模态框仍会渲染,但隐藏的捕获视图为空,导致预览生成失败并显示"预览加载失败"。建议在这种情况下提供更明确的用户提示。

♻️ 可选优化
+	if (!currentTrack) {
+		return (
+			<>
+				<Dialog.Title>分享歌曲</Dialog.Title>
+				<Dialog.Content style={styles.contentArea}>
+					<View style={styles.loadingContainer}>
+						<Text variant='bodyMedium' style={styles.loadingText}>
+							暂无播放中的歌曲
+						</Text>
+					</View>
+				</Dialog.Content>
+				<Dialog.Actions>
+					<Button onPress={() => close('SongShare')}>关闭</Button>
+				</Dialog.Actions>
+			</>
+		)
+	}
+
 	return (
 		<>
 			<Dialog.Title>分享歌曲</Dialog.Title>
src/features/player/components/sharing/SongShareCard.tsx (1)

22-22: 分享 URL 可能过长影响二维码可扫描性。

shareUrl 包含完整的 coverUrl,可能导致 URL 非常长,生成的二维码密度过高难以扫描。考虑移除 cover 参数或在服务端通过 id 查询封面。

src/features/player/components/sharing/LyricsShareCard.tsx (2)

28-47: SongShareCard 存在大量重复代码。

shareUrl 构造、背景色提取逻辑、底部 QR 码和品牌区域与 SongShareCard.tsx 几乎完全相同。建议抽取公共逻辑。

♻️ 建议重构方向
  1. 创建自定义 hook useShareCardTheme(coverUrl) 来处理背景色提取
  2. 抽取 ShareCardFooter 组件来复用 QR 码和品牌展示
  3. 创建 buildShareUrl(track) 工具函数
// hooks/useShareCardTheme.ts
export const useShareCardTheme = (coverUrl?: string | null) => {
  const theme = useTheme()
  const [backgroundColor, setBackgroundColor] = useState(
    theme.colors.elevation.level3,
  )
  // ... color extraction logic
  return backgroundColor
}

40-43: 清理开发注释。

这些注释看起来是开发过程中的备注,建议在合并前清理或转换为 TODO/FIXME 格式以便追踪。

src/components/modals/player/LyricsSelectionModal.tsx (3)

215-234: renderItem 依赖 selectedIndices 导致不必要的重渲染。

每次选择变化时 selectedIndices Set 引用改变,导致 renderItem 重新创建。虽然 LyricItem 使用了 memo,但由于 isSelected 对每个 item 都重新计算,可能导致性能问题。

♻️ 建议优化

考虑使用 extraData 属性让 FlashList 知道何时需要重新渲染:

 			<FlashList
 				data={lyrics}
 				keyExtractor={keyExtractor}
 				renderItem={renderItem}
+				extraData={selectedIndices}
+				estimatedItemSize={60}
 			/>

或者将 isSelected 计算移到 LyricItem 内部,通过 context 传递 selectedIndices


146-171: generatePreview 存在重复的图片捕获逻辑。

handleShare 中的图片捕获逻辑(Lines 156-170)与 generatePreview(Lines 128-135)重复。这种模式在 SongShareModal 中也存在。建议抽取为共享工具函数。

♻️ 建议抽取公共逻辑
// utils/viewShot.ts
export const captureShareCard = async (
  viewShotRef: React.RefObject<ViewShot>,
  fileNamePrefix: string
): Promise<string> => {
  const fileName = `${fileNamePrefix}-${Date.now()}`
  return captureRef(viewShotRef, {
    format: 'png',
    quality: 1,
    result: 'tmpfile',
    fileName,
  })
}

然后在两个模态框中复用此函数。


99-115: 选择变化时清除 previewUri 但未重置 showPreview

当用户在预览模式下通过某种方式触发选择变化时,previewUri 被清除但 showPreview 仍为 true,可能导致短暂的 UI 不一致状态。

♻️ 建议修复
 	const toggleSelection = useCallback((index: number) => {
 		setSelectedIndices((prev) => {
 			// ... existing logic
 		})
 		// 选择变化后清除旧预览
 		setPreviewUri(null)
+		setShowPreview(false)
 	}, [])

Comment thread src/features/player/components/sharing/SongShareCard.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant