diff --git a/changes/changelog.d/756.feature b/changes/changelog.d/756.feature new file mode 100644 index 0000000000000000000000000000000000000000..402b0e589ffb2c255d6ab392f822afab77b84b21 --- /dev/null +++ b/changes/changelog.d/756.feature @@ -0,0 +1 @@ +Dark theme (#756) diff --git a/front/public/index.html b/front/public/index.html index 7b09feaf028a141997212222c7967639038a1bd1..142419ca63f3295dadbf8773931f2cbe7e37e1cc 100644 --- a/front/public/index.html +++ b/front/public/index.html @@ -9,7 +9,7 @@ <title>Funkwhale</title> </head> -<body> +<body class="theme-light" id="body"> <noscript> <strong>We're sorry but Funkwhale doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> diff --git a/front/src/App.vue b/front/src/App.vue index 5711466c5bb9cd29eaec058174a9fe7a4c5376b9..e401d475e05955a42eb24c0625325306b1d9e956 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -251,6 +251,14 @@ export default { this.$store.dispatch('instance/fetchSettings') this.fetchNodeInfo() }, + '$store.state.ui.theme': { + immediate: true, + handler (newValue, oldValue) { + let oldTheme = oldValue || 'light' + document.body.classList.remove(`theme-${oldTheme}`) + document.body.classList.add(`theme-${newValue}`) + }, + }, '$store.state.auth.authenticated' (newValue) { if (!newValue) { this.disconnect() diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue index 2d06b50c768e368c538b15bbcf6b33a00c6b42ab..d758b456bf9edaea8b6af87efe6e349a7e640778 100644 --- a/front/src/components/Footer.vue +++ b/front/src/components/Footer.vue @@ -33,6 +33,14 @@ <a href="https://funkwhale.audio/apps" class="item" target="_blank"><translate translate-context="Footer/*/List item.Link">Mobile and desktop apps</translate></a> <div role="button" class="item" @click="$emit('show:shortcuts-modal')"><translate translate-context="*/*/*/Noun">Keyboard shortcuts</translate></div> </div> + <div class="ui form"> + <div class="ui field"> + <label><translate translate-context="Footer/Settings/Dropdown.Label/Short, Verb">Change theme</translate></label> + <select class="ui dropdown" :value="$store.state.ui.theme" @change="$store.commit('ui/theme', $event.target.value)"> + <option v-for="theme in themes" :key="theme.key" :value="theme.key">{{ theme.name }}</option> + </select> + </div> + </div> </section> <section class="four wide column"> <h4 v-translate translate-context="Footer/*/Link" class="ui header">Getting help</h4> @@ -76,15 +84,18 @@ export default { parser.href = url return parser.hostname }, + themes () { + return [ + { + name: this.$pgettext('Footer/Settings/Dropdown.Label/Theme name', 'Light'), + key: 'light' + }, + { + name: this.$pgettext('Footer/Settings/Dropdown.Label/Theme name', 'Dark'), + key: 'dark' + } + ] + } } } </script> -<style scoped> -footer p { - color: grey; -} - -footer#footer div.item:hover { - color: rgba(0, 0, 0, 0.87); -} -</style> diff --git a/front/src/components/notifications/NotificationRow.vue b/front/src/components/notifications/NotificationRow.vue index b8e145647d03340fb62b8edc981f25179f341a9a..95f8e36440f486d1e7916b813a2301639e3efbd8 100644 --- a/front/src/components/notifications/NotificationRow.vue +++ b/front/src/components/notifications/NotificationRow.vue @@ -126,7 +126,4 @@ export default { .read > span { cursor: pointer; } -.disabled-row { - color: rgba(40, 40, 40, 0.3); -} </style> diff --git a/front/src/components/playlists/Card.vue b/front/src/components/playlists/Card.vue index 60a322ab4c86486842d79f176b6b536b6714041e..6efc2350e00b990d0410c0e8c644340a5b7c3f4f 100644 --- a/front/src/components/playlists/Card.vue +++ b/front/src/components/playlists/Card.vue @@ -59,12 +59,10 @@ export default { <!-- Add "scoped" attribute to limit CSS to this component only --> <style> - .playlist.card .header .ellipsis.vertical.large.grey { font-size: 1.2em; margin-right: 0; } - </style> <style scoped> .card .header { @@ -72,8 +70,7 @@ export default { } .attached.button { - background-color: rgb(243, 244, 245); - background-size: 25% ; + background-size: 25%; background-repeat: no-repeat; background-origin: border-box; background-position: 0 0, 33.33% 0, 66.67% 0, 100% 0; @@ -82,5 +79,4 @@ export default { font-size: 4em; box-shadow: 0px 0px 0px 1px rgba(34, 36, 38, 0.15) inset !important; } - </style> diff --git a/front/src/store/index.js b/front/src/store/index.js index 791dbb1e92fd1d3add3509ea67575dc734c64dd8..126368e23eaa6759b05bcfde1b9c71bbc5207e5e 100644 --- a/front/src/store/index.js +++ b/front/src/store/index.js @@ -40,7 +40,7 @@ export default new Vuex.Store({ }), createPersistedState({ key: 'ui', - paths: ['ui.currentLanguage', 'ui.momentLocale'] + paths: ['ui.currentLanguage', 'ui.momentLocale', 'ui.theme'] }), createPersistedState({ key: 'radios', diff --git a/front/src/store/ui.js b/front/src/store/ui.js index 8a8bc1da01854a6b630c5a3877bca91217feb524..d13a92d8820776bbee8c052e90f36c7c52cb1a56 100644 --- a/front/src/store/ui.js +++ b/front/src/store/ui.js @@ -10,6 +10,7 @@ export default { maxMessages: 100, messageDisplayDuration: 10000, messages: [], + theme: 'light', notifications: { inbox: 0, pendingReviewEdits: 0, @@ -39,6 +40,9 @@ export default { computeLastDate: (state) => { state.lastDate = new Date() }, + theme: (state, value) => { + state.theme = value + }, addMessage (state, message) { state.messages.push(message) if (state.messages.length > state.maxMessages) { diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 8f8ee8b16f514dac63480f0375774166e17db18f..bc527415c7658b82a418f0f678398f01f8627d3e 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -10,6 +10,10 @@ Import this file into your LESS project to use Semantic UI without build tools */ +// Those semantic-ui-css/*.scss don't exist in the package, but we create them +// via scripts/link-scss-files.sh on postinstall, so we can include theme +// under a class namespace + /* Global */ @import "~semantic-ui-css/components/reset.css"; // we use our custom site css here to avoid loading google font @@ -96,6 +100,7 @@ body { #app > main, #app > .main { flex: 1; } + .instance-chooser { margin-top: 2em; } @@ -126,14 +131,10 @@ body { margin-left: 0; margin-right: 0; border: none; - box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15); .ui.item { border: none; border-bottom-style: none; margin-bottom: 0px; - &.active { - box-shadow: inset 0px -2px 0px 0px #000; - } } @include media(">tablet") { padding: 0 2.5rem; @@ -148,7 +149,6 @@ body { @include media(">widedesktop") { left: $widedesktop-sidebar-width; } - background-color: white; .item { padding-top: 1.5em; padding-bottom: 1.5em; @@ -208,9 +208,6 @@ body { } } -.discrete { - color: rgba(0, 0, 0, 0.87); -} .link { cursor: pointer; } @@ -355,3 +352,8 @@ input + .help { .table td .ui.dropdown { min-width: 150px; } + + + +@import "./themes/_light.scss"; +@import "./themes/_dark.scss"; diff --git a/front/src/style/themes/_dark.scss b/front/src/style/themes/_dark.scss new file mode 100644 index 0000000000000000000000000000000000000000..f612914ca87ad3e14876d8026ec95757ffb2e380 --- /dev/null +++ b/front/src/style/themes/_dark.scss @@ -0,0 +1,222 @@ +$background-color: rgb(43, 58, 66); +$button-hover-color: rgb(33, 48, 56); +$light-background-color: rgb(51, 71, 82); +$input-background-color: rgb(189, 211, 222); +$loading-background-color: rgba(43, 58, 66, 0.9); +$text-color: rgb(223, 235, 240); +$discrete-text-color: rgba(223, 235, 240, 0.904); +$border-color: rgb(63, 88, 102); +$light-shadow-color: rgba(223, 235, 240, 0.15); +$shadow-color: rgba(63, 102, 97, 0.95); +$box-shadow: 0px 1px 3px 0px rgba(63, 88, 102, 0.95), 0px 0px 0px 1px rgba(63, 88, 102, 0.98); +$link-color: rgb(255, 144, 0); + +.theme-dark { + background-color: $background-color; + .ui.labeled.input { + input, .label { + background-color: $input-background-color; + &::placeholder { + color: $light-background-color; + + } + + } + } + .ui.statistics .statistic { + > .label, > .value { + color: $text-color; + } + } + .ui.link.list.list .active.item, .ui.link.list.list .active.item a:not(.ui) { + color: inherit; + } + .ui.form textarea, .ui.form select, .ui.selection.dropdown, .ui.dropdown.selected, .ui.dropdown .menu .selected.item, .ui.form input:not([type]), .ui.form input[type="date"], .ui.form input[type="datetime-local"], .ui.form input[type="email"], .ui.form input[type="number"], .ui.form input[type="password"], .ui.form input[type="search"], .ui.form input[type="tel"], .ui.form input[type="time"], .ui.form input[type="text"], .ui.form input[type="file"], .ui.form input[type="url"] { + background-color: $input-background-color; + &::placeholder { + color: $light-background-color; + + } + } + .ui.dropdown .menu .item:hover { + background-color: $light-background-color; + color: $text-color; + + } + .main.pusher > .ui.secondary.menu { + background-color: $background-color; + box-shadow: inset 0px -2px 0px 0px $light-background-color; + .ui.item { + color: $text-color; + &.active { + box-shadow: inset 0px -2px 0px 0px $shadow-color; + } + } + } + .ui.modal { + > .header, > .content, > .actions { + background-color: $background-color; + } + > .header { + border-bottom: 1px solid $border-color; + } + + > .actions { + border-top: 1px solid $border-color; + } + } + main, .main, footer, .modal { + + .ui.menu { + background-color: $light-background-color; + .item { + + color: $text-color; + } + } + .ui.secondary.menu .dropdown.item:hover, .ui.secondary.menu .link.item:hover, .ui.secondary.menu a.item:hover { + background: $background-color; + color: $text-color; + } + .header, .ui.form .field > label, .sub.header { + color: $text-color; + } + .ui.attached.header { + background-color: transparent; + } + .ui.toggle.checkbox input:checked ~ .box, .ui.toggle.checkbox input:checked ~ label { + color: $text-color !important; + } + .ui.toggle.checkbox .box::before, .ui.toggle.checkbox label::before { + background-color: $light-background-color; + } + a:not(.ui):not(.discrete) { + color: $link-color; + } + .ui.segment:not(.basic) { + background-color: $light-background-color; + } + .ui.list, .ui.dropdown { + .item, div.item, a.item, .button.item { + background-color: $background-color; + color: $discrete-text-color; + } + .selected.item:not(:hover) { + color: $background-color; + } + } + .ui.divided.items > .item:not(:first-child) { + border-top: 1px solid $border-color; + } + .ui.items { + .extra { + color: $discrete-text-color; + } + } + label, .toggle label { + color: $text-color !important; + } + &, .main.pusher, .ui.vertical.segment { + color: $text-color; + background-color: $background-color; + } + + .discrete { + color: $discrete-text-color; + } + + .ui.table thead th, .ui.table { + color: $text-color; + } + .ui.divider:not(.vertical):not(.horizontal) { + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; + } + .ui.cards > .card, .ui.card { + color: $text-color; + background-color: $background-color; + box-shadow: $box-shadow; + .content, .header, .description { + color: $text-color; + } + .extra, .meta { + color: $discrete-text-color; + } + } + .playlist.card { + .attached.button { + background-color: $light-background-color; + } + } + + // buttons + [class='ui button ui button'], [class='ui button'], [class='ui icon button'], [class='ui fluid button'], [class='ui cancel button'] { + background-color: $light-background-color; + color: $text-color; + &:hover { + background-color: $button-hover-color; + + } + } + .ui.buttons > .ui.button:not(.basic):not(.inverted), .ui.buttons:not(.basic):not(.inverted) > .button { + box-shadow: 0px 0px 0px 1px $light-shadow-color inset; + + } + .ui.basic.buttons:not(:hover):not(.green):not(.orange):not(.yellow):not(.red) .button, .ui.basic.button { + box-shadow: 0px 0px 0px 1px $text-color inset; + &:not(:hover):not(.green):not(.orange):not(.yellow):not(.red) { + color: $text-color !important; + } + } + .ui.basic.buttons .button, .ui.basic.button { + &:hover { + color: $text-color !important; + } + + } + .ui.basic.buttons:not(.green):not(.orange):not(.yellow):not(.red) .button:hover, .ui.basic.button:not(.green):not(.orange):not(.yellow):not(.red):hover, .ui.basic.button:not(.green):not(.orange):not(.yellow):not(.red):active, .ui.basic.button:not(.green):not(.orange):not(.yellow):not(.red):focus { + color: $background-color !important; + } + // loading /dimmers + .ui.loading.form::before { + background-color: $loading-background-color; + } + .ui.inverted.dimmer { + background-color: $loading-background-color; + } + // table + .ui.basic.table tbody tr, .ui.table tr td { + border-bottom: 1px solid $border-color; + } + .ui.table thead th { + border-bottom: 1px solid $border-color; + } + .ui.table { + &:not(.basic) { + &, thead th { + background-color: $light-background-color; + } + } + } + } + .ui.link.list.list a.item:hover, .ui.link.list.list .item a:not(.ui):not(.button):hover { + color: $link-color; + } + [data-tooltip]::after { + background-color: $light-background-color; + color: $text-color; + } + .ui.progress > .label { + color: $text-color; + + } + i.grey.icon { + color: $text-color !important; + } + input { + &::selection, &::-moz-selection { + background: $background-color; + color: $text-color; + } + } +} diff --git a/front/src/style/themes/_light.scss b/front/src/style/themes/_light.scss new file mode 100644 index 0000000000000000000000000000000000000000..a6e1a0cdf621edaf3b7ed5dd57d0a6e8c4727b02 --- /dev/null +++ b/front/src/style/themes/_light.scss @@ -0,0 +1,35 @@ + + +.theme-light { + + .main.pusher > .ui.secondary.menu { + box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15); + background-color: white; + .ui.item { + &.active { + box-shadow: inset 0px -2px 0px 0px #000; + } + } + } + + .discrete { + color: rgba(0, 0, 0, 0.87); + } + .playlist.card { + .attached.button { + background-color: rgb(243, 244, 245); + } + } + + .disabled-row { + color: rgba(40, 40, 40, 0.3); + } + footer p { + color: grey; + } + + footer#footer div.item:hover { + color: rgba(0, 0, 0, 0.87); + } + +}