Theme Viewer + Theme Updater (#2952)
This commit is contained in:
parent
24302d4fcc
commit
38e7c1c131
35 changed files with 4563 additions and 284 deletions
36
UI/Web/package-lock.json
generated
36
UI/Web/package-lock.json
generated
|
@ -504,6 +504,7 @@
|
|||
"version": "17.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.4.tgz",
|
||||
"integrity": "sha512-TVWjpZSI/GIXTYsmVgEKYjBckcW8Aj62DcxLNehRFR+c7UB95OY3ZFjU8U4jL0XvWPgTkkVWQVq+P6N4KCBsyw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.23.9",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
|
@ -531,6 +532,7 @@
|
|||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
|
||||
"integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
|
@ -559,12 +561,14 @@
|
|||
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
|
@ -745,6 +749,7 @@
|
|||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
|
||||
"integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
|
@ -773,12 +778,14 @@
|
|||
"node_modules/@babel/core/node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/core/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
|
@ -5622,6 +5629,7 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
|
@ -5634,6 +5642,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
@ -5905,6 +5914,7 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
|
@ -6216,6 +6226,7 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
|
@ -6507,7 +6518,8 @@
|
|||
"node_modules/convert-source-map": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
|
@ -7409,6 +7421,7 @@
|
|||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
|
@ -7418,6 +7431,7 @@
|
|||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
|
@ -8526,6 +8540,7 @@
|
|||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -9207,6 +9222,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
|
@ -11047,6 +11063,7 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
@ -12436,6 +12453,7 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
|
@ -12447,6 +12465,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
@ -12457,7 +12476,8 @@
|
|||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
"version": "1.4.2",
|
||||
|
@ -12925,7 +12945,7 @@
|
|||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.71.1",
|
||||
|
@ -13044,6 +13064,7 @@
|
|||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
|
@ -13058,6 +13079,7 @@
|
|||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
|
@ -13068,7 +13090,8 @@
|
|||
"node_modules/semver/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
|
@ -14199,6 +14222,7 @@
|
|||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export interface SiteThemeUpdatedEvent {
|
||||
themeName: string;
|
||||
}
|
|
@ -3,9 +3,9 @@
|
|||
*/
|
||||
export enum ThemeProvider {
|
||||
System = 1,
|
||||
User = 2
|
||||
Custom = 2,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Theme for the whole instance
|
||||
*/
|
||||
|
@ -20,4 +20,8 @@
|
|||
* The actual class the root is defined against. It is generated at the backend.
|
||||
*/
|
||||
selector: string;
|
||||
}
|
||||
description: string;
|
||||
previewUrls: Array<string>;
|
||||
author: string;
|
||||
|
||||
}
|
||||
|
|
10
UI/Web/src/app/_models/theme/downloadable-site-theme.ts
Normal file
10
UI/Web/src/app/_models/theme/downloadable-site-theme.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface DownloadableSiteTheme {
|
||||
name: string;
|
||||
cssUrl: string;
|
||||
previewUrls: Array<string>;
|
||||
author: string;
|
||||
isCompatible: boolean;
|
||||
lastCompatibleVersion: string;
|
||||
alreadyDownloaded: boolean;
|
||||
description: string;
|
||||
}
|
|
@ -16,8 +16,8 @@ export class SiteThemeProviderPipe implements PipeTransform {
|
|||
switch(provider) {
|
||||
case ThemeProvider.System:
|
||||
return this.translocoService.translate('site-theme-provider-pipe.system');
|
||||
case ThemeProvider.User:
|
||||
return this.translocoService.translate('site-theme-provider-pipe.user');
|
||||
case ThemeProvider.Custom:
|
||||
return this.translocoService.translate('site-theme-provider-pipe.custom');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { UserUpdateEvent } from '../_models/events/user-update-event';
|
|||
import { User } from '../_models/user';
|
||||
import {DashboardUpdateEvent} from "../_models/events/dashboard-update-event";
|
||||
import {SideNavUpdateEvent} from "../_models/events/sidenav-update-event";
|
||||
import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event";
|
||||
|
||||
export enum EVENTS {
|
||||
UpdateAvailable = 'UpdateAvailable',
|
||||
|
@ -98,7 +99,11 @@ export enum EVENTS {
|
|||
/**
|
||||
* User's sidenav needs to be re-rendered
|
||||
*/
|
||||
SideNavUpdate = 'SideNavUpdate'
|
||||
SideNavUpdate = 'SideNavUpdate',
|
||||
/**
|
||||
* A Theme was updated and UI should refresh to get the latest version
|
||||
*/
|
||||
SiteThemeUpdated= 'SiteThemeUpdated'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
@ -194,6 +199,13 @@ export class MessageHubService {
|
|||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.SiteThemeUpdated, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.SiteThemeUpdated,
|
||||
payload: resp.body as SiteThemeUpdatedEvent
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.DashboardUpdate, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.DashboardUpdate,
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
import { DOCUMENT } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {
|
||||
DestroyRef,
|
||||
inject,
|
||||
Inject,
|
||||
Injectable,
|
||||
Renderer2,
|
||||
RendererFactory2,
|
||||
SecurityContext
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { map, ReplaySubject, take } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
import { NotificationProgressEvent } from '../_models/events/notification-progress-event';
|
||||
import { SiteTheme, ThemeProvider } from '../_models/preferences/site-theme';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { EVENTS, MessageHubService } from './message-hub.service';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {DestroyRef, inject, Inject, Injectable, Renderer2, RendererFactory2, SecurityContext} from '@angular/core';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {map, ReplaySubject, take} from 'rxjs';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {ConfirmService} from '../shared/confirm.service';
|
||||
import {NotificationProgressEvent} from '../_models/events/notification-progress-event';
|
||||
import {SiteTheme, ThemeProvider} from '../_models/preferences/site-theme';
|
||||
import {TextResonse} from '../_types/text-response';
|
||||
import {EVENTS, MessageHubService} from './message-hub.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {translate} from "@ngneat/transloco";
|
||||
import {DownloadableSiteTheme} from "../_models/theme/downloadable-site-theme";
|
||||
import {NgxFileDropEntry} from "ngx-file-drop";
|
||||
import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event";
|
||||
|
||||
|
||||
@Injectable({
|
||||
|
@ -52,18 +47,45 @@ export class ThemeService {
|
|||
|
||||
messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {
|
||||
|
||||
if (message.event !== EVENTS.NotificationProgress) return;
|
||||
const notificationEvent = (message.payload as NotificationProgressEvent);
|
||||
if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;
|
||||
if (message.event === EVENTS.NotificationProgress) {
|
||||
const notificationEvent = (message.payload as NotificationProgressEvent);
|
||||
if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;
|
||||
|
||||
if (notificationEvent.eventType === 'ended') {
|
||||
if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe(() => {
|
||||
if (notificationEvent.eventType === 'ended') {
|
||||
if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.event === EVENTS.SiteThemeUpdated) {
|
||||
const evt = (message.payload as SiteThemeUpdatedEvent);
|
||||
this.currentTheme$.pipe(take(1)).subscribe(currentTheme => {
|
||||
if (currentTheme && currentTheme.name !== EVENTS.SiteThemeProgress) return;
|
||||
console.log('Active theme has been updated, refreshing theme');
|
||||
this.setTheme(currentTheme.name);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
getDownloadableThemes() {
|
||||
return this.httpClient.get<Array<DownloadableSiteTheme>>(this.baseUrl + 'theme/browse');
|
||||
}
|
||||
|
||||
downloadTheme(theme: DownloadableSiteTheme) {
|
||||
return this.httpClient.post<SiteTheme>(this.baseUrl + 'theme/download-theme', theme);
|
||||
}
|
||||
|
||||
uploadTheme(themeFile: File, fileEntry: NgxFileDropEntry) {
|
||||
const formData = new FormData()
|
||||
formData.append('formFile', themeFile, fileEntry.relativePath);
|
||||
|
||||
return this.httpClient.post<SiteTheme>(this.baseUrl + 'theme/upload-theme', formData);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
|
||||
}
|
||||
|
@ -113,6 +135,12 @@ export class ThemeService {
|
|||
this.unsetThemes();
|
||||
}
|
||||
|
||||
deleteTheme(themeId: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'theme?themeId=' + themeId).pipe(map(() => {
|
||||
this.getThemes().subscribe(() => {});
|
||||
}));
|
||||
}
|
||||
|
||||
setDefault(themeId: number) {
|
||||
return this.httpClient.post(this.baseUrl + 'theme/update-default', {themeId: themeId}).pipe(map(() => {
|
||||
// Refresh the cache when a default state is changed
|
||||
|
@ -148,7 +176,7 @@ export class ThemeService {
|
|||
this.unsetThemes();
|
||||
this.renderer.addClass(this.document.querySelector('body'), theme.selector);
|
||||
|
||||
if (theme.provider === ThemeProvider.User && !this.hasThemeInHead(theme.name)) {
|
||||
if (theme.provider !== ThemeProvider.System && !this.hasThemeInHead(theme.name)) {
|
||||
// We need to load the styles into the browser
|
||||
this.fetchThemeContent(theme.id).subscribe(async (content) => {
|
||||
if (content === null) {
|
||||
|
|
|
@ -13,7 +13,6 @@ import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.compon
|
|||
import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {ServerService} from "./_services/server.service";
|
||||
import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component";
|
||||
import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component";
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -16,27 +16,19 @@
|
|||
</div>
|
||||
<div>by {{stack.author}} • {{t('series-count', {num: stack.seriesCount | number})}} • <span><i class="fa-solid fa-layer-group me-1" aria-hidden="true"></i>{{t('restack-count', {num: stack.restackCount | number})}}</span></div>
|
||||
</li>
|
||||
} @empty {
|
||||
@if (isLoading) {
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
} @else {
|
||||
<p>{{t('nothing-found')}}</p>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<!-- <div class="col-auto">-->
|
||||
<!-- <a class="btn btn-icon" href="https://wiki.kavitareader.com/guides/features/readinglists#creating-a-reading-list-via-cbl" target="_blank" rel="noopener noreferrer">Help</a>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-auto">-->
|
||||
<!-- <button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-auto">-->
|
||||
<!-- <button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-auto">-->
|
||||
<!-- <button type="button" class="btn btn-primary" (click)="nextStep()" [disabled]="!canMoveToNextStep()">{{t(NextButtonLabel)}}</button>-->
|
||||
<!-- </div>-->
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-secondary" (click)="ngbModal.dismiss()">{{t('close')}}</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" (click)="ngbModal.dismiss()">{{t('close')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -10,6 +10,7 @@ import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
|||
import {forkJoin} from "rxjs";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-mal-collection-modal',
|
||||
|
@ -18,7 +19,8 @@ import {DecimalPipe} from "@angular/common";
|
|||
TranslocoDirective,
|
||||
ReactiveFormsModule,
|
||||
Select2Module,
|
||||
DecimalPipe
|
||||
DecimalPipe,
|
||||
LoadingComponent
|
||||
],
|
||||
templateUrl: './import-mal-collection-modal.component.html',
|
||||
styleUrl: './import-mal-collection-modal.component.scss',
|
||||
|
|
|
@ -1,32 +1,141 @@
|
|||
<ng-container *transloco="let t; read:'theme-manager'">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>{{t('title')}}</h3></div>
|
||||
<div class="col-4" *ngIf="isAdmin">
|
||||
<button class="btn btn-primary float-end" (click)="scan()">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> {{t('scan')}}
|
||||
</button>
|
||||
</div>
|
||||
<h3>{{t('title')}}</h3>
|
||||
</div>
|
||||
|
||||
<p *ngIf="isAdmin">
|
||||
{{t('looking-for-theme')}}<a href="https://github.com/Kareadita/Themes" target="_blank" rel="noopener noreferrer">{{t('looking-for-theme-continued')}}</a>
|
||||
</p>
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
<div class="row g-0">
|
||||
<h4>{{t('site-themes')}}</h4>
|
||||
<ng-container *ngFor="let theme of (themeService.themes$ | async)">
|
||||
<div class="card col-auto me-3 mb-3" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{theme.name | sentenceCase}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{theme.provider | siteThemeProvider}}</h6>
|
||||
<button class="btn btn-secondary me-2" [disabled]="theme.isDefault" *ngIf="isAdmin" (click)="updateDefault(theme)">{{t('set-default')}}</button>
|
||||
<button class="btn btn-primary" (click)="applyTheme(theme)" [disabled]="currentTheme?.id === theme.id">{{currentTheme?.id === theme.id ? t('applied') : t('apply')}}</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 theme-container">
|
||||
<div class="col-md-3">
|
||||
<div class="pe-2">
|
||||
<ul style="height: 100%" class="list-group list-group-flush">
|
||||
|
||||
@for (theme of themeService.themes$ | async; track theme.name) {
|
||||
<ng-container [ngTemplateOutlet]="themeOption" [ngTemplateOutletContext]="{ $implicit: theme}"></ng-container>
|
||||
}
|
||||
|
||||
@for (theme of downloadableThemes; track theme.name) {
|
||||
<ng-container [ngTemplateOutlet]="themeOption" [ngTemplateOutletContext]="{ $implicit: theme}"></ng-container>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
@if (selectedTheme === undefined) {
|
||||
|
||||
<div class="row pb-4">
|
||||
<div class="mx-auto">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
@if (hasAdmin$ | async) {
|
||||
{{t('preview-default-admin')}}
|
||||
} @else {
|
||||
{{t('preview-default')}}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@if (files && files.length > 0) {
|
||||
<app-loading [loading]="isUploadingTheme"></app-loading>
|
||||
} @else if (hasAdmin$ | async) {
|
||||
<ngx-file-drop (onFileDrop)="dropped($event)" [accept]="acceptableExtensions" [directory]="false"
|
||||
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
|
||||
|
||||
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
|
||||
<div class="row g-0 mt-3 pb-3">
|
||||
<div class="mx-auto">
|
||||
<div class="row g-0 mb-3">
|
||||
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
|
||||
<span class="ps-1 pe-1">•</span>
|
||||
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
</ngx-file-drop>
|
||||
}
|
||||
|
||||
}
|
||||
@else {
|
||||
<h4>
|
||||
{{selectedTheme.name | sentenceCase}}
|
||||
<div class="float-end">
|
||||
@if (selectedTheme.isSiteTheme) {
|
||||
@if (selectedTheme.name !== 'Dark') {
|
||||
<button class="btn btn-danger me-1" (click)="deleteTheme(selectedTheme.site!)">{{t('delete')}}</button>
|
||||
}
|
||||
@if (hasAdmin$ | async) {
|
||||
<button class="btn btn-secondary me-1" [disabled]="selectedTheme.site?.isDefault" (click)="updateDefault(selectedTheme.site!)">{{t('set-default')}}</button>
|
||||
}
|
||||
<button class="btn btn-primary me-1" [disabled]="currentTheme && selectedTheme.name === currentTheme.name" (click)="applyTheme(selectedTheme.site!)">{{t('apply')}}</button>
|
||||
} @else {
|
||||
<button class="btn btn-primary" [disabled]="selectedTheme.downloadable?.alreadyDownloaded" (click)="downloadTheme(selectedTheme.downloadable!)">{{t('download')}}</button>
|
||||
}
|
||||
</div>
|
||||
</h4>
|
||||
@if(!selectedTheme.isSiteTheme) {
|
||||
<p>{{selectedTheme.downloadable!.description | defaultValue}}</p>
|
||||
|
||||
<app-carousel-reel [items]="selectedTheme.downloadable!.previewUrls" title="Preview">
|
||||
<ng-template #carouselItem let-item>
|
||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
||||
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
} @else {
|
||||
<p>{{selectedTheme.site!.description | defaultValue}}</p>
|
||||
|
||||
<app-carousel-reel [items]="selectedTheme.site!.previewUrls" title="Preview">
|
||||
<ng-template #carouselItem let-item>
|
||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
||||
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #themeOption let-item>
|
||||
@if (item !== undefined) {
|
||||
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start {{selectedTheme && selectedTheme.name === item.name ? 'active' : ''}}" (click)="selectTheme(item)">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">{{item.name | sentenceCase}}</div>
|
||||
|
||||
@if (item.hasOwnProperty('provider')) {
|
||||
{{item.provider | siteThemeProvider}}
|
||||
} @else if (item.hasOwnProperty('lastCompatibleVersion')) {
|
||||
{{ThemeProvider.Custom | siteThemeProvider}} • v{{item.lastCompatibleVersion}}
|
||||
}
|
||||
@if (currentTheme && item.name === currentTheme.name) {
|
||||
• {{t('active-theme')}}
|
||||
}
|
||||
</div>
|
||||
@if (item.hasOwnProperty('isDefault') && item.isDefault) {
|
||||
<i class="fa-solid fa-star" [attr.aria-label]="t('default-theme')"></i>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//.theme-container {
|
||||
// max-height: calc(100 * var(--vh));
|
||||
// overflow-y: auto;
|
||||
//}
|
||||
|
||||
.chooser {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 158px);
|
||||
grid-gap: 0.5rem;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
ngx-file-drop ::ng-deep > div {
|
||||
// styling for the outer drop box
|
||||
width: 100%;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: 5px;
|
||||
height: 100px;
|
||||
margin: auto;
|
||||
|
||||
> div {
|
||||
// styling for the inner box (template)
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
}
|
|
@ -6,17 +6,36 @@ import {
|
|||
inject,
|
||||
} from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { distinctUntilChanged, take } from 'rxjs';
|
||||
import {distinctUntilChanged, map, take} from 'rxjs';
|
||||
import { ThemeService } from 'src/app/_services/theme.service';
|
||||
import { SiteTheme } from 'src/app/_models/preferences/site-theme';
|
||||
import {SiteTheme, ThemeProvider} from 'src/app/_models/preferences/site-theme';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import { SiteThemeProviderPipe } from '../../_pipes/site-theme-provider.pipe';
|
||||
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
|
||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {tap} from "rxjs/operators";
|
||||
import {NgIf, NgFor, AsyncPipe, NgTemplateOutlet} from '@angular/common';
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {shareReplay} from "rxjs/operators";
|
||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {DownloadableSiteTheme} from "../../_models/theme/downloadable-site-theme";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {SafeUrlPipe} from "../../_pipes/safe-url.pipe";
|
||||
import {ScrobbleProvider} from "../../_services/scrobbling.service";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {FileSystemFileEntry, NgxFileDropEntry, NgxFileDropModule} from "ngx-file-drop";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {Select2Module} from "ng-select2-component";
|
||||
import {LoadingComponent} from "../../shared/loading/loading.component";
|
||||
|
||||
interface ThemeContainer {
|
||||
downloadable?: DownloadableSiteTheme;
|
||||
site?: SiteTheme;
|
||||
isSiteTheme: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-manager',
|
||||
|
@ -24,51 +43,125 @@ import {tap} from "rxjs/operators";
|
|||
styleUrls: ['./theme-manager.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, AsyncPipe, SentenceCasePipe, SiteThemeProviderPipe, TranslocoDirective]
|
||||
imports: [NgIf, NgFor, AsyncPipe, SentenceCasePipe, SiteThemeProviderPipe, TranslocoDirective, CarouselReelComponent, SeriesCardComponent, ImageComponent, DefaultValuePipe, NgTemplateOutlet, SafeUrlPipe, NgxFileDropModule, ReactiveFormsModule, Select2Module, LoadingComponent]
|
||||
})
|
||||
export class ThemeManagerComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly themeService = inject(ThemeService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
|
||||
protected readonly ThemeProvider = ThemeProvider;
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
currentTheme: SiteTheme | undefined;
|
||||
isAdmin: boolean = false;
|
||||
user: User | undefined;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly translocService = inject(TranslocoService);
|
||||
selectedTheme: ThemeContainer | undefined;
|
||||
downloadableThemes: Array<DownloadableSiteTheme> = [];
|
||||
hasAdmin$ = this.accountService.currentUser$.pipe(
|
||||
takeUntilDestroyed(this.destroyRef), shareReplay({refCount: true, bufferSize: 1}),
|
||||
map(c => c && this.accountService.hasAdminRole(c))
|
||||
);
|
||||
|
||||
files: NgxFileDropEntry[] = [];
|
||||
acceptableExtensions = ['.css'].join(',');
|
||||
isUploadingTheme: boolean = false;
|
||||
|
||||
|
||||
constructor(public themeService: ThemeService, private accountService: AccountService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) {
|
||||
|
||||
themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged()).subscribe(theme => {
|
||||
constructor() {
|
||||
|
||||
this.loadDownloadableThemes();
|
||||
|
||||
this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged()).subscribe(theme => {
|
||||
this.currentTheme = theme;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
this.isAdmin = accountService.hasAdminRole(user);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
loadDownloadableThemes() {
|
||||
this.themeService.getDownloadableThemes().subscribe(d => {
|
||||
this.downloadableThemes = d;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
applyTheme(theme: SiteTheme) {
|
||||
if (!this.user) return;
|
||||
async deleteTheme(theme: SiteTheme) {
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-theme'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pref = Object.assign({}, this.user.preferences);
|
||||
pref.theme = theme;
|
||||
this.accountService.updatePreferences(pref).subscribe();
|
||||
this.themeService.deleteTheme(theme.id).subscribe(_ => {
|
||||
this.removeDownloadedTheme(theme);
|
||||
this.loadDownloadableThemes();
|
||||
});
|
||||
}
|
||||
|
||||
removeDownloadedTheme(theme: SiteTheme) {
|
||||
this.selectedTheme = undefined;
|
||||
this.downloadableThemes = this.downloadableThemes.filter(d => d.name !== theme.name);
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
applyTheme(theme: SiteTheme) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (!user) return;
|
||||
const pref = Object.assign({}, user.preferences);
|
||||
pref.theme = theme;
|
||||
this.accountService.updatePreferences(pref).subscribe();
|
||||
// Updating theme emits the new theme to load on the themes$
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
updateDefault(theme: SiteTheme) {
|
||||
this.themeService.setDefault(theme.id).subscribe(() => {
|
||||
this.toastr.success(this.translocService.translate('theme-manager.updated-toastr', {name: theme.name}));
|
||||
this.toastr.success(translate('theme-manager.updated-toastr', {name: theme.name}));
|
||||
});
|
||||
}
|
||||
|
||||
scan() {
|
||||
this.themeService.scan().subscribe(() => {
|
||||
this.toastr.info(this.translocService.translate('theme-manager.scan-queued'));
|
||||
selectTheme(theme: SiteTheme | DownloadableSiteTheme) {
|
||||
if (theme.hasOwnProperty('provider')) {
|
||||
this.selectedTheme = {
|
||||
isSiteTheme: true,
|
||||
site: theme as SiteTheme,
|
||||
name: theme.name
|
||||
};
|
||||
} else {
|
||||
this.selectedTheme = {
|
||||
isSiteTheme: false,
|
||||
downloadable: theme as DownloadableSiteTheme,
|
||||
name: theme.name
|
||||
};
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
downloadTheme(theme: DownloadableSiteTheme) {
|
||||
this.themeService.downloadTheme(theme).subscribe(theme => {
|
||||
this.removeDownloadedTheme(theme);
|
||||
});
|
||||
}
|
||||
|
||||
public dropped(files: NgxFileDropEntry[]) {
|
||||
this.files = files;
|
||||
for (const droppedFile of files) {
|
||||
// Is it a file?
|
||||
if (droppedFile.fileEntry.isFile) {
|
||||
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
|
||||
fileEntry.file((file: File) => {
|
||||
this.themeService.uploadTheme(file, droppedFile).subscribe(t => {
|
||||
this.isUploadingTheme = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
this.isUploadingTheme = true;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,15 +182,22 @@
|
|||
|
||||
"theme-manager": {
|
||||
"title": "Theme Manager",
|
||||
"looking-for-theme": "Looking for a light or e-ink theme? We have some custom themes you can use on our ",
|
||||
"looking-for-theme-continued": "theme github.",
|
||||
"scan": "Scan",
|
||||
"description": "Kavita comes in my colors, find a color scheme that meets your needs or build one yourself and share it. Themes may be applied for your account or applied to all accounts.",
|
||||
"site-themes": "Site Themes",
|
||||
"set-default": "Set Default",
|
||||
"default-theme": "Default theme",
|
||||
"download": "{{changelog.download}}",
|
||||
"apply": "{{common.apply}}",
|
||||
"applied": "Applied",
|
||||
"active-theme": "Active",
|
||||
"updated-toastr": "Site default has been updated to {{name}}",
|
||||
"scan-queued": "A site theme scan has been queued"
|
||||
"scan-queued": "A site theme scan has been queued",
|
||||
"delete": "{{common.delete}}",
|
||||
"drag-n-drop": "{{cover-image-chooser.drag-n-drop}}",
|
||||
"upload": "{{cover-image-chooser.upload}}",
|
||||
"upload-continued": "a css file",
|
||||
"preview-default": "Select a theme first",
|
||||
"preview-default-admin": "Select a theme first or upload one manually"
|
||||
},
|
||||
|
||||
"theme": {
|
||||
|
@ -212,7 +219,7 @@
|
|||
|
||||
"site-theme-provider-pipe": {
|
||||
"system": "System",
|
||||
"user": "User"
|
||||
"custom": "{{device-platform-pipe.custom}}"
|
||||
},
|
||||
|
||||
"manage-devices": {
|
||||
|
@ -1583,14 +1590,17 @@
|
|||
"promote-tooltip": "Promotion means that the collection can be seen server-wide, not just for you. All series within this collection will still have user-access restrictions placed on them."
|
||||
},
|
||||
|
||||
|
||||
"browse-themes-modal": {
|
||||
"title": "Browse Themes"
|
||||
},
|
||||
|
||||
"import-mal-collection-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"title": "MAL Interest Stack Import",
|
||||
"description": "Import your MAL Interest Stacks and create Collections within Kavita",
|
||||
"series-count": "{{common.series-count}}",
|
||||
"restack-count": "{{num}} Restacks"
|
||||
"restack-count": "{{num}} Restacks",
|
||||
"nothing-found": ""
|
||||
},
|
||||
|
||||
"edit-chapter-progress": {
|
||||
|
@ -1946,7 +1956,10 @@
|
|||
"rejected-cover-upload": "The image could not be fetched due to server refusing request. Please download and upload from file instead.",
|
||||
"invalid-confirmation-url": "Invalid confirmation url",
|
||||
"invalid-confirmation-email": "Invalid confirmation email",
|
||||
"invalid-password-reset-url": "Invalid reset password url"
|
||||
"invalid-password-reset-url": "Invalid reset password url",
|
||||
"delete-theme-in-use": "Theme is currently in use by at least one user, cannot delete",
|
||||
"theme-manual-upload": "There was an issue creating Theme from manual upload",
|
||||
"theme-already-in-use": "Theme already exists by that name"
|
||||
},
|
||||
|
||||
"metadata-builder": {
|
||||
|
@ -2185,7 +2198,8 @@
|
|||
"confirm-delete-collections": "Are you sure you want to delete multiple collections?",
|
||||
"collections-deleted": "Collections deleted",
|
||||
"pdf-book-mode-screen-size": "Screen too small for Book mode",
|
||||
"stack-imported": "Stack Imported"
|
||||
"stack-imported": "Stack Imported",
|
||||
"confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal"
|
||||
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue