mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
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:
parent
98983995a3
commit
c773c279ca
300
ui/src/share/EmbedCodeField.jsx
Normal file
300
ui/src/share/EmbedCodeField.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user