1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

Add grid size changer #716

This commit is contained in:
Patrik J. Braun 2023-09-11 16:31:50 +02:00
parent 232e91c6fc
commit 26d94e0482
17 changed files with 606 additions and 337 deletions

View File

@ -5,6 +5,7 @@ import {UserRoles} from '../../entities/UserDTO';
import {ConfigProperty, SubConfigClass} from 'typeconfig/common'; import {ConfigProperty, SubConfigClass} from 'typeconfig/common';
import {SearchQueryDTO} from '../../entities/SearchQueryDTO'; import {SearchQueryDTO} from '../../entities/SearchQueryDTO';
import {DefaultsJobs} from '../../entities/job/JobDTO'; import {DefaultsJobs} from '../../entities/job/JobDTO';
import {GridSizes} from '../../entities/GridSizes';
declare let $localize: (s: TemplateStringsArray) => string; declare let $localize: (s: TemplateStringsArray) => string;
if (typeof $localize === 'undefined') { if (typeof $localize === 'undefined') {
@ -986,6 +987,17 @@ export class ClientGalleryConfig {
}) })
defaultSearchGroupingMethod: ClientGroupingConfig = new ClientGroupingConfig(GroupByTypes.Date, false); defaultSearchGroupingMethod: ClientGroupingConfig = new ClientGroupingConfig(GroupByTypes.Date, false);
@ConfigProperty({
type: GridSizes,
tags: {
name: $localize`Default grid size`,
githubIssue: 716,
priority: ConfigPriority.advanced,
},
description: $localize`Default grid size that is used to render photos and videos.`
})
defaultGidSize: GridSizes = GridSizes.medium;
@ConfigProperty({ @ConfigProperty({
tags: { tags: {
name: $localize`Sort directories by date`, name: $localize`Sort directories by date`,

View File

@ -0,0 +1,7 @@
export enum GridSizes {
extraSmall = 10,
small = 20,
medium = 30,
large = 40,
extraLarge = 50
}

View File

@ -112,6 +112,7 @@ import {NgIconsModule} from '@ng-icons/core';
import { import {
ionAddOutline, ionAddOutline,
ionAlbumsOutline, ionAlbumsOutline,
ionAppsOutline,
ionArrowDownOutline, ionArrowDownOutline,
ionArrowUpOutline, ionArrowUpOutline,
ionBrowsersOutline, ionBrowsersOutline,
@ -137,6 +138,7 @@ import {
ionFunnelOutline, ionFunnelOutline,
ionGitBranchOutline, ionGitBranchOutline,
ionGlobeOutline, ionGlobeOutline,
ionGridOutline,
ionHammerOutline, ionHammerOutline,
ionImageOutline, ionImageOutline,
ionImagesOutline, ionImagesOutline,
@ -162,6 +164,7 @@ import {
ionSettingsOutline, ionSettingsOutline,
ionShareSocialOutline, ionShareSocialOutline,
ionShuffleOutline, ionShuffleOutline,
ionSquareOutline,
ionStar, ionStar,
ionStarOutline, ionStarOutline,
ionStopOutline, ionStopOutline,
@ -176,7 +179,6 @@ import {
ionVolumeMuteOutline, ionVolumeMuteOutline,
ionWarningOutline ionWarningOutline
} from '@ng-icons/ionicons'; } from '@ng-icons/ionicons';
import {SortingMethodIconComponent} from './ui/sorting-method-icon/sorting-method-icon.component';
import {SafeHtmlPipe} from './pipes/SafeHTMLPipe'; import {SafeHtmlPipe} from './pipes/SafeHTMLPipe';
import {DatePipe} from '@angular/common'; import {DatePipe} from '@angular/common';
import {ParseIntPipe} from './pipes/ParseIntPipe'; import {ParseIntPipe} from './pipes/ParseIntPipe';
@ -185,6 +187,10 @@ import {
} from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component'; } from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component';
import {ContentLoaderService} from './ui/gallery/contentLoader.service'; import {ContentLoaderService} from './ui/gallery/contentLoader.service';
import {FileDTOToRelativePathPipe} from './pipes/FileDTOToRelativePathPipe'; import {FileDTOToRelativePathPipe} from './pipes/FileDTOToRelativePathPipe';
import {StringifyGridSize} from './pipes/StringifyGridSize';
import {GalleryNavigatorService} from './ui/gallery/navigator/navigator.service';
import {GridSizeIconComponent} from './ui/utils/grid-size-icon/grid-size-icon.component';
import {SortingMethodIconComponent} from './ui/utils/sorting-method-icon/sorting-method-icon.component';
@Injectable() @Injectable()
export class MyHammerConfig extends HammerGestureConfig { export class MyHammerConfig extends HammerGestureConfig {
@ -246,7 +252,8 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline, ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline,
ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline, ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline,
ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline, ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline,
ionBrowsersOutline, ionUnlinkOutline ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline,
ionAppsOutline
}), }),
ClipboardModule, ClipboardModule,
TooltipModule.forRoot(), TooltipModule.forRoot(),
@ -325,6 +332,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
StringifySearchQuery, StringifySearchQuery,
StringifyEnum, StringifyEnum,
StringifySearchType, StringifySearchType,
StringifyGridSize,
FileDTOToPathPipe, FileDTOToPathPipe,
FileDTOToRelativePathPipe, FileDTOToRelativePathPipe,
PhotoFilterPipe, PhotoFilterPipe,
@ -332,6 +340,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
UsersComponent, UsersComponent,
SharingsListComponent, SharingsListComponent,
SortingMethodIconComponent, SortingMethodIconComponent,
GridSizeIconComponent,
SafeHtmlPipe, SafeHtmlPipe,
SortingMethodSettingsEntryComponent SortingMethodSettingsEntryComponent
], ],
@ -350,6 +359,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
ContentLoaderService, ContentLoaderService,
FilterService, FilterService,
GallerySortingService, GallerySortingService,
GalleryNavigatorService,
MapService, MapService,
BlogService, BlogService,
SearchQueryParserService, SearchQueryParserService,

View File

@ -0,0 +1,12 @@
import {Pipe, PipeTransform} from '@angular/core';
import {EnumTranslations} from '../ui/EnumTranslations';
import {GridSizes} from '../../../common/entities/GridSizes';
@Pipe({name: 'stringifyGridSize'})
export class StringifyGridSize implements PipeTransform {
transform(gs: GridSizes): string {
return EnumTranslations[GridSizes[gs]];
}
}

View File

@ -4,6 +4,7 @@ import {ReIndexingSensitivity} from '../../../common/config/private/PrivateConfi
import {SearchQueryTypes} from '../../../common/entities/SearchQueryDTO'; import {SearchQueryTypes} from '../../../common/entities/SearchQueryDTO';
import {ConfigStyle} from './settings/settings.service'; import {ConfigStyle} from './settings/settings.service';
import {SortByTypes,GroupByTypes} from '../../../common/entities/SortingMethods'; import {SortByTypes,GroupByTypes} from '../../../common/entities/SortingMethods';
import {GridSizes} from '../../../common/entities/GridSizes';
export const EnumTranslations: Record<string, string> = {}; export const EnumTranslations: Record<string, string> = {};
export const enumToTranslatedArray = (EnumType: any): { key: number; value: string }[] => { export const enumToTranslatedArray = (EnumType: any): { key: number; value: string }[] => {
@ -55,6 +56,12 @@ EnumTranslations[SortByTypes[SortByTypes.FileSize]] = $localize`file size`;
EnumTranslations[GroupByTypes[GroupByTypes.NoGrouping]] = $localize`don't group`; EnumTranslations[GroupByTypes[GroupByTypes.NoGrouping]] = $localize`don't group`;
EnumTranslations[GridSizes[GridSizes.extraSmall]] = $localize`extra small`;
EnumTranslations[GridSizes[GridSizes.small]] = $localize`small`;
EnumTranslations[GridSizes[GridSizes.medium]] = $localize`medium`;
EnumTranslations[GridSizes[GridSizes.large]] = $localize`big`;
EnumTranslations[GridSizes[GridSizes.extraLarge]] = $localize`extra large`;
EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.url]] = $localize`Url`; EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.url]] = $localize`Url`;
EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.search]] = $localize`Search`; EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.search]] = $localize`Search`;
EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.gallery]] = $localize`Gallery`; EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.gallery]] = $localize`Gallery`;

View File

@ -10,6 +10,7 @@ import {SearchQueryDTO, SearchQueryTypes,} from '../../../../common/entities/Sea
import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {ContentWrapperWithError} from './contentLoader.service'; import {ContentWrapperWithError} from './contentLoader.service';
import {ThemeModes} from '../../../../common/config/public/ClientConfig'; import {ThemeModes} from '../../../../common/config/public/ClientConfig';
import {GridSizes} from '../../../../common/entities/GridSizes';
interface CacheItem<T> { interface CacheItem<T> {
timestamp: number; timestamp: number;
@ -24,6 +25,7 @@ export class GalleryCacheService {
private static readonly SEARCH_PREFIX = 'SEARCH:'; private static readonly SEARCH_PREFIX = 'SEARCH:';
private static readonly SORTING_PREFIX = 'SORTING:'; private static readonly SORTING_PREFIX = 'SORTING:';
private static readonly GROUPING_PREFIX = 'GROUPING:'; private static readonly GROUPING_PREFIX = 'GROUPING:';
private static readonly GRID_SIZE_PREFIX = 'GRID_SIZE:';
private static readonly VERSION = 'VERSION'; private static readonly VERSION = 'VERSION';
private static readonly SLIDESHOW_SPEED = 'SLIDESHOW_SPEED'; private static readonly SLIDESHOW_SPEED = 'SLIDESHOW_SPEED';
private static THEME_MODE = 'THEME_MODE'; private static THEME_MODE = 'THEME_MODE';
@ -36,8 +38,8 @@ export class GalleryCacheService {
const onNewVersion = (ver: string) => { const onNewVersion = (ver: string) => {
if ( if (
ver !== null && ver !== null &&
localStorage.getItem(GalleryCacheService.VERSION) !== ver localStorage.getItem(GalleryCacheService.VERSION) !== ver
) { ) {
GalleryCacheService.deleteCache(); GalleryCacheService.deleteCache();
localStorage.setItem(GalleryCacheService.VERSION, ver); localStorage.setItem(GalleryCacheService.VERSION, ver);
@ -49,7 +51,7 @@ export class GalleryCacheService {
private static wasAReload(): boolean { private static wasAReload(): boolean {
const perfEntries = performance.getEntriesByType( const perfEntries = performance.getEntriesByType(
'navigation' 'navigation'
) as PerformanceNavigationTiming[]; ) as PerformanceNavigationTiming[];
return perfEntries && perfEntries[0] && perfEntries[0].type === 'reload'; return perfEntries && perfEntries[0] && perfEntries[0].type === 'reload';
} }
@ -59,8 +61,8 @@ export class GalleryCacheService {
if (tmp != null) { if (tmp != null) {
const value: CacheItem<ContentWrapperWithError> = JSON.parse(tmp); const value: CacheItem<ContentWrapperWithError> = JSON.parse(tmp);
if ( if (
value.timestamp < value.timestamp <
Date.now() - Config.Search.searchCacheTimeout Date.now() - Config.Search.searchCacheTimeout
) { ) {
localStorage.removeItem(key); localStorage.removeItem(key);
return null; return null;
@ -76,14 +78,14 @@ export class GalleryCacheService {
const toRemove = []; const toRemove = [];
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
if ( if (
localStorage.key(i).startsWith(GalleryCacheService.CONTENT_PREFIX) || localStorage.key(i).startsWith(GalleryCacheService.CONTENT_PREFIX) ||
localStorage.key(i).startsWith(GalleryCacheService.SEARCH_PREFIX) || localStorage.key(i).startsWith(GalleryCacheService.SEARCH_PREFIX) ||
localStorage localStorage
.key(i) .key(i)
.startsWith(GalleryCacheService.INSTANT_SEARCH_PREFIX) || .startsWith(GalleryCacheService.INSTANT_SEARCH_PREFIX) ||
localStorage localStorage
.key(i) .key(i)
.startsWith(GalleryCacheService.AUTO_COMPLETE_PREFIX) .startsWith(GalleryCacheService.AUTO_COMPLETE_PREFIX)
) { ) {
toRemove.push(localStorage.key(i)); toRemove.push(localStorage.key(i));
} }
@ -154,9 +156,9 @@ export class GalleryCacheService {
} }
private setSortOrGroup( private setSortOrGroup(
prefix: string, prefix: string,
cw: ContentWrapper, cw: ContentWrapper,
sorting: SortingMethod | GroupingMethod sorting: SortingMethod | GroupingMethod
): void { ): void {
try { try {
let key = prefix; let key = prefix;
@ -172,23 +174,62 @@ export class GalleryCacheService {
} }
} }
removeGridSize(cw: ContentWrapperWithError): void {
let key = GalleryCacheService.GRID_SIZE_PREFIX;
if (cw?.searchResult?.searchQuery) {
key += JSON.stringify(cw.searchResult.searchQuery);
} else {
key += cw?.directory?.path + '/' + cw?.directory?.name;
}
localStorage.removeItem(key);
}
getGridSize(cw: ContentWrapperWithError): GridSizes {
let key = GalleryCacheService.GRID_SIZE_PREFIX;
if (cw?.searchResult?.searchQuery) {
key += JSON.stringify(cw.searchResult.searchQuery);
} else {
key += cw?.directory?.path + '/' + cw?.directory?.name;
}
const tmp = localStorage.getItem(key);
if (tmp != null) {
return parseInt(tmp);
}
return null;
}
setGridSize(cw: ContentWrapperWithError, gs: GridSizes) {
try {
let key = GalleryCacheService.GRID_SIZE_PREFIX;
if (cw?.searchResult?.searchQuery) {
key += JSON.stringify(cw.searchResult.searchQuery);
} else {
key += cw?.directory?.path + '/' + cw?.directory?.name;
}
localStorage.setItem(key, gs.toString());
} catch (e) {
this.reset();
console.error(e);
}
}
public getAutoComplete( public getAutoComplete(
text: string, text: string,
type: SearchQueryTypes type: SearchQueryTypes
): IAutoCompleteItem[] { ): IAutoCompleteItem[] {
if (Config.Gallery.enableCache === false) { if (Config.Gallery.enableCache === false) {
return null; return null;
} }
const key = const key =
GalleryCacheService.AUTO_COMPLETE_PREFIX + GalleryCacheService.AUTO_COMPLETE_PREFIX +
text + text +
(type ? '_' + type : ''); (type ? '_' + type : '');
const tmp = localStorage.getItem(key); const tmp = localStorage.getItem(key);
if (tmp != null) { if (tmp != null) {
const value: CacheItem<IAutoCompleteItem[]> = JSON.parse(tmp); const value: CacheItem<IAutoCompleteItem[]> = JSON.parse(tmp);
if ( if (
value.timestamp < value.timestamp <
Date.now() - Config.Search.AutoComplete.cacheTimeout Date.now() - Config.Search.AutoComplete.cacheTimeout
) { ) {
localStorage.removeItem(key); localStorage.removeItem(key);
return null; return null;
@ -199,17 +240,17 @@ export class GalleryCacheService {
} }
public setAutoComplete( public setAutoComplete(
text: string, text: string,
type: SearchQueryTypes, type: SearchQueryTypes,
items: Array<IAutoCompleteItem> items: Array<IAutoCompleteItem>
): void { ): void {
if (Config.Gallery.enableCache === false) { if (Config.Gallery.enableCache === false) {
return; return;
} }
const key = const key =
GalleryCacheService.AUTO_COMPLETE_PREFIX + GalleryCacheService.AUTO_COMPLETE_PREFIX +
text + text +
(type ? '_' + type : ''); (type ? '_' + type : '');
const tmp: CacheItem<Array<IAutoCompleteItem>> = { const tmp: CacheItem<Array<IAutoCompleteItem>> = {
timestamp: Date.now(), timestamp: Date.now(),
item: items, item: items,
@ -256,7 +297,7 @@ export class GalleryCacheService {
} }
try { try {
const value = localStorage.getItem( const value = localStorage.getItem(
GalleryCacheService.CONTENT_PREFIX + Utils.concatUrls(directoryName) GalleryCacheService.CONTENT_PREFIX + Utils.concatUrls(directoryName)
); );
if (value != null) { if (value != null) {
return JSON.parse(value); return JSON.parse(value);
@ -273,8 +314,8 @@ export class GalleryCacheService {
} }
const key = const key =
GalleryCacheService.CONTENT_PREFIX + GalleryCacheService.CONTENT_PREFIX +
Utils.concatUrls(cw.directory.path, cw.directory.name); Utils.concatUrls(cw.directory.path, cw.directory.name);
if (cw.directory.isPartial === true && localStorage.getItem(key)) { if (cw.directory.isPartial === true && localStorage.getItem(key)) {
return; return;
} }
@ -299,8 +340,8 @@ export class GalleryCacheService {
try { try {
const directoryKey = const directoryKey =
GalleryCacheService.CONTENT_PREFIX + GalleryCacheService.CONTENT_PREFIX +
Utils.concatUrls(media.directory.path, media.directory.name); Utils.concatUrls(media.directory.path, media.directory.name);
const value = localStorage.getItem(directoryKey); const value = localStorage.getItem(directoryKey);
if (value != null) { if (value != null) {
const directory: ParentDirectoryDTO = JSON.parse(value); const directory: ParentDirectoryDTO = JSON.parse(value);
@ -332,8 +373,8 @@ export class GalleryCacheService {
localStorage.clear(); localStorage.clear();
localStorage.setItem('currentUser', currentUserStr); localStorage.setItem('currentUser', currentUserStr);
localStorage.setItem( localStorage.setItem(
GalleryCacheService.VERSION, GalleryCacheService.VERSION,
this.versionService.version.value this.versionService.version.value
); );
} catch (e) { } catch (e) {
// ignoring errors // ignoring errors

View File

@ -26,6 +26,8 @@ import {MediaDTO, MediaDTOUtils,} from '../../../../../common/entities/MediaDTO'
import {QueryParams} from '../../../../../common/QueryParams'; import {QueryParams} from '../../../../../common/QueryParams';
import {GallerySortingService, MediaGroup} from '../navigator/sorting.service'; import {GallerySortingService, MediaGroup} from '../navigator/sorting.service';
import {GroupByTypes} from '../../../../../common/entities/SortingMethods'; import {GroupByTypes} from '../../../../../common/entities/SortingMethods';
import {GalleryNavigatorService} from '../navigator/navigator.service';
import {GridSizes} from '../../../../../common/entities/GridSizes';
@Component({ @Component({
selector: 'app-gallery-grid', selector: 'app-gallery-grid',
@ -33,7 +35,7 @@ import {GroupByTypes} from '../../../../../common/entities/SortingMethods';
styleUrls: ['./grid.gallery.component.css'], styleUrls: ['./grid.gallery.component.css'],
}) })
export class GalleryGridComponent export class GalleryGridComponent
implements OnInit, OnChanges, AfterViewInit, OnDestroy { implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@ViewChild('gridContainer', {static: false}) gridContainer: ElementRef; @ViewChild('gridContainer', {static: false}) gridContainer: ElementRef;
@ViewChildren(GalleryPhotoComponent) @ViewChildren(GalleryPhotoComponent)
gridPhotoQL: QueryList<GalleryPhotoComponent>; gridPhotoQL: QueryList<GalleryPhotoComponent>;
@ -45,9 +47,11 @@ export class GalleryGridComponent
public IMAGE_MARGIN = 2; public IMAGE_MARGIN = 2;
isAfterViewInit = false; isAfterViewInit = false;
subscriptions: { subscriptions: {
girdSize: Subscription;
route: Subscription; route: Subscription;
} = { } = {
route: null, route: null,
girdSize: null
}; };
delayedRenderUpToPhoto: string = null; delayedRenderUpToPhoto: string = null;
private scrollListenerPhotos: GalleryPhotoComponent[] = []; private scrollListenerPhotos: GalleryPhotoComponent[] = [];
@ -61,12 +65,13 @@ export class GalleryGridComponent
public readonly blogOpen = Config.Gallery.InlineBlogStartsOpen; public readonly blogOpen = Config.Gallery.InlineBlogStartsOpen;
constructor( constructor(
private overlayService: OverlayService, private overlayService: OverlayService,
private changeDetector: ChangeDetectorRef, private changeDetector: ChangeDetectorRef,
public queryService: QueryService, public queryService: QueryService,
private router: Router, private router: Router,
public sortingService: GallerySortingService, public sortingService: GallerySortingService,
private route: ActivatedRoute public navigatorService: GalleryNavigatorService,
private route: ActivatedRoute
) { ) {
} }
@ -76,30 +81,57 @@ export class GalleryGridComponent
} }
this.updateContainerDimensions(); this.updateContainerDimensions();
this.mergeNewPhotos(); this.mergeNewPhotos();
this.helperTime = window.setTimeout((): void => { this.renderMinimalPhotos();
this.renderPhotos();
if (this.delayedRenderUpToPhoto) {
this.renderUpToMedia(this.delayedRenderUpToPhoto);
}
}, 0);
} }
ngOnInit(): void { ngOnInit(): void {
this.subscriptions.route = this.route.queryParams.subscribe( this.subscriptions.route = this.route.queryParams.subscribe(
(params: Params): void => { (params: Params): void => {
if ( if (
params[QueryParams.gallery.photo] && params[QueryParams.gallery.photo] &&
params[QueryParams.gallery.photo] !== '' params[QueryParams.gallery.photo] !== ''
) { ) {
this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo]; this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo];
if (!this.mediaGroups?.length) { if (!this.mediaGroups?.length) {
return; return;
} }
this.renderUpToMedia(params[QueryParams.gallery.photo]); this.renderUpToMedia(params[QueryParams.gallery.photo]);
}
} }
}
); );
this.subscriptions.girdSize = this.navigatorService.girdSize.subscribe(gs => {
switch (gs) {
case GridSizes.extraSmall:
this.TARGET_COL_COUNT = 12;
this.MIN_ROW_COUNT = 5;
this.MAX_ROW_COUNT = 10;
break;
case GridSizes.small:
this.TARGET_COL_COUNT = 8;
this.MIN_ROW_COUNT = 3;
this.MAX_ROW_COUNT = 8;
break;
case GridSizes.medium:
this.TARGET_COL_COUNT = 5;
this.MIN_ROW_COUNT = 2;
this.MAX_ROW_COUNT = 5;
break;
case GridSizes.large:
this.TARGET_COL_COUNT = 2;
this.MIN_ROW_COUNT = 1;
this.MAX_ROW_COUNT = 3;
break;
case GridSizes.extraLarge:
this.TARGET_COL_COUNT = 1;
this.MIN_ROW_COUNT = 1;
this.MAX_ROW_COUNT = 2;
break;
}
this.clearRenderedPhotos();
this.renderMinimalPhotos();
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -114,6 +146,10 @@ export class GalleryGridComponent
this.subscriptions.route.unsubscribe(); this.subscriptions.route.unsubscribe();
this.subscriptions.route = null; this.subscriptions.route = null;
} }
if (this.subscriptions.girdSize !== null) {
this.subscriptions.girdSize.unsubscribe();
this.subscriptions.girdSize = null;
}
} }
@HostListener('window:resize') @HostListener('window:resize')
@ -137,6 +173,18 @@ export class GalleryGridComponent
}, 100); }, 100);
} }
/*
Renders some photos. If nothing specified, this amount should be enough
* */
private renderMinimalPhotos() {
this.helperTime = window.setTimeout((): void => {
this.renderPhotos();
if (this.delayedRenderUpToPhoto) {
this.renderUpToMedia(this.delayedRenderUpToPhoto);
}
}, 0);
}
photoClicked(media: MediaDTO): void { photoClicked(media: MediaDTO): void {
this.router.navigate([], { this.router.navigate([], {
queryParams: this.queryService.getParams(media), queryParams: this.queryService.getParams(media),
@ -149,19 +197,14 @@ export class GalleryGridComponent
if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) { if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) {
this.gridPhotoQL.changes.subscribe((): void => { this.gridPhotoQL.changes.subscribe((): void => {
this.scrollListenerPhotos = this.gridPhotoQL.filter( this.scrollListenerPhotos = this.gridPhotoQL.filter(
(pc): boolean => pc.ScrollListener (pc): boolean => pc.ScrollListener
); );
}); });
} }
this.updateContainerDimensions(); this.updateContainerDimensions();
this.clearRenderedPhotos(); this.clearRenderedPhotos();
this.helperTime = window.setTimeout((): void => { this.renderMinimalPhotos();
this.renderPhotos();
if (this.delayedRenderUpToPhoto) {
this.renderUpToMedia(this.delayedRenderUpToPhoto);
}
}, 0);
this.isAfterViewInit = true; this.isAfterViewInit = true;
} }
@ -225,7 +268,7 @@ export class GalleryGridComponent
// if all check passed, nothing to delete from the last group // if all check passed, nothing to delete from the last group
if (!diffFound && if (!diffFound &&
lastOkIndex.media == this.mediaGroups[lastOkIndex.groups].media.length - 1) { lastOkIndex.media == this.mediaGroups[lastOkIndex.groups].media.length - 1) {
firstDeleteIndex.groups = lastOkIndex.groups; firstDeleteIndex.groups = lastOkIndex.groups;
firstDeleteIndex.media = lastOkIndex.media + 1; firstDeleteIndex.media = lastOkIndex.media + 1;
} }
@ -248,16 +291,16 @@ export class GalleryGridComponent
public renderARow(): number { public renderARow(): number {
if ( if (
!this.isMoreToRender() || !this.isMoreToRender() ||
this.containerWidth === 0 this.containerWidth === 0
) { ) {
return null; return null;
} }
// step group // step group
if (this.mediaToRender.length == 0 || if (this.mediaToRender.length == 0 ||
this.mediaToRender[this.mediaToRender.length - 1].media.length >= this.mediaToRender[this.mediaToRender.length - 1].media.length >=
this.mediaGroups[this.mediaToRender.length - 1].media.length) { this.mediaGroups[this.mediaToRender.length - 1].media.length) {
this.mediaToRender.push({ this.mediaToRender.push({
name: this.mediaGroups[this.mediaToRender.length].name, name: this.mediaGroups[this.mediaToRender.length].name,
date: this.mediaGroups[this.mediaToRender.length].date, date: this.mediaGroups[this.mediaToRender.length].date,
@ -269,10 +312,10 @@ export class GalleryGridComponent
const minRowHeight = this.screenHeight / this.MAX_ROW_COUNT; const minRowHeight = this.screenHeight / this.MAX_ROW_COUNT;
const photoRowBuilder = new GridRowBuilder( const photoRowBuilder = new GridRowBuilder(
this.mediaGroups[this.mediaToRender.length - 1].media, this.mediaGroups[this.mediaToRender.length - 1].media,
this.mediaToRender[this.mediaToRender.length - 1].media.length, this.mediaToRender[this.mediaToRender.length - 1].media.length,
this.IMAGE_MARGIN, this.IMAGE_MARGIN,
this.containerWidth - this.overlayService.getPhantomScrollbarWidth() this.containerWidth - this.overlayService.getPhantomScrollbarWidth()
); );
photoRowBuilder.addPhotos(this.TARGET_COL_COUNT); photoRowBuilder.addPhotos(this.TARGET_COL_COUNT);
@ -285,13 +328,13 @@ export class GalleryGridComponent
const noFullRow = photoRowBuilder.calcRowHeight() > maxRowHeight; const noFullRow = photoRowBuilder.calcRowHeight() > maxRowHeight;
// if the row is not full, make it average sized // if the row is not full, make it average sized
const rowHeight = noFullRow ? (minRowHeight + maxRowHeight) / 2 : const rowHeight = noFullRow ? (minRowHeight + maxRowHeight) / 2 :
Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight); Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight);
const imageHeight = rowHeight - this.IMAGE_MARGIN * 2; const imageHeight = rowHeight - this.IMAGE_MARGIN * 2;
photoRowBuilder.getPhotoRow().forEach((media): void => { photoRowBuilder.getPhotoRow().forEach((media): void => {
const imageWidth = imageHeight * MediaDTOUtils.calcAspectRatio(media); const imageWidth = imageHeight * MediaDTOUtils.calcAspectRatio(media);
this.mediaToRender[this.mediaToRender.length - 1].media.push( this.mediaToRender[this.mediaToRender.length - 1].media.push(
new GridMedia(media, imageWidth, imageHeight, this.mediaToRender[this.mediaToRender.length - 1].media.length) new GridMedia(media, imageWidth, imageHeight, this.mediaToRender[this.mediaToRender.length - 1].media.length)
); );
}); });
@ -302,23 +345,23 @@ export class GalleryGridComponent
@HostListener('window:scroll') @HostListener('window:scroll')
onScroll(): void { onScroll(): void {
if ( if (
!this.onScrollFired && !this.onScrollFired &&
this.mediaGroups && this.mediaGroups &&
// should we trigger this at all? // should we trigger this at all?
(this.isMoreToRender() || (this.isMoreToRender() ||
this.scrollListenerPhotos.length > 0) this.scrollListenerPhotos.length > 0)
) { ) {
window.requestAnimationFrame((): void => { window.requestAnimationFrame((): void => {
this.renderPhotos(); this.renderPhotos();
if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) { if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) {
this.scrollListenerPhotos.forEach( this.scrollListenerPhotos.forEach(
(pc: GalleryPhotoComponent): void => { (pc: GalleryPhotoComponent): void => {
pc.onScroll(); pc.onScroll();
} }
); );
this.scrollListenerPhotos = this.scrollListenerPhotos.filter( this.scrollListenerPhotos = this.scrollListenerPhotos.filter(
(pc): boolean => pc.ScrollListener (pc): boolean => pc.ScrollListener
); );
} }
@ -340,7 +383,7 @@ export class GalleryGridComponent
let mediaIndex = -1; let mediaIndex = -1;
for (let i = 0; i < this.mediaGroups.length; ++i) { for (let i = 0; i < this.mediaGroups.length; ++i) {
mediaIndex = this.mediaGroups[i].media.findIndex( mediaIndex = this.mediaGroups[i].media.findIndex(
(p): boolean => this.queryService.getMediaStringId(p) === mediaStringId (p): boolean => this.queryService.getMediaStringId(p) === mediaStringId
); );
if (mediaIndex !== -1) { if (mediaIndex !== -1) {
groupIndex = i; groupIndex = i;
@ -356,11 +399,11 @@ export class GalleryGridComponent
// so not required to render more, but the scrollbar does not trigger more photos to render // so not required to render more, but the scrollbar does not trigger more photos to render
// (on lightbox navigation) // (on lightbox navigation)
while ( while (
(this.mediaToRender.length - 1 < groupIndex && (this.mediaToRender.length - 1 < groupIndex &&
this.mediaToRender[this.mediaToRender.length - 1]?.media?.length < mediaIndex) && this.mediaToRender[this.mediaToRender.length - 1]?.media?.length < mediaIndex) &&
this.renderARow() !== null this.renderARow() !== null
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
) { ) {
} }
} }
@ -378,13 +421,13 @@ export class GalleryGridComponent
private shouldRenderMore(offset = 0): boolean { private shouldRenderMore(offset = 0): boolean {
const bottomOffset = this.getMaxRowHeight() * 2; const bottomOffset = this.getMaxRowHeight() * 2;
return ( return (
Config.Gallery.enableOnScrollRendering === false || Config.Gallery.enableOnScrollRendering === false ||
PageHelper.ScrollY >= PageHelper.ScrollY >=
document.body.clientHeight + document.body.clientHeight +
offset - offset -
window.innerHeight - window.innerHeight -
bottomOffset || bottomOffset ||
(document.body.clientHeight + offset) * 0.85 < window.innerHeight (document.body.clientHeight + offset) * 0.85 < window.innerHeight
); );
} }
@ -393,9 +436,9 @@ export class GalleryGridComponent
return; return;
} }
if ( if (
this.containerWidth === 0 || this.containerWidth === 0 ||
!this.isMoreToRender() || !this.isMoreToRender() ||
!this.shouldRenderMore() !this.shouldRenderMore()
) { ) {
return; return;
} }
@ -403,10 +446,10 @@ export class GalleryGridComponent
let renderedContentHeight = 0; let renderedContentHeight = 0;
while ( while (
this.isMoreToRender() && this.isMoreToRender() &&
(this.shouldRenderMore(renderedContentHeight) === true || (this.shouldRenderMore(renderedContentHeight) === true ||
this.getNumberOfRenderedMedia() < numberOfPhotos) this.getNumberOfRenderedMedia() < numberOfPhotos)
) { ) {
const ret = this.renderARow(); const ret = this.renderARow();
if (ret === null) { if (ret === null) {
throw new Error('Grid media rendering failed'); throw new Error('Grid media rendering failed');
@ -417,7 +460,7 @@ export class GalleryGridComponent
private isMoreToRender() { private isMoreToRender() {
return this.mediaToRender.length < this.mediaGroups.length || return this.mediaToRender.length < this.mediaGroups.length ||
(this.mediaToRender[this.mediaToRender.length - 1]?.media.length || 0) < this.mediaGroups[this.mediaToRender.length - 1]?.media.length; (this.mediaToRender[this.mediaToRender.length - 1]?.media.length || 0) < this.mediaGroups[this.mediaToRender.length - 1]?.media.length;
} }
getNumberOfRenderedMedia() { getNumberOfRenderedMedia() {
@ -433,9 +476,9 @@ export class GalleryGridComponent
PageHelper.showScrollY(); PageHelper.showScrollY();
// if the width changed a bit or the height changed a lot // if the width changed a bit or the height changed a lot
if ( if (
this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth || this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth ||
this.screenHeight < window.innerHeight * 0.75 || this.screenHeight < window.innerHeight * 0.75 ||
this.screenHeight > window.innerHeight * 1.25 this.screenHeight > window.innerHeight * 1.25
) { ) {
this.screenHeight = window.innerHeight; this.screenHeight = window.innerHeight;
this.containerWidth = this.gridContainer.nativeElement.parentElement.clientWidth; this.containerWidth = this.gridContainer.nativeElement.parentElement.clientWidth;

View File

@ -1,167 +1,195 @@
<div #navigator class="container-fluid pt-1 pb-1 pe-0 ps-0 bg-body-secondary"> <div #navigator class="container-fluid pt-1 pb-1 pe-0 ps-0 bg-body-secondary">
<nav class="d-md-flex row" aria-label="breadcrumb"> <nav class="d-md-flex row" aria-label="breadcrumb">
<div class="col-auto"> <div class="col-auto">
<ol *ngIf="isDirectory" id="directory-path" class="mb-0 mt-1 breadcrumb"> <ol *ngIf="isDirectory" id="directory-path" class="mb-0 mt-1 breadcrumb">
<li *ngFor="let path of routes | async" class="breadcrumb-item"> <li *ngFor="let path of routes | async" class="breadcrumb-item">
<a *ngIf="path.route" [routerLink]="['/gallery',path.route]" <a *ngIf="path.route" [routerLink]="['/gallery',path.route]"
[title]="path.title || ''" [title]="path.title || ''"
[queryParams]="queryService.getParams()">{{path.name}}</a> [queryParams]="queryService.getParams()">{{path.name}}</a>
<ng-container *ngIf="!path.route">{{path.name}}</ng-container> <ng-container *ngIf="!path.route">{{path.name}}</ng-container>
</li> </li>
</ol> </ol>
<ol *ngIf="isSearch" class="mb-0 mt-1 breadcrumb"> <ol *ngIf="isSearch" class="mb-0 mt-1 breadcrumb">
<li class="active"> <li class="active">
<ng-container i18n>Searching for:</ng-container> <ng-container i18n>Searching for:</ng-container>
<strong> {{contentLoaderService.content.value?.searchResult?.searchQuery | searchQuery}}</strong> <strong> {{contentLoaderService.content.value?.searchResult?.searchQuery | searchQuery}}</strong>
</li> </li>
</ol> </ol>
</div>
<div class="ms-auto text-end col-auto">
<ng-container *ngIf="ItemCount> 0 && config.Gallery.NavBar.showItemCount">
<div class="photos-count">
{{ItemCount}} <span i18n>items</span>
</div> </div>
<div class="divider">&nbsp;</div> <div class="ms-auto text-end col-auto">
</ng-container> <ng-container *ngIf="ItemCount> 0 && config.Gallery.NavBar.showItemCount">
<div class="photos-count">
{{ItemCount}} <span i18n>items</span>
</div>
<div class="divider">&nbsp;</div>
</ng-container>
<ng-container *ngIf="config.Gallery.enableDownloadZip && isDirectory && ItemCount > 0"> <ng-container *ngIf="config.Gallery.enableDownloadZip && isDirectory && ItemCount > 0">
<a [href]="getDownloadZipLink()" <a [href]="getDownloadZipLink()"
class="btn btn-outline-secondary btn-navigator"> class="btn btn-outline-secondary btn-navigator">
<ng-icon name="ionDownloadOutline" title="Download" i18n-title></ng-icon> <ng-icon name="ionDownloadOutline" title="Download" i18n-title></ng-icon>
</a> </a>
<div class="divider">&nbsp;</div> <div class="divider">&nbsp;</div>
</ng-container> </ng-container>
<ng-container *ngIf="config.Gallery.enableDirectoryFlattening && isDirectory && authService.canSearch()"> <ng-container *ngIf="config.Gallery.enableDirectoryFlattening && isDirectory && authService.canSearch()">
<a <a
[routerLink]="['/search', getDirectoryFlattenSearchQuery()]" [routerLink]="['/search', getDirectoryFlattenSearchQuery()]"
class="btn btn-outline-secondary btn-navigator"> class="btn btn-outline-secondary btn-navigator">
<ng-icon name="ionGitBranchOutline" <ng-icon name="ionGitBranchOutline"
title="Show all subdirectories" i18n-title></ng-icon> title="Show all subdirectories" i18n-title></ng-icon>
</a> </a>
<div class="divider">&nbsp;</div> <div class="divider">&nbsp;</div>
</ng-container> </ng-container>
<ng-container *ngIf="ItemCount> 0"> <ng-container *ngIf="ItemCount> 0">
<a class="btn btn-outline-secondary btn-navigator" <a class="btn btn-outline-secondary btn-navigator"
[class.btn-secondary]="filterService.activeFilters.value.areFiltersActive" [class.btn-secondary]="filterService.activeFilters.value.areFiltersActive"
[class.btn-outline-secondary]="!filterService.activeFilters.value.areFiltersActive" [class.btn-outline-secondary]="!filterService.activeFilters.value.areFiltersActive"
(click)="showFilters = ! showFilters"> (click)="showFilters = ! showFilters">
<ng-icon name="ionFunnelOutline" <ng-icon name="ionFunnelOutline"
title="Filters" i18n-title></ng-icon> title="Filters" i18n-title></ng-icon>
</a> </a>
<div class="divider">&nbsp;</div> <div class="divider">&nbsp;</div>
</ng-container> </ng-container>
<div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom right" <div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom right"
[insideClick]="true" [insideClick]="true"
title="Sort and group" i18n-title> title="Sort and group" i18n-title>
<button id="button-alignment" dropdownToggle type="button" <button id="button-alignment" dropdownToggle type="button"
class="btn dropdown-toggle btn-outline-secondary btn-navigator" class="btn dropdown-toggle btn-outline-secondary btn-navigator"
[class.btn-secondary]="!isDefaultSortingAndGrouping()" [class.btn-secondary]="!isDefaultSortingAndGrouping()"
[class.btn-outline-secondary]="isDefaultSortingAndGrouping()" [class.btn-outline-secondary]="isDefaultSortingAndGrouping()"
aria-controls="sorting-dropdown"> aria-controls="sorting-dropdown">
<ng-icon *ngIf="sortingService.sorting.value.ascending !== null" <ng-icon *ngIf="sortingService.sorting.value.ascending !== null"
[name]="!sortingService.sorting.value.ascending ? 'ionArrowDownOutline' : 'ionArrowUpOutline'"></ng-icon> [name]="!sortingService.sorting.value.ascending ? 'ionArrowDownOutline' : 'ionArrowUpOutline'"></ng-icon>
<app-sorting-method-icon [method]="sortingService.sorting.value.method"></app-sorting-method-icon> <app-sorting-method-icon [method]="sortingService.sorting.value.method"></app-sorting-method-icon>
<div class="grouping-icon" *ngIf="sortingService.grouping.value.method !== GroupByTypes.NoGrouping"> <div class="grouping-icon" *ngIf="sortingService.grouping.value.method !== GroupByTypes.NoGrouping">
<div> <div>
<ng-icon <ng-icon
[name]="!sortingService.grouping.value.ascending ? 'ionArrowDownOutline' : 'ionArrowUpOutline'"></ng-icon> [name]="!sortingService.grouping.value.ascending ? 'ionArrowDownOutline' : 'ionArrowUpOutline'"></ng-icon>
<app-sorting-method-icon [method]="sortingService.grouping.value.method"></app-sorting-method-icon> <app-sorting-method-icon
</div> [method]="sortingService.grouping.value.method"></app-sorting-method-icon>
<div class="ps-1" i18n> </div>
group <div class="ps-1" i18n>
</div> group
</div> </div>
</button> </div>
<div id="sorting-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-alignment">
<div class="row flex-nowrap">
<div class="col p-1 border-end">
<h6 class="ps-2" i18n>Sorting
<button class="btn btn-outline-primary btn-group-follow btn-sm"
[class.btn-outline-primary]="groupingFollowSorting"
[class.btn-outline-secondary]="!groupingFollowSorting"
(click)="groupingFollowSorting=!groupingFollowSorting"
title="Grouping follows sorting" i18n-title>
<ng-icon class="" name="ionLinkOutline"></ng-icon>
</button> </button>
<div id="sorting-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-alignment">
<div class="row flex-nowrap">
<div class="col p-1 border-end">
<h6 class="ps-2" i18n>Sorting
<button class="btn btn-outline-primary btn-group-follow btn-sm"
[class.btn-outline-primary]="groupingFollowSorting"
[class.btn-outline-secondary]="!groupingFollowSorting"
(click)="groupingFollowSorting=!groupingFollowSorting"
title="Grouping follows sorting" i18n-title>
<ng-icon class="" name="ionLinkOutline"></ng-icon>
</button>
</h6> </h6>
<div class="row"> <div class="row">
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem" <div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.sorting.value.method == type.key" [class.active]="sortingService.sorting.value.method == type.key"
*ngFor="let type of sortingByTypes" *ngFor="let type of sortingByTypes"
(click)="setSortingBy(type.key)"> (click)="setSortingBy(type.key)">
<div class="me-2 d-inline-block"> <div class="me-2 d-inline-block">
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon> <app-sorting-method-icon [method]="type.key"></app-sorting-method-icon>
</div> </div>
<div class="d-inline-block">{{type.key | stringifySorting}}</div> <div class="d-inline-block">{{type.key | stringifySorting}}</div>
</div>
<ng-container *ngIf="isDirectionalSort(sortingService.sorting.value.method)">
<hr>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.sorting.value.ascending == true"
(click)="setSortingAscending(true)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowUpOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>ascending</div>
</div>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.sorting.value.ascending == false"
(click)="setSortingAscending(false)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowDownOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>descending</div>
</div>
</ng-container>
</div>
</div>
<div class="col p-1">
<h6 class="ps-2" i18n>Grouping</h6>
<div class="row">
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.grouping.value.method == type.key"
*ngFor="let type of groupingByTypes"
(click)="setGroupingBy(type.key)">
<div class="me-2 d-inline-block">
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon>
</div>
<div class="d-inline-block">{{type.key | stringifySorting}}</div>
</div>
<ng-container *ngIf="isDirectionalSort(sortingService.grouping.value.method)">
<hr>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.grouping.value.ascending == true"
(click)="setGroupingAscending(true)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowUpOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>ascending</div>
</div>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.grouping.value.ascending == false"
(click)="setGroupingAscending(false)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowDownOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>descending</div>
</div>
</ng-container>
</div>
</div>
</div>
</div> </div>
<ng-container *ngIf="isDirectionalSort(sortingService.sorting.value.method)">
<hr>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.sorting.value.ascending == true"
(click)="setSortingAscending(true)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowUpOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>ascending</div>
</div>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.sorting.value.ascending == false"
(click)="setSortingAscending(false)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowDownOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>descending</div>
</div>
</ng-container>
</div>
</div> </div>
<div class="col p-1"> <div class="divider">&nbsp;</div>
<h6 class="ps-2" i18n>Grouping</h6> <div class="btn-group" dropdown #dropdown="bs-dropdown" placement="bottom right"
<div class="row"> [insideClick]="true"
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem" title="Grid size" i18n-title>
[class.active]="sortingService.grouping.value.method == type.key" <button id="button-grid-size" dropdownToggle type="button"
*ngFor="let type of groupingByTypes" class="btn dropdown-toggle btn-outline-secondary btn-navigator"
(click)="setGroupingBy(type.key)"> [class.btn-secondary]="!navigatorService.isDefaultGridSize()"
<div class="me-2 d-inline-block"> [class.btn-outline-secondary]="navigatorService.isDefaultGridSize()"
<app-sorting-method-icon [method]="type.key"></app-sorting-method-icon> aria-controls="grid-size-dropdown">
</div> <app-grid-size-icon [method]="navigatorService.girdSize.value"></app-grid-size-icon>
<div class="d-inline-block">{{type.key | stringifySorting}}</div> </button>
<div id="grid-size-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right"
role="menu" aria-labelledby="button-alignment">
<h6 class="ps-2" i18n>Grid size</h6>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="navigatorService.girdSize.value == type.key"
*ngFor="let type of gridSizes"
(click)="navigatorService.setGridSize(type.key)">
<div class="me-2 d-inline-block">
<app-grid-size-icon [method]="type.key"></app-grid-size-icon>
</div>
<div class="d-inline-block">{{type.key | stringifyGridSize}}</div>
</div>
</div> </div>
<ng-container *ngIf="isDirectionalSort(sortingService.grouping.value.method)">
<hr>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.grouping.value.ascending == true"
(click)="setGroupingAscending(true)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowUpOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>ascending</div>
</div>
<div class="dropdown-item sorting-grouping-item ps-3 pe-3" role="menuitem"
[class.active]="sortingService.grouping.value.ascending == false"
(click)="setGroupingAscending(false)">
<div class="me-2 d-inline-block">
<ng-icon name="ionArrowDownOutline"></ng-icon>
</div>
<div class="d-inline-block" i18n>descending</div>
</div>
</ng-container>
</div>
</div> </div>
</div>
</div> </div>
</div> </nav>
</div>
</nav>
</div> </div>

View File

@ -15,6 +15,8 @@ import {PageHelper} from '../../../model/page.helper';
import {BsDropdownDirective} from 'ngx-bootstrap/dropdown'; import {BsDropdownDirective} from 'ngx-bootstrap/dropdown';
import {FilterService} from '../filter/filter.service'; import {FilterService} from '../filter/filter.service';
import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '../contentLoader.service'; import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '../contentLoader.service';
import {GalleryNavigatorService} from './navigator.service';
import {GridSizes} from '../../../../../common/entities/GridSizes';
@Component({ @Component({
selector: 'app-gallery-navbar', selector: 'app-gallery-navbar',
@ -25,6 +27,7 @@ import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '.
export class GalleryNavigatorComponent { export class GalleryNavigatorComponent {
public readonly sortingByTypes: { key: number; value: string }[] = []; public readonly sortingByTypes: { key: number; value: string }[] = [];
public readonly groupingByTypes: { key: number; value: string }[] = []; public readonly groupingByTypes: { key: number; value: string }[] = [];
public readonly gridSizes: { key: number; value: string }[] = [];
public readonly config = Config; public readonly config = Config;
// DefaultSorting = Config.Gallery.defaultPhotoSortingMethod; // DefaultSorting = Config.Gallery.defaultPhotoSortingMethod;
public readonly SearchQueryTypes = SearchQueryTypes; public readonly SearchQueryTypes = SearchQueryTypes;
@ -45,81 +48,83 @@ export class GalleryNavigatorComponent {
public groupingFollowSorting = true; // if grouping should be set after sorting automatically public groupingFollowSorting = true; // if grouping should be set after sorting automatically
constructor( constructor(
public authService: AuthenticationService, public authService: AuthenticationService,
public queryService: QueryService, public queryService: QueryService,
public contentLoaderService: ContentLoaderService, public contentLoaderService: ContentLoaderService,
public filterService: FilterService, public filterService: FilterService,
public sortingService: GallerySortingService, public sortingService: GallerySortingService,
private router: Router, public navigatorService: GalleryNavigatorService,
public sanitizer: DomSanitizer private router: Router,
public sanitizer: DomSanitizer
) { ) {
this.sortingByTypes = Utils.enumToArray(SortByTypes); this.sortingByTypes = Utils.enumToArray(SortByTypes);
// can't group by random // can't group by random
this.groupingByTypes = Utils.enumToArray(GroupByTypes); this.groupingByTypes = Utils.enumToArray(GroupByTypes);
this.gridSizes = Utils.enumToArray(GridSizes);
this.RootFolderName = $localize`Home`; this.RootFolderName = $localize`Home`;
this.wrappedContent = this.contentLoaderService.content; this.wrappedContent = this.contentLoaderService.content;
this.directoryContent = this.wrappedContent.pipe( this.directoryContent = this.wrappedContent.pipe(
map((c) => (c.directory ? c.directory : c.searchResult)) map((c) => (c.directory ? c.directory : c.searchResult))
); );
this.routes = this.contentLoaderService.content.pipe( this.routes = this.contentLoaderService.content.pipe(
map((c) => { map((c) => {
this.parentPath = null; this.parentPath = null;
if (!c.directory) { if (!c.directory) {
return []; return [];
}
const path = c.directory.path.replace(new RegExp('\\\\', 'g'), '/');
const dirs = path.split('/');
dirs.push(c.directory.name);
// removing empty strings
for (let i = 0; i < dirs.length; i++) {
if (!dirs[i] || 0 === dirs[i].length || '.' === dirs[i]) {
dirs.splice(i, 1);
i--;
} }
}
const user = this.authService.user.value; const path = c.directory.path.replace(new RegExp('\\\\', 'g'), '/');
const arr: NavigatorPath[] = [];
// create root link const dirs = path.split('/');
if (dirs.length === 0) { dirs.push(c.directory.name);
arr.push({name: this.RootFolderName, route: null});
} else {
arr.push({
name: this.RootFolderName,
route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions)
? '/'
: null,
});
}
// create rest navigation // removing empty strings
dirs.forEach((name, index) => { for (let i = 0; i < dirs.length; i++) {
const route = dirs.slice(0, index + 1).join('/'); if (!dirs[i] || 0 === dirs[i].length || '.' === dirs[i]) {
if (dirs.length - 1 === index) { dirs.splice(i, 1);
arr.push({name, route: null}); i--;
}
}
const user = this.authService.user.value;
const arr: NavigatorPath[] = [];
// create root link
if (dirs.length === 0) {
arr.push({name: this.RootFolderName, route: null});
} else { } else {
arr.push({ arr.push({
name, name: this.RootFolderName,
route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions) route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions)
? route ? '/'
: null, : null,
}); });
} }
});
// parent directory has a shortcut to navigate to // create rest navigation
if (arr.length >= 2 && arr[arr.length - 2].route) { dirs.forEach((name, index) => {
this.parentPath = arr[arr.length - 2].route; const route = dirs.slice(0, index + 1).join('/');
arr[arr.length - 2].title = $localize`key: alt + up`; if (dirs.length - 1 === index) {
} arr.push({name, route: null});
return arr; } else {
arr.push({
name,
route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions)
? route
: null,
});
}) }
});
// parent directory has a shortcut to navigate to
if (arr.length >= 2 && arr[arr.length - 2].route) {
this.parentPath = arr[arr.length - 2].route;
arr[arr.length - 2].title = $localize`key: alt + up`;
}
return arr;
})
); );
} }
@ -134,18 +139,19 @@ export class GalleryNavigatorComponent {
get ItemCount(): number { get ItemCount(): number {
const c = this.contentLoaderService.content.value; const c = this.contentLoaderService.content.value;
return c.directory return c.directory
? c.directory.mediaCount ? c.directory.mediaCount
: c.searchResult : c.searchResult
? c.searchResult.media.length ? c.searchResult.media.length
: 0; : 0;
} }
isDefaultSortingAndGrouping(): boolean { isDefaultSortingAndGrouping(): boolean {
return this.sortingService.isDefaultSortingAndGrouping( return this.sortingService.isDefaultSortingAndGrouping(
this.contentLoaderService.content.value this.contentLoaderService.content.value
); );
} }
isDirectionalSort(value: number) { isDirectionalSort(value: number) {
return Utils.isValidEnumInt(SortByDirectionalTypes, value); return Utils.isValidEnumInt(SortByDirectionalTypes, value);
} }
@ -161,8 +167,8 @@ export class GalleryNavigatorComponent {
this.sortingService.setSorting(s); this.sortingService.setSorting(s);
// you cannot group by random // you cannot group by random
if (!this.isDirectionalSort(sorting) || if (!this.isDirectionalSort(sorting) ||
// if grouping is disabled, do not update it // if grouping is disabled, do not update it
this.sortingService.grouping.value.method === GroupByTypes.NoGrouping || !this.groupingFollowSorting this.sortingService.grouping.value.method === GroupByTypes.NoGrouping || !this.groupingFollowSorting
) { ) {
return; return;
} }
@ -202,12 +208,12 @@ export class GalleryNavigatorComponent {
queryParams += e[0] + '=' + e[1]; queryParams += e[0] + '=' + e[1];
}); });
return Utils.concatUrls( return Utils.concatUrls(
Config.Server.urlBase, Config.Server.urlBase,
Config.Server.apiPath, Config.Server.apiPath,
'/gallery/zip/', '/gallery/zip/',
c.directory.path, c.directory.path,
c.directory.name, c.directory.name,
'?' + queryParams '?' + queryParams
); );
} }
@ -229,8 +235,8 @@ export class GalleryNavigatorComponent {
return; return;
} }
this.router.navigate(['/gallery', this.parentPath], this.router.navigate(['/gallery', this.parentPath],
{queryParams: this.queryService.getParams()}) {queryParams: this.queryService.getParams()})
.catch(console.error); .catch(console.error);
} }
@HostListener('window:keydown', ['$event']) @HostListener('window:keydown', ['$event'])

View File

@ -0,0 +1,61 @@
import {Injectable} from '@angular/core';
import {GalleryCacheService} from '../cache.gallery.service';
import {BehaviorSubject} from 'rxjs';
import {Config} from '../../../../../common/config/public/Config';
import {ContentLoaderService} from '../contentLoader.service';
import {GridSizes} from '../../../../../common/entities/GridSizes';
@Injectable()
export class GalleryNavigatorService {
public girdSize: BehaviorSubject<GridSizes>;
constructor(
private galleryCacheService: GalleryCacheService,
private galleryService: ContentLoaderService,
) {
// TODO load def instead
this.girdSize = new BehaviorSubject(this.getDefaultGridSize());
this.galleryService.content.subscribe((c) => {
if (c) {
if (c) {
const gs = this.galleryCacheService.getGridSize(c);
if (gs !== null) {
this.girdSize.next(gs);
} else {
this.girdSize.next(this.getDefaultGridSize());
}
}
}
});
}
setGridSize(gs: GridSizes) {
this.girdSize.next(gs);
if (this.galleryService.content.value) {
if (
!this.isDefaultGridSize()
) {
this.galleryCacheService.setGridSize(
this.galleryService.content.value,
gs
);
} else {
this.galleryCacheService.removeGridSize(
this.galleryService.content.value
);
}
}
}
isDefaultGridSize(): boolean {
return this.girdSize.value === this.getDefaultGridSize();
}
getDefaultGridSize(): GridSizes {
return Config.Gallery.defaultGidSize;
}
}

View File

@ -0,0 +1,3 @@
.line-height-0 {
line-height: 0;
}

View File

@ -0,0 +1,27 @@
<ng-container [ngSwitch]="method">
<ng-container *ngSwitchCase="GridSizes.extraSmall">
<ng-icon name="ionAppsOutline"></ng-icon>
</ng-container>
<div class="align-middle" *ngSwitchCase="GridSizes.small">
<div class="d-flex" style="margin-bottom: 0.1em">
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
</div>
<div class="d-flex">
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
<ng-icon class="line-height-0" strokeWidth="80" size="0.33em" name="ionSquareOutline"></ng-icon>
</div>
</div>
<ng-container *ngSwitchCase="GridSizes.medium">
<ng-icon name="ionGridOutline"></ng-icon>
</ng-container>
<ng-container *ngSwitchCase="GridSizes.large">
<ng-icon strokeWidth="70" class="line-height-0 align-middle" size="0.5em" name="ionSquareOutline"></ng-icon>
<ng-icon strokeWidth="70" class="line-height-0 align-middle" size="0.5em" name="ionSquareOutline"></ng-icon>
</ng-container>
<ng-container *ngSwitchCase="GridSizes.extraLarge">
<ng-icon name="ionSquareOutline"></ng-icon>
</ng-container>
</ng-container>

View File

@ -0,0 +1,12 @@
import {Component, Input} from '@angular/core';
import {GridSizes} from '../../../../../common/entities/GridSizes';
@Component({
selector: 'app-grid-size-icon',
templateUrl: './grid-size-icon.component.html',
styleUrls: ['./grid-size-icon.component.css']
})
export class GridSizeIconComponent {
@Input() method: number;
public readonly GridSizes = GridSizes;
}

View File

@ -1,5 +1,5 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {GroupSortByTypes} from '../../../../common/entities/SortingMethods'; import {GroupSortByTypes} from '../../../../../common/entities/SortingMethods';
@Component({ @Component({
selector: 'app-sorting-method-icon', selector: 'app-sorting-method-icon',
@ -8,5 +8,5 @@ import {GroupSortByTypes} from '../../../../common/entities/SortingMethods';
}) })
export class SortingMethodIconComponent { export class SortingMethodIconComponent {
@Input() method: number; @Input() method: number;
GroupSortByTypes = GroupSortByTypes; public readonly GroupSortByTypes = GroupSortByTypes;
} }

View File

@ -58,7 +58,7 @@ ng-icon {
font-size: 1.15em font-size: 1.15em
} }
ng-icon svg { ng-icon:not([strokeWidth]) svg {
vertical-align: unset; vertical-align: unset;
--ng-icon__stroke-width: 40; --ng-icon__stroke-width: 40;
} }