Compare commits

...

10 Commits

Author SHA1 Message Date
Paul Becker
f00af7f983 feat(ui): add Dracula theme (#5023)
Signed-off-by: Paul Becker <p@becker.kiwi>
2026-02-12 16:42:34 -05:00
Deluan Quintão
875ffc2b78 fix(ui): update Danish, Portuguese (BR) translations from POEditor (#5039)
Co-authored-by: navidrome-bot <navidrome-bot@navidrome.org>
2026-02-12 16:38:57 -05:00
ChekeredList71
885334c819 fix(ui): update Hungarian translation (#5041)
* new strings added

* "empty" solved

---------

Co-authored-by: ChekeredList71 <asd@asd.com>
2026-02-12 16:36:05 -05:00
Deluan
ff86b9f2b9 ci: add GitHub Actions workflow for pushing translations to POEditor 2026-02-12 16:32:58 -05:00
Xabi
13d3d510f5 fix(ui): update Basque localisation (#5038)
* Update Basque localisation

Added missing strings and a couple of improvements.

* Update resources/i18n/eu.json

typo

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-12 15:52:37 -05:00
fxj368
656009e5f8 fix(i18n) update Chinese Simplified translation (#5025)
* Update Chinese Simplified translation

* fix some structural issue and an incorrect translation
2026-02-12 15:49:20 -05:00
Deluan
06b3a1f33e fix(insights): update HasCustomPID logic to use default constants
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-12 14:33:25 -05:00
Kendall Garner
0f4e8376cb feat(ui): add download config toml link, disable copy when clipboard not available (#5035) 2026-02-12 10:54:04 -05:00
Deluan
199cde4109 fix: upgrade go-taglib to latest version
Updated the go-taglib dependency to pick up the latest bug fixes from
the forked repository. This resolves an issue reported in #5037.
2026-02-12 10:12:04 -05:00
Deluan
897de02a84 docs: documents how subsonic e2e tests are structured 2026-02-11 22:49:41 -05:00
16 changed files with 1823 additions and 691 deletions

138
.github/workflows/push-translations.sh vendored Executable file
View File

@@ -0,0 +1,138 @@
#!/bin/sh
set -e
I18N_DIR=resources/i18n
# Normalize JSON for deterministic comparison:
# remove empty/null attributes, sort keys alphabetically
process_json() {
jq 'walk(if type == "object" then with_entries(select(.value != null and .value != "" and .value != [] and .value != {})) | to_entries | sort_by(.key) | from_entries else . end)' "$1"
}
# Get list of all languages configured in the POEditor project
get_language_list() {
curl -s -X POST https://api.poeditor.com/v2/languages/list \
-d api_token="${POEDITOR_APIKEY}" \
-d id="${POEDITOR_PROJECTID}"
}
# Extract language name from the language list JSON given a language code
get_language_name() {
lang_code="$1"
lang_list="$2"
echo "$lang_list" | jq -r ".result.languages[] | select(.code == \"$lang_code\") | .name"
}
# Extract language code from a file path (e.g., "resources/i18n/fr.json" -> "fr")
get_lang_code() {
filepath="$1"
filename=$(basename "$filepath")
echo "${filename%.*}"
}
# Export the current translation for a language from POEditor (v2 API)
export_language() {
lang_code="$1"
response=$(curl -s -X POST https://api.poeditor.com/v2/projects/export \
-d api_token="${POEDITOR_APIKEY}" \
-d id="${POEDITOR_PROJECTID}" \
-d language="$lang_code" \
-d type="key_value_json")
url=$(echo "$response" | jq -r '.result.url')
if [ -z "$url" ] || [ "$url" = "null" ]; then
echo "Failed to export $lang_code: $response" >&2
return 1
fi
echo "$url"
}
# Flatten nested JSON to POEditor languages/update format.
# POEditor uses term + context pairs, where:
# term = the leaf key name
# context = the parent path as "key1"."key2"."key3" (empty for root keys)
flatten_to_poeditor() {
jq -c '[paths(scalars) as $p |
{
"term": ($p | last | tostring),
"context": (if ($p | length) > 1 then ($p[:-1] | map("\"" + tostring + "\"") | join(".")) else "" end),
"translation": {"content": getpath($p)}
}
]' "$1"
}
# Update translations for a language in POEditor via languages/update API
update_language() {
lang_code="$1"
file="$2"
flatten_to_poeditor "$file" > /tmp/poeditor_data.json
response=$(curl -s -X POST https://api.poeditor.com/v2/languages/update \
-d api_token="${POEDITOR_APIKEY}" \
-d id="${POEDITOR_PROJECTID}" \
-d language="$lang_code" \
--data-urlencode data@/tmp/poeditor_data.json)
rm -f /tmp/poeditor_data.json
status=$(echo "$response" | jq -r '.response.status')
if [ "$status" != "success" ]; then
echo "Failed to update $lang_code: $response" >&2
return 1
fi
parsed=$(echo "$response" | jq -r '.result.translations.parsed')
added=$(echo "$response" | jq -r '.result.translations.added')
updated=$(echo "$response" | jq -r '.result.translations.updated')
echo " Translations - parsed: $parsed, added: $added, updated: $updated"
}
# --- Main ---
if [ $# -eq 0 ]; then
echo "Usage: $0 <file1> [file2] ..."
echo "No files specified. Nothing to do."
exit 0
fi
lang_list=$(get_language_list)
upload_count=0
for file in "$@"; do
if [ ! -f "$file" ]; then
echo "Warning: File not found: $file, skipping"
continue
fi
lang_code=$(get_lang_code "$file")
lang_name=$(get_language_name "$lang_code" "$lang_list")
if [ -z "$lang_name" ]; then
echo "Warning: Language code '$lang_code' not found in POEditor, skipping $file"
continue
fi
echo "Processing $lang_name ($lang_code)..."
# Export current state from POEditor
url=$(export_language "$lang_code")
curl -sSL "$url" -o poeditor_export.json
# Normalize both files for comparison
process_json "$file" > local_normalized.json
process_json poeditor_export.json > remote_normalized.json
# Compare normalized versions
if diff -q local_normalized.json remote_normalized.json > /dev/null 2>&1; then
echo " No differences, skipping"
else
echo " Differences found, updating POEditor..."
update_language "$lang_code" "$file"
upload_count=$((upload_count + 1))
fi
rm -f poeditor_export.json local_normalized.json remote_normalized.json
done
echo ""
echo "Done. Updated $upload_count translation(s) in POEditor."

32
.github/workflows/push-translations.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: POEditor export
on:
push:
branches:
- master
paths:
- 'resources/i18n/*.json'
jobs:
push-translations:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'navidrome' }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Detect changed translation files
id: changed
run: |
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- 'resources/i18n/*.json' | tr '\n' ' ')
echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "Changed translation files: $CHANGED_FILES"
- name: Push translations to POEditor
if: ${{ steps.changed.outputs.files != '' }}
env:
POEDITOR_APIKEY: ${{ secrets.POEDITOR_APIKEY }}
POEDITOR_PROJECTID: ${{ secrets.POEDITOR_PROJECTID }}
run: |
.github/workflows/push-translations.sh ${{ steps.changed.outputs.files }}

View File

@@ -220,7 +220,7 @@ var staticData = sync.OnceValue(func() insights.Data {
data.Config.ScanWatcherWait = uint64(math.Trunc(conf.Server.Scanner.WatcherWait.Seconds()))
data.Config.ScanOnStartup = conf.Server.Scanner.ScanOnStartup
data.Config.ReverseProxyConfigured = conf.Server.ExtAuth.TrustedSources != ""
data.Config.HasCustomPID = conf.Server.PID.Track != "" || conf.Server.PID.Album != ""
data.Config.HasCustomPID = conf.Server.PID.Track != consts.DefaultTrackPID || conf.Server.PID.Album != consts.DefaultAlbumPID
data.Config.HasCustomTags = len(conf.Server.Tags) > 0
return data

2
go.mod
View File

@@ -7,7 +7,7 @@ replace (
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d
// Fork to implement raw tags support
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260209170351-c057626454d0
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e
)
require (

4
go.sum
View File

@@ -36,8 +36,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/deluan/go-taglib v0.0.0-20260209170351-c057626454d0 h1:R8fMzz++cqdQ3DVjzrmAKmZFr2PT8vT8pQEfRzxms00=
github.com/deluan/go-taglib v0.0.0-20260209170351-c057626454d0/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e h1:pwx3kmHzl1N28coJV2C1zfm2ZF0qkQcGX+Z6BvXteB4=
github.com/deluan/go-taglib v0.0.0-20260212150743-3f1b97cb0d1e/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcHCKTtwzrd0HGfEmoODFA/qnk4=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6nFwQmfwgINcmSMj0xF0oHnoyRSPe4=

View File

@@ -333,76 +333,76 @@
},
"plugin": {
"name": "Plugin |||| Plugins",
"actions": {
"addConfig": "Tilføj konfiguration",
"disable": "Deaktivér",
"disabledDueToError": "Ret fejlen før aktivering",
"disabledLibrariesRequired": "Vælg biblioteker før aktivering",
"disabledUsersRequired": "Vælg brugere før aktivering",
"enable": "Aktivér",
"rescan": "Genskan"
},
"fields": {
"allLibraries": "Tillad alle biblioteker",
"allUsers": "Tillad alle brugere",
"id": "ID",
"name": "Navn",
"description": "Beskrivelse",
"version": "Version",
"author": "Forfatter",
"website": "Hjemmeside",
"permissions": "Tilladelser",
"enabled": "Aktiveret",
"status": "Status",
"path": "Sti",
"lastError": "Fejl",
"hasError": "Fejl",
"updatedAt": "Opdateret",
"createdAt": "Installeret",
"configKey": "Nøgle",
"configValue": "Værdi",
"createdAt": "Installeret",
"description": "Beskrivelse",
"enabled": "Aktiveret",
"hasError": "Fejl",
"id": "ID",
"lastError": "Fejl",
"name": "Navn",
"path": "Sti",
"permissions": "Tilladelser",
"selectedLibraries": "Valgte biblioteker",
"allUsers": "Tillad alle brugere",
"selectedUsers": "Valgte brugere",
"status": "Status",
"updatedAt": "Opdateret",
"version": "Version",
"website": "Hjemmeside"
"allLibraries": "Tillad alle biblioteker",
"selectedLibraries": "Valgte biblioteker"
},
"messages": {
"allLibrariesHelp": "Når aktiveret, vil pluginet have adgang til alle biblioteker, inklusiv dem der oprettes i fremtiden.",
"allUsersHelp": "Når aktiveret, vil pluginet have adgang til alle brugere, inklusiv dem der oprettes i fremtiden.",
"clickPermissions": "Klik på en tilladelse for detaljer",
"configHelp": "Konfigurér pluginet med nøgle-værdi-par. Lad stå tomt, hvis pluginet ikke kræver konfiguration.",
"configValidationError": "Konfigurationsvalidering mislykkedes:",
"librariesRequired": "Dette plugin kræver adgang til biblioteksoplysninger. Vælg hvilke biblioteker pluginet kan tilgå, eller aktivér 'Tillad alle biblioteker'.",
"noConfig": "Ingen konfiguration angivet",
"noLibraries": "Ingen biblioteker valgt",
"noUsers": "Ingen brugere valgt",
"permissionReason": "Årsag",
"requiredHosts": "Påkrævede værter",
"schemaRenderError": "Kan ikke vise konfigurationsformularen. Pluginets skema er muligvis ugyldigt.",
"usersRequired": "Dette plugin kræver adgang til brugeroplysninger. Vælg hvilke brugere pluginet kan tilgå, eller aktivér 'Tillad alle brugere'."
"sections": {
"status": "Status",
"info": "Pluginoplysninger",
"configuration": "Konfiguration",
"manifest": "Manifest",
"usersPermission": "Brugertilladelse",
"libraryPermission": "Bibliotekstilladelse"
},
"status": {
"enabled": "Aktiveret",
"disabled": "Deaktiveret"
},
"actions": {
"enable": "Aktivér",
"disable": "Deaktivér",
"disabledDueToError": "Ret fejlen før aktivering",
"disabledUsersRequired": "Vælg brugere før aktivering",
"disabledLibrariesRequired": "Vælg biblioteker før aktivering",
"addConfig": "Tilføj konfiguration",
"rescan": "Genskan"
},
"notifications": {
"disabled": "Plugin deaktiveret",
"enabled": "Plugin aktiveret",
"error": "Fejl ved opdatering af plugin",
"updated": "Plugin opdateret"
"disabled": "Plugin deaktiveret",
"updated": "Plugin opdateret",
"error": "Fejl ved opdatering af plugin"
},
"validation": {
"invalidJson": "Konfigurationen skal være gyldig JSON"
},
"messages": {
"configHelp": "Konfigurér pluginet med nøgle-værdi-par. Lad stå tomt, hvis pluginet ikke kræver konfiguration.",
"clickPermissions": "Klik på en tilladelse for detaljer",
"noConfig": "Ingen konfiguration angivet",
"allUsersHelp": "Når aktiveret, vil pluginet have adgang til alle brugere, inklusiv dem der oprettes i fremtiden.",
"noUsers": "Ingen brugere valgt",
"permissionReason": "Årsag",
"usersRequired": "Dette plugin kræver adgang til brugeroplysninger. Vælg hvilke brugere pluginet kan tilgå, eller aktivér 'Tillad alle brugere'.",
"allLibrariesHelp": "Når aktiveret, vil pluginet have adgang til alle biblioteker, inklusiv dem der oprettes i fremtiden.",
"noLibraries": "Ingen biblioteker valgt",
"librariesRequired": "Dette plugin kræver adgang til biblioteksoplysninger. Vælg hvilke biblioteker pluginet kan tilgå, eller aktivér 'Tillad alle biblioteker'.",
"requiredHosts": "Påkrævede hosts",
"configValidationError": "Konfigurationsvalidering mislykkedes:",
"schemaRenderError": "Kan ikke vise konfigurationsformularen. Pluginets skema er muligvis ugyldigt."
},
"placeholders": {
"configKey": "nøgle",
"configValue": "værdi"
},
"sections": {
"configuration": "Konfiguration",
"info": "Pluginoplysninger",
"libraryPermission": "Bibliotekstilladelse",
"manifest": "Manifest",
"status": "Status",
"usersPermission": "Brugertilladelse"
},
"status": {
"disabled": "Deaktiveret",
"enabled": "Aktiveret"
},
"validation": {
"invalidJson": "Konfigurationen skal være gyldig JSON"
}
}
},
@@ -674,7 +674,8 @@
"exportSuccess": "Konfigurationen eksporteret til udklipsholder i TOML-format",
"exportFailed": "Kunne ikke kopiere konfigurationen",
"devFlagsHeader": "Udviklingsflagget (med forbehold for ændring/fjernelse)",
"devFlagsComment": "Disse er eksperimental-indstillinger og kan blive fjernet i fremtidige udgaver"
"devFlagsComment": "Disse er eksperimental-indstillinger og kan blive fjernet i fremtidige udgaver",
"downloadToml": ""
}
},
"activity": {

View File

@@ -2,7 +2,7 @@
"languageName": "Euskara",
"resources": {
"song": {
"name": "Abestia |||| Abestiak",
"name": "Abestia |||| Abesti",
"fields": {
"albumArtist": "Albumaren artista",
"duration": "Iraupena",
@@ -10,6 +10,7 @@
"playCount": "Erreprodukzioak",
"title": "Titulua",
"artist": "Artista",
"composer": "Konpositorea",
"album": "Albuma",
"path": "Fitxategiaren bidea",
"libraryName": "Liburutegia",
@@ -33,9 +34,9 @@
"grouping": "Multzokatzea",
"mood": "Aldartea",
"participants": "Partaide gehiago",
"tags": "Traola gehiago",
"mappedTags": "Esleitutako traolak",
"rawTags": "Traola gordinak",
"tags": "Etiketa gehiago",
"mappedTags": "Esleitutako etiketak",
"rawTags": "Etiketa gordinak",
"missing": "Ez da aurkitu"
},
"actions": {
@@ -46,11 +47,12 @@
"shuffleAll": "Erreprodukzio aleatorioa",
"download": "Deskargatu",
"playNext": "Hurrengoa",
"info": "Erakutsi informazioa"
"info": "Erakutsi informazioa",
"instantMix": "Berehalako nahastea"
}
},
"album": {
"name": "Albuma |||| Albumak",
"name": "Albuma |||| Album",
"fields": {
"albumArtist": "Albumaren artista",
"artist": "Artista",
@@ -66,7 +68,7 @@
"date": "Recording Date",
"originalDate": "Jatorrizkoa",
"releaseDate": "Argitaratze-data",
"releases": "Argitaratzea |||| Argitaratzeak",
"releases": "Argitaratzea |||| Argitaratze",
"released": "Argitaratua",
"updatedAt": "Aktualizatze-data:",
"comment": "Iruzkina",
@@ -101,7 +103,7 @@
}
},
"artist": {
"name": "Artista |||| Artistak",
"name": "Artista |||| Artista",
"fields": {
"name": "Izena",
"albumCount": "Album kopurua",
@@ -330,6 +332,80 @@
"scanInProgress": "Araketa abian da…",
"noLibrariesAssigned": "Ez da liburutegirik egokitu erabiltzaile honentzat"
}
},
"plugin": {
"name": "Plugina |||| Plugin",
"fields": {
"id": "IDa",
"name": "Izena",
"description": "Deskribapena",
"version": "Bertsioa",
"author": "Autorea",
"website": "Webgunea",
"permissions": "Baimenak",
"enabled": "Gaituta",
"status": "Egoera",
"path": "Bidea",
"lastError": "Errorea",
"hasError": "Errorea",
"updatedAt": "Eguneratuta",
"createdAt": "Instalatuta",
"configKey": "Gakoa",
"configValue": "Balioa",
"allUsers": "Baimendu erabiltzaile guztiak",
"selectedUsers": "Hautatutako erabiltzaileak",
"allLibraries": "Baimendu liburutegi guztiak",
"selectedLibraries": "Hautatutako liburutegiak"
},
"sections": {
"status": "Egoera",
"info": "Pluginaren informazioa",
"configuration": "Konfigurazioa",
"manifest": "Manifestua",
"usersPermission": "Erabiltzaileen baimenak",
"libraryPermission": "Liburutegien baimenak"
},
"status": {
"enabled": "Gaituta",
"disabled": "Ezgaituta"
},
"actions": {
"enable": "Gaitu",
"disable": "Ezgaitu",
"disabledDueToError": "Konpondu errorea gaitu baino lehen",
"disabledUsersRequired": "Hautatu erabiltzaileak gaitu baino lehen",
"disabledLibrariesRequired": "Hautatu liburutegiak gaitu baino lehen",
"addConfig": "Gehitu konfigurazioa",
"rescan": "Arakatu berriro"
},
"notifications": {
"enabled": "Plugina gaituta",
"disabled": "Plugina ezgaituta",
"updated": "Plugina eguneratuta",
"error": "Errorea plugina eguneratzean"
},
"validation": {
"invalidJson": "Konfigurazioa baliozko JSON-a izan behar da"
},
"messages": {
"configHelp": "Konfiguratu plugina gako-balio bikoteak erabiliz. Utzi hutsik pluginak konfiguraziorik behar ez badu.",
"configValidationError": "Huts egin du konfigurazioaren balidazioak:",
"schemaRenderError": "Ezin izan da konfigurazioaren formularioa bihurtu. Litekeena da pluginaren eskema baliozkoa ez izatea.",
"clickPermissions": "Sakatu baimen batean xehetasunetarako",
"noConfig": "Ez da konfiguraziorik ezarri",
"allUsersHelp": "Gaituta dagoenean, pluginak erabiltzaile guztiak atzitu ditzazke, baita etorkizunean sortuko direnak ere.",
"noUsers": "Ez da erabiltzailerik hautatu",
"permissionReason": "Arrazoia",
"usersRequired": "Plugin honek erabiltzaileen informaziora sarbidea behar du. Hautatu zein erabiltzaile atzitu dezakeen pluginak, edo gaitu 'Baimendu erabiltzaile guztiak'.",
"allLibrariesHelp": "Gaituta dagoenean, pluginak liburutegi guztietara izango du sarbidea, baita etorkizunean sortuko direnetara ere.",
"noLibraries": "Ez da liburutegirik hautatu",
"librariesRequired": "Plugin honek liburutegien informaziora sarbidea behar du. Hautatu zein liburutegi atzitu dezakeen pluginak, edo gaitu 'Baimendu liburutegi guztiak'.",
"requiredHosts": "Beharrezko ostatatzaileak"
},
"placeholders": {
"configKey": "gakoa",
"configValue": "balioa"
}
}
},
"ra": {
@@ -483,6 +559,7 @@
"transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.",
"songsAddedToPlaylist": "Abesti bat zerrendara gehitu da |||| %{smart_count} abesti zerrendara gehitu dira",
"noSimilarSongsFound": "Ez da antzeko abestirik aurkitu",
"startingInstantMix": "Berehalako nahastea kargatzen…",
"noTopSongsFound": "Ez da aparteko abestirik aurkitu",
"noPlaylistsAvailable": "Ez dago zerrendarik erabilgarri",
"delete_user_title": "Ezabatu '%{name}' erabiltzailea",

View File

@@ -10,6 +10,7 @@
"playCount": "Lejátszások",
"title": "Cím",
"artist": "Előadó",
"composer": "Zeneszerző",
"album": "Album",
"path": "Elérési út",
"libraryName": "Könyvtár",
@@ -46,7 +47,8 @@
"shuffleAll": "Keverés",
"download": "Letöltés",
"playNext": "Lejátszás következőként",
"info": "Részletek"
"info": "Részletek",
"instantMix": "Instant keverés"
}
},
"album": {
@@ -325,6 +327,80 @@
"scanInProgress": "Szkennelés folyamatban...",
"noLibrariesAssigned": "Ehhez a felhasználóhoz nincsenek könyvtárak adva"
}
},
"plugin": {
"name": "Kiegészítő |||| Kiegészítők",
"fields": {
"id": "ID",
"name": "Név",
"description": "Leírás",
"version": "Verzió",
"author": "Fejlesztő",
"website": "Weboldal",
"permissions": "Engedélyek",
"enabled": "Engedélyezve",
"status": "Státusz",
"path": "Útvonal",
"lastError": "Hiba",
"hasError": "Hiba",
"updatedAt": "Frissítve",
"createdAt": "Telepítve",
"configKey": "Kulcs",
"configValue": "Érték",
"allUsers": "Összes felhasználó engedélyezése",
"selectedUsers": "Kiválasztott felhasználók engedélyezése",
"allLibraries": "Összes könyvtár engedélyezése",
"selectedLibraries": "Kiválasztott könyvtárak engedélyezése"
},
"sections": {
"status": "Státusz",
"info": "Kiegészítő információi",
"configuration": "Konfiguráció",
"manifest": "Manifest",
"usersPermission": "Felhasználói engedélyek",
"libraryPermission": "Könyvtári engedélyek"
},
"status": {
"enabled": "Engedélyezve",
"disabled": "Letiltva"
},
"actions": {
"enable": "Engedélyezés",
"disable": "Letiltás",
"disabledDueToError": "Javítsd ki a kiegészítő hibáját",
"disabledUsersRequired": "Válassz felhasználókat",
"disabledLibrariesRequired": "Válassz könyvtárakat",
"addConfig": "Konfiguráció hozzáadása",
"rescan": "Újraszkennelés"
},
"notifications": {
"enabled": "Kiegészítő engedélyezve",
"disabled": "Kiegészítő letiltva",
"updated": "Kiegészítő frissítve",
"error": "Hiba történt a kiegészítő frissítése közben"
},
"validation": {
"invalidJson": "A konfigurációs JSON érvénytelen"
},
"messages": {
"configHelp": "Konfiguráld a kiegészítőt kulcs-érték párokkal. Hagyd a mezőt üresen, ha nincs szükség konfigurációra.",
"configValidationError": "Helytelen konfiguráció:",
"schemaRenderError": "Nem sikerült megjeleníteni a konfigurációs űrlapot. A bővítmény sémája érvénytelen lehet.",
"clickPermissions": "Kattints egy engedélyre a részletekért",
"noConfig": "Nincs konfiguráció beállítva",
"allUsersHelp": "Engedélyezés esetén ez a kiegészítő hozzá fog férni minden jelenlegi és jövőben létrehozott felhasználóhoz.",
"noUsers": "Nincsenek kiválasztott felhasználók",
"permissionReason": "Indok",
"usersRequired": "Ez a kiegészítő hozzáférést kér felhasználói információkhoz. Válaszd ki, melyik felhasználókat érheti el, vagy az 'Összes felhasználó engedélyezése' opciót.",
"allLibrariesHelp": "Engedélyezés esetén ez a kiegészítő hozzá fog férni minden jelenlegi és jövőben létrehozott könyvtárhoz.",
"noLibraries": "Nincs kiválasztott könyvtár",
"librariesRequired": "Ez a kiegészítő hozzáférést kér könyvtárinformációkhoz. Válaszd ki, melyik könyvtárakat érheti el, vagy az 'Összes könyvtár engedélyezése' opciót.",
"requiredHosts": "Szükséges hostok"
},
"placeholders": {
"configKey": "kulcs",
"configValue": "érték"
}
}
},
"ra": {
@@ -402,7 +478,7 @@
"loading": "Betöltés",
"not_found": "Nem található",
"show": "%{name} #%{id}",
"empty": "Nincs %{name} még.",
"empty": "Nincsenek %{name}.",
"invite": "Szeretnél egyet hozzáadni?"
},
"input": {
@@ -478,6 +554,7 @@
"transcodingEnabled": "A Navidrome jelenleg a következőkkel fut %{config}, ez lehetővé teszi a rendszerparancsok futtatását az átkódolási beállításokból a webes felület segítségével. Javasoljuk, hogy biztonsági okokból tiltsd ezt le, és csak az átkódolási beállítások konfigurálásának idejére kapcsold be.",
"songsAddedToPlaylist": "1 szám hozzáadva a lejátszási listához |||| %{smart_count} szám hozzáadva a lejátszási listához",
"noSimilarSongsFound": "Nem találhatóak hasonló számok",
"startingInstantMix": "Instant keverés töltődik...",
"noTopSongsFound": "Nincsenek top számok",
"noPlaylistsAvailable": "Nem áll rendelkezésre",
"delete_user_title": "Felhasználó törlése '%{name}'",
@@ -591,6 +668,7 @@
"currentValue": "Jelenlegi érték",
"configurationFile": "Konfigurációs fájl",
"exportToml": "Konfiguráció exportálása (TOML)",
"downloadToml": "Konfiguráció letöltése (TOML)",
"exportSuccess": "Konfiguráció kiexportálva a vágólapra, TOML formában",
"exportFailed": "Nem sikerült kimásolni a konfigurációt",
"devFlagsHeader": "Fejlesztői beállítások (változások/eltávolítás jogát fenntartjuk)",

View File

@@ -674,7 +674,8 @@
"exportSuccess": "Configuração exportada para o clipboard em formato TOML",
"exportFailed": "Falha ao copiar configuração",
"devFlagsHeader": "Flags de Desenvolvimento (sujeitas a mudança/remoção)",
"devFlagsComment": "Estas são configurações experimentais e podem ser removidas em versões futuras"
"devFlagsComment": "Estas são configurações experimentais e podem ser removidas em versões futuras",
"downloadToml": "Baixar configuração (TOML)"
}
},
"activity": {

View File

File diff suppressed because it is too large Load Diff

113
server/e2e/doc.go Normal file
View File

@@ -0,0 +1,113 @@
// Package e2e provides end-to-end integration tests for the Navidrome Subsonic API.
//
// These tests exercise the full HTTP request/response cycle through the Subsonic API router,
// using a real SQLite database and real repository implementations while stubbing out external
// services (artwork, streaming, scrobbling, etc.) with noop implementations.
//
// # Test Infrastructure
//
// The suite uses [Ginkgo] v2 as the test runner and [Gomega] for assertions. It is invoked
// through the standard Go test entry point [TestSubsonicE2E], which initializes the test
// environment, creates a temporary SQLite database, and runs the specs.
//
// # Setup and Teardown
//
// During [BeforeSuite], the test infrastructure:
//
// 1. Creates a temporary SQLite database with WAL journal mode.
// 2. Initializes the schema via [db.Init].
// 3. Creates two test users: an admin ("admin") and a regular user ("regular"),
// both with the password "password".
// 4. Creates a single library ("Music Library") backed by a fake in-memory filesystem
// (scheme "fake:///music") using the [storagetest] package.
// 5. Populates the filesystem with a set of test tracks spanning multiple artists,
// albums, genres, and years.
// 6. Runs the scanner to import all metadata into the database.
// 7. Takes a snapshot of the database to serve as a golden baseline for test isolation.
//
// # Test Data
//
// The fake filesystem contains the following music library structure:
//
// Rock/The Beatles/Abbey Road/
// 01 - Come Together.mp3 (1969, Rock)
// 02 - Something.mp3 (1969, Rock)
// Rock/The Beatles/Help!/
// 01 - Help.mp3 (1965, Rock)
// Rock/Led Zeppelin/IV/
// 01 - Stairway To Heaven.mp3 (1971, Rock)
// Jazz/Miles Davis/Kind of Blue/
// 01 - So What.mp3 (1959, Jazz)
// Pop/
// 01 - Standalone Track.mp3 (2020, Pop)
//
// # Database Isolation
//
// Before each top-level Describe block, the [setupTestDB] function restores the database
// to its golden snapshot state using SQLite's ATTACH DATABASE mechanism. This copies all
// table data from the snapshot back into the main database, providing each test group with
// a clean, consistent starting state without the overhead of re-scanning the filesystem.
//
// A fresh [subsonic.Router] is also created for each test group, wired with real data store
// repositories and noop stubs for external services:
//
// - noopArtwork: returns [model.ErrNotFound] for all artwork requests.
// - noopStreamer: returns [model.ErrNotFound] for all stream requests.
// - noopArchiver: returns [model.ErrNotFound] for all archive requests.
// - noopProvider: returns empty results for all external metadata lookups.
// - noopPlayTracker: silently discards all scrobble events.
//
// # Request Helpers
//
// Tests build HTTP requests using the [buildReq] helper, which constructs a Subsonic API
// request with authentication parameters (username, password, API version "1.16.1", client
// name "test-client", and JSON format). Convenience wrappers include:
//
// - [doReq]: sends a request as the admin user and returns the parsed JSON response.
// - [doReqWithUser]: sends a request as a specific user.
// - [doRawReq] / [doRawReqWithUser]: returns the raw [httptest.ResponseRecorder] for
// binary content or status code inspection.
//
// Responses are parsed via [parseJSONResponse], which unwraps the Subsonic JSON envelope
// and returns the inner response map.
//
// # Test Organization
//
// Each test file covers a logical group of Subsonic API endpoints:
//
// - subsonic_system_test.go: ping, getLicense, getOpenSubsonicExtensions
// - subsonic_browsing_test.go: getMusicFolders, getIndexes, getArtists, getMusicDirectory,
// getArtist, getAlbum, getSong, getGenres
// - subsonic_searching_test.go: search2, search3
// - subsonic_album_lists_test.go: getAlbumList, getAlbumList2
// - subsonic_playlists_test.go: createPlaylist, getPlaylist, getPlaylists,
// updatePlaylist, deletePlaylist
// - subsonic_media_annotation_test.go: star, unstar, getStarred, setRating, scrobble
// - subsonic_media_retrieval_test.go: stream, download, getCoverArt, getAvatar,
// getLyrics, getLyricsBySongId
// - subsonic_bookmarks_test.go: createBookmark, getBookmarks, deleteBookmark,
// savePlayQueue, getPlayQueue
// - subsonic_radio_test.go: getInternetRadioStations, createInternetRadioStation,
// updateInternetRadioStation, deleteInternetRadioStation
// - subsonic_sharing_test.go: createShare, getShares, updateShare, deleteShare
// - subsonic_users_test.go: getUser, getUsers
// - subsonic_scan_test.go: getScanStatus, startScan
// - subsonic_multiuser_test.go: multi-user isolation and permission enforcement
// - subsonic_multilibrary_test.go: multi-library access control and data isolation
//
// Some test groups use Ginkgo's Ordered decorator to run tests sequentially within a block,
// allowing later tests to depend on state created by earlier ones (e.g., creating a playlist
// and then verifying it can be retrieved).
//
// # Running
//
// The e2e tests are included in the standard test suite and can be run with:
//
// make test PKG=./server/e2e # Run only e2e tests
// make test # Run all tests including e2e
// make test-race # Run with race detector
//
// [Ginkgo]: https://onsi.github.io/ginkgo/
// [Gomega]: https://onsi.github.io/gomega/
// [storagetest]: /core/storage/storagetest
package e2e

View File

@@ -9,6 +9,7 @@ import TableBody from '@material-ui/core/TableBody'
import TableRow from '@material-ui/core/TableRow'
import TableCell from '@material-ui/core/TableCell'
import Paper from '@material-ui/core/Paper'
import CloudDownloadIcon from '@material-ui/icons/CloudDownload'
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder'
import FileCopyIcon from '@material-ui/icons/FileCopy'
import Button from '@material-ui/core/Button'
@@ -245,6 +246,21 @@ const ConfigTabContent = ({ configData }) => {
}
}
const handleDownloadToml = () => {
const tomlContent = configToToml(configData, translate)
const tomlFile = new File([tomlContent], 'navidrome.toml', {
type: 'text/plain',
})
const tomlFileLink = document.createElement('a')
const tomlFileUrl = URL.createObjectURL(tomlFile)
tomlFileLink.href = tomlFileUrl
tomlFileLink.download = tomlFile.name
tomlFileLink.click()
URL.revokeObjectURL(tomlFileUrl)
}
return (
<div className={classes.configContainer}>
<Button
@@ -252,11 +268,23 @@ const ConfigTabContent = ({ configData }) => {
startIcon={<FileCopyIcon />}
onClick={handleCopyToml}
className={classes.copyButton}
disabled={!configData}
disabled={
!configData || !navigator.clipboard || !window.isSecureContext
}
size="small"
>
{translate('about.config.exportToml')}
</Button>
<Button
variant="outlined"
startIcon={<CloudDownloadIcon />}
onClick={handleDownloadToml}
className={classes.copyButton}
disabled={!configData}
size="small"
>
{translate('about.config.downloadToml')}
</Button>
<TableContainer className={classes.tableContainer}>
<Table size="small" stickyHeader>
<TableHead>

View File

@@ -673,6 +673,7 @@
"currentValue": "Current Value",
"configurationFile": "Configuration File",
"exportToml": "Export Configuration (TOML)",
"downloadToml": "Download Configuration (TOML)",
"exportSuccess": "Configuration exported to clipboard in TOML format",
"exportFailed": "Failed to copy configuration",
"devFlagsHeader": "Development Flags (subject to change/removal)",

View File

@@ -0,0 +1,181 @@
const stylesheet = `
/* Icon hover: pink */
.react-jinke-music-player-main svg:active, .react-jinke-music-player-main svg:hover {
color: #ff79c6
}
/* Progress bar: purple */
.react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-handle, .react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-track {
background-color: #bd93f9
}
/* Volume bar: green */
.sound-operation .rc-slider-handle, .sound-operation .rc-slider-track {
background-color: #50fa7b !important
}
.sound-operation .rc-slider-handle:active {
box-shadow: 0 0 2px #50fa7b !important
}
/* Scrollbar: comment */
.react-jinke-music-player-main ::-webkit-scrollbar-thumb {
background-color: #6272a4;
}
.react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-handle:active {
box-shadow: 0 0 2px #bd93f9
}
/* Now playing icon: cyan */
.react-jinke-music-player-main .audio-item.playing svg {
color: #8be9fd
}
/* Now playing artist: cyan */
.react-jinke-music-player-main .audio-item.playing .player-singer {
color: #8be9fd !important
}
/* Loading spinner: orange */
.react-jinke-music-player-main .loading svg {
color: #ffb86c !important
}
.react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-handle {
border: hidden;
box-shadow: rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px;
}
.rc-slider-rail, .rc-slider-track {
height: 6px;
}
.rc-slider {
padding: 3px 0;
}
.sound-operation > div:nth-child(4) {
transform: translateX(-50%) translateY(5%) !important;
}
.sound-operation {
padding: 4px 0;
}
/* Player panel background */
.react-jinke-music-player-main .music-player-panel {
background-color: #282a36;
color: #f8f8f2;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
}
/* Song title in player: foreground */
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-title {
color: #f8f8f2;
}
/* Duration/time text: yellow */
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .duration, .react-jinke-music-player-main .music-player-panel .panel-content .player-content .current-time {
color: #f1fa8c
}
/* Audio list panel */
.audio-lists-panel {
background-color: #282a36;
bottom: 6.25rem;
box-shadow: rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px;
}
.audio-lists-panel-content .audio-item.playing {
background-color: transparent;
}
.audio-lists-panel-content .audio-item:nth-child(2n+1) {
background-color: transparent;
}
/* Playlist hover: current line */
.audio-lists-panel-content .audio-item:active,
.audio-lists-panel-content .audio-item:hover {
background-color: #44475a;
}
.audio-lists-panel-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
box-shadow: none;
}
/* Playlist header text: orange */
.audio-lists-panel-header-title {
color: #ffb86c;
}
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .audio-lists-btn {
background-color: transparent;
box-shadow: none;
}
.audio-lists-panel-content .audio-item {
line-height: 32px;
}
.react-jinke-music-player-main .music-player-panel .panel-content .img-content {
box-shadow: rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px;
}
/* Lyrics: yellow */
.react-jinke-music-player-main .music-player-lyric {
color: #f1fa8c;
-webkit-text-stroke: 0.5px #282a36;
font-weight: bolder;
}
/* Lyric button active: yellow */
.react-jinke-music-player-main .lyric-btn-active, .react-jinke-music-player-main .lyric-btn-active svg {
color: #f1fa8c !important;
}
/* Playlist now playing: cyan */
.audio-lists-panel-content .audio-item.playing, .audio-lists-panel-content .audio-item.playing svg {
color: #8be9fd
}
/* Playlist hover icons: pink */
.audio-lists-panel-content .audio-item:active .group:not(.player-delete) svg, .audio-lists-panel-content .audio-item:hover .group:not(.player-delete) svg {
color: #ff79c6
}
.audio-lists-panel-content .audio-item .player-icons {
scale: 75%;
}
/* Mobile */
.react-jinke-music-player-mobile-cover {
border: none;
box-shadow: rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px;
}
.react-jinke-music-player .music-player-controller {
border: none;
box-shadow: rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px;
color: #bd93f9;
}
.react-jinke-music-player .music-player-controller .music-player-controller-setting {
color: rgba(189, 147, 249, 0.3);
}
/* Mobile progress: green */
.react-jinke-music-player-mobile-progress .rc-slider-handle, .react-jinke-music-player-mobile-progress .rc-slider-track {
background-color: #50fa7b;
}
.react-jinke-music-player-mobile-progress .rc-slider-handle {
border: none;
}
`
export default stylesheet

397
ui/src/themes/dracula.js Normal file
View File

@@ -0,0 +1,397 @@
import stylesheet from './dracula.css.js'
// Dracula color palette
const background = '#282a36'
const currentLine = '#44475a'
const foreground = '#f8f8f2'
const comment = '#6272a4'
const cyan = '#8be9fd'
const green = '#50fa7b'
const pink = '#ff79c6'
const purple = '#bd93f9'
const orange = '#ffb86c'
const red = '#ff5555'
const yellow = '#f1fa8c'
// Darker shade for surfaces
const surface = '#21222c'
// For Album, Playlist play button
const musicListActions = {
alignItems: 'center',
'@global': {
'button:first-child:not(:only-child)': {
'@media screen and (max-width: 720px)': {
transform: 'scale(1.5)',
margin: '1rem',
'&:hover': {
transform: 'scale(1.6) !important',
},
},
transform: 'scale(2)',
margin: '1.5rem',
minWidth: 0,
padding: 5,
transition: 'transform .3s ease',
backgroundColor: `${green} !important`,
color: background,
borderRadius: 500,
border: 0,
'&:hover': {
transform: 'scale(2.1)',
backgroundColor: `${green} !important`,
border: 0,
},
},
'button:only-child': {
margin: '1.5rem',
},
'button:first-child>span:first-child': {
padding: 0,
},
'button:first-child>span:first-child>span': {
display: 'none',
},
'button>span:first-child>span, button:not(:first-child)>span:first-child>svg':
{
color: foreground,
},
},
}
export default {
themeName: 'Dracula',
palette: {
primary: {
main: purple,
},
secondary: {
main: currentLine,
contrastText: foreground,
},
error: {
main: red,
},
type: 'dark',
background: {
default: background,
paper: surface,
},
},
overrides: {
MuiPaper: {
root: {
color: foreground,
backgroundColor: surface,
},
},
MuiAppBar: {
positionFixed: {
backgroundColor: `${currentLine} !important`,
boxShadow:
'rgba(20, 21, 28, 0.25) 0px 4px 6px, rgba(20, 21, 28, 0.1) 0px 5px 7px',
},
},
MuiDrawer: {
root: {
background: background,
},
},
MuiButton: {
textPrimary: {
color: purple,
},
textSecondary: {
color: foreground,
},
},
MuiIconButton: {
root: {
color: foreground,
},
},
MuiChip: {
root: {
backgroundColor: currentLine,
},
},
MuiFormGroup: {
root: {
color: foreground,
},
},
MuiFormLabel: {
root: {
color: comment,
'&$focused': {
color: purple,
},
},
},
MuiToolbar: {
root: {
backgroundColor: `${surface} !important`,
},
},
MuiOutlinedInput: {
root: {
'& $notchedOutline': {
borderColor: currentLine,
},
'&:hover $notchedOutline': {
borderColor: comment,
},
'&$focused $notchedOutline': {
borderColor: purple,
},
},
},
MuiFilledInput: {
root: {
backgroundColor: currentLine,
'&:hover': {
backgroundColor: comment,
},
'&$focused': {
backgroundColor: currentLine,
},
},
},
MuiTableRow: {
root: {
transition: 'background-color .3s ease',
'&:hover': {
backgroundColor: `${currentLine} !important`,
},
},
},
MuiTableHead: {
root: {
color: foreground,
background: surface,
},
},
MuiTableCell: {
root: {
color: foreground,
background: `${surface} !important`,
borderBottom: `1px solid ${currentLine}`,
},
head: {
color: `${yellow} !important`,
background: `${currentLine} !important`,
},
body: {
color: `${foreground} !important`,
},
},
MuiSwitch: {
colorSecondary: {
'&$checked': {
color: green,
},
'&$checked + $track': {
backgroundColor: green,
},
},
},
NDAlbumGridView: {
albumName: {
marginTop: '0.5rem',
fontWeight: 700,
color: foreground,
},
albumSubtitle: {
color: comment,
},
albumContainer: {
backgroundColor: surface,
borderRadius: '8px',
padding: '.75rem',
transition: 'background-color .3s ease',
'&:hover': {
backgroundColor: currentLine,
},
},
albumPlayButton: {
backgroundColor: green,
borderRadius: '50%',
boxShadow: '0 8px 8px rgb(0 0 0 / 30%)',
padding: '0.35rem',
transition: 'padding .3s ease',
'&:hover': {
background: `${green} !important`,
padding: '0.45rem',
},
},
},
NDPlaylistDetails: {
container: {
background: `linear-gradient(${currentLine}, transparent)`,
borderRadius: 0,
paddingTop: '2.5rem !important',
boxShadow: 'none',
},
title: {
fontWeight: 700,
color: foreground,
},
details: {
fontSize: '.875rem',
color: comment,
},
},
NDAlbumDetails: {
root: {
background: `linear-gradient(${currentLine}, transparent)`,
borderRadius: 0,
boxShadow: 'none',
},
cardContents: {
alignItems: 'center',
paddingTop: '1.5rem',
},
recordName: {
fontWeight: 700,
color: foreground,
},
recordArtist: {
fontSize: '.875rem',
fontWeight: 700,
color: pink,
},
recordMeta: {
fontSize: '.875rem',
color: comment,
},
},
NDCollapsibleComment: {
commentBlock: {
fontSize: '.875rem',
color: comment,
},
},
NDAlbumShow: {
albumActions: musicListActions,
},
NDPlaylistShow: {
playlistActions: musicListActions,
},
NDAudioPlayer: {
audioTitle: {
color: foreground,
fontSize: '0.875rem',
},
songTitle: {
fontWeight: 400,
},
songInfo: {
fontSize: '0.675rem',
color: comment,
},
},
NDLogin: {
systemNameLink: {
color: purple,
},
welcome: {
color: foreground,
},
card: {
minWidth: 300,
background: background,
},
button: {
boxShadow: '3px 3px 5px #191a21',
},
},
NDMobileArtistDetails: {
bgContainer: {
background: `linear-gradient(to bottom, rgba(40 42 54 / 72%), ${surface})!important`,
},
},
RaLayout: {
content: {
padding: '0 !important',
background: surface,
},
root: {
backgroundColor: background,
},
},
RaList: {
content: {
backgroundColor: surface,
},
},
RaListToolbar: {
toolbar: {
backgroundColor: background,
padding: '0 .55rem !important',
},
},
RaSidebar: {
fixed: {
backgroundColor: background,
},
drawerPaper: {
backgroundColor: `${background} !important`,
},
},
MuiTableSortLabel: {
root: {
color: `${yellow} !important`,
'&:hover': {
color: `${orange} !important`,
},
'&$active': {
color: `${orange} !important`,
'&& $icon': {
color: `${orange} !important`,
},
},
},
},
RaMenuItemLink: {
root: {
color: foreground,
'&[aria-current="page"]': {
color: `${pink} !important`,
},
'&[aria-current="page"] .MuiListItemIcon-root': {
color: `${pink} !important`,
},
},
active: {
color: `${pink} !important`,
'& .MuiListItemIcon-root': {
color: `${pink} !important`,
},
},
},
RaLink: {
link: {
color: cyan,
},
},
RaButton: {
button: {
margin: '0 5px 0 5px',
},
},
RaPaginationActions: {
currentPageButton: {
border: `2px solid ${purple}`,
},
button: {
backgroundColor: currentLine,
minWidth: 48,
margin: '0 4px',
},
},
},
player: {
theme: 'dark',
stylesheet,
},
}

View File

@@ -9,6 +9,7 @@ import ElectricPurpleTheme from './electricPurple'
import NordTheme from './nord'
import GruvboxDarkTheme from './gruvboxDark'
import CatppuccinMacchiatoTheme from './catppuccinMacchiato'
import DraculaTheme from './dracula'
import NuclearTheme from './nuclear'
import AmusicTheme from './amusic'
import SquiddiesGlassTheme from './SquiddiesGlass'
@@ -22,6 +23,7 @@ export default {
// New themes should be added here, in alphabetic order
AmusicTheme,
CatppuccinMacchiatoTheme,
DraculaTheme,
ElectricPurpleTheme,
ExtraDarkTheme,
GreenTheme,