mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: implement global playlist functionality with UI updates
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
8861eebe21
commit
c7b93805ce
@ -200,6 +200,7 @@
|
||||
"duration": "Duração",
|
||||
"ownerName": "Dono",
|
||||
"public": "Pública",
|
||||
"global": "Global",
|
||||
"updatedAt": "Últ. Atualização",
|
||||
"createdAt": "Data de Criação",
|
||||
"songCount": "Músicas",
|
||||
@ -222,7 +223,8 @@
|
||||
"duplicate_song": "Adicionar músicas duplicadas",
|
||||
"song_exist": "Algumas destas músicas já existem na playlist. Você quer adicionar as duplicadas ou ignorá-las?",
|
||||
"noPlaylistsFound": "Nenhuma playlist encontrada",
|
||||
"noPlaylists": "Nenhuma playlist disponível"
|
||||
"noPlaylists": "Nenhuma playlist disponível",
|
||||
"globalPlaylistPublicDisabled": "Playlists globais são sempre públicas"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
|
||||
@ -11,5 +11,7 @@ export const isReadOnly = (ownerId) => {
|
||||
|
||||
export const isSmartPlaylist = (pls) => !!pls.rules
|
||||
|
||||
export const isGlobalPlaylist = (pls) => isSmartPlaylist(pls) && pls.global
|
||||
|
||||
export const canChangeTracks = (pls) =>
|
||||
isWritable(pls.ownerId) && !isSmartPlaylist(pls)
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
isWritable,
|
||||
isReadOnly,
|
||||
isSmartPlaylist,
|
||||
isGlobalPlaylist,
|
||||
canChangeTracks,
|
||||
} from './playlistUtils'
|
||||
|
||||
@ -56,6 +57,28 @@ describe('playlistUtils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGlobalPlaylist', () => {
|
||||
it('returns true if playlist is smart and global', () => {
|
||||
const playlist = { rules: [], global: true }
|
||||
expect(isGlobalPlaylist(playlist)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if playlist is smart but not global', () => {
|
||||
const playlist = { rules: [], global: false }
|
||||
expect(isGlobalPlaylist(playlist)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false if playlist is not smart even if global is true', () => {
|
||||
const playlist = { global: true }
|
||||
expect(isGlobalPlaylist(playlist)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false if playlist is not smart and not global', () => {
|
||||
const playlist = {}
|
||||
expect(isGlobalPlaylist(playlist)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('canChangeTracks', () => {
|
||||
it('returns true if user is the owner and playlist is not smart', () => {
|
||||
localStorage.setItem('userId', 'user1')
|
||||
|
||||
@ -200,6 +200,7 @@
|
||||
"duration": "Duration",
|
||||
"ownerName": "Owner",
|
||||
"public": "Public",
|
||||
"global": "Global",
|
||||
"updatedAt": "Updated at",
|
||||
"createdAt": "Created at",
|
||||
"songCount": "Songs",
|
||||
@ -222,7 +223,8 @@
|
||||
"duplicate_song": "Add duplicated songs",
|
||||
"song_exist": "There are duplicates being added to the playlist. Would you like to add the duplicates or skip them?",
|
||||
"noPlaylistsFound": "No playlists found",
|
||||
"noPlaylists": "No playlists available"
|
||||
"noPlaylists": "No playlists available",
|
||||
"globalPlaylistPublicDisabled": "Global playlists are always public"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
|
||||
@ -2,15 +2,22 @@ import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Chip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@material-ui/core'
|
||||
import PublicIcon from '@material-ui/icons/Public'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useTranslate } from 'react-admin'
|
||||
import { useCallback, useState, useEffect } from 'react'
|
||||
import Lightbox from 'react-image-lightbox'
|
||||
import 'react-image-lightbox/style.css'
|
||||
import { CollapsibleComment, DurationField, SizeField } from '../common'
|
||||
import {
|
||||
CollapsibleComment,
|
||||
DurationField,
|
||||
SizeField,
|
||||
isGlobalPlaylist,
|
||||
} from '../common'
|
||||
import subsonic from '../subsonic'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
@ -77,6 +84,10 @@ const useStyles = makeStyles(
|
||||
marginTop: '1em',
|
||||
marginBottom: '0.5em',
|
||||
},
|
||||
globalChip: {
|
||||
marginLeft: '0.5em',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'NDPlaylistDetails',
|
||||
@ -146,6 +157,14 @@ const PlaylistDetails = (props) => {
|
||||
className={classes.title}
|
||||
>
|
||||
{record.name || translate('ra.page.loading')}
|
||||
{isGlobalPlaylist(record) && (
|
||||
<Chip
|
||||
icon={<PublicIcon />}
|
||||
label={translate('resources.playlist.fields.global')}
|
||||
size="small"
|
||||
className={classes.globalChip}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography component="p" className={classes.stats}>
|
||||
{record.songCount ? (
|
||||
|
||||
@ -11,7 +11,16 @@ import {
|
||||
ReferenceInput,
|
||||
SelectInput,
|
||||
} from 'react-admin'
|
||||
import { isWritable, Title } from '../common'
|
||||
import { useForm } from 'react-final-form'
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { isWritable, isSmartPlaylist, Title } from '../common'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
tooltipWrapper: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
})
|
||||
|
||||
const SyncFragment = ({ formData, variant, ...rest }) => {
|
||||
return (
|
||||
@ -28,9 +37,48 @@ const PlaylistTitle = ({ record }) => {
|
||||
return <Title subTitle={`${resourceName} "${record ? record.name : ''}"`} />
|
||||
}
|
||||
|
||||
const PublicInput = ({ record, formData }) => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
const isGlobal = isSmartPlaylist(record) && formData?.global
|
||||
const disabled = !isWritable(record.ownerId) || isGlobal
|
||||
|
||||
const input = <BooleanInput source="public" disabled={disabled} />
|
||||
|
||||
if (isGlobal) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={translate(
|
||||
'resources.playlist.message.globalPlaylistPublicDisabled',
|
||||
)}
|
||||
>
|
||||
<div className={classes.tooltipWrapper}>{input}</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
const GlobalInput = ({ record }) => {
|
||||
const form = useForm()
|
||||
const handleChange = (value) => {
|
||||
if (value) {
|
||||
form.change('public', true)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<BooleanInput
|
||||
source="global"
|
||||
disabled={!isWritable(record.ownerId)}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PlaylistEditForm = (props) => {
|
||||
const { record } = props
|
||||
const { permissions } = usePermissions()
|
||||
const isSmart = isSmartPlaylist(record)
|
||||
return (
|
||||
<SimpleForm redirect="list" variant={'outlined'} {...props}>
|
||||
<TextInput source="name" validate={required()} />
|
||||
@ -50,7 +98,10 @@ const PlaylistEditForm = (props) => {
|
||||
) : (
|
||||
<TextField source="ownerName" />
|
||||
)}
|
||||
<BooleanInput source="public" disabled={!isWritable(record.ownerId)} />
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => <PublicInput record={record} formData={formData} />}
|
||||
</FormDataConsumer>
|
||||
{isSmart && <GlobalInput record={record} />}
|
||||
<FormDataConsumer>
|
||||
{(formDataProps) => <SyncFragment {...formDataProps} />}
|
||||
</FormDataConsumer>
|
||||
|
||||
@ -14,8 +14,10 @@ import {
|
||||
useRecordContext,
|
||||
BulkDeleteButton,
|
||||
usePermissions,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import {
|
||||
@ -23,6 +25,7 @@ import {
|
||||
List,
|
||||
Writable,
|
||||
isWritable,
|
||||
isGlobalPlaylist,
|
||||
useSelectedFields,
|
||||
useResourceRefresh,
|
||||
} from '../common'
|
||||
@ -59,6 +62,7 @@ const PlaylistFilter = (props) => {
|
||||
const TogglePublicInput = ({ resource, source }) => {
|
||||
const record = useRecordContext()
|
||||
const notify = useNotify()
|
||||
const translate = useTranslate()
|
||||
const [togglePublic] = useUpdate(
|
||||
resource,
|
||||
record.id,
|
||||
@ -79,13 +83,29 @@ const TogglePublicInput = ({ resource, source }) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
return (
|
||||
const isGlobal = isGlobalPlaylist(record)
|
||||
const disabled = !isWritable(record.ownerId) || isGlobal
|
||||
|
||||
const switchElement = (
|
||||
<Switch
|
||||
checked={record[source]}
|
||||
onClick={handleClick}
|
||||
disabled={!isWritable(record.ownerId)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isGlobal) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={translate(
|
||||
'resources.playlist.message.globalPlaylistPublicDisabled',
|
||||
)}
|
||||
>
|
||||
<span>{switchElement}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return switchElement
|
||||
}
|
||||
|
||||
const ToggleAutoImport = ({ resource, source }) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user