mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-02-07 13:41:44 +02:00
parent
63236c30fd
commit
d1ed607647
@ -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/",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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, ''');
|
||||
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(
|
||||
[
|
||||
'/',
|
||||
|
@ -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})
|
||||
|
@ -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,
|
||||
|
24
src/frontend/app/icon.component.ts
Normal file
24
src/frontend/app/icon.component.ts
Normal 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() {
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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 = {
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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') +
|
||||
')'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title img {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -4,9 +4,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title img {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
@media screen and ( max-width: 500px ) {
|
||||
.title h1 {
|
||||
|
@ -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 |
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user