add embed code.

# Navidrome 分享播放器嵌入代码使用示例

## 功能说明

在分享详情页(如 `http://127.0.0.1:4533/app/#/share/895AGkthN4`),现在会显示四种嵌入代码选项:

### 1. 左下角悬浮播放器 (推荐)

这是最适合博客和网页的嵌入方式,提供:
- 🎵 可折叠的悬浮按钮
- 📱 响应式设计,支持移动端
- 🎨 美观的渐变样式
- 👆 点击展开/收起播放器
- 🔒 点击外部区域自动收起

**效果预览:**
- 收起状态:左下角显示一个圆形音乐图标按钮
- 展开状态:显示完整的播放器界面(380x520px)

### 2. 基础 iframe

最简单的嵌入方式,适合:
- 固定位置显示
- 快速集成
- 无需额外样式

### 3. 响应式 iframe

自适应布局嵌入,适合:
- 需要响应式设计的页面
- 博客文章内容区域
- 16:9 宽高比显示

### 4. 右下角悬浮播放器

与左下角版本功能相同,但显示在右下角,可根据网页布局选择。

---

## 使用方法

### 步骤 1:创建分享

1. 在 Navidrome 中选择要分享的歌曲、专辑或播放列表
2. 点击"分享"按钮创建分享链接
3. 进入分享详情页

### 步骤 2:获取嵌入代码

1. 在分享详情页向下滚动到"嵌入代码 (Embed Code)"区域
2. 选择需要的嵌入类型(推荐"左下角悬浮播放器")
3. 点击复制按钮复制代码

### 步骤 3:嵌入到网页

将复制的代码粘贴到您的网页 HTML 中,通常在 `</body>` 标签之前。

---

## 代码示例

### 示例 1:博客文章中添加悬浮播放器

```html
<!DOCTYPE html>
<html>
<head>
    <title>我的博客文章</title>
</head>
<body>
    <article>
        <h1>我的音乐分享</h1>
        <p>这是我最喜欢的音乐收藏...</p>
    </article>

    <!-- Navidrome 悬浮播放器 -->
    <!-- 将从 Navidrome 复制的完整代码粘贴在这里 -->
    <div id="navidrome-floating-player">
        ...
    </div>
</body>
</html>
```

### 示例 2:WordPress 博客

在 WordPress 中使用自定义 HTML 块:

1. 添加"自定义 HTML"块
2. 粘贴嵌入代码
3. 发布文章

### 示例 3:个人网站多个页面共享

将嵌入代码添加到网站模板的 footer 中,所有页面都会显示悬浮播放器。

---

## 自定义样式

如果需要自定义悬浮播放器的位置或样式,可以修改嵌入代码中的 CSS:

### 修改位置

```css
/* 修改为右下角 */
#navidrome-floating-player {
  right: 20px;  /* 改为 right */
  bottom: 20px;
}

/* 修改为右上角 */
#navidrome-floating-player {
  right: 20px;
  top: 20px;  /* 改为 top */
}
```

### 修改大小

```css
/* 展开时的大小 */
#nav-player-container.nav-expanded {
  width: 450px;    /* 修改宽度 */
  height: 600px;   /* 修改高度 */
}

/* 按钮大小 */
#nav-player-toggle {
  width: 70px;     /* 修改按钮宽度 */
  height: 70px;    /* 修改按钮高度 */
}
```

### 修改颜色主题

```css
/* 按钮渐变色 */
#nav-player-toggle {
  background: linear-gradient(135deg, #FF6B6B 0%, #4ECDC4 100%);
}
```

---

## 注意事项

1. **跨域问题**:确保 Navidrome 服务器配置允许 iframe 嵌入
2. **HTTPS**:如果您的网站使用 HTTPS,Navidrome 也需要配置 HTTPS
3. **分享过期**:嵌入的播放器依赖分享链接,注意设置合适的过期时间
4. **移动端优化**:悬浮播放器已包含移动端适配,但建议在移动设备上测试效果

---

## 浏览器兼容性

悬浮播放器支持所有现代浏览器:
-  Chrome/Edge (88+)
-  Firefox (85+)
-  Safari (14+)
-  iOS Safari (14+)
-  Android Chrome (88+)

---

## 功能特性

### 悬浮播放器特性

-  平滑展开/收起动画
- 🎯 自动定位到角落
- 🖱️ 悬停放大效果
- 📱 移动端自适应
- 🎨 渐变色设计
- 🔊 完整的 APlayer 功能支持

### APlayer 功能

- 🎵 播放/暂停控制
- ⏭️ 上一首/下一首
- 🔀 随机播放
- 🔁 循环模式
- 🎚️ 音量控制
- 📋 播放列表
- 📥 下载功能(如果启用)

---

## 技术支持

如果遇到问题,可以:
1. 检查浏览器控制台是否有错误
2. 确认 Navidrome 服务器正常运行
3. 验证分享链接是否有效
4. 提交 Issue 到 Navidrome GitHub 仓库
This commit is contained in:
Sora 2025-12-18 14:11:32 +08:00
parent 98983995a3
commit c773c279ca
2 changed files with 309 additions and 1 deletions

View File

@ -0,0 +1,300 @@
import React, { useState } from 'react'
import {
Box,
Typography,
TextField,
IconButton,
Tabs,
Tab,
makeStyles,
Snackbar,
} from '@material-ui/core'
import FileCopyIcon from '@material-ui/icons/FileCopy'
const useStyles = makeStyles((theme) => ({
root: {
marginBottom: theme.spacing(3),
},
tabPanel: {
marginTop: theme.spacing(2),
},
codeField: {
fontFamily: 'monospace',
fontSize: '12px',
'& .MuiInputBase-root': {
fontFamily: 'monospace',
fontSize: '12px',
},
},
copyButton: {
marginLeft: theme.spacing(1),
},
header: {
display: 'flex',
alignItems: 'center',
marginBottom: theme.spacing(1),
},
}))
const TabPanel = ({ children, value, index, ...other }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`embed-tabpanel-${index}`}
aria-labelledby={`embed-tab-${index}`}
{...other}
>
{value === index && <Box py={2}>{children}</Box>}
</div>
)
}
export const EmbedCodeField = ({ url, title = 'Music Player' }) => {
const classes = useStyles()
const [tabValue, setTabValue] = useState(0)
const [snackbarOpen, setSnackbarOpen] = useState(false)
const handleTabChange = (event, newValue) => {
setTabValue(newValue)
}
const handleCopy = (text) => {
navigator.clipboard.writeText(text).then(() => {
setSnackbarOpen(true)
})
}
const handleSnackbarClose = () => {
setSnackbarOpen(false)
}
// iframe
const iframeEmbed = `<iframe src="${url}" width="100%" height="450" frameborder="0" allowfullscreen></iframe>`
// iframe
const responsiveEmbed = `<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe src="${url}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" frameborder="0" allowfullscreen></iframe>
</div>`
//
const floatingPlayerEmbed = `<!-- Navidrome 悬浮播放器 -->
<div id="navidrome-floating-player">
<div id="nav-player-container" class="nav-collapsed">
<div id="nav-player-toggle" onclick="toggleNavPlayer()">
<span id="nav-toggle-icon">🎵</span>
</div>
<div id="nav-player-content">
<iframe src="${url}" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>
<style>
#navidrome-floating-player {
position: fixed;
left: 20px;
bottom: 20px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#nav-player-container {
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
overflow: hidden;
transition: all 0.3s ease;
}
#nav-player-container.nav-collapsed {
width: 60px;
height: 60px;
}
#nav-player-container.nav-expanded {
width: 380px;
height: 520px;
}
#nav-player-toggle {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 12px;
transition: all 0.3s ease;
}
#nav-player-toggle:hover {
transform: scale(1.05);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}
#nav-toggle-icon {
font-size: 28px;
transition: transform 0.3s ease;
}
#nav-player-container.nav-expanded #nav-toggle-icon {
transform: rotate(90deg);
}
#nav-player-content {
display: none;
width: 380px;
height: 460px;
}
#nav-player-container.nav-expanded #nav-player-content {
display: block;
}
#nav-player-content iframe {
width: 100%;
height: 100%;
border: none;
}
/* 移动端适配 */
@media (max-width: 768px) {
#navidrome-floating-player {
left: 10px;
bottom: 10px;
}
#nav-player-container.nav-expanded {
width: calc(100vw - 20px);
height: 480px;
max-width: 380px;
}
#nav-player-content {
width: 100%;
}
}
</style>
<script>
function toggleNavPlayer() {
const container = document.getElementById('nav-player-container');
if (container.classList.contains('nav-collapsed')) {
container.classList.remove('nav-collapsed');
container.classList.add('nav-expanded');
} else {
container.classList.remove('nav-expanded');
container.classList.add('nav-collapsed');
}
}
//
document.addEventListener('click', function(event) {
const player = document.getElementById('navidrome-floating-player');
const container = document.getElementById('nav-player-container');
if (player && !player.contains(event.target) &&
container.classList.contains('nav-expanded')) {
toggleNavPlayer();
}
});
</script>`
//
const floatingPlayerRightEmbed = floatingPlayerEmbed
.replace('left: 20px;', 'right: 20px;')
.replace('left: 10px;', 'right: 10px;')
const embedOptions = [
{
label: '左下角悬浮播放器',
code: floatingPlayerEmbed,
description: '可折叠的左下角悬浮播放器,用户可点击展开/收起',
},
{
label: '基础 iframe',
code: iframeEmbed,
description: '简单的 iframe 嵌入,适合固定位置显示',
},
{
label: '响应式 iframe',
code: responsiveEmbed,
description: '16:9 响应式布局,自适应不同屏幕宽度',
},
{
label: '右下角悬浮播放器',
code: floatingPlayerRightEmbed,
description: '可折叠的右下角悬浮播放器(备选位置)',
},
]
return (
<Box className={classes.root}>
<Typography variant="body2" color="textSecondary" gutterBottom>
嵌入代码 (Embed Code)
</Typography>
<Tabs
value={tabValue}
onChange={handleTabChange}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
scrollButtons="auto"
>
{embedOptions.map((option, index) => (
<Tab key={index} label={option.label} />
))}
</Tabs>
{embedOptions.map((option, index) => (
<TabPanel key={index} value={tabValue} index={index}>
<Typography
variant="caption"
color="textSecondary"
display="block"
gutterBottom
>
{option.description}
</Typography>
<Box display="flex" alignItems="flex-start">
<TextField
fullWidth
multiline
rows={option.code.split('\n').length > 20 ? 20 : 12}
variant="outlined"
value={option.code}
className={classes.codeField}
InputProps={{
readOnly: true,
}}
/>
<IconButton
className={classes.copyButton}
onClick={() => handleCopy(option.code)}
color="primary"
size="small"
title="复制代码"
>
<FileCopyIcon />
</IconButton>
</Box>
<Typography variant="caption" color="textSecondary" display="block">
提示将此代码复制并粘贴到您的网页 HTML 中即可使用
</Typography>
</TabPanel>
))}
<Snackbar
open={snackbarOpen}
autoHideDuration={2000}
onClose={handleSnackbarClose}
message="代码已复制到剪贴板"
/>
</Box>
)
}

View File

@ -7,9 +7,10 @@ import {
TextInput,
} from 'react-admin'
import { sharePlayerUrl, shareAPlayerUrl } from '../utils'
import { Link, Box, Typography } from '@material-ui/core'
import { Link, Box, Typography, Divider } from '@material-ui/core'
import { DateField } from '../common'
import config from '../config'
import { EmbedCodeField } from './EmbedCodeField'
export const ShareEdit = (props) => {
const { id, basePath, hasCreate, ...rest } = props
@ -34,6 +35,13 @@ export const ShareEdit = (props) => {
{aplayerUrl}
</Link>
</Box>
<Box mb={3}>
<Divider />
</Box>
<EmbedCodeField url={aplayerUrl} title="Navidrome Music Player" />
<Box mb={3}>
<Divider />
</Box>
<TextInput source="description" />
{config.enableDownloads && <BooleanInput source="downloadable" />}
<DateTimeInput source="expiresAt" />