mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-18 11:48:07 -05:00
Compare commits
4 Commits
global-nsp
...
remove_def
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ccc18ba02 | ||
|
|
c5447a637a | ||
|
|
b9247ba34e | ||
|
|
510acde3db |
@@ -23,7 +23,5 @@ RUN DOWNLOAD_ARCH="linux-${TARGETARCH}" \
|
||||
&& rmdir /usr/include/taglib \
|
||||
&& rm /tmp/cross-taglib.tar.gz /usr/provenance.json
|
||||
|
||||
ENV CGO_CFLAGS_ALLOW="--define-prefix"
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
|
||||
1
.github/workflows/pipeline.yml
vendored
1
.github/workflows/pipeline.yml
vendored
@@ -15,7 +15,6 @@ concurrency:
|
||||
|
||||
env:
|
||||
CROSS_TAGLIB_VERSION: "2.1.1-1"
|
||||
CGO_CFLAGS_ALLOW: "--define-prefix"
|
||||
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }}
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -94,7 +94,6 @@ RUN --mount=type=bind,source=. \
|
||||
# Setup CGO cross-compilation environment
|
||||
xx-go --wrap
|
||||
export CGO_ENABLED=1
|
||||
export CGO_CFLAGS_ALLOW="--define-prefix"
|
||||
export PKG_CONFIG_PATH=/taglib/lib/pkgconfig
|
||||
cat $(go env GOENV)
|
||||
|
||||
|
||||
1
Makefile
1
Makefile
@@ -2,7 +2,6 @@ GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ')
|
||||
NODE_VERSION=$(shell cat .nvmrc)
|
||||
|
||||
# Set global environment variables, required for most targets
|
||||
export CGO_CFLAGS_ALLOW=--define-prefix
|
||||
export ND_ENABLEINSIGHTSCOLLECTOR=false
|
||||
|
||||
ifneq ("$(wildcard .git/HEAD)","")
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package taglib
|
||||
|
||||
/*
|
||||
#cgo !windows pkg-config: --define-prefix taglib
|
||||
#cgo windows pkg-config: taglib
|
||||
#cgo pkg-config: taglib
|
||||
#cgo illumos LDFLAGS: -lstdc++ -lsendfile
|
||||
#cgo linux darwin CXXFLAGS: -std=c++11
|
||||
#cgo darwin LDFLAGS: -L/opt/homebrew/opt/taglib/lib
|
||||
|
||||
@@ -168,9 +168,10 @@ func (s *playlists) parseNSP(_ context.Context, pls *model.Playlist, reader io.R
|
||||
if nsp.Comment != "" {
|
||||
pls.Comment = nsp.Comment
|
||||
}
|
||||
pls.Global = nsp.Global
|
||||
if nsp.Global {
|
||||
pls.Public = true
|
||||
if nsp.Public != nil {
|
||||
pls.Public = *nsp.Public
|
||||
} else {
|
||||
pls.Public = conf.Server.DefaultPlaylistPublicVisibility
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -408,16 +409,13 @@ func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist)
|
||||
newPls.Name = pls.Name
|
||||
newPls.Comment = pls.Comment
|
||||
newPls.OwnerID = pls.OwnerID
|
||||
// Preserve Public from existing playlist, unless the new playlist is Global
|
||||
if !newPls.Global {
|
||||
newPls.Public = pls.Public
|
||||
}
|
||||
newPls.Public = pls.Public
|
||||
newPls.EvaluatedAt = &time.Time{}
|
||||
} else {
|
||||
log.Info(ctx, "Adding synced playlist", "playlist", newPls.Name, "path", newPls.Path, "owner", owner.UserName)
|
||||
newPls.OwnerID = owner.ID
|
||||
// Only apply default visibility if not a global playlist (which is always public)
|
||||
if !newPls.Global {
|
||||
// For NSP files, Public may already be set from the file; for M3U, use server default
|
||||
if !newPls.IsSmartPlaylist() {
|
||||
newPls.Public = conf.Server.DefaultPlaylistPublicVisibility
|
||||
}
|
||||
}
|
||||
@@ -483,7 +481,7 @@ type nspFile struct {
|
||||
criteria.Criteria
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
Global bool `json:"global"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
|
||||
func (i *nspFile) UnmarshalJSON(data []byte) error {
|
||||
@@ -494,6 +492,8 @@ func (i *nspFile) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
i.Name, _ = m["name"].(string)
|
||||
i.Comment, _ = m["comment"].(string)
|
||||
i.Global, _ = m["global"].(bool)
|
||||
if public, ok := m["public"].(bool); ok {
|
||||
i.Public = &public
|
||||
}
|
||||
return json.Unmarshal(data, &i.Criteria)
|
||||
}
|
||||
|
||||
@@ -107,20 +107,32 @@ var _ = Describe("Playlists", func() {
|
||||
Expect(pls.Rules.Order).To(Equal("desc"))
|
||||
Expect(pls.Rules.Limit).To(Equal(100))
|
||||
Expect(pls.Rules.Expression).To(BeAssignableToTypeOf(criteria.All{}))
|
||||
Expect(pls.Global).To(BeFalse())
|
||||
Expect(pls.Public).To(BeFalse())
|
||||
})
|
||||
It("returns an error if the playlist is not well-formed", func() {
|
||||
_, err := ps.ImportFile(ctx, folder, "invalid_json.nsp")
|
||||
Expect(err.Error()).To(ContainSubstring("line 19, column 1: invalid character '\\n'"))
|
||||
})
|
||||
It("parses global attribute and sets playlist to public", func() {
|
||||
pls, err := ps.ImportFile(ctx, folder, "global_smart_playlist.nsp")
|
||||
It("parses NSP with public: true and creates public playlist", func() {
|
||||
pls, err := ps.ImportFile(ctx, folder, "public_playlist.nsp")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Name).To(Equal("Global Smart Playlist"))
|
||||
Expect(pls.Global).To(BeTrue())
|
||||
Expect(pls.Name).To(Equal("Public Playlist"))
|
||||
Expect(pls.Public).To(BeTrue())
|
||||
})
|
||||
It("parses NSP with public: false and creates private playlist", func() {
|
||||
pls, err := ps.ImportFile(ctx, folder, "private_playlist.nsp")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Name).To(Equal("Private Playlist"))
|
||||
Expect(pls.Public).To(BeFalse())
|
||||
})
|
||||
It("uses server default when public field is absent", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.DefaultPlaylistPublicVisibility = true
|
||||
|
||||
pls, err := ps.ImportFile(ctx, folder, "recently_played.nsp")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Name).To(Equal("Recently Played"))
|
||||
Expect(pls.Public).To(BeTrue()) // Should be true since server default is true
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Cross-library relative paths", func() {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE playlist ADD COLUMN global BOOL DEFAULT FALSE NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE playlist DROP COLUMN global;
|
||||
-- +goose StatementEnd
|
||||
@@ -27,7 +27,6 @@ type Playlist struct {
|
||||
// SmartPlaylist attributes
|
||||
Rules *criteria.Criteria `structs:"rules" json:"rules"`
|
||||
EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"`
|
||||
Global bool `structs:"global" json:"global"`
|
||||
}
|
||||
|
||||
func (pls Playlist) IsSmartPlaylist() bool {
|
||||
|
||||
@@ -227,9 +227,9 @@ func (r *playlistRepository) refreshSmartPlaylist(pls *model.Playlist) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only refresh for owners, unless the playlist is marked as global
|
||||
// Never refresh other users' playlists
|
||||
usr := loggedUser(r.ctx)
|
||||
if pls.OwnerID != usr.ID && !pls.Global {
|
||||
if pls.OwnerID != usr.ID {
|
||||
log.Trace(r.ctx, "Not refreshing smart playlist from other user", "playlist", pls.Name, "id", pls.ID)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/criteria"
|
||||
@@ -148,67 +147,6 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Context("Global smart playlists", func() {
|
||||
var globalPls model.Playlist
|
||||
var otherUserRepo model.PlaylistRepository
|
||||
|
||||
BeforeEach(func() {
|
||||
// Force smart playlist refresh
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.SmartPlaylistRefreshDelay = -1 * time.Second
|
||||
|
||||
// Create a global smart playlist owned by the admin user
|
||||
globalPls = model.Playlist{Name: "Global Smart", OwnerID: "userid", Rules: rules, Global: true, Public: true}
|
||||
Expect(repo.Put(&globalPls)).To(Succeed())
|
||||
|
||||
// Create a different user context (using regularUser who has library access)
|
||||
otherCtx := log.NewContext(GinkgoT().Context())
|
||||
otherCtx = request.WithUser(otherCtx, regularUser)
|
||||
otherUserRepo = NewPlaylistRepository(otherCtx, GetDBXBuilder())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
_ = repo.Delete(globalPls.ID)
|
||||
})
|
||||
|
||||
It("stores and retrieves the Global attribute", func() {
|
||||
savedPls, err := repo.Get(globalPls.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(savedPls.Global).To(BeTrue())
|
||||
})
|
||||
|
||||
It("allows non-owner to refresh a global smart playlist", func() {
|
||||
// Verify the playlist can be retrieved by non-owner and has Global=true
|
||||
plsCheck, err := otherUserRepo.Get(globalPls.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(plsCheck.Global).To(BeTrue(), "Global should be true when retrieved by non-owner")
|
||||
Expect(plsCheck.IsSmartPlaylist()).To(BeTrue(), "Should be smart playlist")
|
||||
Expect(plsCheck.EvaluatedAt).To(BeNil(), "Should not be evaluated yet")
|
||||
|
||||
// Non-owner requests the playlist with refresh
|
||||
_, err = otherUserRepo.GetWithTracks(globalPls.ID, true, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Re-fetch to verify EvaluatedAt was updated in DB
|
||||
pls, err := otherUserRepo.Get(globalPls.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.EvaluatedAt).ToNot(BeNil(), "Global smart playlist should be refreshed for non-owner")
|
||||
})
|
||||
|
||||
It("does not allow non-owner to refresh a non-global smart playlist", func() {
|
||||
// Create a non-global smart playlist
|
||||
nonGlobalPls := model.Playlist{Name: "Non-Global Smart", OwnerID: "userid", Rules: rules, Global: false, Public: true}
|
||||
Expect(repo.Put(&nonGlobalPls)).To(Succeed())
|
||||
DeferCleanup(func() { _ = repo.Delete(nonGlobalPls.ID) })
|
||||
|
||||
// Non-owner requests the playlist with refresh
|
||||
pls, err := otherUserRepo.GetWithTracks(nonGlobalPls.ID, true, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// EvaluatedAt should be nil because the playlist was not refreshed
|
||||
Expect(pls.EvaluatedAt).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("invalid rules", func() {
|
||||
It("fails to Put it in the DB", func() {
|
||||
rules = &criteria.Criteria{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Discord Rich Presence Plugin (Rust)
|
||||
|
||||
A Navidrome plugin that displays your currently playing track on Discord using Rich Presence. This is the Rust implementation demonstrating how to use the generated `nd-host` library.
|
||||
A Navidrome plugin that displays your currently playing track on Discord using Rich Presence. This is the Rust implementation demonstrating how to use the `nd-pdk` library.
|
||||
|
||||
## ⚠️ Warning
|
||||
|
||||
@@ -21,20 +21,20 @@ This plugin is for **demonstration purposes only**. It requires storing your Dis
|
||||
|
||||
## Capabilities
|
||||
|
||||
This plugin implements three capabilities to demonstrate the nd-host library:
|
||||
This plugin implements multiple capabilities to demonstrate the nd-pdk library:
|
||||
|
||||
- **Scrobbler**: Receives now-playing events from Navidrome
|
||||
- **SchedulerCallback**: Handles heartbeat and activity clearing timers
|
||||
- **WebSocketCallback**: Communicates with Discord gateway
|
||||
- **WebSocketCallback**: Communicates with Discord gateway (text, binary, error, and close handlers)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence):
|
||||
|
||||
| Key | Description | Example |
|
||||
|---------------|-------------------------------------------|--------------------------------|
|
||||
| `clientid` | Your Discord application ID | `123456789012345678` |
|
||||
| `user.<name>` | Discord token for the specified user | `user.alice` = `token123` |
|
||||
| Key | Description | Example |
|
||||
|---------------|--------------------------------------|---------------------------|
|
||||
| `clientid` | Your Discord application ID | `123456789012345678` |
|
||||
| `user.<name>` | Discord token for the specified user | `user.alice` = `token123` |
|
||||
|
||||
Each user is configured as a separate key with the `user.` prefix.
|
||||
|
||||
@@ -69,27 +69,30 @@ make discord-rich-presence-rs.ndp
|
||||
3. Enable and configure the plugin in the Navidrome UI (Settings → Plugins)
|
||||
4. Restart Navidrome if needed
|
||||
|
||||
## Using nd-host Library
|
||||
## Using nd-pdk Library
|
||||
|
||||
This plugin demonstrates how to use the generated Rust host function wrappers:
|
||||
This plugin demonstrates how to use the Rust plugin development kit:
|
||||
|
||||
```rust
|
||||
use nd_host::{artwork, cache, scheduler, websocket};
|
||||
use nd_pdk::host::{artwork, cache, scheduler, websocket};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Get artwork URL
|
||||
let (url, _) = artwork::artwork_get_track_url(track_id, 300)?;
|
||||
let url = artwork::get_track_url(track_id, 300)?;
|
||||
|
||||
// Cache operations
|
||||
cache::cache_set_string("key", "value", 3600)?;
|
||||
let (value, exists) = cache::cache_get_string("key")?;
|
||||
cache::set_string("key", "value", 3600)?;
|
||||
if let Some(value) = cache::get_string("key")? {
|
||||
// Use the cached value
|
||||
}
|
||||
|
||||
// Schedule tasks
|
||||
scheduler::scheduler_schedule_one_time(60, "payload", "task-id")?;
|
||||
scheduler::scheduler_schedule_recurring("@every 30s", "heartbeat", "heartbeat-task")?;
|
||||
scheduler::schedule_one_time(60, "payload", "task-id")?;
|
||||
scheduler::schedule_recurring("@every 30s", "heartbeat", "heartbeat-task")?;
|
||||
|
||||
// WebSocket operations
|
||||
let conn_id = websocket::websocket_connect("wss://example.com/socket")?;
|
||||
websocket::websocket_send_text(&conn_id, "Hello")?;
|
||||
let conn_id = websocket::connect("wss://example.com/socket", HashMap::new(), "my-conn")?;
|
||||
websocket::send_text(&conn_id, "Hello")?;
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
@@ -42,10 +43,11 @@ func TestPlugins(t *testing.T) {
|
||||
|
||||
func buildTestPlugins(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
start := time.Now()
|
||||
t.Logf("[BeforeSuite] Current working directory: %s", path)
|
||||
cmd := exec.Command("make", "-C", path)
|
||||
out, err := cmd.CombinedOutput()
|
||||
t.Logf("[BeforeSuite] Make output: %s", string(out))
|
||||
t.Logf("[BeforeSuite] Make output: %s elapsed: %s", string(out), time.Since(start))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build test plugins: %v", err)
|
||||
}
|
||||
|
||||
@@ -200,7 +200,6 @@
|
||||
"duration": "Duração",
|
||||
"ownerName": "Dono",
|
||||
"public": "Pública",
|
||||
"global": "Global",
|
||||
"updatedAt": "Últ. Atualização",
|
||||
"createdAt": "Data de Criação",
|
||||
"songCount": "Músicas",
|
||||
@@ -223,8 +222,7 @@
|
||||
"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",
|
||||
"globalPlaylistPublicDisabled": "Playlists globais são sempre públicas"
|
||||
"noPlaylists": "Nenhuma playlist disponível"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "Global Smart Playlist",
|
||||
"comment": "Available for evaluation by any user",
|
||||
"global": true,
|
||||
"all": [
|
||||
{"is": {"loved": true}}
|
||||
],
|
||||
"sort": "title",
|
||||
"order": "asc"
|
||||
}
|
||||
11
tests/fixtures/playlists/private_playlist.nsp
vendored
Normal file
11
tests/fixtures/playlists/private_playlist.nsp
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Private Playlist",
|
||||
"comment": "A smart playlist that is explicitly private",
|
||||
"public": false,
|
||||
"all": [
|
||||
{"is": {"loved": true}}
|
||||
],
|
||||
"sort": "title",
|
||||
"order": "asc",
|
||||
"limit": 100
|
||||
}
|
||||
11
tests/fixtures/playlists/public_playlist.nsp
vendored
Normal file
11
tests/fixtures/playlists/public_playlist.nsp
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Public Playlist",
|
||||
"comment": "A smart playlist that is public",
|
||||
"public": true,
|
||||
"all": [
|
||||
{"inTheLast": {"lastPlayed": 30}}
|
||||
],
|
||||
"sort": "lastPlayed",
|
||||
"order": "desc",
|
||||
"limit": 50
|
||||
}
|
||||
@@ -9,9 +9,7 @@ export const isReadOnly = (ownerId) => {
|
||||
return !isWritable(ownerId)
|
||||
}
|
||||
|
||||
export const isSmartPlaylist = (pls) => !!pls?.rules
|
||||
|
||||
export const isGlobalPlaylist = (pls) => isSmartPlaylist(pls) && !!pls?.global
|
||||
export const isSmartPlaylist = (pls) => !!pls.rules
|
||||
|
||||
export const canChangeTracks = (pls) =>
|
||||
isWritable(pls?.ownerId) && !isSmartPlaylist(pls)
|
||||
isWritable(pls.ownerId) && !isSmartPlaylist(pls)
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
isWritable,
|
||||
isReadOnly,
|
||||
isSmartPlaylist,
|
||||
isGlobalPlaylist,
|
||||
canChangeTracks,
|
||||
} from './playlistUtils'
|
||||
|
||||
@@ -57,28 +56,6 @@ 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,7 +200,6 @@
|
||||
"duration": "Duration",
|
||||
"ownerName": "Owner",
|
||||
"public": "Public",
|
||||
"global": "Global",
|
||||
"updatedAt": "Updated at",
|
||||
"createdAt": "Created at",
|
||||
"songCount": "Songs",
|
||||
@@ -223,8 +222,7 @@
|
||||
"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",
|
||||
"globalPlaylistPublicDisabled": "Global playlists are always public"
|
||||
"noPlaylists": "No playlists available"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
|
||||
@@ -2,22 +2,15 @@ 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,
|
||||
isGlobalPlaylist,
|
||||
} from '../common'
|
||||
import { CollapsibleComment, DurationField, SizeField } from '../common'
|
||||
import subsonic from '../subsonic'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
@@ -84,10 +77,6 @@ const useStyles = makeStyles(
|
||||
marginTop: '1em',
|
||||
marginBottom: '0.5em',
|
||||
},
|
||||
globalChip: {
|
||||
marginLeft: '0.5em',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'NDPlaylistDetails',
|
||||
@@ -157,14 +146,6 @@ 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,16 +11,7 @@ import {
|
||||
ReferenceInput,
|
||||
SelectInput,
|
||||
} from 'react-admin'
|
||||
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',
|
||||
},
|
||||
})
|
||||
import { isWritable, Title } from '../common'
|
||||
|
||||
const SyncFragment = ({ formData, variant, ...rest }) => {
|
||||
return (
|
||||
@@ -37,48 +28,9 @@ 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()} />
|
||||
@@ -98,10 +50,7 @@ const PlaylistEditForm = (props) => {
|
||||
) : (
|
||||
<TextField source="ownerName" />
|
||||
)}
|
||||
<FormDataConsumer>
|
||||
{({ formData }) => <PublicInput record={record} formData={formData} />}
|
||||
</FormDataConsumer>
|
||||
{isSmart && <GlobalInput record={record} />}
|
||||
<BooleanInput source="public" disabled={!isWritable(record.ownerId)} />
|
||||
<FormDataConsumer>
|
||||
{(formDataProps) => <SyncFragment {...formDataProps} />}
|
||||
</FormDataConsumer>
|
||||
|
||||
@@ -14,10 +14,8 @@ 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 {
|
||||
@@ -25,7 +23,6 @@ import {
|
||||
List,
|
||||
Writable,
|
||||
isWritable,
|
||||
isGlobalPlaylist,
|
||||
useSelectedFields,
|
||||
useResourceRefresh,
|
||||
} from '../common'
|
||||
@@ -62,7 +59,6 @@ const PlaylistFilter = (props) => {
|
||||
const TogglePublicInput = ({ resource, source }) => {
|
||||
const record = useRecordContext()
|
||||
const notify = useNotify()
|
||||
const translate = useTranslate()
|
||||
const [togglePublic] = useUpdate(
|
||||
resource,
|
||||
record.id,
|
||||
@@ -83,29 +79,13 @@ const TogglePublicInput = ({ resource, source }) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const isGlobal = isGlobalPlaylist(record)
|
||||
const disabled = !isWritable(record.ownerId) || isGlobal
|
||||
|
||||
const switchElement = (
|
||||
return (
|
||||
<Switch
|
||||
checked={record[source]}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
disabled={!isWritable(record.ownerId)}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isGlobal) {
|
||||
return (
|
||||
<Tooltip
|
||||
title={translate(
|
||||
'resources.playlist.message.globalPlaylistPublicDisabled',
|
||||
)}
|
||||
>
|
||||
<span>{switchElement}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return switchElement
|
||||
}
|
||||
|
||||
const ToggleAutoImport = ({ resource, source }) => {
|
||||
|
||||
Reference in New Issue
Block a user