Merge 5bf6832d34
into 6dcfe4d455
This commit is contained in:
commit
8e4305de5b
|
@ -0,0 +1,75 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationContext(upAddSyncPlayqueueColumnToUserTable, downAddSyncPlayqueueColumnToUserTable)
|
||||
}
|
||||
|
||||
func upAddSyncPlayqueueColumnToUserTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
create table user_dg_tmp
|
||||
(
|
||||
id varchar(255) not null
|
||||
primary key,
|
||||
user_name varchar(255) default '' not null
|
||||
unique,
|
||||
name varchar(255) default '' not null,
|
||||
email varchar(255) default '' not null,
|
||||
password varchar(255) default '' not null,
|
||||
is_admin bool default FALSE not null,
|
||||
last_login_at datetime,
|
||||
last_access_at datetime,
|
||||
created_at datetime not null,
|
||||
updated_at datetime not null,
|
||||
sync_playqueue bool default FALSE not null
|
||||
);
|
||||
|
||||
insert into user_dg_tmp(id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at) select id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at from user;
|
||||
|
||||
drop table user;
|
||||
|
||||
alter table user_dg_tmp rename to user;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notice(tx, "A full rescan needs to be performed to import more tags")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func downAddSyncPlayqueueColumnToUserTable(ctx context.Context, tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
create table user_dg_tmp
|
||||
(
|
||||
id varchar(255) not null
|
||||
primary key,
|
||||
user_name varchar(255) default '' not null
|
||||
unique,
|
||||
name varchar(255) default '' not null,
|
||||
email varchar(255) default '' not null,
|
||||
password varchar(255) default '' not null,
|
||||
is_admin bool default FALSE not null,
|
||||
last_login_at datetime,
|
||||
last_access_at datetime,
|
||||
created_at datetime not null,
|
||||
updated_at datetime not null,
|
||||
);
|
||||
|
||||
insert into user_dg_tmp(id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at) select id, user_name, name, email, password, is_admin, last_login_at, last_access_at, created_at, updated_at from user;
|
||||
|
||||
drop table user;
|
||||
|
||||
alter table user_dg_tmp rename to user;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notice(tx, "A full rescan needs to be performed to import more tags")
|
||||
return forceFullRescan(tx)
|
||||
}
|
|
@ -3,15 +3,16 @@ package model
|
|||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID string `structs:"id" json:"id"`
|
||||
UserName string `structs:"user_name" json:"userName"`
|
||||
Name string `structs:"name" json:"name"`
|
||||
Email string `structs:"email" json:"email"`
|
||||
IsAdmin bool `structs:"is_admin" json:"isAdmin"`
|
||||
LastLoginAt *time.Time `structs:"last_login_at" json:"lastLoginAt"`
|
||||
LastAccessAt *time.Time `structs:"last_access_at" json:"lastAccessAt"`
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
ID string `structs:"id" json:"id"`
|
||||
UserName string `structs:"user_name" json:"userName"`
|
||||
Name string `structs:"name" json:"name"`
|
||||
Email string `structs:"email" json:"email"`
|
||||
IsAdmin bool `structs:"is_admin" json:"isAdmin"`
|
||||
SyncPlayqueue bool `structs:"sync_playqueue" json:"syncPlayqueue"`
|
||||
LastLoginAt *time.Time `structs:"last_login_at" json:"lastLoginAt"`
|
||||
LastAccessAt *time.Time `structs:"last_access_at" json:"lastAccessAt"`
|
||||
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
|
||||
|
||||
// This is only available on the backend, and it is never sent over the wire
|
||||
Password string `structs:"-" json:"-"`
|
||||
|
|
|
@ -73,6 +73,7 @@ func buildAuthPayload(user *model.User) map[string]interface{} {
|
|||
"name": user.Name,
|
||||
"username": user.UserName,
|
||||
"isAdmin": user.IsAdmin,
|
||||
"sync": user.SyncPlayqueue,
|
||||
}
|
||||
if conf.Server.EnableGravatar && user.Email != "" {
|
||||
payload["avatar"] = gravatar.Url(user.Email, 50)
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"jukeboxRole": false,
|
||||
"shareRole": false,
|
||||
"videoConversionRole": false,
|
||||
"syncPlayqueue": false,
|
||||
"folder": [
|
||||
1
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false">
|
||||
<user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false" syncPlayqueue="false">
|
||||
<folder>1</folder>
|
||||
</user>
|
||||
</subsonic-response>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"streamRole": false,
|
||||
"jukeboxRole": false,
|
||||
"shareRole": false,
|
||||
"videoConversionRole": false
|
||||
"videoConversionRole": false,
|
||||
"syncPlayqueue": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"></user>
|
||||
<user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false" syncPlayqueue="false"></user>
|
||||
</subsonic-response>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"jukeboxRole": false,
|
||||
"shareRole": false,
|
||||
"videoConversionRole": false,
|
||||
"syncPlayqueue": false,
|
||||
"folder": [
|
||||
1
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<users>
|
||||
<user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="true" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false">
|
||||
<user username="deluan" email="navidrome@deluan.com" scrobblingEnabled="false" adminRole="true" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false" syncPlayqueue="false">
|
||||
<folder>1</folder>
|
||||
</user>
|
||||
</users>
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"streamRole": false,
|
||||
"jukeboxRole": false,
|
||||
"shareRole": false,
|
||||
"videoConversionRole": false
|
||||
"videoConversionRole": false,
|
||||
"syncPlayqueue": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
|
||||
<users>
|
||||
<user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false"></user>
|
||||
<user username="deluan" scrobblingEnabled="false" adminRole="false" settingsRole="false" downloadRole="false" uploadRole="false" playlistRole="false" coverArtRole="false" commentRole="false" podcastRole="false" streamRole="false" jukeboxRole="false" shareRole="false" videoConversionRole="false" syncPlayqueue="false"></user>
|
||||
</users>
|
||||
</subsonic-response>
|
||||
|
|
|
@ -323,6 +323,7 @@ type User struct {
|
|||
JukeboxRole bool `xml:"jukeboxRole,attr" json:"jukeboxRole"`
|
||||
ShareRole bool `xml:"shareRole,attr" json:"shareRole"`
|
||||
VideoConversionRole bool `xml:"videoConversionRole,attr" json:"videoConversionRole"`
|
||||
SyncPlayqueue bool `xml:"syncPlayqueue,attr" json:"syncPlayqueue"`
|
||||
Folder []int32 `xml:"folder,omitempty" json:"folder,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ func (api *Router) GetUser(r *http.Request) (*responses.Subsonic, error) {
|
|||
response.User.Username = loggedUser.UserName
|
||||
response.User.AdminRole = loggedUser.IsAdmin
|
||||
response.User.Email = loggedUser.Email
|
||||
response.User.SyncPlayqueue = loggedUser.SyncPlayqueue
|
||||
response.User.StreamRole = true
|
||||
response.User.ScrobblingEnabled = true
|
||||
response.User.DownloadRole = conf.Server.EnableDownloads
|
||||
|
@ -41,6 +42,7 @@ func (api *Router) GetUsers(r *http.Request) (*responses.Subsonic, error) {
|
|||
user.DownloadRole = conf.Server.EnableDownloads
|
||||
user.ShareRole = conf.Server.EnableSharing
|
||||
user.JukeboxRole = conf.Server.Jukebox.Enabled
|
||||
user.SyncPlayqueue = loggedUser.SyncPlayqueue
|
||||
response := newResponse()
|
||||
response.Users = &responses.Users{User: []responses.User{user}}
|
||||
return response, nil
|
||||
|
|
|
@ -23,6 +23,7 @@ import subsonic from '../subsonic'
|
|||
import locale from './locale'
|
||||
import { keyMap } from '../hotkeys'
|
||||
import keyHandlers from './keyHandlers'
|
||||
import { PLAYER_PLAY_TRACKS, filterSongs } from '../actions'
|
||||
|
||||
function calculateReplayGain(preAmp, gain, peak) {
|
||||
if (gain === undefined || peak === undefined) {
|
||||
|
@ -264,8 +265,16 @@ const Player = () => {
|
|||
)
|
||||
}
|
||||
}
|
||||
if (localStorage.getItem('sync') === 'true') {
|
||||
let ids = ''
|
||||
for (let i = 0; i < playerState.queue.length; i++) {
|
||||
let song = playerState.queue[i]['trackId']
|
||||
ids += `&id=${song}`
|
||||
}
|
||||
subsonic.syncPlayQueue(currentPlaying(info).data, ids)
|
||||
}
|
||||
},
|
||||
[context, dispatch, showNotifications, startTime],
|
||||
[context, dispatch, showNotifications, startTime, playerState.queue],
|
||||
)
|
||||
|
||||
const onAudioPlayTrackChange = useCallback(() => {
|
||||
|
@ -342,5 +351,15 @@ const Player = () => {
|
|||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
//TODO possible breakage
|
||||
//to be implemented on the Player Side
|
||||
export const playTracks = (data, ids, selectedId, timestamp) => {
|
||||
const songs = filterSongs(data, ids)
|
||||
return {
|
||||
type: PLAYER_PLAY_TRACKS,
|
||||
id: selectedId || Object.keys(songs)[0],
|
||||
data: songs,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
export { Player }
|
||||
|
|
|
@ -3,9 +3,27 @@ import { useGetOne } from 'react-admin'
|
|||
import { GlobalHotKeys } from 'react-hotkeys'
|
||||
import { LoveButton, useToggleLove } from '../common'
|
||||
import { keyMap } from '../hotkeys'
|
||||
import config from '../config'
|
||||
import { UpdateQueueButton } from '../common/UpdateQueueButton'
|
||||
|
||||
const Placeholder = () => <LoveButton disabled={true} resource={'song'} />
|
||||
const Placeholder = () => {
|
||||
return (
|
||||
<>
|
||||
{config.enableFavourites && (
|
||||
<LoveButton disabled={true} resource={'song'} />
|
||||
)}
|
||||
<UpdateQueueButton label={'queue'} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const GetSongId = (data) => {
|
||||
let songIDs = []
|
||||
for (var i = 0; i < data.length; i++) songIDs.push(data[i].id)
|
||||
return songIDs
|
||||
}
|
||||
|
||||
export { GetSongId }
|
||||
const Toolbar = ({ id }) => {
|
||||
const { data, loading } = useGetOne('song', id)
|
||||
const [toggleLove, toggling] = useToggleLove('song', data)
|
||||
|
@ -22,6 +40,7 @@ const Toolbar = ({ id }) => {
|
|||
resource={'song'}
|
||||
disabled={loading || toggling}
|
||||
/>
|
||||
<UpdateQueueButton label="queue" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ function storeAuthenticationInfo(authInfo) {
|
|||
localStorage.setItem('username', authInfo.username)
|
||||
authInfo.avatar && localStorage.setItem('avatar', authInfo.avatar)
|
||||
localStorage.setItem('role', authInfo.isAdmin ? 'admin' : 'regular')
|
||||
localStorage.setItem('sync', authInfo.sync ? 'true' : 'false')
|
||||
localStorage.setItem('subsonic-salt', authInfo.subsonicSalt)
|
||||
localStorage.setItem('subsonic-token', authInfo.subsonicToken)
|
||||
localStorage.setItem('lastfm-apikey', authInfo.lastFMApiKey)
|
||||
|
@ -101,6 +102,7 @@ const removeItems = () => {
|
|||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('avatar')
|
||||
localStorage.removeItem('role')
|
||||
localStorage.removeItem('sync')
|
||||
localStorage.removeItem('subsonic-salt')
|
||||
localStorage.removeItem('subsonic-token')
|
||||
localStorage.removeItem('lastfm-apikey')
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined'
|
||||
import { IconButton } from '@material-ui/core'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { playTracks } from '../actions'
|
||||
import { httpClient } from '../dataProvider'
|
||||
import subsonic from '../subsonic'
|
||||
|
||||
const UpdateQueueButton = ({ record, size, className }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
//this one is used when we use the album list to play the songs(does not support duplicate songs)
|
||||
const queueBuilderId = (data, object) => {
|
||||
let songObj = {}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
songObj[data[i].id] =
|
||||
object.json[
|
||||
object.json.findIndex((index) => {
|
||||
return index.id === data[i].id
|
||||
})
|
||||
]
|
||||
}
|
||||
return songObj
|
||||
}
|
||||
|
||||
//supports duplicate songs
|
||||
const queueBuilderInc = (data, object) => {
|
||||
let songObj = {}
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
songObj[i] =
|
||||
object.json[
|
||||
object.json.findIndex((index) => {
|
||||
return index.id === data[i].id
|
||||
})
|
||||
]
|
||||
}
|
||||
return songObj
|
||||
}
|
||||
|
||||
const updateQueueButton = useCallback(() => {
|
||||
//gets the data of the currently playing songs and formats the data
|
||||
const getSongData = async (data, state) => {
|
||||
let idString = `/api/song?id=${data[0].id}`
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
idString = `${idString}&id=${data[i].id}`
|
||||
}
|
||||
const object = await httpClient(idString)
|
||||
return state === false
|
||||
? queueBuilderInc(data, object)
|
||||
: queueBuilderId(data, object)
|
||||
}
|
||||
if (localStorage.getItem('sync') === 'false') {
|
||||
return
|
||||
}
|
||||
subsonic
|
||||
.getStoredQueue()
|
||||
.then((res) => {
|
||||
let data = JSON.parse(res.body)
|
||||
getSongData(
|
||||
data['subsonic-response'].playQueue.entry,
|
||||
data['subsonic-response'].playQueue.current.length > 4 ? true : false,
|
||||
)
|
||||
.then((res) => {
|
||||
let res_new = {}
|
||||
let data_ids
|
||||
let current
|
||||
let timestamp
|
||||
timestamp = data['subsonic-response'].playQueue.position
|
||||
if (data['subsonic-response'].playQueue.current.length > 4) {
|
||||
data_ids = data['subsonic-response'].playQueue.entry.map(
|
||||
(s) => s.id,
|
||||
)
|
||||
res_new = res
|
||||
current = res[data['subsonic-response'].playQueue.current].id
|
||||
} else {
|
||||
let size = data['subsonic-response'].playQueue.entry.length
|
||||
//dealing with the pass by reference
|
||||
for (let i = 0; i < size; i++) {
|
||||
let temp = Object.assign({}, res[i])
|
||||
temp.mediaFileId = res[i].id
|
||||
temp.id = `${i + 1}`
|
||||
res_new[i + 1] = temp
|
||||
}
|
||||
|
||||
data_ids = Array.from({ length: size }, (v, i) => `${++i}`)
|
||||
current = data['subsonic-response'].playQueue.current
|
||||
}
|
||||
dispatch(playTracks(res_new, data_ids, current, timestamp))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
id="updateQueue"
|
||||
onClick={(e) => {
|
||||
updateQueueButton()
|
||||
}}
|
||||
aria-label="Get updated Queue"
|
||||
size={size}
|
||||
>
|
||||
<CloudDownloadOutlinedIcon fontSize={size} />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
UpdateQueueButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
}
|
||||
|
||||
UpdateQueueButton.defaultProps = {
|
||||
label: 'Get updated Queue',
|
||||
size: 'small',
|
||||
}
|
||||
const GetTime = async (localSt) => {
|
||||
var dateCache
|
||||
if (localStorage.getItem('username') === null) {
|
||||
return
|
||||
}
|
||||
if (localStorage.getItem('sync') === 'false') {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof localSt !== 'undefined') {
|
||||
dateCache = localSt.player.lastUpdatedAt
|
||||
}
|
||||
subsonic.getStoredQueue().then((res) => {
|
||||
if (res.json['subsonic-response'].status === 'ok') {
|
||||
let date = Date.parse(res.json['subsonic-response'].playQueue.changed)
|
||||
|
||||
if (typeof dateCache === 'undefined' || date > dateCache) {
|
||||
document.getElementById('updateQueue').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { UpdateQueueButton, GetTime }
|
|
@ -18,6 +18,7 @@ const initialState = {
|
|||
clear: false,
|
||||
volume: config.defaultUIVolume / 100,
|
||||
savedPlayIndex: 0,
|
||||
lastUpdatedAt: 0,
|
||||
}
|
||||
|
||||
const pad = (value) => {
|
||||
|
@ -175,6 +176,7 @@ const reduceCurrent = (state, { data }) => {
|
|||
playIndex: undefined,
|
||||
savedPlayIndex,
|
||||
volume: data.volume,
|
||||
lastUpdatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,12 @@ const createAdminStore = ({
|
|||
const state = store.getState()
|
||||
saveState({
|
||||
theme: state.theme,
|
||||
player: pick(state.player, ['queue', 'volume', 'savedPlayIndex']),
|
||||
player: pick(state.player, [
|
||||
'queue',
|
||||
'volume',
|
||||
'savedPlayIndex',
|
||||
'lastUpdatedAt',
|
||||
]),
|
||||
albumView: state.albumView,
|
||||
settings: state.settings,
|
||||
})
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { GetTime } from '../common/UpdateQueueButton'
|
||||
|
||||
export const loadState = () => {
|
||||
try {
|
||||
const serializedState = localStorage.getItem('state')
|
||||
if (serializedState === null) {
|
||||
GetTime(undefined)
|
||||
return undefined
|
||||
}
|
||||
GetTime(JSON.parse(serializedState))
|
||||
return JSON.parse(serializedState)
|
||||
} catch (err) {
|
||||
return undefined
|
||||
|
|
|
@ -78,6 +78,25 @@ const streamUrl = (id, options) => {
|
|||
)
|
||||
}
|
||||
|
||||
const syncPlayQueue = (current, queue) => {
|
||||
return current === undefined
|
||||
? httpClient(url('savePlayQueue') + queue)
|
||||
: httpClient(
|
||||
url('savePlayQueue') +
|
||||
queue +
|
||||
`¤t=${current.song.id}` +
|
||||
syncTimePlayed(current),
|
||||
)
|
||||
}
|
||||
const syncTimePlayed = (current) => {
|
||||
// TODO: add the time to a enviramental variable or to sync settings option
|
||||
return current.duration > 480
|
||||
? `&position=${Math.trunc(current.currentTime) * 1000}`
|
||||
: ''
|
||||
}
|
||||
|
||||
const getStoredQueue = () => httpClient(url('getPlayQueue'))
|
||||
|
||||
export default {
|
||||
url,
|
||||
scrobble,
|
||||
|
@ -90,6 +109,8 @@ export default {
|
|||
getScanStatus,
|
||||
getCoverArtUrl,
|
||||
streamUrl,
|
||||
syncPlayQueue,
|
||||
getStoredQueue,
|
||||
getAlbumInfo,
|
||||
getArtistInfo,
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ const UserCreate = (props) => {
|
|||
validate={[required()]}
|
||||
/>
|
||||
<BooleanInput source="isAdmin" defaultValue={false} />
|
||||
<BooleanInput source="syncPlayqueue" defaultValue={false} />
|
||||
</SimpleForm>
|
||||
</Create>
|
||||
)
|
||||
|
|
|
@ -90,6 +90,7 @@ const UserEdit = (props) => {
|
|||
notify('resources.user.notifications.updated', 'info', {
|
||||
smart_count: 1,
|
||||
})
|
||||
localStorage.setItem('sync', values.syncPlayqueue ? 'true' : 'false')
|
||||
permissions === 'admin' ? redirect('/user') : refresh()
|
||||
} catch (error) {
|
||||
if (error.body.errors) {
|
||||
|
@ -139,6 +140,7 @@ const UserEdit = (props) => {
|
|||
{permissions === 'admin' && (
|
||||
<BooleanInput source="isAdmin" initialValue={false} />
|
||||
)}
|
||||
<BooleanInput source="syncPlayqueue" initialValue={false} />
|
||||
<DateField variant="body1" source="lastLoginAt" showTime />
|
||||
{/*<DateField source="lastAccessAt" showTime />*/}
|
||||
<DateField variant="body1" source="updatedAt" showTime />
|
||||
|
|
|
@ -40,6 +40,7 @@ const UserList = (props) => {
|
|||
<TextField source="userName" />
|
||||
<TextField source="name" />
|
||||
<BooleanField source="isAdmin" />
|
||||
<BooleanField source="syncPlayqueue" />
|
||||
<DateField source="lastLoginAt" sortByOrder={'DESC'} />
|
||||
<DateField source="updatedAt" sortByOrder={'DESC'} />
|
||||
</Datagrid>
|
||||
|
|
Loading…
Reference in New Issue