Merge pull request #324 from RobertSmits/dark-theme

Better dark theme and theme switcher
pull/329/head
Alex 2023-10-03 18:29:14 +03:00 committed by GitHub
commit a1e5a3117c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 37 deletions

View File

@ -35,7 +35,7 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __UID__: user under which MeTube will run. Defaults to `1000`. * __UID__: user under which MeTube will run. Defaults to `1000`.
* __GID__: group under which MeTube will run. Defaults to `1000`. * __GID__: group under which MeTube will run. Defaults to `1000`.
* __UMASK__: umask value used by MeTube. Defaults to `022`. * __UMASK__: umask value used by MeTube. Defaults to `022`.
* __DARK_MODE__: if set to `true`, the UI will be in dark mode. Defaults to `false`. * __DEFAULT_THEME__: default theme to use for the ui, can be set to `light`, `dark` or `auto`. Defaults to `auto`.
* __DOWNLOAD_DIR__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise. * __DOWNLOAD_DIR__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise.
* __AUDIO_DOWNLOAD_DIR__: path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`. * __AUDIO_DOWNLOAD_DIR__: path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`.
* __DOWNLOAD_DIRS_INDEXABLE__: if `true`, the download dirs (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the webserver. Defaults to `false`. * __DOWNLOAD_DIRS_INDEXABLE__: if `true`, the download dirs (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the webserver. Defaults to `false`.

View File

@ -31,10 +31,10 @@ class Config:
'HOST': '0.0.0.0', 'HOST': '0.0.0.0',
'PORT': '8081', 'PORT': '8081',
'BASE_DIR': '', 'BASE_DIR': '',
'DARK_MODE': 'false' 'DEFAULT_THEME': 'auto'
} }
_BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DARK_MODE') _BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN')
def __init__(self): def __init__(self):
for k, v in self._DEFAULTS.items(): for k, v in self._DEFAULTS.items():
@ -173,7 +173,8 @@ def get_custom_dirs():
@routes.get(config.URL_PREFIX) @routes.get(config.URL_PREFIX)
def index(request): def index(request):
response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/index.html')) response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/index.html'))
response.set_cookie('metube_dark', 'true' if config.DARK_MODE else 'false') if 'metube_theme' not in request.cookies:
response.set_cookie('metube_theme', config.DEFAULT_THEME)
return response return response
if config.URL_PREFIX != '/': if config.URL_PREFIX != '/':

View File

@ -31,7 +31,7 @@
"src/styles.sass" "src/styles.sass"
], ],
"scripts": [ "scripts": [
"node_modules/bootstrap/dist/js/bootstrap.min.js", "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
] ]
}, },
"configurations": { "configurations": {

View File

@ -1,4 +1,4 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">MeTube</a> <a class="navbar-brand" href="#">MeTube</a>
<!-- <!--
@ -13,10 +13,30 @@
</ul> </ul>
</div> </div>
--> -->
<div class="ms-auto"> <div class="navbar-nav ms-auto">
<button class="btn btn-outline-light button-toggle-theme" aria-label="Toggle theme" (click)="themeChanged()"> <div class="nav-item dropdown">
<fa-icon [icon]="darkMode ? faSun : faMoon"></fa-icon> <button class="btn btn-link nav-link py-2 px-0 px-sm-2 dropdown-toggle d-flex align-items-center"
</button> id="theme-select"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
data-bs-display="static">
<fa-icon [icon]="activeTheme.icon"></fa-icon>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="theme-select">
<li *ngFor="let theme of themes">
<button type="button" class="dropdown-item d-flex align-items-center" [ngClass]="{'active' : activeTheme == theme}" (click)="themeChanged(theme)">
<span class="me-2 opacity-50">
<fa-icon [icon]="theme.icon"></fa-icon>
</span>
{{ theme.displayName }}
<span class="ms-auto" [ngClass]="{'d-none' : activeTheme != theme}">
<fa-icon [icon]="faCheck"></fa-icon>
</span>
</button>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</nav> </nav>
@ -133,8 +153,8 @@
</td> </td>
<td> <td>
<div style="display: inline-block; width: 1.5rem;"> <div style="display: inline-block; width: 1.5rem;">
<fa-icon *ngIf="download.value.status == 'finished'" [icon]="faCheckCircle" style="color: green;"></fa-icon> <fa-icon *ngIf="download.value.status == 'finished'" [icon]="faCheckCircle" class="text-success"></fa-icon>
<fa-icon *ngIf="download.value.status == 'error'" [icon]="faTimesCircle" style="color: red;"></fa-icon> <fa-icon *ngIf="download.value.status == 'error'" [icon]="faTimesCircle" class="text-danger"></fa-icon>
</div> </div>
<span ngbTooltip="{{download.value.msg}}"><a *ngIf="!!download.value.filename; else noDownloadLink" href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a></span> <span ngbTooltip="{{download.value.msg}}"><a *ngIf="!!download.value.filename; else noDownloadLink" href="{{buildDownloadLink(download.value)}}" target="_blank">{{ download.value.title }}</a></span>
<ng-template #noDownloadLink>{{ download.value.title }}</ng-template> <ng-template #noDownloadLink>{{ download.value.title }}</ng-template>

View File

@ -23,13 +23,11 @@ button.add-url
padding-left: 5px padding-left: 5px
padding-right: 5px padding-right: 5px
$metube-section-color-bg: rgba(0,0,0,.07)
.metube-section-header .metube-section-header
font-size: 1.8rem font-size: 1.8rem
font-weight: 300 font-weight: 300
position: relative position: relative
background: $metube-section-color-bg background: var(--bs-secondary-bg)
padding: 0.5rem 0 padding: 0.5rem 0
margin-top: 3.5rem margin-top: 3.5rem
@ -40,8 +38,8 @@ $metube-section-color-bg: rgba(0,0,0,.07)
bottom: 0 bottom: 0
left: -9999px left: -9999px
right: 0 right: 0
border-left: 9999px solid $metube-section-color-bg border-left: 9999px solid var(--bs-secondary-bg)
box-shadow: 9999px 0 0 $metube-section-color-bg box-shadow: 9999px 0 0 var(--bs-secondary-bg)
button:hover button:hover
text-decoration: none text-decoration: none

View File

@ -1,12 +1,13 @@
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons';
import { faRedoAlt, faSun, faMoon, faExternalLinkAlt, faDownload } from '@fortawesome/free-solid-svg-icons'; import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload } from '@fortawesome/free-solid-svg-icons';
import { CookieService } from 'ngx-cookie-service'; import { CookieService } from 'ngx-cookie-service';
import { map, Observable, of } from 'rxjs'; import { map, Observable, of } from 'rxjs';
import { Download, DownloadsService, Status } from './downloads.service'; import { Download, DownloadsService, Status } from './downloads.service';
import { MasterCheckboxComponent } from './master-checkbox.component'; import { MasterCheckboxComponent } from './master-checkbox.component';
import { Formats, Format, Quality } from './formats'; import { Formats, Format, Quality } from './formats';
import { Theme, Themes } from './theme';
import {KeyValue} from "@angular/common"; import {KeyValue} from "@angular/common";
@Component({ @Component({
@ -23,7 +24,8 @@ export class AppComponent implements AfterViewInit {
folder: string; folder: string;
customNamePrefix: string; customNamePrefix: string;
addInProgress = false; addInProgress = false;
darkMode: boolean; themes: Theme[] = Themes;
activeTheme: Theme;
customDirs$: Observable<string[]>; customDirs$: Observable<string[]>;
@ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent;
@ -39,6 +41,8 @@ export class AppComponent implements AfterViewInit {
faRedoAlt = faRedoAlt; faRedoAlt = faRedoAlt;
faSun = faSun; faSun = faSun;
faMoon = faMoon; faMoon = faMoon;
faCheck = faCheck;
faCircleHalfStroke = faCircleHalfStroke;
faDownload = faDownload; faDownload = faDownload;
faExternalLinkAlt = faExternalLinkAlt; faExternalLinkAlt = faExternalLinkAlt;
@ -47,11 +51,18 @@ export class AppComponent implements AfterViewInit {
// Needs to be set or qualities won't automatically be set // Needs to be set or qualities won't automatically be set
this.setQualities() this.setQualities()
this.quality = cookieService.get('metube_quality') || 'best'; this.quality = cookieService.get('metube_quality') || 'best';
this.setupTheme(cookieService) this.activeTheme = this.getPreferredTheme(cookieService);
} }
ngOnInit() { ngOnInit() {
this.customDirs$ = this.getMatchingCustomDir(); this.customDirs$ = this.getMatchingCustomDir();
this.setTheme(this.activeTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (this.activeTheme.id === 'auto') {
this.setTheme(this.activeTheme);
}
});
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -96,7 +107,7 @@ export class AppComponent implements AfterViewInit {
} }
isAudioType() { isAudioType() {
return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a' || this.format == 'opus' || this.format == 'wav' return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a' || this.format == 'opus' || this.format == 'wav';
} }
getMatchingCustomDir() : Observable<string[]> { getMatchingCustomDir() : Observable<string[]> {
@ -112,25 +123,27 @@ export class AppComponent implements AfterViewInit {
})); }));
} }
setupTheme(cookieService) { getPreferredTheme(cookieService: CookieService) {
if (cookieService.check('metube_dark')) { let theme = 'auto';
this.darkMode = cookieService.get('metube_dark') === "true" if (cookieService.check('metube_theme')) {
} else { theme = cookieService.get('metube_theme');
this.darkMode = window.matchMedia("prefers-color-scheme: dark").matches
} }
this.setTheme()
return this.themes.find(x => x.id === theme) ?? this.themes.find(x => x.id === 'auto');
} }
themeChanged() { themeChanged(theme: Theme) {
this.darkMode = !this.darkMode this.cookieService.set('metube_theme', theme.id, { expires: 3650 });
this.cookieService.set('metube_dark', this.darkMode.toString(), { expires: 3650 }); this.setTheme(theme);
this.setTheme()
} }
setTheme() { setTheme(theme: Theme) {
const doc = document.querySelector('html') this.activeTheme = theme;
const filter = this.darkMode ? "invert(1) hue-rotate(180deg)" : "" if (theme.id === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
doc.style.filter = filter document.documentElement.setAttribute('data-bs-theme', 'dark');
} else {
document.documentElement.setAttribute('data-bs-theme', theme.id);
}
} }
formatChanged() { formatChanged() {

26
ui/src/app/theme.ts Normal file
View File

@ -0,0 +1,26 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faCircleHalfStroke, faMoon, faSun } from "@fortawesome/free-solid-svg-icons";
export interface Theme {
id: string;
displayName: string;
icon: IconDefinition;
}
export const Themes: Theme[] = [
{
id: 'light',
displayName: 'Light',
icon: faSun,
},
{
id: 'dark',
displayName: 'Dark',
icon: faMoon,
},
{
id: 'auto',
displayName: 'Auto',
icon: faCircleHalfStroke,
},
];

View File

@ -2,4 +2,10 @@
/* Importing Bootstrap SCSS file. */ /* Importing Bootstrap SCSS file. */
@import 'node_modules/bootstrap/scss/bootstrap' @import 'node_modules/bootstrap/scss/bootstrap'
@import '~@ng-select/ng-select/themes/default.theme.css' @import '~@ng-select/ng-select/themes/default.theme.css'
.navbar
background-color: var(--bs-dark) !important
[data-bs-theme="dark"] &
background-color: var(--bs-dark-bg-subtle) !important