1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-02-07 13:41:44 +02:00

Creating configurable SVG icon #587 #331 #325

This commit is contained in:
Patrik J. Braun 2023-08-08 22:25:43 +02:00
parent 63236c30fd
commit d1ed607647
26 changed files with 256 additions and 99 deletions

View File

@ -92,7 +92,6 @@
"tsConfig": "src/frontend/tsconfig.app.json",
"polyfills": "src/frontend/polyfills.ts",
"assets": [
"src/frontend/assets",
"src/frontend/robots.txt",
{
"glob": "**/*",
@ -199,7 +198,6 @@
"src/frontend/styles.css"
],
"assets": [
"src/frontend/assets",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images/",

View File

@ -4,15 +4,16 @@ import * as os from 'os';
import * as crypto from 'crypto';
import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config';
import {PhotoWorker, RendererInput, ThumbnailSourceType,} from '../threading/PhotoWorker';
import {MediaRendererInput, PhotoWorker, SvgRendererInput, ThumbnailSourceType,} from '../threading/PhotoWorker';
import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter';
import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO';
import {SupportedFormats} from '../../../common/SupportedFormats';
import {PersonEntry} from '../database/enitites/PersonEntry';
import {SVGIconConfig} from '../../../common/config/public/ClientConfig';
export class PhotoProcessing {
private static initDone = false;
private static taskQue: ITaskExecuter<RendererInput, void> = null;
private static taskQue: ITaskExecuter<MediaRendererInput | SvgRendererInput, void> = null;
private static readonly CONVERTED_EXTENSION = '.webp';
public static init(): void {
@ -101,7 +102,7 @@ export class PhotoProcessing {
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as RendererInput;
} as MediaRendererInput;
input.cut.width = Math.min(
input.cut.width,
photo.metadata.size.width - input.cut.left
@ -240,6 +241,7 @@ export class PhotoProcessing {
return false;
}
public static async generateThumbnail(
mediaPath: string,
size: number,
@ -267,7 +269,7 @@ export class PhotoProcessing {
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as RendererInput;
} as MediaRendererInput;
const outDir = path.dirname(input.outPath);
@ -280,5 +282,42 @@ export class PhotoProcessing {
const extension = path.extname(fullPath).toLowerCase();
return SupportedFormats.WithDots.Photos.indexOf(extension) !== -1;
}
public static async renderSVG(
svgString: SVGIconConfig,
outPath: string,
color = '#000'
): Promise<string> {
// check if file already exist
try {
await fsp.access(outPath, fsConstants.R_OK);
return outPath;
} catch (e) {
// ignoring errors
}
const size = 256;
// run on other thread
const input = {
type: ThumbnailSourceType.Photo,
svgString: `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg"
viewBox="${Config.Server.svgIcon.viewBox || '0 0 512 512'}">
<path fill="${color}" d="${Config.Server.svgIcon.path}"/></svg>`,
size: size,
outPath,
makeSquare: false,
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as SvgRendererInput;
const outDir = path.dirname(input.outPath);
await fsp.mkdir(outDir, {recursive: true});
await this.taskQue.execute(input);
return outPath;
}
}

View File

@ -1,32 +1,32 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import * as sharp from 'sharp';
import {Metadata, Sharp} from 'sharp';
import {Logger} from '../../Logger';
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
import {FFmpegFactory} from '../FFmpegFactory';
const path = require('path');
import * as path from 'path';
const sharp = require('sharp');
sharp.cache(false);
export class PhotoWorker {
private static videoRenderer: (input: RendererInput) => Promise<void> = null;
private static videoRenderer: (input: MediaRendererInput) => Promise<void> = null;
public static render(input: RendererInput): Promise<void> {
public static render(input: SvgRendererInput | MediaRendererInput): Promise<void> {
if (input.type === ThumbnailSourceType.Photo) {
return this.renderFromImage(input);
}
if (input.type === ThumbnailSourceType.Video) {
return this.renderFromVideo(input);
return this.renderFromVideo(input as MediaRendererInput);
}
throw new Error('Unsupported media type to render thumbnail:' + input.type);
}
public static renderFromImage(input: RendererInput): Promise<void> {
public static renderFromImage(input: SvgRendererInput | MediaRendererInput): Promise<void> {
return ImageRendererFactory.render(input);
}
public static renderFromVideo(input: RendererInput): Promise<void> {
public static renderFromVideo(input: MediaRendererInput): Promise<void> {
if (PhotoWorker.videoRenderer === null) {
PhotoWorker.videoRenderer = VideoRendererFactory.build();
}
@ -39,15 +39,13 @@ export enum ThumbnailSourceType {
Video = 2,
}
export interface RendererInput {
interface RendererInput {
type: ThumbnailSourceType;
mediaPath: string;
size: number;
makeSquare: boolean;
outPath: string;
quality: number;
useLanczos3: boolean;
smartSubsample: boolean;
cut?: {
left: number;
top: number;
@ -56,10 +54,19 @@ export interface RendererInput {
};
}
export interface MediaRendererInput extends RendererInput {
mediaPath: string;
smartSubsample: boolean;
}
export interface SvgRendererInput extends RendererInput {
svgString: string;
}
export class VideoRendererFactory {
public static build(): (input: RendererInput) => Promise<void> {
public static build(): (input: MediaRendererInput) => Promise<void> {
const ffmpeg = FFmpegFactory.get();
return (input: RendererInput): Promise<void> => {
return (input: MediaRendererInput): Promise<void> => {
return new Promise((resolve, reject): void => {
Logger.silly('[FFmpeg] rendering thumbnail: ' + input.mediaPath);
@ -122,16 +129,22 @@ export class VideoRendererFactory {
export class ImageRendererFactory {
public static async render(input: RendererInput): Promise<void> {
Logger.silly(
'[SharpRenderer] rendering photo:' +
input.mediaPath +
', size:' +
input.size
);
const image: Sharp = sharp(input.mediaPath, {failOnError: false});
const metadata: Metadata = await image.metadata();
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
let image: Sharp;
if ((input as MediaRendererInput).mediaPath) {
Logger.silly(
'[SharpRenderer] rendering photo:' +
(input as MediaRendererInput).mediaPath +
', size:' +
input.size
);
image = sharp((input as MediaRendererInput).mediaPath, {failOnError: false});
} else {
const svg_buffer = Buffer.from((input as SvgRendererInput).svgString);
image = sharp(svg_buffer, { density: 450 });
}
const metadata: Metadata = await image.metadata();
const kernel =
input.useLanczos3 === true
? sharp.kernel.lanczos3
@ -157,7 +170,17 @@ export class ImageRendererFactory {
fit: 'cover',
});
}
await image.rotate().webp({effort: 6, quality: input.quality, smartSubsample: input.smartSubsample}).toFile(input.outPath);
if ((input as MediaRendererInput).mediaPath) {
await image.rotate().webp({
effort: 6,
quality: input.quality,
smartSubsample: (input as MediaRendererInput).smartSubsample
}).toFile(input.outPath);
} else {
if ((input as SvgRendererInput).svgString) {
await image.rotate().png({effort: 6, quality: input.quality}).toFile(input.outPath);
}
}
}
}

View File

@ -11,6 +11,7 @@ import {UserDTO} from '../../common/entities/UserDTO';
import {ServerTimeEntry} from '../middlewares/ServerTimingMWs';
import {ClientConfig, TAGS} from '../../common/config/public/ClientConfig';
import {QueryParams} from '../../common/QueryParams';
import {PhotoProcessing} from '../model/fileprocessing/PhotoProcessing';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -101,7 +102,7 @@ export class PublicRouter {
.replace(/'/g, '&#039;');
res.tpl.Config = confCopy;
res.tpl.customHTMLHead = Config.Server.customHTMLHead;
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th=>th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th => th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
res.tpl.usedTheme = selectedTheme;
return next();
@ -118,7 +119,11 @@ export class PublicRouter {
name: Config.Server.applicationTitle,
icons: [
{
src: 'assets/icon_inv.png',
src: 'icon_inv.svg',
sizes: 'any',
},
{
src: 'icon_inv.png',
sizes: '48x48 72x72 96x96 128x128 256x256',
},
],
@ -133,6 +138,47 @@ export class PublicRouter {
});
});
app.get('/icon.svg', (req: Request, res: Response) => {
res.set('Cache-control', 'public, max-age=31536000');
res.send('<svg xmlns="http://www.w3.org/2000/svg"' +
' viewBox="' + (Config.Server.svgIcon.viewBox || '0 0 512 512') + '">' +
'<path d="' + Config.Server.svgIcon.path + '"/></svg>');
});
app.get('/icon_inv.svg', (req: Request, res: Response) => {
res.set('Cache-control', 'public, max-age=31536000');
res.send('<svg xmlns="http://www.w3.org/2000/svg"' +
' viewBox="' + (Config.Server.svgIcon.viewBox || '0 0 512 512') + '">' +
'<path fill="#FFF" d="' + Config.Server.svgIcon.path + '"/></svg>');
});
app.get('/icon.png', async (req: Request, res: Response, next: NextFunction) => {
try {
const p = path.join(ProjectPath.TempFolder, '/icon.png');
await PhotoProcessing.renderSVG(Config.Server.svgIcon, p);
res.sendFile(p, {
maxAge: 31536000,
dotfiles: 'allow',
});
} catch (e) {
return next(e);
}
});
app.get('/icon_inv.png', async (req: Request, res: Response, next: NextFunction) => {
try {
const p = path.join(ProjectPath.TempFolder, '/icon_inv.png');
await PhotoProcessing.renderSVG(Config.Server.svgIcon, p, '#FFF');
res.sendFile(p, {
maxAge: 31536000,
dotfiles: 'allow',
});
} catch (e) {
return next(e);
}
});
app.get(
[
'/',

View File

@ -1240,6 +1240,19 @@ export class ClientServiceConfig {
}
})
customHTMLHead: string = '';
@ConfigProperty({
type: SVGIconConfig,
tags: {
name: $localize`Svg Icon`,
uiType: 'SVGIconConfig',
priority: ConfigPriority.advanced
} as TAGS,
description: $localize`Sets the icon of the app`,
})
// Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
svgIcon: SVGIconConfig = new SVGIconConfig(`0 0 407 512`, 'M372 232.5l-3.7-6.5c.1-46.4-21.4-65.3-46.5-79.7 7.6-2 15.4-3.6 17.6-13.2 13.1-3.3 15.8-9.4 17.1-15.8 3.4-2.3 14.8-8.7 13.6-19.7 6.4-4.4 10-10.1 8.1-18.1 6.9-7.5 8.7-13.7 5.8-19.4 8.3-10.3 4.6-15.6 1.1-20.9 6.2-11.2.7-23.2-16.6-21.2-6.9-10.1-21.9-7.8-24.2-7.8-2.6-3.2-6-6-16.5-4.7-6.8-6.1-14.4-5-22.3-2.1-9.3-7.3-15.5-1.4-22.6.8C271.6.6 269 5.5 263.5 7.6c-12.3-2.6-16.1 3-22 8.9l-6.9-.1c-18.6 10.8-27.8 32.8-31.1 44.1-3.3-11.3-12.5-33.3-31.1-44.1l-6.9.1c-5.9-5.9-9.7-11.5-22-8.9-5.6-2-8.1-7-19.4-3.4-4.6-1.4-8.9-4.4-13.9-4.3-2.6.1-5.5 1-8.7 3.5-7.9-3-15.5-4-22.3 2.1-10.5-1.3-14 1.4-16.5 4.7-2.3 0-17.3-2.3-24.2 7.8C21.2 16 15.8 28 22 39.2c-3.5 5.4-7.2 10.7 1.1 20.9-2.9 5.7-1.1 11.9 5.8 19.4-1.8 8 1.8 13.7 8.1 18.1-1.2 11 10.2 17.4 13.6 19.7 1.3 6.4 4 12.4 17.1 15.8 2.2 9.5 10 11.2 17.6 13.2-25.1 14.4-46.6 33.3-46.5 79.7l-3.7 6.5c-28.8 17.2-54.7 72.7-14.2 117.7 2.6 14.1 7.1 24.2 11 35.4 5.9 45.2 44.5 66.3 54.6 68.8 14.9 11.2 30.8 21.8 52.2 29.2C159 504.2 181 512 203 512h1c22.1 0 44-7.8 64.2-28.4 21.5-7.4 37.3-18 52.2-29.2 10.2-2.5 48.7-23.6 54.6-68.8 3.9-11.2 8.4-21.3 11-35.4 40.6-45.1 14.7-100.5-14-117.7zm-22.2-8c-1.5 18.7-98.9-65.1-82.1-67.9 45.7-7.5 83.6 19.2 82.1 67.9zm-43 93.1c-24.5 15.8-59.8 5.6-78.8-22.8s-14.6-64.2 9.9-80c24.5-15.8 59.8-5.6 78.8 22.8s14.6 64.2-9.9 80zM238.9 29.3c.8 4.2 1.8 6.8 2.9 7.6 5.4-5.8 9.8-11.7 16.8-17.3 0 3.3-1.7 6.8 2.5 9.4 3.7-5 8.8-9.5 15.5-13.3-3.2 5.6-.6 7.3 1.2 9.6 5.1-4.4 10-8.8 19.4-12.3-2.6 3.1-6.2 6.2-2.4 9.8 5.3-3.3 10.6-6.6 23.1-8.9-2.8 3.1-8.7 6.3-5.1 9.4 6.6-2.5 14-4.4 22.1-5.4-3.9 3.2-7.1 6.3-3.9 8.8 7.1-2.2 16.9-5.1 26.4-2.6l-6 6.1c-.7.8 14.1.6 23.9.8-3.6 5-7.2 9.7-9.3 18.2 1 1 5.8.4 10.4 0-4.7 9.9-12.8 12.3-14.7 16.6 2.9 2.2 6.8 1.6 11.2.1-3.4 6.9-10.4 11.7-16 17.3 1.4 1 3.9 1.6 9.7.9-5.2 5.5-11.4 10.5-18.8 15 1.3 1.5 5.8 1.5 10 1.6-6.7 6.5-15.3 9.9-23.4 14.2 4 2.7 6.9 2.1 10 2.1-5.7 4.7-15.4 7.1-24.4 10 1.7 2.7 3.4 3.4 7.1 4.1-9.5 5.3-23.2 2.9-27 5.6.9 2.7 3.6 4.4 6.7 5.8-15.4.9-57.3-.6-65.4-32.3 15.7-17.3 44.4-37.5 93.7-62.6-38.4 12.8-73 30-102 53.5-34.3-15.9-10.8-55.9 5.8-71.8zm-34.4 114.6c24.2-.3 54.1 17.8 54 34.7-.1 15-21 27.1-53.8 26.9-32.1-.4-53.7-15.2-53.6-29.8 0-11.9 26.2-32.5 53.4-31.8zm-123-12.8c3.7-.7 5.4-1.5 7.1-4.1-9-2.8-18.7-5.3-24.4-10 3.1 0 6 .7 10-2.1-8.1-4.3-16.7-7.7-23.4-14.2 4.2-.1 8.7 0 10-1.6-7.4-4.5-13.6-9.5-18.8-15 5.8.7 8.3.1 9.7-.9-5.6-5.6-12.7-10.4-16-17.3 4.3 1.5 8.3 2 11.2-.1-1.9-4.2-10-6.7-14.7-16.6 4.6.4 9.4 1 10.4 0-2.1-8.5-5.8-13.3-9.3-18.2 9.8-.1 24.6 0 23.9-.8l-6-6.1c9.5-2.5 19.3.4 26.4 2.6 3.2-2.5-.1-5.6-3.9-8.8 8.1 1.1 15.4 2.9 22.1 5.4 3.5-3.1-2.3-6.3-5.1-9.4 12.5 2.3 17.8 5.6 23.1 8.9 3.8-3.6.2-6.7-2.4-9.8 9.4 3.4 14.3 7.9 19.4 12.3 1.7-2.3 4.4-4 1.2-9.6 6.7 3.8 11.8 8.3 15.5 13.3 4.1-2.6 2.5-6.2 2.5-9.4 7 5.6 11.4 11.5 16.8 17.3 1.1-.8 2-3.4 2.9-7.6 16.6 15.9 40.1 55.9 6 71.8-29-23.5-63.6-40.7-102-53.5 49.3 25 78 45.3 93.7 62.6-8 31.8-50 33.2-65.4 32.3 3.1-1.4 5.8-3.2 6.7-5.8-4-2.8-17.6-.4-27.2-5.6zm60.1 24.1c16.8 2.8-80.6 86.5-82.1 67.9-1.5-48.7 36.5-75.5 82.1-67.9zM38.2 342c-23.7-18.8-31.3-73.7 12.6-98.3 26.5-7 9 107.8-12.6 98.3zm91 98.2c-13.3 7.9-45.8 4.7-68.8-27.9-15.5-27.4-13.5-55.2-2.6-63.4 16.3-9.8 41.5 3.4 60.9 25.6 16.9 20 24.6 55.3 10.5 65.7zm-26.4-119.7c-24.5-15.8-28.9-51.6-9.9-80s54.3-38.6 78.8-22.8 28.9 51.6 9.9 80c-19.1 28.4-54.4 38.6-78.8 22.8zM205 496c-29.4 1.2-58.2-23.7-57.8-32.3-.4-12.7 35.8-22.6 59.3-22 23.7-1 55.6 7.5 55.7 18.9.5 11-28.8 35.9-57.2 35.4zm58.9-124.9c.2 29.7-26.2 53.8-58.8 54-32.6.2-59.2-23.8-59.4-53.4v-.6c-.2-29.7 26.2-53.8 58.8-54 32.6-.2 59.2 23.8 59.4 53.4v.6zm82.2 42.7c-25.3 34.6-59.6 35.9-72.3 26.3-13.3-12.4-3.2-50.9 15.1-72 20.9-23.3 43.3-38.5 58.9-26.6 10.5 10.3 16.7 49.1-1.7 72.3zm22.9-73.2c-21.5 9.4-39-105.3-12.6-98.3 43.9 24.7 36.3 79.6 12.6 98.3z');
}
@SubConfigClass({tags: {client: true}, softReadonly: true})

View File

@ -80,7 +80,7 @@ import {GallerySearchFieldBaseComponent} from './ui/gallery/search/search-field-
import {AppRoutingModule} from './app.routing';
import {CookieService} from 'ngx-cookie-service';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
import {icon, Marker} from 'leaflet';
import {Marker} from 'leaflet';
import {AlbumsComponent} from './ui/albums/albums.component';
import {AlbumComponent} from './ui/albums/album/album.component';
import {AlbumsService} from './ui/albums/albums.service';
@ -108,6 +108,7 @@ import {ThemeService} from './model/theme.service';
import {StringifyEnum} from './pipes/StringifyEnum';
import {StringifySearchType} from './pipes/StringifySearchType';
import {MarkerFactory} from './ui/gallery/map/MarkerFactory';
import {IconComponent} from './icon.component';
@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
@ -165,6 +166,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
],
declarations: [
AppComponent,
IconComponent,
LoginComponent,
ShareLoginComponent,
GalleryComponent,

View File

@ -0,0 +1,24 @@
import {Component, Input} from '@angular/core';
import {Config} from '../../common/config/public/Config';
@Component({
selector: 'app-icon',
template: `
<svg xmlns="http://www.w3.org/2000/svg"
[attr.width]="width"
[attr.height]="height"
fill="currentcolor"
[attr.viewBox]="Config.Server.svgIcon.viewBox || '0 0 512 512'">
<path [attr.d]="Config.Server.svgIcon.path"/>
</svg>`,
})
export class IconComponent {
@Input() width: number;
@Input() height: number;
protected readonly Config = Config;
constructor() {
}
}

View File

@ -35,11 +35,13 @@ a {
}
.no-image {
position: absolute;
color: #7f7f7f;
font-size: 80px;
top: calc(50% - 40px);
left: calc(50% - 40px);
display: block;
color: var(--bs-secondary-color);
width: 100px;
top: calc(50%);
left: calc(50%);
transform: translate(-50%, -50%);
position: relative;
}
.photo {

View File

@ -10,9 +10,7 @@
*ngIf="thumbnail && thumbnail.Available"
[style.background-image]="getSanitizedThUrl()"></div>
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"
aria-hidden="true">
</span>
<app-icon *ngIf="!thumbnail || !thumbnail.Available" class="no-image"></app-icon>
</div>

View File

@ -11,6 +11,7 @@ import { AlbumBaseDTO } from '../../../../../common/entities/album/AlbumBaseDTO'
import { Media } from '../../gallery/Media';
import { SavedSearchDTO } from '../../../../../common/entities/album/SavedSearchDTO';
import { UserRoles } from '../../../../../common/entities/UserDTO';
import {Config} from '../../../../../common/config/public/Config';
@Component({
selector: 'app-album',
@ -21,6 +22,7 @@ import { UserRoles } from '../../../../../common/entities/UserDTO';
export class AlbumComponent implements OnInit, OnDestroy {
@Input() album: AlbumBaseDTO;
@Input() size: number;
public readonly svgIcon = Config.Server.svgIcon;
public thumbnail: Thumbnail = null;

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { NetworkService } from '../../model/network/network.service';
import { BehaviorSubject } from 'rxjs';
import { AlbumBaseDTO } from '../../../../common/entities/album/AlbumBaseDTO';
import { SearchQueryDTO } from '../../../../common/entities/SearchQueryDTO';
import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service';
import {BehaviorSubject} from 'rxjs';
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
@Injectable()
export class AlbumsService {

View File

@ -7,7 +7,7 @@
<div class="container-fluid">
<a class="navbar-brand d-none d-sm-block" [routerLink]="['/gallery']"
[queryParams]="queryService.getParams()">
<img src="assets/icon_inv.png" width="30" height="30" class="d-inline-block align-top" alt="">
<app-icon class="d-inline-block align-top" [width]="30" [height]="30" ></app-icon>
<strong class="d-none d-lg-inline-block">{{title}}</strong>
</a>
<div class="collapse navbar-collapse text-center" id="navbarCollapse" [collapse]="collapsed">

View File

@ -32,6 +32,7 @@ export class FrameComponent {
public readonly NavigationLinkTypes = NavigationLinkTypes;
public readonly stringify = JSON.stringify;
public readonly themesEnabled = Config.Gallery.Themes.enabled;
public readonly svgIcon = Config.Server.svgIcon;
/* sticky top navbar */
private lastScroll = {

View File

@ -19,10 +19,13 @@ a {
.no-image {
color: #7f7f7f;
font-size: 80px;
top: calc(50% - 40px);
left: calc(50% - 40px);
display: block;
color: var(--bs-secondary-color);
width: 100px;
top: calc(50%);
left: calc(50%);
transform: translate(-50%, -50%);
position: relative;
}
.photo {

View File

@ -11,9 +11,7 @@
*ngIf="thumbnail && thumbnail.Available"
[style.background-image]="getSanitizedThUrl()"></div>
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"
aria-hidden="true">
</span>
<app-icon *ngIf="!thumbnail || !thumbnail.Available" class="no-image"></app-icon>
</div>
<!--Info box -->
<div class="info rounded-bottom">

View File

@ -1,15 +1,13 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { SubDirectoryDTO } from '../../../../../../common/entities/DirectoryDTO';
import { RouterLink } from '@angular/router';
import { Utils } from '../../../../../../common/Utils';
import { Media } from '../../Media';
import {
Thumbnail,
ThumbnailManagerService,
} from '../../thumbnailManager.service';
import { QueryService } from '../../../../model/query.service';
import { PreviewPhotoDTO } from '../../../../../../common/entities/PhotoDTO';
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
import {SubDirectoryDTO} from '../../../../../../common/entities/DirectoryDTO';
import {RouterLink} from '@angular/router';
import {Utils} from '../../../../../../common/Utils';
import {Media} from '../../Media';
import {Thumbnail, ThumbnailManagerService,} from '../../thumbnailManager.service';
import {QueryService} from '../../../../model/query.service';
import {PreviewPhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import {Config} from '../../../../../../common/config/public/Config';
@Component({
selector: 'app-gallery-directory',
@ -26,7 +24,8 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
private thumbnailService: ThumbnailManagerService,
private sanitizer: DomSanitizer,
public queryService: QueryService
) {}
) {
}
public get SamplePhoto(): PreviewPhotoDTO {
return this.directory.preview;
@ -35,10 +34,10 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
getSanitizedThUrl(): SafeStyle {
return this.sanitizer.bypassSecurityTrustStyle(
'url(' +
this.thumbnail.Src.replace(/\(/g, '%28')
.replace(/'/g, '%27')
.replace(/\)/g, '%29') +
')'
this.thumbnail.Src.replace(/\(/g, '%28')
.replace(/'/g, '%27')
.replace(/\)/g, '%29') +
')'
);
}

View File

@ -4,9 +4,6 @@
text-align: center;
}
.title img {
height: 80px;
}
.card {

View File

@ -3,7 +3,9 @@
<app-language></app-language>
</div>
<div class="row title align-self-center">
<h1><img src="assets/icon.png"/>{{title}}</h1>
<h1>
<app-icon [width]="80"></app-icon>
{{title}}</h1>
</div>
<div class="row card align-self-center">
<div class="card-body">

View File

@ -157,7 +157,7 @@
(click)="showIconModal(iconModalTmp)">
<svg xmlns="http://www.w3.org/2000/svg"
width="1em"
fill="var(--bs-btn-color)"
fill="currentcolor"
[attr.viewBox]="state.value.viewBox || '0 0 512 512'">
<path [attr.d]="state.value.path"/>
</svg>
@ -176,7 +176,7 @@
<div class="col text-center">
<svg xmlns="http://www.w3.org/2000/svg"
width="2em"
fill="var(--bs-body-color)"
fill="currentcolor"
[attr.viewBox]="state.value.viewBox || '0 0 512 512'">
<path [attr.d]="state.value.path"/>
</svg>

View File

@ -79,11 +79,11 @@
<ng-container *ngFor="let ck of getKeys(rStates)">
<ng-container *ngIf="!(rStates.value.__state[ck].shouldHide && rStates.value.__state[ck].shouldHide())">
<app-settings-entry
*ngIf="(ck!=='enabled' || !topLevel) && !rStates.value.__state[ck].isConfigType"
*ngIf="(ck!=='enabled' || !topLevel) && !isExpandableConfig(rStates.value.__state[ck])"
[name]="confPath+'_'+ck"
[ngModel]="rStates?.value.__state[ck]">
</app-settings-entry>
<ng-container *ngIf="rStates.value.__state[ck].isConfigType">
<ng-container *ngIf="isExpandableConfig(rStates.value.__state[ck])">
<div class="card mt-2 mb-2" *ngIf="topLevel && rStates?.value.__state[ck].tags?.uiIcon" [id]="ConfigPath+'.'+ck">
<div class="card-body">
<h5 class="card-title"><span

View File

@ -94,17 +94,17 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
}
this.name = this.states.tags?.name || this.ConfigPath;
this.nestedConfigs = [];
for (const key of this.getKeys(this.states)) {
if (this.states.value.__state[key].isConfigType &&
this.states?.value.__state[key].tags?.uiIcon) {
this.nestedConfigs.push({
id: this.ConfigPath + '.' + key,
name: this.states?.value.__state[key].tags?.name,
icon: this.states?.value.__state[key].tags?.uiIcon,
visible: () => !(this.states.value.__state[key].shouldHide && this.states.value.__state[key].shouldHide())
});
}
for (const key of this.getKeys(this.states)) {
if (this.states.value.__state[key].isConfigType &&
this.states?.value.__state[key].tags?.uiIcon) {
this.nestedConfigs.push({
id: this.ConfigPath + '.' + key,
name: this.states?.value.__state[key].tags?.name,
icon: this.states?.value.__state[key].tags?.uiIcon,
visible: () => !(this.states.value.__state[key].shouldHide && this.states.value.__state[key].shouldHide())
});
}
}
}
@ -270,6 +270,10 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
this.getSettings();
}
isExpandableConfig(c: ConfigState) {
return c.isConfigType && c.tags?.uiType !== 'SVGIconConfig';
}
public async save(): Promise<boolean> {
this.inProgress = true;
@ -306,8 +310,8 @@ export class TemplateComponent implements OnInit, OnChanges, OnDestroy, ISetting
}
const s = states.value.__state;
const keys = Object.keys(s).sort((a, b) => {
if ((s[a].isConfigType || s[a].isConfigArrayType) !== (s[b].isConfigType || s[b].isConfigArrayType)) {
if (s[a].isConfigType || s[a].isConfigArrayType) {
if ((this.isExpandableConfig(s[a]) || s[a].isConfigArrayType) !== (this.isExpandableConfig(s[b]) || s[b].isConfigArrayType)) {
if (this.isExpandableConfig(s[a]) || s[a].isConfigArrayType) {
return 1;
} else {
return -1;

View File

@ -4,9 +4,6 @@
text-align: center;
}
.title img {
height: 80px;
}
@media screen and ( max-width: 500px ) {
.title h1 {

View File

@ -3,12 +3,13 @@
<app-language></app-language>
</div>
<div class="row title align-self-center">
<h1><img src="assets/icon.png"/>{{title}}</h1>
<h1><app-icon [width]="80"></app-icon>{{title}}</h1>
</div>
<div class="row card align-self-center">
<div class="card-body">
<div *ngIf="(shareService.currentSharing | async) == shareService.UnknownSharingKey"
class="h3 text-center text-danger" i18n>Unknown sharing key.</div>
<div *ngIf="(shareService.currentSharing | async) == shareService.UnknownSharingKey"
class="h3 text-center text-danger" i18n>Unknown sharing key.
</div>
<form *ngIf="(shareService.currentSharing | async) != shareService.UnknownSharingKey"
name="form" id="form" class="form-horizontal" #LoginForm="ngForm" (submit)="onLogin()">
<div class="error-message" [hidden]="loginError==false" i18n>Wrong password</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -4,14 +4,16 @@
<base href="<%= Config.Server.urlBase %>/"/>
<meta charset="UTF-8">
<title>Loading..</title>
<link rel="shortcut icon" href="assets/icon.png">
<link rel="shortcut icon" href="icon.svg">
<link rel="shortcut icon" sizes="256x256" href="icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="assets/icon.png">
<link rel="apple-touch-icon" href="icon.svg">
<link rel="apple-touch-icon" sizes="256x256" href="icon.png">
<link rel="manifest" crossorigin="use-credentials" href="manifest.json">
@ -33,8 +35,14 @@
-webkit-transform: translate(-50%, -50%);
left: 50%;
text-align: center">
<img src="assets/icon.png" style="max-width: 256px"/>
<h2>Loading...</h2>
<svg xmlns="http://www.w3.org/2000/svg"
class="d-inline-block align-top"
style="width: 200px;max-width: calc(50vw);max-height: calc(50vh);"
fill="currentcolor"
viewBox="<%- Config.Server.svgIcon.viewBox || '0 0 512 512' %>">
<path d="<%- Config.Server.svgIcon.path %>"/>
</svg>
<h2 style="margin-top: 1em">Loading...</h2>
</div>
</app-pi-gallery2>