Compare commits

...

9 Commits

Author SHA1 Message Date
Deluan
678efed9b3 fix(ui): enhance error handling by returning field info and path in validation errors
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 18:19:55 -05:00
Deluan
f3532ec9e6 fix(ui): remove "None" MenuItem from OutlinedEnumControl
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 18:19:55 -05:00
Deluan
fa016528c4 fix(ui): simplify error handling in control state hook
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 18:19:55 -05:00
Deluan
6a57fd71cf fix(plugins): enforce minimum user tokens and require users field
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 18:19:55 -05:00
Deluan
2fb383b58a fix(ui): use stock array renderer for plugins config form
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 18:19:55 -05:00
Deluan
6fce30c133 feat(ui): enhance comment input in PlaylistEdit with multiline support and resizing
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 13:27:10 -05:00
Boris Rorsvort
6c7f8314e2 fix(ui): UI issues & styling coherence (#4910)
* fix: ui issues and styles

* fix linter
2026-01-20 12:45:33 -05:00
Boris Rorsvort
37aa54fe06 feat(ui): Add Nautiline like theme (#4909)
* wip

* add main file

* fixes

* linting

* refactor

* fix player

* fix lint

* fix pr comments

* Add font locally

* fix: quickfix
2026-01-20 12:11:47 -05:00
Deluan
fae58bb390 chore(deps): update Go dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 06:51:19 -05:00
17 changed files with 1013 additions and 417 deletions

6
go.mod
View File

@@ -59,14 +59,14 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/sirupsen/logrus v1.9.3
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.11.0
github.com/unrolled/secure v1.17.0
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342
go.senan.xyz/taglib v0.0.0-00010101000000-000000000000
go.senan.xyz/taglib v0.11.1
go.uber.org/goleak v1.3.0
golang.org/x/image v0.35.0
golang.org/x/net v0.49.0
@@ -98,7 +98,7 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect

10
go.sum
View File

@@ -110,8 +110,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdxohPneMrhGH2YfQuH5hRIK9u4/XCeUtw=
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc=
github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440 h1:oKBqR+eQXiIM7X8K1JEg9aoTEePLq/c6Awe484abOuA=
github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -245,8 +245,8 @@ github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -275,7 +275,6 @@ github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRci
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -364,7 +363,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -4,10 +4,10 @@ go 1.25
require (
github.com/extism/go-pdk v1.1.3
github.com/onsi/ginkgo/v2 v2.27.3
github.com/onsi/gomega v1.38.3
github.com/onsi/ginkgo/v2 v2.27.5
github.com/onsi/gomega v1.39.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
golang.org/x/tools v0.40.0
golang.org/x/tools v0.41.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -16,11 +16,11 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
)

View File

@@ -20,8 +20,8 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -32,10 +32,10 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE=
github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -54,18 +54,18 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -42,7 +42,7 @@
"type": "array",
"title": "User Tokens",
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
"default": [{}],
"minItems": 1,
"items": {
"type": "object",
"properties": {
@@ -63,7 +63,7 @@
}
}
},
"required": ["clientid"]
"required": ["clientid", "users"]
},
"uiSchema": {
"type": "VerticalLayout",

View File

@@ -46,7 +46,7 @@
"type": "array",
"title": "User Tokens",
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
"default": [{}],
"minItems": 1,
"items": {
"type": "object",
"properties": {
@@ -67,7 +67,7 @@
}
}
},
"required": ["clientid"]
"required": ["clientid", "users"]
},
"uiSchema": {
"type": "VerticalLayout",

View File

Binary file not shown.

View File

@@ -228,7 +228,7 @@ const AlbumDetails = (props) => {
let notes =
albumInfo?.notes?.replace(new RegExp('<.*>', 'g'), '') || record.notes
if (notes !== undefined) {
if (notes) {
notes += '..'
}
@@ -340,7 +340,7 @@ const AlbumDetails = (props) => {
)}
</Typography>
)}
{isDesktop && (
{isDesktop && notes && (
<Collapse
collapsedHeight={'2.75em'}
in={expanded}
@@ -364,7 +364,7 @@ const AlbumDetails = (props) => {
{!isDesktop && record['comment'] && (
<CollapsibleComment record={record} />
)}
{!isDesktop && (
{!isDesktop && notes && (
<div className={classes.notes}>
<Collapse collapsedHeight={'1.5em'} in={expanded} timeout={'auto'}>
<Typography

View File

@@ -4,18 +4,26 @@ import FavoriteIcon from '@material-ui/icons/Favorite'
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles'
import clsx from 'clsx'
import { useToggleLove } from './useToggleLove'
import { useRecordContext } from 'react-admin'
import config from '../config'
import { isDateSet } from '../utils/validations'
const useStyles = makeStyles({
love: {
color: (props) => props.color,
visibility: (props) =>
props.visible === false ? 'hidden' : props.loved ? 'visible' : 'inherit',
const useStyles = makeStyles(
{
love: {
color: (props) => props.color,
visibility: (props) =>
props.visible === false
? 'hidden'
: props.loved
? 'visible'
: 'inherit',
},
},
})
{ name: 'NDLoveButton' },
)
export const LoveButton = ({
resource,
@@ -25,9 +33,11 @@ export const LoveButton = ({
component: Button,
addLabel,
disabled,
className,
record: recordProp,
...rest
}) => {
const record = useRecordContext(rest) || {}
const record = useRecordContext({ record: recordProp }) || {}
const classes = useStyles({ color, visible, loved: record.starred })
const [toggleLove, loading] = useToggleLove(resource, record)
@@ -48,7 +58,7 @@ export const LoveButton = ({
onClick={handleToggleLove}
size={'small'}
disabled={disabled || loading || record.missing}
className={classes.love}
className={clsx(classes.love, className)}
title={
isDateSet(record.starredAt)
? new Date(record.starredAt).toLocaleString()

View File

@@ -28,6 +28,9 @@ import { useDispatch } from 'react-redux'
const useStyles = makeStyles((theme) => ({
user: {},
button: {
color: 'inherit',
},
avatar: {
width: theme.spacing(4),
height: theme.spacing(4),
@@ -72,12 +75,11 @@ const UserMenu = (props) => {
<div className={classes.user}>
<Tooltip title={label && translate(label, { _: label })}>
<IconButton
className={classes.button}
aria-label={label && translate(label, { _: label })}
aria-owns={open ? 'menu-appbar' : null}
aria-haspopup={true}
color="inherit"
onClick={handleMenu}
size={'small'}
>
{loaded && identity.avatar ? (
<Avatar

View File

@@ -34,7 +34,15 @@ const PlaylistEditForm = (props) => {
return (
<SimpleForm redirect="list" variant={'outlined'} {...props}>
<TextInput source="name" validate={required()} />
<TextInput multiline source="comment" />
<TextInput
multiline
minRows={3}
source="comment"
fullWidth
inputProps={{
style: { resize: 'vertical' },
}}
/>
{permissions === 'admin' ? (
<ReferenceInput
source="ownerId"

View File

@@ -1,276 +0,0 @@
import React, { useCallback, useMemo } from 'react'
import {
composePaths,
computeLabel,
createDefaultValue,
isObjectArrayWithNesting,
isPrimitiveArrayControl,
rankWith,
findUISchema,
Resolve,
} from '@jsonforms/core'
import {
JsonFormsDispatch,
withJsonFormsArrayLayoutProps,
} from '@jsonforms/react'
import range from 'lodash/range'
import merge from 'lodash/merge'
import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'
import { Add, Delete } from '@material-ui/icons'
import { makeStyles } from '@material-ui/core/styles'
const useStyles = makeStyles((theme) => ({
arrayItem: {
position: 'relative',
padding: theme.spacing(2),
marginBottom: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
'&:last-child': {
marginBottom: 0,
},
},
deleteButton: {
position: 'absolute',
top: theme.spacing(1),
right: theme.spacing(1),
},
itemContent: {
paddingRight: theme.spacing(4), // Space for delete button
},
}))
// Default translations for array controls
const defaultTranslations = {
addTooltip: 'Add',
addAriaLabel: 'Add button',
removeTooltip: 'Delete',
removeAriaLabel: 'Delete button',
noDataMessage: 'No data',
}
// Simplified array item renderer - clean card layout
// eslint-disable-next-line react-refresh/only-export-components
const ArrayItem = ({
index,
path,
schema,
uischema,
uischemas,
rootSchema,
renderers,
cells,
enabled,
removeItems,
translations,
disableRemove,
}) => {
const classes = useStyles()
const childPath = composePaths(path, `${index}`)
const foundUISchema = useMemo(
() =>
findUISchema(
uischemas,
schema,
uischema.scope,
path,
undefined,
uischema,
rootSchema,
),
[uischemas, schema, path, uischema, rootSchema],
)
return (
<Box className={classes.arrayItem}>
{enabled && !disableRemove && (
<Tooltip
title={translations.removeTooltip}
className={classes.deleteButton}
>
<IconButton
onClick={() => removeItems(path, [index])()}
size="small"
aria-label={translations.removeAriaLabel}
>
<Delete fontSize="small" />
</IconButton>
</Tooltip>
)}
<Box className={classes.itemContent}>
<JsonFormsDispatch
enabled={enabled}
schema={schema}
uischema={foundUISchema}
path={childPath}
key={childPath}
renderers={renderers}
cells={cells}
/>
</Box>
</Box>
)
}
// Array toolbar with add button
// eslint-disable-next-line react-refresh/only-export-components
const ArrayToolbar = ({
label,
description,
enabled,
addItem,
path,
createDefault,
translations,
disableAdd,
}) => (
<Box mb={1}>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h6">{label}</Typography>
{!disableAdd && (
<Tooltip
title={translations.addTooltip}
aria-label={translations.addAriaLabel}
>
<IconButton
onClick={addItem(path, createDefault())}
disabled={!enabled}
size="small"
>
<Add />
</IconButton>
</Tooltip>
)}
</Box>
{description && (
<Typography variant="caption" color="textSecondary">
{description}
</Typography>
)}
</Box>
)
const useArrayStyles = makeStyles((theme) => ({
container: {
marginBottom: theme.spacing(2),
},
}))
// Main array layout component - items always expanded
// eslint-disable-next-line react-refresh/only-export-components
const AlwaysExpandedArrayLayoutComponent = (props) => {
const arrayClasses = useArrayStyles()
const {
enabled,
data,
path,
schema,
uischema,
addItem,
removeItems,
renderers,
cells,
label,
description,
required,
rootSchema,
config,
uischemas,
disableAdd,
disableRemove,
} = props
const innerCreateDefaultValue = useCallback(
() => createDefaultValue(schema, rootSchema),
[schema, rootSchema],
)
const appliedUiSchemaOptions = merge({}, config, uischema.options)
const doDisableAdd = disableAdd || appliedUiSchemaOptions.disableAdd
const doDisableRemove = disableRemove || appliedUiSchemaOptions.disableRemove
const translations = defaultTranslations
return (
<div className={arrayClasses.container}>
<ArrayToolbar
translations={translations}
label={computeLabel(
label,
required,
appliedUiSchemaOptions.hideRequiredAsterisk,
)}
description={description}
path={path}
enabled={enabled}
addItem={addItem}
createDefault={innerCreateDefaultValue}
disableAdd={doDisableAdd}
/>
<div>
{data > 0 ? (
range(data).map((index) => (
<ArrayItem
key={index}
index={index}
path={path}
schema={schema}
uischema={uischema}
uischemas={uischemas}
rootSchema={rootSchema}
renderers={renderers}
cells={cells}
enabled={enabled}
removeItems={removeItems}
translations={translations}
disableRemove={doDisableRemove}
/>
))
) : (
<Typography color="textSecondary">
{translations.noDataMessage}
</Typography>
)}
</div>
</div>
)
}
// Wrap with JSONForms HOC
const WrappedArrayLayout = withJsonFormsArrayLayoutProps(
AlwaysExpandedArrayLayoutComponent,
)
// Custom tester that matches arrays but NOT enum arrays
// Enum arrays should be handled by MaterialEnumArrayRenderer (for checkboxes)
const isNonEnumArrayControl = (uischema, schema) => {
// First check if it matches our base conditions (object array or primitive array)
const baseCheck =
isObjectArrayWithNesting(uischema, schema) ||
isPrimitiveArrayControl(uischema, schema)
if (!baseCheck) {
return false
}
// Resolve the actual schema for this control using JSONForms utility
const rootSchema = schema
const resolved = Resolve.schema(rootSchema, uischema?.scope, rootSchema)
// Exclude enum arrays (uniqueItems + oneOf/enum) - let MaterialEnumArrayRenderer handle them
if (resolved?.uniqueItems && resolved?.items) {
const { items } = resolved
if (items.oneOf?.every((e) => e.const !== undefined) || items.enum) {
return false
}
}
return true
}
// Export as a renderer entry with high priority (5 > default 4)
// Matches both object arrays with nesting and primitive arrays, but NOT enum arrays
export const AlwaysExpandedArrayLayout = {
tester: rankWith(5, isNonEnumArrayControl),
renderer: WrappedArrayLayout,
}

View File

@@ -4,76 +4,37 @@ import { Card, CardContent, Typography, Box } from '@material-ui/core'
import Alert from '@material-ui/lab/Alert'
import { SchemaConfigEditor } from './SchemaConfigEditor'
// Navigate schema by path parts to find the title for a field
const findFieldTitle = (schema, parts) => {
// Format error with field title and full path for nested fields
const formatError = (error, schema) => {
// Get path parts from various error formats
const rawPath =
error.dataPath || error.property || error.instancePath?.replace(/\//g, '.')
const parts = rawPath?.split('.').filter(Boolean) || []
// Navigate schema to find field title, build bracket-notation path
let currentSchema = schema
let fieldName = parts[parts.length - 1] // Default to last part
let fieldName = parts[parts.length - 1]
const pathParts = []
for (const part of parts) {
if (!currentSchema) break
// Skip array indices (just move to items schema)
if (/^\d+$/.test(part)) {
if (currentSchema.items) {
currentSchema = currentSchema.items
}
continue
}
// Navigate to property and always update fieldName
if (currentSchema.properties?.[part]) {
const propSchema = currentSchema.properties[part]
fieldName = propSchema.title || part
currentSchema = propSchema
pathParts.push(`[${part}]`)
currentSchema = currentSchema?.items
} else {
fieldName = currentSchema?.properties?.[part]?.title || part
pathParts.push(part)
currentSchema = currentSchema?.properties?.[part]
}
}
return fieldName
}
const path = pathParts.join('.').replace(/\.\[/g, '[')
const isNested = path.includes('[') || path.includes('.')
// Replace property name in message with full path for nested fields
const message = isNested
? error.message.replace(/'[^']+'\s*$/, `'${path}'`)
: error.message
// Extract human-readable field name from JSONForms error
const getFieldName = (error, schema) => {
// JSONForms errors can have different path formats:
// - dataPath: "users.1.token" (dot-separated)
// - instancePath: "/users/1/token" (slash-separated)
// - property: "users.1.username" (dot-separated)
const dataPath = error.dataPath || ''
const instancePath = error.instancePath || ''
const property = error.property || ''
// Try dataPath first (dot-separated like "users.1.token")
if (dataPath) {
const parts = dataPath.split('.').filter(Boolean)
if (parts.length > 0) {
return findFieldTitle(schema, parts)
}
}
// Try property (also dot-separated)
if (property) {
const parts = property.split('.').filter(Boolean)
if (parts.length > 0) {
return findFieldTitle(schema, parts)
}
}
// Fall back to instancePath (slash-separated like "/users/1/token")
if (instancePath) {
const parts = instancePath.split('/').filter(Boolean)
if (parts.length > 0) {
return findFieldTitle(schema, parts)
}
}
// Try to extract from schemaPath like "#/properties/users/items/properties/username/minLength"
const schemaPath = error.schemaPath || ''
const propMatches = [...schemaPath.matchAll(/\/properties\/([^/]+)/g)]
if (propMatches.length > 0) {
const parts = propMatches.map((m) => m[1])
return findFieldTitle(schema, parts)
}
return null
return { fieldName, message }
}
export const ConfigCard = ({
@@ -99,14 +60,10 @@ export const ConfigCard = ({
// Format validation errors with proper field names
const formattedErrors = useMemo(() => {
if (!hasConfigSchema) {
return []
}
const { schema } = manifest.config
return validationErrors.map((error) => ({
fieldName: getFieldName(error, schema),
message: error.message,
}))
if (!hasConfigSchema) return []
return validationErrors.map((error) =>
formatError(error, manifest.config.schema),
)
}, [validationErrors, manifest, hasConfigSchema])
if (!hasConfigSchema) {
@@ -139,12 +96,14 @@ export const ConfigCard = ({
</Box>
)}
<SchemaConfigEditor
schema={schema}
uiSchema={uiSchema}
data={configData}
onChange={handleChange}
/>
<Box mt={formattedErrors.length > 0 ? 0 : 2}>
<SchemaConfigEditor
schema={schema}
uiSchema={uiSchema}
data={configData}
onChange={handleChange}
/>
</Box>
</CardContent>
</Card>
)

View File

@@ -40,18 +40,14 @@ const useStyles = makeStyles(
/**
* Hook for common control state (focus, validation, description visibility)
* Tracks "touched" state to only show errors after the user has interacted with the field
*/
const useControlState = (props) => {
const { config, uischema, description, visible, errors } = props
const [isFocused, setIsFocused] = useState(false)
const [isTouched, setIsTouched] = useState(false)
const appliedUiSchemaOptions = merge({}, config, uischema?.options)
// errors is a string when there are validation errors, empty/undefined when valid
const hasErrors = errors && errors.length > 0
// Only show as invalid after the field has been touched (blurred)
const showError = isTouched && hasErrors
const showError = errors && errors.length > 0
const showDescription = !isDescriptionHidden(
visible,
@@ -63,10 +59,7 @@ const useControlState = (props) => {
const helperText = showError ? errors : showDescription ? description : ''
const handleFocus = () => setIsFocused(true)
const handleBlur = () => {
setIsFocused(false)
setIsTouched(true)
}
const handleBlur = () => setIsFocused(false)
return {
isFocused,
@@ -220,9 +213,6 @@ const OutlinedEnumControl = (props) => {
label={label}
fullWidth
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{options?.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}

View File

@@ -6,7 +6,6 @@ import { makeStyles } from '@material-ui/core/styles'
import { Typography } from '@material-ui/core'
import { useTranslate } from 'react-admin'
import Ajv from 'ajv'
import { AlwaysExpandedArrayLayout } from './AlwaysExpandedArrayLayout'
import {
OutlinedTextRenderer,
OutlinedNumberRenderer,
@@ -135,7 +134,6 @@ const customRenderers = [
OutlinedNumberRenderer,
OutlinedEnumRenderer,
OutlinedOneOfEnumRenderer,
AlwaysExpandedArrayLayout,
// Then all the standard material renderers
...materialRenderers,
]

View File

@@ -12,6 +12,7 @@ import CatppuccinMacchiatoTheme from './catppuccinMacchiato'
import NuclearTheme from './nuclear'
import AmusicTheme from './amusic'
import SquiddiesGlassTheme from './SquiddiesGlass'
import NautilineTheme from './nautiline'
export default {
// Classic default themes
@@ -27,6 +28,7 @@ export default {
GruvboxDarkTheme,
LigeraTheme,
MonokaiTheme,
NautilineTheme,
NordTheme,
NuclearTheme,
SpotifyTheme,

905
ui/src/themes/nautiline.js Normal file
View File

@@ -0,0 +1,905 @@
/**
* Nautiline Theme for Navidrome
* Light theme inspired by the Nautiline iOS app
*/
// ============================================
// CONFIGURATION
// ============================================
const ACCENT_COLOR = '#009688' // Material teal
// ============================================
// DESIGN TOKENS
// ============================================
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}
const rgb = hexToRgb(ACCENT_COLOR)
const rgba = (alpha) =>
rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})` : 'transparent'
const tokens = {
colors: {
accent: {
main: ACCENT_COLOR,
faded: rgba(0.1),
hover: rgba(0.15),
},
background: {
primary: '#FFFFFF',
secondary: '#F5F5F7',
tertiary: '#E5E5EA',
},
text: {
primary: '#1A1A1A',
secondary: '#8E8E93',
tertiary: '#AEAEB2',
},
ui: {
separator: 'rgba(0, 0, 0, 0.08)',
shadow: 'rgba(0, 0, 0, 0.04)',
glassBg: 'rgba(255, 255, 255, 0.72)',
},
},
typography: {
fontFamily: {
base: [
'-apple-system',
'BlinkMacSystemFont',
'"SF Pro Text"',
'"Helvetica Neue"',
'Arial',
'sans-serif',
].join(','),
heading: '"Unbounded", sans-serif',
},
fontFace: `
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 300 800;
font-display: swap;
src: url('/fonts/Unbounded-Variable.woff2') format('woff2');
}
`,
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '0.75rem',
lg: '1rem',
xl: '1.5rem',
},
radii: {
sm: '0.25rem',
md: '0.5rem',
lg: '0.625rem',
xl: '0.75rem',
full: '50%',
pill: '1rem',
},
breakpoints: {
xs: 599,
sm: 600,
md: 720,
lg: 1280,
},
sizing: {
cover: {
sm: '14em',
lg: '18em',
},
icon: '1.25rem',
iconMinWidth: '2.5rem',
},
blur: '1.25rem',
}
const { colors, typography, spacing, radii, sizing, breakpoints } = tokens
// ============================================
// REUSABLE STYLE FACTORIES
// ============================================
const headingStyle = (weight, letterSpacing) => ({
fontFamily: typography.fontFamily.heading,
fontWeight: weight,
...(letterSpacing && { letterSpacing }),
})
const coverSizing = () => ({
[`@media (min-width: ${breakpoints.sm}px)`]: {
height: sizing.cover.sm,
width: sizing.cover.sm,
minWidth: sizing.cover.sm,
},
[`@media (min-width: ${breakpoints.lg}px)`]: {
height: sizing.cover.lg,
width: sizing.cover.lg,
minWidth: sizing.cover.lg,
},
})
const customTooltipStyle = () => ({
display: 'inline',
position: 'absolute',
bottom: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginBottom: spacing.xs,
fontSize: '0.75rem',
whiteSpace: 'nowrap',
backgroundColor: colors.text.primary,
color: colors.background.primary,
padding: `${spacing.xs} ${spacing.sm}`,
borderRadius: radii.sm,
zIndex: 9999,
})
const actionButtonsStyle = () => ({
padding: `${spacing.lg} 0`,
alignItems: 'center',
'@global': {
button: {
border: '1px solid transparent',
backgroundColor: colors.background.secondary,
color: colors.text.secondary,
margin: `0 ${spacing.sm}`,
borderRadius: radii.full,
minWidth: 0,
padding: spacing.lg,
position: 'relative',
'&:hover': {
backgroundColor: `${colors.background.tertiary} !important`,
border: '1px solid transparent',
},
},
'button:first-child:not(:only-child)': {
[`@media screen and (max-width: ${breakpoints.md}px)`]: {
transform: 'scale(1.5)',
margin: spacing.lg,
'&:hover': {
transform: 'scale(1.6) !important',
},
},
transform: 'scale(2)',
margin: spacing.xl,
minWidth: 0,
padding: '0.3125rem',
transition: 'transform .3s ease',
background: colors.accent.main,
color: '#fff',
borderRadius: radii.full,
border: 0,
'&:hover': {
transform: 'scale(2.1)',
backgroundColor: `${colors.accent.main} !important`,
border: 0,
},
},
'button:only-child': {
margin: spacing.xl,
},
'button:first-child>span:first-child': {
padding: 0,
},
'button>span:first-child>span': {
display: 'none',
},
'button:not(:first-child):hover>span:first-child>span':
customTooltipStyle(),
'button:not(:first-child)>span:first-child>svg': {
color: colors.text.secondary,
},
},
})
const menuIconStyle = () => ({
color: colors.text.primary,
minWidth: sizing.iconMinWidth,
'& svg': {
fontSize: sizing.icon,
},
})
const activeLinkStyle = {
color: `${colors.accent.main} !important`,
'& .MuiListItemIcon-root': {
color: `${colors.accent.main} !important`,
},
}
// ============================================
// THEME DEFINITION
// ============================================
// Note: !important declarations are required to override react-admin and third-party component styles
const NautilineTheme = {
themeName: 'Nautiline',
palette: {
type: 'light',
primary: {
main: colors.accent.main,
contrastText: '#FFFFFF',
},
secondary: {
main: colors.accent.main,
contrastText: '#FFFFFF',
},
background: {
default: colors.background.primary,
paper: colors.background.primary,
},
text: {
primary: colors.text.primary,
secondary: colors.text.secondary,
},
action: {
active: colors.accent.main,
hover: colors.accent.faded,
selected: colors.accent.faded,
},
},
typography: {
fontFamily: typography.fontFamily.base,
h1: headingStyle(700, '-0.02em'),
h2: headingStyle(700, '-0.02em'),
h3: headingStyle(600, '-0.01em'),
h4: headingStyle(600),
h5: headingStyle(600),
h6: headingStyle(600),
subtitle1: { fontWeight: 500 },
subtitle2: { fontWeight: 500 },
body1: { fontWeight: 400 },
body2: { fontWeight: 400 },
button: { fontWeight: 500, textTransform: 'none' },
},
shape: {
borderRadius: radii.xl,
},
overrides: {
MuiCssBaseline: {
'@global': {
'@font-face': {
fontFamily: 'Unbounded',
fontStyle: 'normal',
fontWeight: '300 800',
fontDisplay: 'swap',
src: "url('/fonts/Unbounded-Variable.woff2') format('woff2')",
},
body: {
backgroundColor: colors.background.primary,
},
},
},
MuiAppBar: {
root: {
boxShadow: 'none',
borderBottom: `1px solid ${colors.ui.separator}`,
},
colorSecondary: {
backgroundColor: colors.background.primary,
color: colors.text.primary,
},
},
MuiToolbar: {
root: {
backgroundColor: colors.background.primary,
},
},
MuiPaper: {
root: {
backgroundColor: colors.background.primary,
},
elevation1: {
boxShadow: `0 0.0625rem 0.1875rem ${colors.ui.shadow}`,
},
elevation2: {
boxShadow: `0 0.125rem ${spacing.sm} ${colors.ui.shadow}`,
},
},
MuiCard: {
root: {
backgroundColor: colors.background.primary,
borderRadius: radii.xl,
boxShadow: `0 0.125rem ${spacing.sm} ${colors.ui.shadow}`,
},
},
MuiButton: {
root: {
borderRadius: radii.md,
textTransform: 'none',
fontWeight: 600,
},
contained: {
boxShadow: 'none',
'&:hover': { boxShadow: 'none' },
},
containedPrimary: {
backgroundColor: colors.accent.main,
'&:hover': {
backgroundColor: colors.accent.main,
filter: 'brightness(0.9)',
},
},
text: {
color: colors.accent.main,
},
},
MuiIconButton: {
root: {
color: colors.text.primary,
'&:hover': {
backgroundColor: colors.accent.faded,
},
},
colorPrimary: {
color: colors.accent.main,
},
sizeSmall: {
padding: spacing.md,
},
},
MuiSvgIcon: {
colorPrimary: {
color: colors.accent.main,
},
},
MuiCheckbox: {
root: {
color: 'rgba(0, 0, 0, 0.15)',
'&$checked': {
color: colors.accent.main,
},
},
},
MuiChip: {
root: {
backgroundColor: colors.background.secondary,
color: colors.text.primary,
borderRadius: radii.pill,
},
colorPrimary: {
backgroundColor: colors.accent.faded,
color: colors.accent.main,
},
},
MuiTableRow: {
root: {
'&:hover': {
backgroundColor: `${colors.accent.faded} !important`,
},
},
},
MuiTableCell: {
root: {
borderBottomColor: 'rgba(0, 0, 0, 0.04)',
},
head: {
backgroundColor: colors.background.secondary,
color: colors.text.secondary,
fontWeight: 600,
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
},
body: {
color: colors.text.primary,
},
},
MuiListItem: {
root: {
color: colors.text.primary,
'&:hover': {
backgroundColor: colors.accent.faded,
},
'&$selected': {
backgroundColor: colors.accent.faded,
color: colors.accent.main,
'& .MuiListItemIcon-root': {
color: colors.accent.main,
},
'&:hover': {
backgroundColor: colors.accent.faded,
},
},
},
button: {
color: colors.text.primary,
'&:hover': {
backgroundColor: colors.accent.faded,
color: colors.text.primary,
},
},
},
MuiListItemIcon: {
root: menuIconStyle(),
},
MuiListItemText: {
primary: {
color: 'inherit',
},
},
MuiMenuItem: {
root: {
fontSize: '0.875rem',
paddingTop: '4px',
paddingBottom: '4px',
paddingLeft: '10px',
margin: '5px',
borderRadius: radii.md,
color: colors.text.primary,
},
},
MuiDrawer: {
paper: {
backgroundColor: colors.background.primary,
borderRight: `1px solid ${colors.ui.separator}`,
},
},
MuiSlider: {
root: {
color: colors.accent.main,
},
track: {
backgroundColor: colors.accent.main,
},
thumb: {
backgroundColor: colors.accent.main,
'&:hover': {
boxShadow: `0 0 0 ${spacing.sm} ${colors.accent.faded}`,
},
},
rail: {
backgroundColor: colors.background.tertiary,
},
},
MuiLinearProgress: {
root: {
backgroundColor: colors.background.tertiary,
borderRadius: radii.sm,
},
bar: {
backgroundColor: colors.accent.main,
borderRadius: radii.sm,
},
},
MuiTabs: {
root: {
borderBottom: `1px solid ${colors.ui.separator}`,
},
indicator: {
backgroundColor: colors.accent.main,
height: '0.1875rem',
borderRadius: '0.1875rem 0.1875rem 0 0',
},
},
MuiTab: {
root: {
textTransform: 'none',
fontWeight: 500,
fontFamily: typography.fontFamily.heading,
'&$selected': {
color: colors.accent.main,
fontWeight: 600,
},
},
},
MuiInputBase: {
root: {
backgroundColor: colors.background.secondary,
borderRadius: radii.lg,
},
},
MuiOutlinedInput: {
root: {
borderRadius: radii.lg,
'& $notchedOutline': {
borderColor: colors.ui.separator,
},
'&:hover $notchedOutline': {
borderColor: colors.text.tertiary,
},
'&$focused $notchedOutline': {
borderColor: colors.accent.main,
borderWidth: '0.125rem',
},
},
},
MuiFilledInput: {
root: {
backgroundColor: colors.background.secondary,
borderRadius: radii.lg,
'&:hover': {
backgroundColor: colors.background.tertiary,
},
'&$focused': {
backgroundColor: colors.background.secondary,
},
},
},
MuiFab: {
primary: {
backgroundColor: colors.accent.main,
'&:hover': {
backgroundColor: colors.accent.main,
filter: 'brightness(0.9)',
},
},
},
MuiAvatar: {
root: {
borderRadius: radii.md,
},
},
MuiRating: {
iconFilled: {
color: colors.accent.main,
},
iconHover: {
color: colors.accent.main,
},
},
MuiTooltip: {
tooltip: {
backgroundColor: colors.text.primary,
color: colors.background.primary,
fontSize: '0.75rem',
padding: `${spacing.xs} ${spacing.sm}`,
borderRadius: radii.sm,
},
},
MuiBottomNavigation: {
root: {
backgroundColor: colors.ui.glassBg,
backdropFilter: `blur(${tokens.blur})`,
borderTop: `1px solid ${colors.ui.separator}`,
},
},
MuiBottomNavigationAction: {
root: {
color: colors.text.secondary,
'&$selected': {
color: colors.accent.main,
},
},
label: {
fontFamily: typography.fontFamily.heading,
fontSize: '0.65rem',
'&$selected': {
fontSize: '0.65rem',
},
},
},
NDAppBar: {
root: {
color: colors.text.primary,
},
},
NDLogin: {
main: {
backgroundColor: colors.background.primary,
},
card: {
backgroundColor: colors.background.primary,
borderRadius: radii.pill,
boxShadow: `0 ${spacing.xs} ${spacing.xl} ${colors.ui.shadow}`,
},
},
NDAlbumGridView: {
albumContainer: {
borderRadius: radii.md,
'& img': {
borderRadius: radii.md,
},
},
albumTitle: {
fontWeight: 600,
color: colors.text.primary,
},
albumSubtitle: {
color: colors.text.secondary,
},
albumPlayButton: {
backgroundColor: colors.accent.main,
borderRadius: radii.full,
boxShadow: `0 ${spacing.sm} ${spacing.sm} rgba(0, 0, 0, 0.15)`,
padding: '0.35rem',
transition: 'padding .3s ease',
'&:hover': {
backgroundColor: `${colors.accent.main} !important`,
padding: '0.45rem',
},
},
},
NDAlbumDetails: {
root: {
[`@media (max-width: ${breakpoints.xs}px)`]: {
padding: '0.7em',
width: '100%',
minWidth: 'unset',
},
},
cardContents: {
[`@media (max-width: ${breakpoints.xs}px)`]: {
flexDirection: 'column',
alignItems: 'center',
},
},
details: {
[`@media (max-width: ${breakpoints.xs}px)`]: {
width: '100%',
},
},
cover: {
borderRadius: radii.md,
},
coverParent: {
marginRight: spacing.xl,
[`@media (max-width: ${breakpoints.xs}px)`]: {
width: '100%',
height: 'auto',
minWidth: 'unset',
aspectRatio: '1',
marginRight: 0,
marginBottom: spacing.lg,
},
...coverSizing(),
},
recordName: {
fontSize: '1.75rem',
fontWeight: 700,
marginBottom: '0.15rem',
},
recordArtist: {
marginBottom: spacing.md,
},
recordMeta: {
marginBottom: spacing.sm,
},
genreList: {
marginTop: spacing.md,
},
loveButton: {
marginLeft: spacing.sm,
},
},
NDAlbumShow: {
albumActions: actionButtonsStyle(),
},
NDPlaylistShow: {
playlistActions: actionButtonsStyle(),
},
NDSubMenu: {
icon: menuIconStyle(),
menuHeader: {
color: colors.text.primary,
'& .MuiTypography-root': {
color: colors.text.primary,
},
},
actionIcon: {
marginLeft: spacing.sm,
},
},
RaMenuItemLink: {
root: {
color: `${colors.text.primary} !important`,
'& .MuiListItemIcon-root': menuIconStyle(),
'&[class*="makeStyles-active"]': activeLinkStyle,
},
active: activeLinkStyle,
},
NDDesktopArtistDetails: {
root: {
[`@media (min-width: ${breakpoints.sm}px)`]: {
padding: '1em',
},
[`@media (min-width: ${breakpoints.lg}px)`]: {
padding: '1em',
},
},
cover: {
borderRadius: radii.md,
...coverSizing(),
},
artistImage: {
borderRadius: radii.md,
marginRight: spacing.xl,
[`@media (min-width: ${breakpoints.sm}px)`]: {
height: sizing.cover.sm,
width: sizing.cover.sm,
minWidth: sizing.cover.sm,
maxHeight: sizing.cover.sm,
minHeight: sizing.cover.sm,
},
[`@media (min-width: ${breakpoints.lg}px)`]: {
height: sizing.cover.lg,
width: sizing.cover.lg,
minWidth: sizing.cover.lg,
maxHeight: sizing.cover.lg,
minHeight: sizing.cover.lg,
},
},
artistName: {
fontSize: '1.75rem',
fontWeight: 700,
marginBottom: spacing.sm,
},
},
NDMobileArtistDetails: {
cover: {
borderRadius: radii.md,
},
artistImage: {
borderRadius: radii.md,
},
},
RaList: {
content: {
overflow: 'visible',
},
},
RaBulkActionsToolbar: {
topToolbar: {
backgroundColor: 'transparent',
boxShadow: 'none',
padding: spacing.sm,
'@global': {
button: {
border: '1px solid transparent',
backgroundColor: colors.background.secondary,
color: colors.text.secondary,
margin: `0 ${spacing.xs}`,
borderRadius: radii.full,
minWidth: 0,
padding: spacing.sm,
position: 'relative',
'&:hover': {
backgroundColor: `${colors.background.tertiary} !important`,
border: '1px solid transparent',
},
},
'button>span:first-child>span': {
display: 'none',
},
'button:hover>span:first-child>span': customTooltipStyle(),
'button>span:first-child>svg': {
color: colors.text.secondary,
},
},
},
},
RaPaginationActions: {
currentPageButton: {
backgroundColor: colors.accent.faded,
},
},
},
player: {
theme: 'light',
stylesheet: `
@font-face {
font-family: 'Unbounded';
font-style: normal;
font-weight: 300 800;
font-display: swap;
src: url('/fonts/Unbounded-Variable.woff2') format('woff2');
}
.react-jinke-music-player-main {
background-color: ${colors.background.primary} !important;
font-family: ${typography.fontFamily.base} !important;
}
.react-jinke-music-player-main .music-player-panel {
background-color: ${colors.ui.glassBg} !important;
backdrop-filter: blur(${tokens.blur}) !important;
-webkit-backdrop-filter: blur(${tokens.blur}) !important;
border-top: 1px solid ${colors.ui.separator} !important;
box-shadow: 0 -0.125rem 1.25rem rgba(0, 0, 0, 0.06) !important;
}
.react-jinke-music-player-main svg {
color: ${colors.text.primary} !important;
}
.react-jinke-music-player-main svg:hover {
color: ${colors.accent.main} !important;
}
.react-jinke-music-player-main .rc-slider-track,
.react-jinke-music-player-main .rc-slider-handle {
background-color: ${colors.accent.main} !important;
}
.react-jinke-music-player-main .rc-slider-handle {
border-color: ${colors.accent.main} !important;
}
.react-jinke-music-player-main .rc-slider-rail {
background-color: ${colors.background.secondary} !important;
}
.react-jinke-music-player-main .rc-slider {
height: 4px !important;
}
.react-jinke-music-player-main .rc-slider-rail,
.react-jinke-music-player-main .rc-slider-track {
height: 4px !important;
border-radius: 2px !important;
}
.react-jinke-music-player-main .rc-slider-handle {
width: 12px !important;
height: 12px !important;
margin-top: -4px !important;
}
.react-jinke-music-player-main .audio-lists-panel,
.react-jinke-music-player-main .audio-lists-panel-content {
background-color: ${colors.background.primary} !important;
}
.react-jinke-music-player-main .audio-lists-panel-content .audio-item {
background-color: transparent !important;
color: ${colors.text.primary} !important;
}
.react-jinke-music-player-main .audio-lists-panel-content .audio-item:hover {
background-color: ${colors.accent.faded} !important;
}
.react-jinke-music-player-main .audio-lists-panel-content .audio-item.playing {
background-color: ${colors.accent.faded} !important;
color: ${colors.accent.main} !important;
}
.react-jinke-music-player-main .lyric-btn-active,
.react-jinke-music-player-main .play-mode-title {
color: ${colors.accent.main} !important;
}
.react-jinke-music-player-main .music-player-panel .player-content .music-player-controller .music-player-info .music-player-title {
color: ${colors.text.primary} !important;
font-weight: 600 !important;
font-family: ${typography.fontFamily.heading} !important;
}
.react-jinke-music-player-main .music-player-panel .player-content .music-player-controller .music-player-info .music-player-artist {
color: ${colors.text.secondary} !important;
}
.react-jinke-music-player-main.mini-player {
background-color: ${colors.ui.glassBg} !important;
backdrop-filter: blur(${tokens.blur}) !important;
-webkit-backdrop-filter: blur(${tokens.blur}) !important;
border-radius: ${radii.xl} !important;
box-shadow: 0 ${spacing.xs} 1.25rem rgba(0, 0, 0, 0.08) !important;
}
.MuiTypography-h1,
.MuiTypography-h2,
.MuiTypography-h3,
.MuiTypography-h4,
.MuiTypography-h5,
.MuiTypography-h6 {
font-family: ${typography.fontFamily.heading} !important;
}
`,
},
}
export default NautilineTheme