Compare commits

..

1 Commits

29 changed files with 1913 additions and 1638 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.4
github.com/sirupsen/logrus v1.9.3
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.11.1
go.senan.xyz/taglib v0.0.0-00010101000000-000000000000
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-20260115054156-294ebfa9ad83 // indirect
github.com/google/pprof v0.0.0-20260111202518-71be6bfdd440 // 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-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
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/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.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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,6 +275,7 @@ 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=
@@ -363,6 +364,7 @@ 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.5
github.com/onsi/gomega v1.39.0
github.com/onsi/ginkgo/v2 v2.27.3
github.com/onsi/gomega v1.38.3
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
golang.org/x/tools v0.41.0
golang.org/x/tools v0.40.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-20260115054156-294ebfa9ad83 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.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-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
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/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.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/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/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.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/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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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=
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=
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!",
"minItems": 1,
"default": [{}],
"items": {
"type": "object",
"properties": {
@@ -63,7 +63,7 @@
}
}
},
"required": ["clientid", "users"]
"required": ["clientid"]
},
"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!",
"minItems": 1,
"default": [{}],
"items": {
"type": "object",
"properties": {
@@ -67,7 +67,7 @@
}
}
},
"required": ["clientid", "users"]
"required": ["clientid"]
},
"uiSchema": {
"type": "VerticalLayout",

View File

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,8 @@
"bitDepth": "Λίγο βάθος",
"sampleRate": "Ποσοστό δειγματοληψίας",
"missing": "Απών",
"libraryName": "Βιβλιοθήκη"
"libraryName": "Βιβλιοθήκη",
"composer": "Συνθέτης"
},
"actions": {
"addToQueue": "Αναπαραγωγη Μετα",
@@ -328,6 +329,80 @@
"scanInProgress": "Σάρωση σε εξέλιξη...",
"noLibrariesAssigned": "Δεν έχουν αντιστοιχιστεί βιβλιοθήκες σε αυτόν τον χρήστη"
}
},
"plugin": {
"name": "Πρόσθετο |||| Πρόσθετα",
"fields": {
"id": "ID",
"name": "Όνομα",
"description": "Περιγραφή",
"version": "Έκδοση",
"author": "Καλλιτέχνης",
"website": "Ιστοσελίδα",
"permissions": "Άδειες",
"enabled": "Ενεργό",
"status": "Κατάσταση",
"path": "Διαδρομή",
"lastError": "Σφάλμα",
"hasError": "Σφάλμα",
"updatedAt": "Ενημερώθηκε",
"createdAt": "Εγκατασταθηκε",
"configKey": "Κλειδί",
"configValue": "Τιμή",
"allUsers": "Επιτρέψτε όλους τους χρήστες",
"selectedUsers": "Επιλογή χρηστών",
"allLibraries": "Επιτρέψτε όλες τις βιβλιοθήκες",
"selectedLibraries": "Επιλεγμένες βιβλιοθήκες"
},
"sections": {
"status": "Κατάσταση",
"info": "Πληροφορίες Πρόσθετου",
"configuration": "Παραμετροποίηση",
"manifest": "Manifest",
"usersPermission": "Άδειες Χρηστών",
"libraryPermission": "Άδειες Βιβλιοθηκών"
},
"status": {
"enabled": "Ενεργό",
"disabled": "Ανενεργό"
},
"actions": {
"enable": "Ενεργοποίηση",
"disable": "Απενεργοποίηση",
"disabledDueToError": "Διορθώστε το σφάλμα πριν την ενεργοποίηση",
"disabledUsersRequired": "Επιλέξτε χρήστες πριν την ενεργοποίηση",
"disabledLibrariesRequired": "Επιλέξτε βιβλιοθήκες πριν την ενεργοποίηση",
"addConfig": "Προσθήκη παραμετροποίησης",
"rescan": "Σάρωση ξανά"
},
"notifications": {
"enabled": "Πρόσθετο ενεργοποιημένο",
"disabled": "Πρόσθετο απενεργοποιημένο",
"updated": "Πρόσθετο ενημερωμένο",
"error": "Σφάλμα κατά την ενημέρωση του πρόσθετου"
},
"validation": {
"invalidJson": "Η παραμετροποίηση πρέπει να είναι συμβατό JSON"
},
"messages": {
"configHelp": "Παραμετροποιήστε το πρόσθετο με χρήση ζεύγων κλειδιών-τιμών. Αφήστε κενό αν το πρόσθετο δεν απαιτεί παραμετροποίηση",
"clickPermissions": "Κάνετε κλικ για λεπτομέρειες αδειών",
"noConfig": "Δεν ορίστηκε παραμετροποίηση",
"allUsersHelp": "Όταν είναι ενεργό, το πρόσθετο θα έχει πρόσβαση σε όλους τους χρήστες, συμπεριλαμβανομένων και όσων δημιουργηθούν στο μέλλον.",
"noUsers": "Δεν επιλέχθηκαν χρήστες",
"permissionReason": "Αιτία",
"usersRequired": "Το πρόσθετο απαιτεί πρόσβαση στις πληροφορίες χρηστών. Ορίστε τους χρήστες που θα έχει πρόσβαση το πρόσθετο, ή ενεργοποιήστε το 'Επιτρέψτε όλους τους χρήστες'",
"allLibrariesHelp": "Όταν είναι ενεργό, το πρόσθετο θα έχει πρόσβαση σε όλες τις βιβλιοθήκες, συμπεριλαμβανομένων και όσων δημιουργηθούν στο μέλλον.",
"noLibraries": "Δεν επιλέχθηκαν βιβλιοθήκες",
"librariesRequired": "Αυτό το πρόσθετο απαιτεί πρόσβαση στις πληροφορίες βιβλιοθήκης. Επιλέξτε σε ποιές βιβλιοθήκες μπορεί να έχει πρόσβαση το πρόσθετο, ή ενεργοποιήστε το 'Επιτρέψτε όλες τις βιβλιοθήκες'",
"requiredHosts": "Απαιτούμενοι hosts",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "κλειδί",
"configValue": "τιμή"
}
}
},
"ra": {

View File

@@ -12,16 +12,12 @@
"artist": "Artista",
"album": "Álbum",
"path": "Ruta del archivo",
"libraryName": "Biblioteca",
"genre": "Género",
"compilation": "Compilación",
"year": "Año",
"size": "Tamaño del archivo",
"updatedAt": "Actualizado el",
"bitRate": "Tasa de bits",
"bitDepth": "Profundidad de bits",
"sampleRate": "Frecuencia de muestreo",
"channels": "Canales",
"discSubtitle": "Subtítulo del disco",
"starred": "Favorito",
"comment": "Comentario",
@@ -29,6 +25,7 @@
"quality": "Calidad",
"bpm": "BPM",
"playDate": "Últimas reproducciones",
"channels": "Canales",
"createdAt": "Creado el",
"grouping": "Agrupación",
"mood": "Estado de ánimo",
@@ -36,17 +33,21 @@
"tags": "Etiquetas",
"mappedTags": "Etiquetas asignadas",
"rawTags": "Etiquetas sin procesar",
"missing": "Faltante"
"bitDepth": "Profundidad de bits",
"sampleRate": "Frecuencia de muestreo",
"missing": "Faltante",
"libraryName": "Biblioteca",
"composer": "Compositor"
},
"actions": {
"addToQueue": "Reproducir después",
"playNow": "Reproducir ahora",
"addToPlaylist": "Agregar a la playlist",
"showInPlaylist": "Mostrar en la lista de reproducción",
"shuffleAll": "Todas aleatorias",
"download": "Descarga",
"playNext": "Siguiente",
"info": "Obtener información"
"info": "Obtener información",
"showInPlaylist": "Mostrar en la lista de reproducción"
}
},
"album": {
@@ -57,38 +58,38 @@
"duration": "Duración",
"songCount": "Canciones",
"playCount": "Reproducciones",
"size": "Tamaño del archivo",
"name": "Nombre",
"libraryName": "Biblioteca",
"genre": "Género",
"compilation": "Compilación",
"year": "Año",
"date": "Fecha de grabación",
"originalDate": "Original",
"releaseDate": "Publicado",
"releases": "Lanzamiento |||| Lanzamientos",
"released": "Publicado",
"updatedAt": "Actualizado el",
"comment": "Comentario",
"rating": "Calificación",
"createdAt": "Creado el",
"size": "Tamaño del archivo",
"originalDate": "Original",
"releaseDate": "Publicado",
"releases": "Lanzamiento |||| Lanzamientos",
"released": "Publicado",
"recordLabel": "Discográfica",
"catalogNum": "Número de catálogo",
"releaseType": "Tipo de lanzamiento",
"grouping": "Agrupación",
"media": "Medios",
"mood": "Estado de ánimo",
"missing": "Faltante"
"date": "Fecha de grabación",
"missing": "Faltante",
"libraryName": "Biblioteca"
},
"actions": {
"playAll": "Reproducir",
"playNext": "Reproducir siguiente",
"addToQueue": "Reproducir después",
"share": "Compartir",
"shuffle": "Aleatorio",
"addToPlaylist": "Agregar a la lista",
"download": "Descargar",
"info": "Obtener información"
"info": "Obtener información",
"share": "Compartir"
},
"lists": {
"all": "Todos",
@@ -106,10 +107,10 @@
"name": "Nombre",
"albumCount": "Número de álbumes",
"songCount": "Número de canciones",
"size": "Tamaño",
"playCount": "Reproducciones",
"rating": "Calificación",
"genre": "Género",
"size": "Tamaño",
"role": "Rol",
"missing": "Faltante"
},
@@ -130,9 +131,9 @@
"maincredit": "Artista del álbum o Artista |||| Artistas del álbum o Artistas"
},
"actions": {
"topSongs": "Más destacadas",
"shuffle": "Aleatorio",
"radio": "Radio"
"radio": "Radio",
"topSongs": "Más destacadas"
}
},
"user": {
@@ -141,7 +142,6 @@
"userName": "Nombre de usuario",
"isAdmin": "Es administrador",
"lastLoginAt": "Último inicio de sesión",
"lastAccessAt": "Último acceso",
"updatedAt": "Actualizado el",
"name": "Nombre",
"password": "Contraseña",
@@ -150,6 +150,7 @@
"currentPassword": "Contraseña actual",
"newPassword": "Nueva contraseña",
"token": "Token",
"lastAccessAt": "Último acceso",
"libraries": "Bibliotecas"
},
"helperTexts": {
@@ -211,9 +212,9 @@
"selectPlaylist": "Seleccione una lista:",
"addNewPlaylist": "Creada \"%{name}\"",
"export": "Exportar",
"saveQueue": "Guardar la fila de reproducción en una playlist",
"makePublic": "Hazla pública",
"makePrivate": "Hazla privada",
"saveQueue": "Guardar la fila de reproducción en una playlist",
"searchOrCreate": "Buscar listas de reproducción o escribe para crear una nueva…",
"pressEnterToCreate": "Pulsa Enter para crear una nueva lista de reproducción",
"removeFromSelection": "Quitar de la selección"
@@ -244,7 +245,6 @@
"username": "Compartido por",
"url": "URL",
"description": "Descripción",
"downloadable": "¿Permitir descargas?",
"contents": "Contenido",
"expiresAt": "Caduca el",
"lastVisitedAt": "Visitado por última vez el",
@@ -252,14 +252,12 @@
"format": "Formato",
"maxBitRate": "Tasa de bits Máx.",
"updatedAt": "Actualizado el",
"createdAt": "Creado el"
},
"notifications": {},
"actions": {}
"createdAt": "Creado el",
"downloadable": "¿Permitir descargas?"
}
},
"missing": {
"name": "Fichero faltante |||| Ficheros faltantes",
"empty": "No faltan archivos",
"fields": {
"path": "Ruta",
"size": "Tamaño",
@@ -272,7 +270,8 @@
},
"notifications": {
"removed": "Eliminado"
}
},
"empty": "No faltan archivos"
},
"library": {
"name": "Biblioteca |||| Bibliotecas",
@@ -302,20 +301,20 @@
},
"actions": {
"scan": "Escanear biblioteca",
"quickScan": "Escaneo rápido",
"fullScan": "Escaneo completo",
"manageUsers": "Gestionar el acceso de usarios",
"viewDetails": "Ver detalles"
"viewDetails": "Ver detalles",
"quickScan": "Escaneo rápido",
"fullScan": "Escaneo completo"
},
"notifications": {
"created": "La biblioteca se creó correctamente",
"updated": "La biblioteca se actualizó correctamente",
"deleted": "La biblioteca se eliminó correctamente",
"scanStarted": "El escaneo de la biblioteca ha comenzado",
"scanCompleted": "El escaneo de la biblioteca se completó",
"quickScanStarted": "Escaneo rápido ha comenzado",
"fullScanStarted": "Escaneo completo ha comenzado",
"scanError": "Error al iniciar el escaneo. Revisa los registros",
"scanCompleted": "El escaneo de la biblioteca se completó"
"scanError": "Error al iniciar el escaneo. Revisa los registros"
},
"validation": {
"nameRequired": "El nombre de la biblioteca es obligatorio",
@@ -396,7 +395,9 @@
"allLibrariesHelp": "Cuando se active, el plugin tendrá acceso a todas las bibliotecas, incluidas las que se creen en el futuro.",
"noLibraries": "Ninguna biblioteca seleccionada",
"librariesRequired": "Este plugin requiere acceso a la información de las bibliotecas. Selecciona a qué bibliotecas puede acceder el plugin, o activa 'Permitir todas las bibliotecas'.",
"requiredHosts": "Hosts requeridos"
"requiredHosts": "Hosts requeridos",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "clave",
@@ -439,7 +440,6 @@
"add": "Añadir",
"back": "Ir atrás",
"bulk_actions": "1 elemento seleccionado |||| %{smart_count} elementos seleccionados",
"bulk_actions_mobile": "1 |||| %{smart_count}",
"cancel": "Cancelar",
"clear_input_value": "Limpiar valor",
"clone": "Duplicar",
@@ -463,6 +463,7 @@
"close_menu": "Cerrar menú",
"unselect": "Deseleccionado",
"skip": "Omitir",
"bulk_actions_mobile": "1 |||| %{smart_count}",
"share": "Compartir",
"download": "Descargar"
},
@@ -554,47 +555,41 @@
"transcodingDisabled": "Cambiar la configuración de la transcodificación a través de la interfaz web esta deshabilitado por motivos de seguridad. Si quieres cambiar (editar o agregar) opciones de transcodificación, reinicia el servidor con la %{config} opción de configuración.",
"transcodingEnabled": "Navidrom se esta ejecutando con %{config}, lo que hace posible ejecutar comandos de sistema desde el apartado de transcodificación en la interfaz web. Recomendamos deshabilitarlo por motivos de seguridad y solo habilitarlo cuando se este configurando opciones de transcodificación.",
"songsAddedToPlaylist": "1 canción agregada a la lista |||| %{smart_count} canciones agregadas a la lista",
"noSimilarSongsFound": "No se encontraron canciones similares",
"noTopSongsFound": "No se encontraron canciones destacadas",
"noPlaylistsAvailable": "Ninguna lista disponible",
"delete_user_title": "Eliminar usuario '%{name}'",
"delete_user_content": "¿Esta seguro de eliminar a este usuario y todos sus datos (incluyendo listas y preferencias)?",
"remove_missing_title": "Eliminar archivos faltantes",
"remove_missing_content": "¿Realmente desea eliminar los archivos faltantes seleccionados de la base de datos? Esto eliminará permanentemente cualquier referencia a ellos, incluidas sus reproducciones y valoraciones.",
"remove_all_missing_title": "Eliminar todos los archivos faltantes",
"remove_all_missing_content": "¿Realmente desea eliminar todos los archivos faltantes de la base de datos? Esto eliminará permanentemente cualquier referencia a ellos, incluidas sus reproducciones y valoraciones.",
"notifications_blocked": "Las notificaciones de este sitio están bloqueadas en tu navegador",
"notifications_not_available": "Este navegador no soporta notificaciones o no ingresaste a Navidrome usando https",
"lastfmLinkSuccess": "Last.fm esta conectado y el scrobbling esta activado",
"lastfmLinkFailure": "No se pudo conectar con Last.fm",
"lastfmUnlinkSuccess": "Last.fm se ha desconectado y el scrobbling se desactivo",
"lastfmUnlinkFailure": "No se pudo desconectar Last.fm",
"listenBrainzLinkSuccess": "Se ha conectado correctamente a ListenBrainz y se activó el scrobbling como el usuario: %{user}",
"listenBrainzLinkFailure": "No se pudo conectar con ListenBrainz: %{error}",
"listenBrainzUnlinkSuccess": "Se desconectó ListenBrainz y se desactivó el scrobbling",
"listenBrainzUnlinkFailure": "No se pudo desconectar ListenBrainz",
"openIn": {
"lastfm": "Ver en Last.fm",
"musicbrainz": "Ver en MusicBrainz"
},
"lastfmLink": "Leer más...",
"listenBrainzLinkSuccess": "Se ha conectado correctamente a ListenBrainz y se activó el scrobbling como el usuario: %{user}",
"listenBrainzLinkFailure": "No se pudo conectar con ListenBrainz: %{error}",
"listenBrainzUnlinkSuccess": "Se desconectó ListenBrainz y se desactivó el scrobbling",
"listenBrainzUnlinkFailure": "No se pudo desconectar ListenBrainz",
"downloadOriginalFormat": "Descargar formato original",
"shareOriginalFormat": "Compartir formato original",
"shareDialogTitle": "Compartir %{resource} '%{name}'",
"shareBatchDialogTitle": "Compartir 1 %{resource} |||| Compartir %{smart_count} %{resource}",
"shareCopyToClipboard": "Copiar al portapapeles: Ctrl+C, Intro",
"shareSuccess": "URL copiada al portapapeles: %{url}",
"shareFailure": "Error al copiar la URL %{url} al portapapeles",
"downloadDialogTitle": "Descargar %{resource} '%{name}' (%{size})",
"downloadOriginalFormat": "Descargar formato original"
"shareCopyToClipboard": "Copiar al portapapeles: Ctrl+C, Intro",
"remove_missing_title": "Eliminar archivos faltantes",
"remove_missing_content": "¿Realmente desea eliminar los archivos faltantes seleccionados de la base de datos? Esto eliminará permanentemente cualquier referencia a ellos, incluidas sus reproducciones y valoraciones.",
"remove_all_missing_title": "Eliminar todos los archivos faltantes",
"remove_all_missing_content": "¿Realmente desea eliminar todos los archivos faltantes de la base de datos? Esto eliminará permanentemente cualquier referencia a ellos, incluidas sus reproducciones y valoraciones.",
"noSimilarSongsFound": "No se encontraron canciones similares",
"noTopSongsFound": "No se encontraron canciones destacadas"
},
"menu": {
"library": "Biblioteca",
"librarySelector": {
"allLibraries": "Todas las bibliotecas (%{count})",
"multipleLibraries": "%{selected} de %{total} bibliotecas",
"selectLibraries": "Seleccionar bibliotecas",
"none": "Ninguno"
},
"settings": "Ajustes",
"version": "Versión",
"theme": "Tema",
@@ -605,7 +600,6 @@
"language": "Idioma",
"defaultView": "Vista por defecto",
"desktop_notifications": "Notificaciones de escritorio",
"lastfmNotConfigured": "La clave API de Last.fm no está configurada",
"lastfmScrobbling": "Scrobble a Last.fm",
"listenBrainzScrobbling": "Scrobble a ListenBrainz",
"replaygain": "Modo de ReplayGain",
@@ -614,13 +608,20 @@
"none": "Desactivado",
"album": "Ganancia del álbum",
"track": "Ganancia de pista"
}
},
"lastfmNotConfigured": "La clave API de Last.fm no está configurada"
}
},
"albumList": "Álbumes",
"about": "Acerca de",
"playlists": "Playlists",
"sharedPlaylists": "Playlists Compartidas",
"about": "Acerca de"
"librarySelector": {
"allLibraries": "Todas las bibliotecas (%{count})",
"multipleLibraries": "%{selected} de %{total} bibliotecas",
"selectLibraries": "Seleccionar bibliotecas",
"none": "Ninguno"
}
},
"player": {
"playListsText": "Fila de reproducción",
@@ -679,17 +680,12 @@
"totalScanned": "Total de carpetas escaneadas",
"quickScan": "Escaneo rápido",
"fullScan": "Escaneo completo",
"selectiveScan": "Selectivo",
"serverUptime": "Uptime del servidor",
"serverDown": "OFFLINE",
"scanType": "Tipo",
"status": "Error de escaneo",
"elapsedTime": "Tiempo transcurrido"
},
"nowPlaying": {
"title": "En reproducción",
"empty": "Nada en reproducción",
"minutesAgo": "Hace %{smart_count} minuto |||| Hace %{smart_count} minutos"
"elapsedTime": "Tiempo transcurrido",
"selectiveScan": "Selectivo"
},
"help": {
"title": "Atajos de teclado de Navidrome",
@@ -699,10 +695,15 @@
"toggle_play": "Reproducir / Pausar",
"prev_song": "Canción anterior",
"next_song": "Siguiente canción",
"current_song": "Canción actual",
"vol_up": "Subir volumen",
"vol_down": "Bajar volumen",
"toggle_love": "Marca esta canción como favorita"
"toggle_love": "Marca esta canción como favorita",
"current_song": "Canción actual"
}
},
"nowPlaying": {
"title": "En reproducción",
"empty": "Nada en reproducción",
"minutesAgo": "Hace %{smart_count} minuto |||| Hace %{smart_count} minutos"
}
}
}

View File

@@ -36,7 +36,8 @@
"bitDepth": "Bittisyvyys",
"sampleRate": "Näytteenottotaajuus",
"missing": "Puuttuva",
"libraryName": "Kirjasto"
"libraryName": "Kirjasto",
"composer": ""
},
"actions": {
"addToQueue": "Lisää jonoon",
@@ -328,6 +329,80 @@
"scanInProgress": "Skannaus käynnissä...",
"noLibrariesAssigned": "Tälle käyttäjälle ei ole määritetty kirjastoja"
}
},
"plugin": {
"name": "Liitännäinen |||| Liitännäiset",
"fields": {
"id": "ID",
"name": "Nimi",
"description": "Kuvaus",
"version": "Versio",
"author": "Tekijä",
"website": "Verkkosivusto",
"permissions": "Oikeudet",
"enabled": "Käytössä",
"status": "Tila",
"path": "Polku",
"lastError": "Virhe",
"hasError": "Virhe",
"updatedAt": "Päivitetty",
"createdAt": "Asennettu",
"configKey": "Avain",
"configValue": "Arvo",
"allUsers": "Salli kaikki käyttäjät",
"selectedUsers": "Valitut käyttäjät",
"allLibraries": "Salli kaikki kirjastot",
"selectedLibraries": "Valitut kirjastot"
},
"sections": {
"status": "Tila",
"info": "Lisäosan tiedot",
"configuration": "Määritykset",
"manifest": "Luettelo",
"usersPermission": "Käyttäjäoikeudet",
"libraryPermission": "Kirjaston oikeudet"
},
"status": {
"enabled": "Käytössä",
"disabled": "Ei käytössä"
},
"actions": {
"enable": "Ota käyttöön",
"disable": "Poista käytöstä",
"disabledDueToError": "Korjaa virhe ennen käyttöönottoa",
"disabledUsersRequired": "Valitse käyttäjät ennen käyttöönottoa",
"disabledLibrariesRequired": "Valitse kirjastot ennen käyttöönottoa",
"addConfig": "Lisää määritykset",
"rescan": "Skannaa uudelleen"
},
"notifications": {
"enabled": "Lisäosa käytössä",
"disabled": "Lisäosa ei käytössä",
"updated": "Lisäosa päivitetty",
"error": "Virhe lisäosaa päivitettäessä"
},
"validation": {
"invalidJson": "Määrityksen on oltava kelvollinen JSON"
},
"messages": {
"configHelp": "Määritä lisäosa avain-arvo-parien avulla. Jätä tyhjäksi, jos lisäosa ei vaadi määrityksiä.",
"clickPermissions": "Napsauta käyttöoikeutta saadaksesi lisätietoja",
"noConfig": "Ei määritettyjä asetuksia",
"allUsersHelp": "Kun tämä on käytössä, laajennuksella on pääsy kaikkiin käyttäjiin, myös tulevaisuudessa luotaviin.",
"noUsers": "Ei valittuja käyttäjiä",
"permissionReason": "Syy",
"usersRequired": "Tämä laajennus vaatii pääsyn käyttäjätietoihin. Valitse käyttäjät, joihin laajennus voi päästä, tai ota käyttöön 'Salli kaikki käyttäjät'.",
"allLibrariesHelp": "Kun tämä on käytössä, laajennuksella on pääsy kaikkiin kirjastoihin, myös tulevaisuudessa luotaviin.",
"noLibraries": "Ei valittuja kirjastoja",
"librariesRequired": "Tämä laajennus vaatii pääsyn kirjastotietoihin. Valitse, mihin kirjastoihin laajennus voi käyttää, tai ota käyttöön 'Salli kaikki kirjastot'.",
"requiredHosts": "Vaaditut palvelimet",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "avain",
"configValue": "arvo"
}
}
},
"ra": {
@@ -586,16 +661,16 @@
},
"tabs": {
"about": "Tietoja",
"config": "Kokoonpano"
"config": "Määritykset"
},
"config": {
"configName": "Konfiguraation nimi",
"environmentVariable": "Ympäristömuuttuja",
"currentValue": "Nykyinen arvo",
"configurationFile": "Konfiguraatiotiedosto",
"exportToml": "Vie konfiguraatio (TOML)",
"exportSuccess": "Konfiguraatio viety leikepöydälle TOML-muodossa",
"exportFailed": "Konfiguraation kopiointi epäonnistui",
"configurationFile": "Määritystiedosto",
"exportToml": "Vie määritys (TOML)",
"exportSuccess": "Määritykset viety leikepöydälle TOML-muodossa",
"exportFailed": "Määritysten kopiointi epäonnistui",
"devFlagsHeader": "Kehitysliput (voivat muuttua/poistua)",
"devFlagsComment": "Nämä ovat kokeellisia asetuksia ja ne voidaan poistaa tulevissa versioissa"
}

View File

@@ -36,7 +36,8 @@
"bitDepth": "Calidade de Bit",
"sampleRate": "Taxa de mostra",
"missing": "Falta",
"libraryName": "Biblioteca"
"libraryName": "Biblioteca",
"composer": "Composición"
},
"actions": {
"addToQueue": "Ao final da cola",
@@ -328,6 +329,80 @@
"scanInProgress": "Escaneo en progreso…",
"noLibrariesAssigned": "Sen bibliotecas asignadas a esta usuaria"
}
},
"plugin": {
"name": "Complemento |||| Complementos",
"fields": {
"id": "ID",
"name": "Nome",
"description": "Descrición",
"version": "Versión",
"author": "Autoría",
"website": "Sitio web",
"permissions": "Permisos",
"enabled": "Activado",
"status": "Estado",
"path": "Ruta",
"lastError": "Erro",
"hasError": "Erro",
"updatedAt": "Actualizado",
"createdAt": "Instalado",
"configKey": "Clave",
"configValue": "Valor",
"allUsers": "Para todas as usuarias",
"selectedUsers": "Usuarias seleccionadas",
"allLibraries": "Permitir todas as bibliotecas",
"selectedLibraries": "Selecciona bibliotecas"
},
"sections": {
"status": "Estado",
"info": "Info do complemento",
"configuration": "Configuración",
"manifest": "Manifesto",
"usersPermission": "Permiso sobre usuarias",
"libraryPermission": "Permiso sobre bibliotecas"
},
"status": {
"enabled": "Activado",
"disabled": "Desactivado"
},
"actions": {
"enable": "Activar",
"disable": "Desactivar",
"disabledDueToError": "Arranxar erro antes de activar",
"disabledUsersRequired": "Selección de usuarias antes de activar",
"disabledLibrariesRequired": "Selección de bibliotecas antes de activar",
"addConfig": "Engadir configuración",
"rescan": "Volver a escanear"
},
"notifications": {
"enabled": "Complemento activado",
"disabled": "Complemento desactivado",
"updated": "Complemento actualizado",
"error": "Erro ao actualizar o complemento"
},
"validation": {
"invalidJson": "A configuración debe ser un JSON válido"
},
"messages": {
"configHelp": "Configura o complemento usando pares clave-valor. Deixa baleiro se o complemento non require configuración.",
"clickPermissions": "Preme nun permiso para ver detalles",
"noConfig": "Sen configuración establecida",
"allUsersHelp": "Ao activalo, o complemento terá acceso a todas as usuarias, incluíndo aquelas que se creen no futuro.",
"noUsers": "Sen usuarias seleccionadas",
"permissionReason": "Motivo",
"usersRequired": "O complemento precisa acceso á información sobre a usuaria. Selecciona as usuarias ás que pode acceder, ou activa 'Todas as usuarias'.",
"allLibrariesHelp": "Ao activalo, o complemento terá acceso a todas as bibliotecas, incluíndo aquelas que se creen no futuro.",
"noLibraries": "Sen bibliotecas seleccionadas",
"librariesRequired": "O complemento precisa acceso á información sobre a biblioteca. Selecciona as bibliotecas ás que pode acceder, ou activa 'Todas as bibliotecas'.",
"requiredHosts": "Servidores requeridos",
"configValidationError": "Fallou a comprobación da configuración:",
"schemaRenderError": "Non se puido aplicar a configuración. O esquema do complemento podería non ser válido."
},
"placeholders": {
"configKey": "clave",
"configValue": "valor"
}
}
},
"ra": {

View File

@@ -36,7 +36,8 @@
"bitDepth": "Bit diepte",
"sampleRate": "Sample waarde",
"missing": "Ontbrekend",
"libraryName": "Bibliotheek"
"libraryName": "Bibliotheek",
"composer": ""
},
"actions": {
"addToQueue": "Voeg toe aan wachtrij",
@@ -328,6 +329,80 @@
"scanInProgress": "Scan is bezig...",
"noLibrariesAssigned": "Geen bibliotheken aan deze gebruiker toegewezen"
}
},
"plugin": {
"name": "Plugin |||| Plugins",
"fields": {
"id": "ID",
"name": "Naam",
"description": "Omschrijving",
"version": "Versie",
"author": "Auteur",
"website": "Website",
"permissions": "Permissies",
"enabled": "Aangezet",
"status": "Status",
"path": "Pad",
"lastError": "Fout",
"hasError": "Fout",
"updatedAt": "Geupdate",
"createdAt": "Geinstalleerd",
"configKey": "Sleutel",
"configValue": "Waarde",
"allUsers": "Alle gebruikers toelaten",
"selectedUsers": "Geselecteerde gebruikers",
"allLibraries": "Alle bibliotheken toestaan",
"selectedLibraries": "Geselecteerde bibliotheken"
},
"sections": {
"status": "Status",
"info": "Plugin informatie",
"configuration": "Configuratie",
"manifest": "Manifest",
"usersPermission": "Gebruikers permissie",
"libraryPermission": "Bibliotheekpermissie"
},
"status": {
"enabled": "Aangezet",
"disabled": "Uitgezet"
},
"actions": {
"enable": "Aanzetten",
"disable": "Uitzetten",
"disabledDueToError": "Herstel de fout voor aanzetten",
"disabledUsersRequired": "Selecteer gebruikers voor aanzetten",
"disabledLibrariesRequired": "Selecteer bibliotheek voor aanzetten",
"addConfig": "Configuratie toevoegen",
"rescan": "Opnieuw scannen"
},
"notifications": {
"enabled": "Plugin actief",
"disabled": "Plugin niet actief",
"updated": "Plugin geupdate",
"error": "Fout bij updaten plugin"
},
"validation": {
"invalidJson": "Configuratie moet geldige JSON zijn"
},
"messages": {
"configHelp": "",
"clickPermissions": "Klik op permissie voor details",
"noConfig": "Geen configuratie ingesteld",
"allUsersHelp": "",
"noUsers": "Geen gebruikers geselecteerd",
"permissionReason": "Reden",
"usersRequired": "",
"allLibrariesHelp": "",
"noLibraries": "Geen bibliotheken geselecteerd",
"librariesRequired": "",
"requiredHosts": "Benodigde hosts",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "Sleutel",
"configValue": "Waarde"
}
}
},
"ra": {

View File

@@ -36,7 +36,8 @@
"bitDepth": "Głębokość próbkowania",
"sampleRate": "Częstotliwość próbkowania",
"missing": "Brak",
"libraryName": "Biblioteka"
"libraryName": "Biblioteka",
"composer": "Kompozytor"
},
"actions": {
"addToQueue": "Odtwarzaj Później",
@@ -328,6 +329,80 @@
"scanInProgress": "Skanowanie w trakcie...",
"noLibrariesAssigned": "Brak bibliotek przypisanych do tego użytkownika"
}
},
"plugin": {
"name": "\nWtyczka |||| Wtyczki",
"fields": {
"id": "ID",
"name": "Nazwa",
"description": "Opis",
"version": "Wersja",
"author": "Autor",
"website": "Witryna",
"permissions": "Uprawnienia",
"enabled": "Aktywny",
"status": "Status",
"path": "Ścieżka",
"lastError": "Błąd",
"hasError": "Błąd",
"updatedAt": "Zaktualizowana",
"createdAt": "Zainstalowana",
"configKey": "Klucz",
"configValue": "Wartość",
"allUsers": "Zezwalaj wszystkim użytkownikom",
"selectedUsers": "Wybrani użytkownicy",
"allLibraries": "Zezwalaj dla wszystkich bibliotek",
"selectedLibraries": ""
},
"sections": {
"status": "",
"info": "",
"configuration": "",
"manifest": "",
"usersPermission": "",
"libraryPermission": ""
},
"status": {
"enabled": "",
"disabled": ""
},
"actions": {
"enable": "",
"disable": "",
"disabledDueToError": "",
"disabledUsersRequired": "",
"disabledLibrariesRequired": "",
"addConfig": "",
"rescan": ""
},
"notifications": {
"enabled": "",
"disabled": "",
"updated": "",
"error": ""
},
"validation": {
"invalidJson": "Konfiguracja musić być w poprawnym formacie JSON"
},
"messages": {
"configHelp": "Użyj par klucz-wartość, aby skonfigurować wtyczkę. Pozostaw puste, jeśli wtyczka nie wymaga konfiguracji.",
"clickPermissions": "Kliknij uprawnienie, aby uzyskać szczegółowe informacje",
"noConfig": "Nie wybrano konfiguracji",
"allUsersHelp": "Po włączeniu wtyczka będzie miała dostęp do wszystkich użytkowników, także tych utworzonych w przyszłości.",
"noUsers": "Nie wybrano użytkowników",
"permissionReason": "Powód",
"usersRequired": "Ta wtyczka wymaga dostępu do informacji o użytkowniku. Wybierz użytkowników, do których wtyczka ma mieć dostęp, lub włącz opcję „Zezwól wszystkim użytkownikom”.",
"allLibrariesHelp": "Po włączeniu wtyczka będzie miała dostęp do wszystkich bibliotek, także tych utworzonych w przyszłości.",
"noLibraries": "Nie wybrano biblioteki",
"librariesRequired": "Wtyczka wymaga dostępu do informacji o bibliotece. Wybierz, dla której biblioteki zezwolić dostęp, lub włącz 'Zezwalaj dla wszystkich bibliotek'.",
"requiredHosts": "",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "klucz",
"configValue": "wartość"
}
}
},
"ra": {

View File

@@ -12,7 +12,6 @@
"artist": "Artista",
"album": "Álbum",
"path": "Arquivo",
"libraryName": "Biblioteca",
"genre": "Gênero",
"compilation": "Coletânea",
"year": "Ano",
@@ -36,7 +35,9 @@
"rawTags": "Tags originais",
"bitDepth": "Profundidade de bits",
"sampleRate": "Taxa de amostragem",
"missing": "Ausente"
"missing": "Ausente",
"libraryName": "Biblioteca",
"composer": "Compositor"
},
"actions": {
"addToQueue": "Adicionar à fila",
@@ -58,7 +59,6 @@
"songCount": "Músicas",
"playCount": "Execuções",
"name": "Nome",
"libraryName": "Biblioteca",
"genre": "Gênero",
"compilation": "Coletânea",
"year": "Ano",
@@ -78,7 +78,8 @@
"media": "Mídia",
"mood": "Mood",
"date": "Data de Lançamento",
"missing": "Ausente"
"missing": "Ausente",
"libraryName": "Biblioteca"
},
"actions": {
"playAll": "Tocar",
@@ -130,9 +131,9 @@
"maincredit": "Artista do Álbum ou Artista |||| Artistas do Álbum ou Artistas"
},
"actions": {
"topSongs": "Mais tocadas",
"shuffle": "Aleatório",
"radio": "Rádio"
"radio": "Rádio",
"topSongs": "Mais tocadas"
}
},
"user": {
@@ -161,14 +162,14 @@
"updated": "Usuário atualizado com sucesso",
"deleted": "Usuário deletado com sucesso"
},
"validation": {
"librariesRequired": "Pelo menos uma biblioteca deve ser selecionada para usuários não-administradores"
},
"message": {
"listenBrainzToken": "Entre seu token do ListenBrainz",
"clickHereForToken": "Clique aqui para obter seu token",
"selectAllLibraries": "Selecionar todas as bibliotecas",
"adminAutoLibraries": "Usuários administradores têm acesso automático a todas as bibliotecas"
},
"validation": {
"librariesRequired": "Pelo menos uma biblioteca deve ser selecionada para usuários não-administradores"
}
},
"player": {
@@ -253,17 +254,15 @@
"updatedAt": "Últ. Atualização",
"createdAt": "Data de Criação",
"downloadable": "Permitir Baixar?"
},
"notifications": {},
"actions": {}
}
},
"missing": {
"name": "Arquivo ausente |||| Arquivos ausentes",
"fields": {
"path": "Caminho",
"size": "Tamanho",
"libraryName": "Biblioteca",
"updatedAt": "Desaparecido em"
"updatedAt": "Desaparecido em",
"libraryName": "Biblioteca"
},
"actions": {
"remove": "Remover",
@@ -302,20 +301,20 @@
},
"actions": {
"scan": "Scanear Biblioteca",
"quickScan": "Scan Rápido",
"fullScan": "Scan Completo",
"manageUsers": "Gerenciar Acesso do Usuário",
"viewDetails": "Ver Detalhes"
"viewDetails": "Ver Detalhes",
"quickScan": "Scan Rápido",
"fullScan": "Scan Completo"
},
"notifications": {
"created": "Biblioteca criada com sucesso",
"updated": "Biblioteca atualizada com sucesso",
"deleted": "Biblioteca excluída com sucesso",
"scanStarted": "Scan da biblioteca iniciada",
"scanCompleted": "Scan da biblioteca concluída",
"quickScanStarted": "Scan rápido iniciado",
"fullScanStarted": "Scan completo iniciado",
"scanError": "Erro ao iniciar o scan. Verifique os logs",
"scanCompleted": "Scan da biblioteca concluída"
"scanError": "Erro ao iniciar o scan. Verifique os logs"
},
"validation": {
"nameRequired": "Nome da biblioteca é obrigatório",
@@ -387,8 +386,6 @@
},
"messages": {
"configHelp": "Configure o plugin usando pares chave-valor. Deixe vazio se o plugin não precisa de configuração.",
"configValidationError": "Falha na validação da configuração:",
"schemaRenderError": "Não foi possível renderizar o formulário de configuração. O schema do plugin pode estar inválido.",
"clickPermissions": "Clique em uma permissão para ver detalhes",
"noConfig": "Nenhuma configuração definida",
"allUsersHelp": "Quando habilitado, o plugin terá acesso a todos os usuários, incluindo os criados no futuro.",
@@ -398,7 +395,9 @@
"allLibrariesHelp": "Quando habilitado, o plugin terá acesso a todas as bibliotecas, incluindo as criadas no futuro.",
"noLibraries": "Nenhuma biblioteca selecionada",
"librariesRequired": "Este plugin requer acesso a informações de bibliotecas. Selecione quais bibliotecas o plugin pode acessar, ou habilite 'Permitir todas as bibliotecas'.",
"requiredHosts": "Hosts necessários"
"requiredHosts": "Hosts necessários",
"configValidationError": "Falha na validação da configuração:",
"schemaRenderError": "Não foi possível renderizar o formulário de configuração. O schema do plugin pode estar inválido."
},
"placeholders": {
"configKey": "chave",
@@ -556,8 +555,6 @@
"transcodingDisabled": "Por questão de segurança, esta tela de configuração está desabilitada. Se você quiser alterar estas configurações, reinicie o servidor com a opção %{config}",
"transcodingEnabled": "Navidrome está sendo executado com a opção %{config}. Isto permite que potencialmente se execute comandos do sistema pela interface Web. É recomendado que vc mantenha esta opção desabilitada, e só a habilite quando precisar configurar opções de Conversão",
"songsAddedToPlaylist": "Música adicionada à playlist |||| %{smart_count} músicas adicionadas à playlist",
"noSimilarSongsFound": "Nenhuma música semelhante encontrada",
"noTopSongsFound": "Nenhuma música mais tocada encontrada",
"noPlaylistsAvailable": "Nenhuma playlist",
"delete_user_title": "Excluir usuário '%{name}'",
"delete_user_content": "Você tem certeza que deseja excluir o usuário e todos os seus dados (incluindo suas playlists e preferências)?",
@@ -587,16 +584,12 @@
"remove_missing_title": "Remover arquivos ausentes",
"remove_missing_content": "Você tem certeza que deseja remover os arquivos selecionados do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações.",
"remove_all_missing_title": "Remover todos os arquivos ausentes",
"remove_all_missing_content": "Você tem certeza que deseja remover todos os arquivos ausentes do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações."
"remove_all_missing_content": "Você tem certeza que deseja remover todos os arquivos ausentes do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações.",
"noSimilarSongsFound": "Nenhuma música semelhante encontrada",
"noTopSongsFound": "Nenhuma música mais tocada encontrada"
},
"menu": {
"library": "Biblioteca",
"librarySelector": {
"allLibraries": "Todas as Bibliotecas (%{count})",
"multipleLibraries": "%{selected} de %{total} Bibliotecas",
"selectLibraries": "Selecionar Bibliotecas",
"none": "Nenhuma"
},
"settings": "Configurações",
"version": "Versão",
"theme": "Tema",
@@ -622,7 +615,13 @@
"albumList": "Álbuns",
"about": "Info",
"playlists": "Playlists",
"sharedPlaylists": "Compartilhadas"
"sharedPlaylists": "Compartilhadas",
"librarySelector": {
"allLibraries": "Todas as Bibliotecas (%{count})",
"multipleLibraries": "%{selected} de %{total} Bibliotecas",
"selectLibraries": "Selecionar Bibliotecas",
"none": "Nenhuma"
}
},
"player": {
"playListsText": "Fila de Execução",
@@ -681,17 +680,12 @@
"totalScanned": "Total de pastas scaneadas",
"quickScan": "Rápido",
"fullScan": "Completo",
"selectiveScan": "Seletivo",
"serverUptime": "Uptime do servidor",
"serverDown": "DESCONECTADO",
"scanType": "Último Scan",
"status": "Erro",
"elapsedTime": "Duração"
},
"nowPlaying": {
"title": "Tocando agora",
"empty": "Nada tocando",
"minutesAgo": "%{smart_count} minuto atrás |||| %{smart_count} minutos atrás"
"elapsedTime": "Duração",
"selectiveScan": "Seletivo"
},
"help": {
"title": "Teclas de atalho",
@@ -706,5 +700,10 @@
"toggle_love": "Marcar/desmarcar favorita",
"current_song": "Vai para música atual"
}
},
"nowPlaying": {
"title": "Tocando agora",
"empty": "Nada tocando",
"minutesAgo": "%{smart_count} minuto atrás |||| %{smart_count} minutos atrás"
}
}
}

View File

@@ -36,7 +36,8 @@
"bitDepth": "Битовая глубина (Bit)",
"sampleRate": "Частота дискретизации (Hz)",
"missing": "Поле отсутствует",
"libraryName": "Библиотека"
"libraryName": "Библиотека",
"composer": "Композитор"
},
"actions": {
"addToQueue": "В очередь",
@@ -328,6 +329,80 @@
"scanInProgress": "Сканирование продолжается...",
"noLibrariesAssigned": "Нет библиотек, назначенных этому пользователю"
}
},
"plugin": {
"name": "Плагин |||| Плагины",
"fields": {
"id": "ID",
"name": "Имя",
"description": "Описание",
"version": "Версия",
"author": "Автор",
"website": "Вебсайт",
"permissions": "Разрешения",
"enabled": "Включено",
"status": "Статус",
"path": "Путь",
"lastError": "Ошибка",
"hasError": "Ошибка",
"updatedAt": "Обновлено",
"createdAt": "Установленный",
"configKey": "Ключ",
"configValue": "Значение",
"allUsers": "Разрешить всем пользователям",
"selectedUsers": "Выбранные пользователи",
"allLibraries": "Разрешить доступ ко всем библиотекам",
"selectedLibraries": "Избранные библиотеки"
},
"sections": {
"status": "Статус",
"info": "Информация о плагине",
"configuration": "Конфигурация",
"manifest": "Манифест",
"usersPermission": "Разрешение пользователей",
"libraryPermission": "Разрешение на использование библиотеки"
},
"status": {
"enabled": "Включено",
"disabled": "Отключить"
},
"actions": {
"enable": "Включить",
"disable": "Отключить",
"disabledDueToError": "Исправьте ошибку перед включением",
"disabledUsersRequired": "Выберите пользователей перед включением",
"disabledLibrariesRequired": "Выберите библиотеки перед включением",
"addConfig": "Добавить конфигурацию",
"rescan": "Повторное сканирование"
},
"notifications": {
"enabled": "Плагин включен",
"disabled": "Плагин отключен",
"updated": "Плагин обновлен",
"error": "Ошибка обновления плагина"
},
"validation": {
"invalidJson": "Конфигурация должна быть в формате JSON, допустимом для всех пользователей"
},
"messages": {
"configHelp": "Настройте плагин, используя пары ключ-значение. Оставьте поле пустым, если плагин не требует настройки.",
"clickPermissions": "Нажмите на разрешение для получения подробной информации",
"noConfig": "Конфигурация не задана",
"allUsersHelp": "При включении плагин получит доступ ко всем пользователям, включая тех, кто будет создан в будущем.",
"noUsers": "Не выбрано ни одного пользователя",
"permissionReason": "Причина",
"usersRequired": "Этому плагину требуется доступ к пользовательской информации. Выберите, к каким пользователям плагин может получить доступ, или включите \"Разрешить всем пользователям\".",
"allLibrariesHelp": "После включения плагин будет иметь доступ ко всем библиотекам, включая те, которые будут созданы в будущем.",
"noLibraries": "Библиотеки не выбраны",
"librariesRequired": "Этому плагину требуется доступ к библиотечной информации. Выберите, к каким библиотекам плагин может получить доступ, или включите \"Разрешить все библиотеки\".",
"requiredHosts": "Необходимые хосты",
"configValidationError": "Проверка конфигурации завершилась неудачей:",
"schemaRenderError": "Не удалось отобразить форму конфигурации. Возможно, схема плагина недействительна."
},
"placeholders": {
"configKey": "ключ",
"configValue": "значение"
}
}
},
"ra": {

View File

@@ -36,7 +36,8 @@
"bitDepth": "Bitna globina",
"sampleRate": "Frekvenca vzorčenja",
"missing": "Manjka",
"libraryName": "Knjižnica"
"libraryName": "Knjižnica",
"composer": "Skladatelj"
},
"actions": {
"addToQueue": "Predvajaj kasneje",
@@ -301,14 +302,19 @@
"actions": {
"scan": "Skeniraj knjižnico",
"manageUsers": "Upravljanje dostopa uporabnikov",
"viewDetails": "Ogled podrobnosti"
"viewDetails": "Ogled podrobnosti",
"quickScan": "Hitro skeniranje",
"fullScan": "Popolno skeniranje"
},
"notifications": {
"created": "Knjižnica je uspešno ustvarjena",
"updated": "Knjižnica je bila uspešno posodobljena",
"deleted": "Knjižnica je uspešno izbrisana",
"scanStarted": "Skeniranje knjižnice se je začelo",
"scanCompleted": "Skeniranje knjižnice končano"
"scanCompleted": "Skeniranje knjižnice končano",
"quickScanStarted": "Hitro skeniranje se je začelo",
"fullScanStarted": "Popolno skeniranje se je začelo",
"scanError": "Napaka pri začetku skeniranja. Preverite dnevnike"
},
"validation": {
"nameRequired": "Ime knjižnice je obvezno",
@@ -323,6 +329,80 @@
"scanInProgress": "Skeniranje v teku...",
"noLibrariesAssigned": "Uporabnik nima dodeljenih knjižnic"
}
},
"plugin": {
"name": "Vtičnik |||| Vtičniki",
"fields": {
"id": "ID",
"name": "Ime",
"description": "Opis",
"version": "Verzija",
"author": "Avtor",
"website": "Spletna stran",
"permissions": "Dovoljenja",
"enabled": "Vključeno",
"status": "Status",
"path": "Pot",
"lastError": "Napaka",
"hasError": "Napaka",
"updatedAt": "Posodobljeno",
"createdAt": "Inštalirano",
"configKey": "Ključ",
"configValue": "Vrednost",
"allUsers": "Dovoli vsem uporabnikom",
"selectedUsers": "Izbrani uporabniki",
"allLibraries": "Dovoli vse knjižnice",
"selectedLibraries": "Izbrane knjižnice"
},
"sections": {
"status": "Status",
"info": "Informacije o vtičniku",
"configuration": "Konfiguracija",
"manifest": "Manifest",
"usersPermission": "Uporabniška dovoljenja",
"libraryPermission": "Knjižnična dovoljenja"
},
"status": {
"enabled": "Vključeno",
"disabled": "Izključeno"
},
"actions": {
"enable": "Vključi",
"disable": "Izključi",
"disabledDueToError": "Popravi napako pred vključitvijo",
"disabledUsersRequired": "Izberi uporabnike pred vključitvijo",
"disabledLibrariesRequired": "Izberi knjižnice pred vključitvijo",
"addConfig": "Dodaj konfiguracijo",
"rescan": "Ponovi skeniranje"
},
"notifications": {
"enabled": "Vtičnik vključen",
"disabled": "Vtičnik izključen",
"updated": "Vtičnik posodobljen",
"error": "Napaka pri posodobitvi vtičnika"
},
"validation": {
"invalidJson": "Konfiguracija mora biti pravilen JSON"
},
"messages": {
"configHelp": "Konfiguriraj vtičnik z uporabo key-value parov. Pusti prazno, če vtičnik ne potrebuje konfiguracije.",
"clickPermissions": "Klikni za dovoljenje o podrobnostih",
"noConfig": "Konfiguracija ni nastavljena",
"allUsersHelp": "Ko vključeno, bo vtičnik imel dostop do vseh uporabnikov, tudi prihodnjih.",
"noUsers": "Uporabniki niso izbrani",
"permissionReason": "Razlog",
"usersRequired": "Vtičnik potrebuje dostop do uporabnikovih informacij. Izberi uporabnike ali vključi dostop vsem uporabnikom.",
"allLibrariesHelp": "Ko vključeno, bo vtičnik imel dostop do vseh knjižnic, tudi prihodnjih.",
"noLibraries": "Ni izbranih knjižnic",
"librariesRequired": "Vtičnik zahteva dostop do knjižnih informacij. Izberi do katerih knjižnic lahko dostopa, ali vključi dostop do vseh knjižnic.",
"requiredHosts": "Zahtevani gostitelji",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "ključ",
"configValue": "vrednost"
}
}
},
"ra": {
@@ -604,7 +684,8 @@
"serverDown": "NEPOVEZAN",
"scanType": "Tip",
"status": "Napaka pri skeniranju",
"elapsedTime": "Pretečeni čas"
"elapsedTime": "Pretečeni čas",
"selectiveScan": "Selektivno"
},
"help": {
"title": "Hitre tipke",

View File

@@ -10,7 +10,6 @@
"playCount": "Spelningar",
"title": "Titel",
"artist": "Artist",
"composer": "Kompositör",
"album": "Album",
"path": "Sökväg",
"genre": "Genre",
@@ -37,7 +36,8 @@
"bitDepth": "Bitdjup",
"sampleRate": "Samplingsfrekvens",
"missing": "Saknade",
"libraryName": "Bibliotek"
"libraryName": "Bibliotek",
"composer": "Kompositör"
},
"actions": {
"addToQueue": "Lägg till i kön",
@@ -329,6 +329,80 @@
"scanInProgress": "Scanning pågår...",
"noLibrariesAssigned": "Inga bibliotek har tilldelats den här användaren"
}
},
"plugin": {
"name": "Tillägg |||| Tillägg",
"fields": {
"id": "ID",
"name": "Namn",
"description": "Beskrivning",
"version": "Version",
"author": "Författare",
"website": "Website",
"permissions": "Behörigheter",
"enabled": "Aktiverad",
"status": "Status",
"path": "Sökväg",
"lastError": "Fel",
"hasError": "Fel",
"updatedAt": "Uppdaterad",
"createdAt": "Installerad",
"configKey": "Nyckel",
"configValue": "Värde",
"allUsers": "Tillåt alla användare",
"selectedUsers": "Valda användare",
"allLibraries": "Tillåt alla bibliotek",
"selectedLibraries": "Valda bibliotek"
},
"sections": {
"status": "Status",
"info": "Tilläggsinformation",
"configuration": "Konfiguration",
"manifest": "Manifest",
"usersPermission": "Användarbehörigheter",
"libraryPermission": "Biblioteksbehörigheter"
},
"status": {
"enabled": "Aktiverad",
"disabled": "Inaktiverad"
},
"actions": {
"enable": "Aktivera",
"disable": "Inaktivera",
"disabledDueToError": "Åtgärda felet innan aktivering",
"disabledUsersRequired": "Välj användare före aktivering",
"disabledLibrariesRequired": "Välj bibliotek före aktivering",
"addConfig": "Lägg till konfiguration",
"rescan": "Scanna om"
},
"notifications": {
"enabled": "Tillägg aktiverat",
"disabled": "Tillägg inaktiverat",
"updated": "Tillägg uppdaterat",
"error": "Fel vid uppdatering av tillägg"
},
"validation": {
"invalidJson": "Konfigurationen måste vara giltig JSON"
},
"messages": {
"configHelp": "Konfigurera tillägget med nyckelvärde-par. Lämna tomt om tillägget inte kräver någon konfiguration.",
"clickPermissions": "Klicka på en behörighet för mer information",
"noConfig": "Ingen konfiguration angiven",
"allUsersHelp": "När den är aktiverad får tillägget tillgång till alla användare, inklusive de som skapas i framtiden.",
"noUsers": "Inga användare valda",
"permissionReason": "Orsak",
"usersRequired": "Detta tillägg kräver åtkomst till användarinformation. Välj vilka användare insticksprogrammet ska ha åtkomst till, eller aktivera 'Tillåt alla användare'.",
"allLibrariesHelp": "När den är aktiverad får tillägget tillgång till alla bibliotek, inklusive de som skapas i framtiden.",
"noLibraries": "Inga bibliotek valda",
"librariesRequired": "Detta tillägg kräver tillgång till biblioteksinformation. Välj vilka bibliotek tillägget kan komma åt eller aktivera 'Tillåt alla bibliotek'.",
"requiredHosts": "Krävda värdar",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "nyckel",
"configValue": "värde"
}
}
},
"ra": {
@@ -545,7 +619,7 @@
"librarySelector": {
"allLibraries": "Alla bibliotek (%{count})",
"multipleLibraries": "%{selected} av %{total} bibliotek",
"selectLibraries": "Valda bibliotek",
"selectLibraries": "Välj bibliotek",
"none": "Inga"
}
},

View File

@@ -36,7 +36,8 @@
"bitDepth": "Bit depth",
"sampleRate": "แซมเปิ้ลเรต",
"missing": "หายไป",
"libraryName": "ห้องสมุด"
"libraryName": "ห้องสมุด",
"composer": ""
},
"actions": {
"addToQueue": "เพิ่มในคิว",
@@ -328,6 +329,80 @@
"scanInProgress": "กำลังสแกน...",
"noLibrariesAssigned": "ไม่มีห้องสมุดสำหรับผู้ใช้นี้"
}
},
"plugin": {
"name": "ปลั๊กอิน |||| ปลั๊กอิน",
"fields": {
"id": "ID",
"name": "ชื่อ",
"description": "รายละเอียด",
"version": "เวอร์ชั่น",
"author": "ผู้สร้าง",
"website": "เว็บไซต์",
"permissions": "การอนุญาติ",
"enabled": "เปิดใช้",
"status": "สถานะ",
"path": "เส้นทาง",
"lastError": "ผิดพลาด",
"hasError": "ผิดพลาด",
"updatedAt": "อัพเดทแล้ว",
"createdAt": "ติดตั้งแล้ว",
"configKey": "คีย์",
"configValue": "ค่า",
"allUsers": "อนุญาติผู้ใช้ทั้งหมด",
"selectedUsers": "ผู้ใช้ถูกเลือก",
"allLibraries": "อนุญาติห้องสมุดเพลงทั้งหมด",
"selectedLibraries": "ห้องสมุดเพลงถูกเลือก"
},
"sections": {
"status": "สถานะ",
"info": "ข้อมูลปลั๊กอิน",
"configuration": "การตั้งค่า",
"manifest": "แสดง",
"usersPermission": "สิทธิของผู้ใช้",
"libraryPermission": "สิทธิของห้องสมุดเพลง"
},
"status": {
"enabled": "เปิดใช้งานแล้ว",
"disabled": "ปิดใช้งานแล้ว"
},
"actions": {
"enable": "เปิดใช้งาน",
"disable": "ปิดใช้งาน",
"disabledDueToError": "แก้ไขข้อผิดพลาดก่อนเปิดใช้งาน",
"disabledUsersRequired": "เลือกผู้ใช้ที่จะเปิดใช้งาน",
"disabledLibrariesRequired": "เลือกห้องสมุดเพลงที่จะเปิดใช้งาน",
"addConfig": "เพิ่มการตั้งค่า",
"rescan": "สแกนซ้ำ"
},
"notifications": {
"enabled": "เปิดใช้ปลั๊กอินแล้ว",
"disabled": "ปิดใช้ปลั๊กอินแล้ว",
"updated": "ปลั๊กอินอัพเดท",
"error": "อัพเดทผิดพลาด"
},
"validation": {
"invalidJson": "ต้องตั้งค่าตามไวยากรณ์ JSON"
},
"messages": {
"configHelp": "ใส่ค่าให้เข้าคู่กับคีย์ของปลั๊กอิน ปล่อยว่างถ้าปลั๊กอินไม่ต้องการใช้",
"clickPermissions": "กดดูรายละเอียดของการอนุญาติ",
"noConfig": "ไม่ได้ตั้งค่า",
"allUsersHelp": "เมื่อเปิดใช้ ปลั๊กอินจะใช้กับผู้ใช้ทุกคน รวมถึงผู้ใช้ใหม่ในอนาคต",
"noUsers": "ไม่ได้เลือกผู้ใช้",
"permissionReason": "เหตุผล",
"usersRequired": "ปลั๊กอินนี้ต้องการเข้าถึงข้อมูลผู้ใช้ เลือกผู้ใช้ที่ต้องการให้ปลั๊กอินเข้าถึงหรือเปิดใช้งานกับผู้ใช้ทั้งหมด",
"allLibrariesHelp": "เมื่อเปิดใช้งาน ปลั๊กอินจะเข้าถึงทุกห้องสมุดเพลง รวมถึงของผู้ใช้ใหม่ในอนาคต",
"noLibraries": "ไม่มีห้องสมุดเพลงถูกเลือก",
"librariesRequired": "ปลั๊กอินนี้ต้องการเข้าถึงข้อมูลห้องสมุดเพลง เลือกห้องสมุดเพลงที่ต้องการให้ปลั๊กอินเข้าถึงหรือเปิดใช้งานกับห้องสมุดเพลงทั้งหมด",
"requiredHosts": "ต้องการ Host",
"configValidationError": "",
"schemaRenderError": ""
},
"placeholders": {
"configKey": "คีย์",
"configValue": "ค่า"
}
}
},
"ra": {

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) {
if (notes !== undefined) {
notes += '..'
}
@@ -340,7 +340,7 @@ const AlbumDetails = (props) => {
)}
</Typography>
)}
{isDesktop && notes && (
{isDesktop && (
<Collapse
collapsedHeight={'2.75em'}
in={expanded}
@@ -364,7 +364,7 @@ const AlbumDetails = (props) => {
{!isDesktop && record['comment'] && (
<CollapsibleComment record={record} />
)}
{!isDesktop && notes && (
{!isDesktop && (
<div className={classes.notes}>
<Collapse collapsedHeight={'1.5em'} in={expanded} timeout={'auto'}>
<Typography

View File

@@ -4,26 +4,18 @@ 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,
@@ -33,11 +25,9 @@ export const LoveButton = ({
component: Button,
addLabel,
disabled,
className,
record: recordProp,
...rest
}) => {
const record = useRecordContext({ record: recordProp }) || {}
const record = useRecordContext(rest) || {}
const classes = useStyles({ color, visible, loved: record.starred })
const [toggleLove, loading] = useToggleLove(resource, record)
@@ -58,7 +48,7 @@ export const LoveButton = ({
onClick={handleToggleLove}
size={'small'}
disabled={disabled || loading || record.missing}
className={clsx(classes.love, className)}
className={classes.love}
title={
isDateSet(record.starredAt)
? new Date(record.starredAt).toLocaleString()

View File

@@ -28,9 +28,6 @@ import { useDispatch } from 'react-redux'
const useStyles = makeStyles((theme) => ({
user: {},
button: {
color: 'inherit',
},
avatar: {
width: theme.spacing(4),
height: theme.spacing(4),
@@ -75,11 +72,12 @@ 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,15 +34,7 @@ const PlaylistEditForm = (props) => {
return (
<SimpleForm redirect="list" variant={'outlined'} {...props}>
<TextInput source="name" validate={required()} />
<TextInput
multiline
minRows={3}
source="comment"
fullWidth
inputProps={{
style: { resize: 'vertical' },
}}
/>
<TextInput multiline source="comment" />
{permissions === 'admin' ? (
<ReferenceInput
source="ownerId"

View File

@@ -0,0 +1,276 @@
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,37 +4,76 @@ import { Card, CardContent, Typography, Box } from '@material-ui/core'
import Alert from '@material-ui/lab/Alert'
import { SchemaConfigEditor } from './SchemaConfigEditor'
// 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
// Navigate schema by path parts to find the title for a field
const findFieldTitle = (schema, parts) => {
let currentSchema = schema
let fieldName = parts[parts.length - 1]
const pathParts = []
let fieldName = parts[parts.length - 1] // Default to last part
for (const part of parts) {
if (!currentSchema) break
// Skip array indices (just move to items schema)
if (/^\d+$/.test(part)) {
pathParts.push(`[${part}]`)
currentSchema = currentSchema?.items
} else {
fieldName = currentSchema?.properties?.[part]?.title || part
pathParts.push(part)
currentSchema = currentSchema?.properties?.[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
}
}
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
return fieldName
}
return { fieldName, 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
}
export const ConfigCard = ({
@@ -60,10 +99,14 @@ export const ConfigCard = ({
// Format validation errors with proper field names
const formattedErrors = useMemo(() => {
if (!hasConfigSchema) return []
return validationErrors.map((error) =>
formatError(error, manifest.config.schema),
)
if (!hasConfigSchema) {
return []
}
const { schema } = manifest.config
return validationErrors.map((error) => ({
fieldName: getFieldName(error, schema),
message: error.message,
}))
}, [validationErrors, manifest, hasConfigSchema])
if (!hasConfigSchema) {
@@ -96,14 +139,12 @@ export const ConfigCard = ({
</Box>
)}
<Box mt={formattedErrors.length > 0 ? 0 : 2}>
<SchemaConfigEditor
schema={schema}
uiSchema={uiSchema}
data={configData}
onChange={handleChange}
/>
</Box>
<SchemaConfigEditor
schema={schema}
uiSchema={uiSchema}
data={configData}
onChange={handleChange}
/>
</CardContent>
</Card>
)

View File

@@ -40,14 +40,18 @@ 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 showError = errors && errors.length > 0
const hasErrors = errors && errors.length > 0
// Only show as invalid after the field has been touched (blurred)
const showError = isTouched && hasErrors
const showDescription = !isDescriptionHidden(
visible,
@@ -59,7 +63,10 @@ const useControlState = (props) => {
const helperText = showError ? errors : showDescription ? description : ''
const handleFocus = () => setIsFocused(true)
const handleBlur = () => setIsFocused(false)
const handleBlur = () => {
setIsFocused(false)
setIsTouched(true)
}
return {
isFocused,
@@ -213,6 +220,9 @@ 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,6 +6,7 @@ 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,
@@ -134,6 +135,7 @@ const customRenderers = [
OutlinedNumberRenderer,
OutlinedEnumRenderer,
OutlinedOneOfEnumRenderer,
AlwaysExpandedArrayLayout,
// Then all the standard material renderers
...materialRenderers,
]

View File

@@ -12,7 +12,6 @@ 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
@@ -28,7 +27,6 @@ export default {
GruvboxDarkTheme,
LigeraTheme,
MonokaiTheme,
NautilineTheme,
NordTheme,
NuclearTheme,
SpotifyTheme,

View File

@@ -1,905 +0,0 @@
/**
* 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