diff --git a/src/common/config/public/ClientConfig.ts b/src/common/config/public/ClientConfig.ts index a88879c8..fc6c1576 100644 --- a/src/common/config/public/ClientConfig.ts +++ b/src/common/config/public/ClientConfig.ts @@ -5,6 +5,7 @@ import {UserRoles} from '../../entities/UserDTO'; import {ConfigProperty, SubConfigClass} from 'typeconfig/common'; import {SearchQueryDTO} from '../../entities/SearchQueryDTO'; import {DefaultsJobs} from '../../entities/job/JobDTO'; +import {GridSizes} from '../../entities/GridSizes'; declare let $localize: (s: TemplateStringsArray) => string; if (typeof $localize === 'undefined') { @@ -986,6 +987,17 @@ export class ClientGalleryConfig { }) 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({ tags: { name: $localize`Sort directories by date`, diff --git a/src/common/entities/GridSizes.ts b/src/common/entities/GridSizes.ts new file mode 100644 index 00000000..3dec3180 --- /dev/null +++ b/src/common/entities/GridSizes.ts @@ -0,0 +1,7 @@ +export enum GridSizes { + extraSmall = 10, + small = 20, + medium = 30, + large = 40, + extraLarge = 50 +} diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 700d1430..1353e2ed 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -112,6 +112,7 @@ import {NgIconsModule} from '@ng-icons/core'; import { ionAddOutline, ionAlbumsOutline, + ionAppsOutline, ionArrowDownOutline, ionArrowUpOutline, ionBrowsersOutline, @@ -137,6 +138,7 @@ import { ionFunnelOutline, ionGitBranchOutline, ionGlobeOutline, + ionGridOutline, ionHammerOutline, ionImageOutline, ionImagesOutline, @@ -162,6 +164,7 @@ import { ionSettingsOutline, ionShareSocialOutline, ionShuffleOutline, + ionSquareOutline, ionStar, ionStarOutline, ionStopOutline, @@ -176,7 +179,6 @@ import { ionVolumeMuteOutline, ionWarningOutline } from '@ng-icons/ionicons'; -import {SortingMethodIconComponent} from './ui/sorting-method-icon/sorting-method-icon.component'; import {SafeHtmlPipe} from './pipes/SafeHTMLPipe'; import {DatePipe} from '@angular/common'; import {ParseIntPipe} from './pipes/ParseIntPipe'; @@ -185,6 +187,10 @@ import { } from './ui/settings/template/settings-entry/sorting-method/sorting-method.settings-entry.component'; import {ContentLoaderService} from './ui/gallery/contentLoader.service'; 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() export class MyHammerConfig extends HammerGestureConfig { @@ -246,7 +252,8 @@ Marker.prototype.options.icon = MarkerFactory.defIcon; ionFlagOutline, ionGlobeOutline, ionPieChartOutline, ionStopOutline, ionTimeOutline, ionCheckmarkOutline, ionPulseOutline, ionResizeOutline, ionCloudOutline, ionChatboxOutline, ionServerOutline, ionFileTrayFullOutline, ionBrushOutline, - ionBrowsersOutline, ionUnlinkOutline + ionBrowsersOutline, ionUnlinkOutline, ionSquareOutline, ionGridOutline, + ionAppsOutline }), ClipboardModule, TooltipModule.forRoot(), @@ -325,6 +332,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon; StringifySearchQuery, StringifyEnum, StringifySearchType, + StringifyGridSize, FileDTOToPathPipe, FileDTOToRelativePathPipe, PhotoFilterPipe, @@ -332,6 +340,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon; UsersComponent, SharingsListComponent, SortingMethodIconComponent, + GridSizeIconComponent, SafeHtmlPipe, SortingMethodSettingsEntryComponent ], @@ -350,6 +359,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon; ContentLoaderService, FilterService, GallerySortingService, + GalleryNavigatorService, MapService, BlogService, SearchQueryParserService, diff --git a/src/frontend/app/pipes/StringifyGridSize.ts b/src/frontend/app/pipes/StringifyGridSize.ts new file mode 100644 index 00000000..8e7d7660 --- /dev/null +++ b/src/frontend/app/pipes/StringifyGridSize.ts @@ -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]]; + } +} + diff --git a/src/frontend/app/ui/EnumTranslations.ts b/src/frontend/app/ui/EnumTranslations.ts index 8903276f..11f5d0d0 100644 --- a/src/frontend/app/ui/EnumTranslations.ts +++ b/src/frontend/app/ui/EnumTranslations.ts @@ -4,6 +4,7 @@ import {ReIndexingSensitivity} from '../../../common/config/private/PrivateConfi import {SearchQueryTypes} from '../../../common/entities/SearchQueryDTO'; import {ConfigStyle} from './settings/settings.service'; import {SortByTypes,GroupByTypes} from '../../../common/entities/SortingMethods'; +import {GridSizes} from '../../../common/entities/GridSizes'; export const EnumTranslations: Record = {}; 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[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.search]] = $localize`Search`; EnumTranslations[NavigationLinkTypes[NavigationLinkTypes.gallery]] = $localize`Gallery`; diff --git a/src/frontend/app/ui/gallery/cache.gallery.service.ts b/src/frontend/app/ui/gallery/cache.gallery.service.ts index 810a96a5..87add682 100644 --- a/src/frontend/app/ui/gallery/cache.gallery.service.ts +++ b/src/frontend/app/ui/gallery/cache.gallery.service.ts @@ -10,6 +10,7 @@ import {SearchQueryDTO, SearchQueryTypes,} from '../../../../common/entities/Sea import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; import {ContentWrapperWithError} from './contentLoader.service'; import {ThemeModes} from '../../../../common/config/public/ClientConfig'; +import {GridSizes} from '../../../../common/entities/GridSizes'; interface CacheItem { timestamp: number; @@ -24,6 +25,7 @@ export class GalleryCacheService { private static readonly SEARCH_PREFIX = 'SEARCH:'; private static readonly SORTING_PREFIX = 'SORTING:'; private static readonly GROUPING_PREFIX = 'GROUPING:'; + private static readonly GRID_SIZE_PREFIX = 'GRID_SIZE:'; private static readonly VERSION = 'VERSION'; private static readonly SLIDESHOW_SPEED = 'SLIDESHOW_SPEED'; private static THEME_MODE = 'THEME_MODE'; @@ -36,8 +38,8 @@ export class GalleryCacheService { const onNewVersion = (ver: string) => { if ( - ver !== null && - localStorage.getItem(GalleryCacheService.VERSION) !== ver + ver !== null && + localStorage.getItem(GalleryCacheService.VERSION) !== ver ) { GalleryCacheService.deleteCache(); localStorage.setItem(GalleryCacheService.VERSION, ver); @@ -49,7 +51,7 @@ export class GalleryCacheService { private static wasAReload(): boolean { const perfEntries = performance.getEntriesByType( - 'navigation' + 'navigation' ) as PerformanceNavigationTiming[]; return perfEntries && perfEntries[0] && perfEntries[0].type === 'reload'; } @@ -59,8 +61,8 @@ export class GalleryCacheService { if (tmp != null) { const value: CacheItem = JSON.parse(tmp); if ( - value.timestamp < - Date.now() - Config.Search.searchCacheTimeout + value.timestamp < + Date.now() - Config.Search.searchCacheTimeout ) { localStorage.removeItem(key); return null; @@ -76,14 +78,14 @@ export class GalleryCacheService { const toRemove = []; for (let i = 0; i < localStorage.length; i++) { if ( - localStorage.key(i).startsWith(GalleryCacheService.CONTENT_PREFIX) || - localStorage.key(i).startsWith(GalleryCacheService.SEARCH_PREFIX) || - localStorage - .key(i) - .startsWith(GalleryCacheService.INSTANT_SEARCH_PREFIX) || - localStorage - .key(i) - .startsWith(GalleryCacheService.AUTO_COMPLETE_PREFIX) + localStorage.key(i).startsWith(GalleryCacheService.CONTENT_PREFIX) || + localStorage.key(i).startsWith(GalleryCacheService.SEARCH_PREFIX) || + localStorage + .key(i) + .startsWith(GalleryCacheService.INSTANT_SEARCH_PREFIX) || + localStorage + .key(i) + .startsWith(GalleryCacheService.AUTO_COMPLETE_PREFIX) ) { toRemove.push(localStorage.key(i)); } @@ -154,9 +156,9 @@ export class GalleryCacheService { } private setSortOrGroup( - prefix: string, - cw: ContentWrapper, - sorting: SortingMethod | GroupingMethod + prefix: string, + cw: ContentWrapper, + sorting: SortingMethod | GroupingMethod ): void { try { 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( - text: string, - type: SearchQueryTypes + text: string, + type: SearchQueryTypes ): IAutoCompleteItem[] { if (Config.Gallery.enableCache === false) { return null; } const key = - GalleryCacheService.AUTO_COMPLETE_PREFIX + - text + - (type ? '_' + type : ''); + GalleryCacheService.AUTO_COMPLETE_PREFIX + + text + + (type ? '_' + type : ''); const tmp = localStorage.getItem(key); if (tmp != null) { const value: CacheItem = JSON.parse(tmp); if ( - value.timestamp < - Date.now() - Config.Search.AutoComplete.cacheTimeout + value.timestamp < + Date.now() - Config.Search.AutoComplete.cacheTimeout ) { localStorage.removeItem(key); return null; @@ -199,17 +240,17 @@ export class GalleryCacheService { } public setAutoComplete( - text: string, - type: SearchQueryTypes, - items: Array + text: string, + type: SearchQueryTypes, + items: Array ): void { if (Config.Gallery.enableCache === false) { return; } const key = - GalleryCacheService.AUTO_COMPLETE_PREFIX + - text + - (type ? '_' + type : ''); + GalleryCacheService.AUTO_COMPLETE_PREFIX + + text + + (type ? '_' + type : ''); const tmp: CacheItem> = { timestamp: Date.now(), item: items, @@ -256,7 +297,7 @@ export class GalleryCacheService { } try { const value = localStorage.getItem( - GalleryCacheService.CONTENT_PREFIX + Utils.concatUrls(directoryName) + GalleryCacheService.CONTENT_PREFIX + Utils.concatUrls(directoryName) ); if (value != null) { return JSON.parse(value); @@ -273,8 +314,8 @@ export class GalleryCacheService { } const key = - GalleryCacheService.CONTENT_PREFIX + - Utils.concatUrls(cw.directory.path, cw.directory.name); + GalleryCacheService.CONTENT_PREFIX + + Utils.concatUrls(cw.directory.path, cw.directory.name); if (cw.directory.isPartial === true && localStorage.getItem(key)) { return; } @@ -299,8 +340,8 @@ export class GalleryCacheService { try { const directoryKey = - GalleryCacheService.CONTENT_PREFIX + - Utils.concatUrls(media.directory.path, media.directory.name); + GalleryCacheService.CONTENT_PREFIX + + Utils.concatUrls(media.directory.path, media.directory.name); const value = localStorage.getItem(directoryKey); if (value != null) { const directory: ParentDirectoryDTO = JSON.parse(value); @@ -332,8 +373,8 @@ export class GalleryCacheService { localStorage.clear(); localStorage.setItem('currentUser', currentUserStr); localStorage.setItem( - GalleryCacheService.VERSION, - this.versionService.version.value + GalleryCacheService.VERSION, + this.versionService.version.value ); } catch (e) { // ignoring errors diff --git a/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts b/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts index 284bdfc4..30872b7a 100644 --- a/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts +++ b/src/frontend/app/ui/gallery/grid/grid.gallery.component.ts @@ -26,6 +26,8 @@ import {MediaDTO, MediaDTOUtils,} from '../../../../../common/entities/MediaDTO' import {QueryParams} from '../../../../../common/QueryParams'; import {GallerySortingService, MediaGroup} from '../navigator/sorting.service'; import {GroupByTypes} from '../../../../../common/entities/SortingMethods'; +import {GalleryNavigatorService} from '../navigator/navigator.service'; +import {GridSizes} from '../../../../../common/entities/GridSizes'; @Component({ selector: 'app-gallery-grid', @@ -33,7 +35,7 @@ import {GroupByTypes} from '../../../../../common/entities/SortingMethods'; styleUrls: ['./grid.gallery.component.css'], }) export class GalleryGridComponent - implements OnInit, OnChanges, AfterViewInit, OnDestroy { + implements OnInit, OnChanges, AfterViewInit, OnDestroy { @ViewChild('gridContainer', {static: false}) gridContainer: ElementRef; @ViewChildren(GalleryPhotoComponent) gridPhotoQL: QueryList; @@ -45,9 +47,11 @@ export class GalleryGridComponent public IMAGE_MARGIN = 2; isAfterViewInit = false; subscriptions: { + girdSize: Subscription; route: Subscription; } = { route: null, + girdSize: null }; delayedRenderUpToPhoto: string = null; private scrollListenerPhotos: GalleryPhotoComponent[] = []; @@ -61,12 +65,13 @@ export class GalleryGridComponent public readonly blogOpen = Config.Gallery.InlineBlogStartsOpen; constructor( - private overlayService: OverlayService, - private changeDetector: ChangeDetectorRef, - public queryService: QueryService, - private router: Router, - public sortingService: GallerySortingService, - private route: ActivatedRoute + private overlayService: OverlayService, + private changeDetector: ChangeDetectorRef, + public queryService: QueryService, + private router: Router, + public sortingService: GallerySortingService, + public navigatorService: GalleryNavigatorService, + private route: ActivatedRoute ) { } @@ -76,30 +81,57 @@ export class GalleryGridComponent } this.updateContainerDimensions(); this.mergeNewPhotos(); - this.helperTime = window.setTimeout((): void => { - this.renderPhotos(); - if (this.delayedRenderUpToPhoto) { - this.renderUpToMedia(this.delayedRenderUpToPhoto); - } - }, 0); + this.renderMinimalPhotos(); } ngOnInit(): void { this.subscriptions.route = this.route.queryParams.subscribe( - (params: Params): void => { - if ( - params[QueryParams.gallery.photo] && - params[QueryParams.gallery.photo] !== '' - ) { - this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo]; - if (!this.mediaGroups?.length) { - return; - } + (params: Params): void => { + if ( + params[QueryParams.gallery.photo] && + params[QueryParams.gallery.photo] !== '' + ) { + this.delayedRenderUpToPhoto = params[QueryParams.gallery.photo]; + if (!this.mediaGroups?.length) { + 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 { @@ -114,6 +146,10 @@ export class GalleryGridComponent this.subscriptions.route.unsubscribe(); this.subscriptions.route = null; } + if (this.subscriptions.girdSize !== null) { + this.subscriptions.girdSize.unsubscribe(); + this.subscriptions.girdSize = null; + } } @HostListener('window:resize') @@ -137,6 +173,18 @@ export class GalleryGridComponent }, 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 { this.router.navigate([], { queryParams: this.queryService.getParams(media), @@ -149,19 +197,14 @@ export class GalleryGridComponent if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) { this.gridPhotoQL.changes.subscribe((): void => { this.scrollListenerPhotos = this.gridPhotoQL.filter( - (pc): boolean => pc.ScrollListener + (pc): boolean => pc.ScrollListener ); }); } this.updateContainerDimensions(); this.clearRenderedPhotos(); - this.helperTime = window.setTimeout((): void => { - this.renderPhotos(); - if (this.delayedRenderUpToPhoto) { - this.renderUpToMedia(this.delayedRenderUpToPhoto); - } - }, 0); + this.renderMinimalPhotos(); this.isAfterViewInit = true; } @@ -225,7 +268,7 @@ export class GalleryGridComponent // if all check passed, nothing to delete from the last group 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.media = lastOkIndex.media + 1; } @@ -248,16 +291,16 @@ export class GalleryGridComponent public renderARow(): number { if ( - !this.isMoreToRender() || - this.containerWidth === 0 + !this.isMoreToRender() || + this.containerWidth === 0 ) { return null; } // step group if (this.mediaToRender.length == 0 || - this.mediaToRender[this.mediaToRender.length - 1].media.length >= - this.mediaGroups[this.mediaToRender.length - 1].media.length) { + this.mediaToRender[this.mediaToRender.length - 1].media.length >= + this.mediaGroups[this.mediaToRender.length - 1].media.length) { this.mediaToRender.push({ name: this.mediaGroups[this.mediaToRender.length].name, date: this.mediaGroups[this.mediaToRender.length].date, @@ -269,10 +312,10 @@ export class GalleryGridComponent const minRowHeight = this.screenHeight / this.MAX_ROW_COUNT; const photoRowBuilder = new GridRowBuilder( - this.mediaGroups[this.mediaToRender.length - 1].media, - this.mediaToRender[this.mediaToRender.length - 1].media.length, - this.IMAGE_MARGIN, - this.containerWidth - this.overlayService.getPhantomScrollbarWidth() + this.mediaGroups[this.mediaToRender.length - 1].media, + this.mediaToRender[this.mediaToRender.length - 1].media.length, + this.IMAGE_MARGIN, + this.containerWidth - this.overlayService.getPhantomScrollbarWidth() ); photoRowBuilder.addPhotos(this.TARGET_COL_COUNT); @@ -285,13 +328,13 @@ export class GalleryGridComponent const noFullRow = photoRowBuilder.calcRowHeight() > maxRowHeight; // if the row is not full, make it average sized const rowHeight = noFullRow ? (minRowHeight + maxRowHeight) / 2 : - Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight); + Math.min(photoRowBuilder.calcRowHeight(), maxRowHeight); const imageHeight = rowHeight - this.IMAGE_MARGIN * 2; photoRowBuilder.getPhotoRow().forEach((media): void => { const imageWidth = imageHeight * MediaDTOUtils.calcAspectRatio(media); 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') onScroll(): void { if ( - !this.onScrollFired && - this.mediaGroups && - // should we trigger this at all? - (this.isMoreToRender() || - this.scrollListenerPhotos.length > 0) + !this.onScrollFired && + this.mediaGroups && + // should we trigger this at all? + (this.isMoreToRender() || + this.scrollListenerPhotos.length > 0) ) { window.requestAnimationFrame((): void => { this.renderPhotos(); if (Config.Gallery.enableOnScrollThumbnailPrioritising === true) { this.scrollListenerPhotos.forEach( - (pc: GalleryPhotoComponent): void => { - pc.onScroll(); - } + (pc: GalleryPhotoComponent): void => { + pc.onScroll(); + } ); this.scrollListenerPhotos = this.scrollListenerPhotos.filter( - (pc): boolean => pc.ScrollListener + (pc): boolean => pc.ScrollListener ); } @@ -340,7 +383,7 @@ export class GalleryGridComponent let mediaIndex = -1; for (let i = 0; i < this.mediaGroups.length; ++i) { mediaIndex = this.mediaGroups[i].media.findIndex( - (p): boolean => this.queryService.getMediaStringId(p) === mediaStringId + (p): boolean => this.queryService.getMediaStringId(p) === mediaStringId ); if (mediaIndex !== -1) { 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 // (on lightbox navigation) while ( - (this.mediaToRender.length - 1 < groupIndex && - this.mediaToRender[this.mediaToRender.length - 1]?.media?.length < mediaIndex) && - this.renderARow() !== null - // eslint-disable-next-line no-empty - ) { + (this.mediaToRender.length - 1 < groupIndex && + this.mediaToRender[this.mediaToRender.length - 1]?.media?.length < mediaIndex) && + this.renderARow() !== null + // eslint-disable-next-line no-empty + ) { } } @@ -378,13 +421,13 @@ export class GalleryGridComponent private shouldRenderMore(offset = 0): boolean { const bottomOffset = this.getMaxRowHeight() * 2; return ( - Config.Gallery.enableOnScrollRendering === false || - PageHelper.ScrollY >= - document.body.clientHeight + - offset - - window.innerHeight - - bottomOffset || - (document.body.clientHeight + offset) * 0.85 < window.innerHeight + Config.Gallery.enableOnScrollRendering === false || + PageHelper.ScrollY >= + document.body.clientHeight + + offset - + window.innerHeight - + bottomOffset || + (document.body.clientHeight + offset) * 0.85 < window.innerHeight ); } @@ -393,9 +436,9 @@ export class GalleryGridComponent return; } if ( - this.containerWidth === 0 || - !this.isMoreToRender() || - !this.shouldRenderMore() + this.containerWidth === 0 || + !this.isMoreToRender() || + !this.shouldRenderMore() ) { return; } @@ -403,10 +446,10 @@ export class GalleryGridComponent let renderedContentHeight = 0; while ( - this.isMoreToRender() && - (this.shouldRenderMore(renderedContentHeight) === true || - this.getNumberOfRenderedMedia() < numberOfPhotos) - ) { + this.isMoreToRender() && + (this.shouldRenderMore(renderedContentHeight) === true || + this.getNumberOfRenderedMedia() < numberOfPhotos) + ) { const ret = this.renderARow(); if (ret === null) { throw new Error('Grid media rendering failed'); @@ -417,7 +460,7 @@ export class GalleryGridComponent private isMoreToRender() { 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() { @@ -433,9 +476,9 @@ export class GalleryGridComponent PageHelper.showScrollY(); // if the width changed a bit or the height changed a lot if ( - this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth || - this.screenHeight < window.innerHeight * 0.75 || - this.screenHeight > window.innerHeight * 1.25 + this.containerWidth !== this.gridContainer.nativeElement.parentElement.clientWidth || + this.screenHeight < window.innerHeight * 0.75 || + this.screenHeight > window.innerHeight * 1.25 ) { this.screenHeight = window.innerHeight; this.containerWidth = this.gridContainer.nativeElement.parentElement.clientWidth; diff --git a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html index 3f0d93e6..dde32229 100644 --- a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html +++ b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.html @@ -1,167 +1,195 @@
-
diff --git a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts index e8124aae..ef908078 100644 --- a/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts +++ b/src/frontend/app/ui/gallery/navigator/navigator.gallery.component.ts @@ -15,6 +15,8 @@ import {PageHelper} from '../../../model/page.helper'; import {BsDropdownDirective} from 'ngx-bootstrap/dropdown'; import {FilterService} from '../filter/filter.service'; import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '../contentLoader.service'; +import {GalleryNavigatorService} from './navigator.service'; +import {GridSizes} from '../../../../../common/entities/GridSizes'; @Component({ selector: 'app-gallery-navbar', @@ -25,6 +27,7 @@ import {ContentLoaderService, ContentWrapperWithError, DirectoryContent} from '. export class GalleryNavigatorComponent { public readonly sortingByTypes: { key: number; value: string }[] = []; public readonly groupingByTypes: { key: number; value: string }[] = []; + public readonly gridSizes: { key: number; value: string }[] = []; public readonly config = Config; // DefaultSorting = Config.Gallery.defaultPhotoSortingMethod; public readonly SearchQueryTypes = SearchQueryTypes; @@ -45,81 +48,83 @@ export class GalleryNavigatorComponent { public groupingFollowSorting = true; // if grouping should be set after sorting automatically constructor( - public authService: AuthenticationService, - public queryService: QueryService, - public contentLoaderService: ContentLoaderService, - public filterService: FilterService, - public sortingService: GallerySortingService, - private router: Router, - public sanitizer: DomSanitizer + public authService: AuthenticationService, + public queryService: QueryService, + public contentLoaderService: ContentLoaderService, + public filterService: FilterService, + public sortingService: GallerySortingService, + public navigatorService: GalleryNavigatorService, + private router: Router, + public sanitizer: DomSanitizer ) { this.sortingByTypes = Utils.enumToArray(SortByTypes); // can't group by random this.groupingByTypes = Utils.enumToArray(GroupByTypes); + this.gridSizes = Utils.enumToArray(GridSizes); this.RootFolderName = $localize`Home`; this.wrappedContent = this.contentLoaderService.content; 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( - map((c) => { - this.parentPath = null; - if (!c.directory) { - 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--; + map((c) => { + this.parentPath = null; + if (!c.directory) { + return []; } - } - const user = this.authService.user.value; - const arr: NavigatorPath[] = []; + const path = c.directory.path.replace(new RegExp('\\\\', 'g'), '/'); - // create root link - if (dirs.length === 0) { - arr.push({name: this.RootFolderName, route: null}); - } else { - arr.push({ - name: this.RootFolderName, - route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions) - ? '/' - : null, - }); - } + const dirs = path.split('/'); + dirs.push(c.directory.name); - // create rest navigation - dirs.forEach((name, index) => { - const route = dirs.slice(0, index + 1).join('/'); - if (dirs.length - 1 === index) { - arr.push({name, route: null}); + // 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 arr: NavigatorPath[] = []; + + // create root link + if (dirs.length === 0) { + arr.push({name: this.RootFolderName, route: null}); } else { arr.push({ - name, - route: UserDTOUtils.isDirectoryPathAvailable(route, user.permissions) - ? route - : null, + name: this.RootFolderName, + route: UserDTOUtils.isDirectoryPathAvailable('/', user.permissions) + ? '/' + : 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; + // create rest navigation + dirs.forEach((name, index) => { + const route = dirs.slice(0, index + 1).join('/'); + if (dirs.length - 1 === index) { + arr.push({name, route: null}); + } 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 { const c = this.contentLoaderService.content.value; return c.directory - ? c.directory.mediaCount - : c.searchResult - ? c.searchResult.media.length - : 0; + ? c.directory.mediaCount + : c.searchResult + ? c.searchResult.media.length + : 0; } isDefaultSortingAndGrouping(): boolean { return this.sortingService.isDefaultSortingAndGrouping( - this.contentLoaderService.content.value + this.contentLoaderService.content.value ); } + isDirectionalSort(value: number) { return Utils.isValidEnumInt(SortByDirectionalTypes, value); } @@ -161,8 +167,8 @@ export class GalleryNavigatorComponent { this.sortingService.setSorting(s); // you cannot group by random if (!this.isDirectionalSort(sorting) || - // if grouping is disabled, do not update it - this.sortingService.grouping.value.method === GroupByTypes.NoGrouping || !this.groupingFollowSorting + // if grouping is disabled, do not update it + this.sortingService.grouping.value.method === GroupByTypes.NoGrouping || !this.groupingFollowSorting ) { return; } @@ -202,12 +208,12 @@ export class GalleryNavigatorComponent { queryParams += e[0] + '=' + e[1]; }); return Utils.concatUrls( - Config.Server.urlBase, - Config.Server.apiPath, - '/gallery/zip/', - c.directory.path, - c.directory.name, - '?' + queryParams + Config.Server.urlBase, + Config.Server.apiPath, + '/gallery/zip/', + c.directory.path, + c.directory.name, + '?' + queryParams ); } @@ -229,8 +235,8 @@ export class GalleryNavigatorComponent { return; } this.router.navigate(['/gallery', this.parentPath], - {queryParams: this.queryService.getParams()}) - .catch(console.error); + {queryParams: this.queryService.getParams()}) + .catch(console.error); } @HostListener('window:keydown', ['$event']) diff --git a/src/frontend/app/ui/gallery/navigator/navigator.service.ts b/src/frontend/app/ui/gallery/navigator/navigator.service.ts new file mode 100644 index 00000000..63eee196 --- /dev/null +++ b/src/frontend/app/ui/gallery/navigator/navigator.service.ts @@ -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; + + 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; + } + +} diff --git a/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.css b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.css new file mode 100644 index 00000000..fb43ce0a --- /dev/null +++ b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.css @@ -0,0 +1,3 @@ +.line-height-0 { + line-height: 0; +} diff --git a/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.html b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.html new file mode 100644 index 00000000..77346305 --- /dev/null +++ b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.html @@ -0,0 +1,27 @@ + + + + +
+
+ + + +
+
+ + + +
+
+ + + + + + + + + + +
diff --git a/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.ts b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.ts new file mode 100644 index 00000000..ae7394f3 --- /dev/null +++ b/src/frontend/app/ui/utils/grid-size-icon/grid-size-icon.component.ts @@ -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; +} diff --git a/src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.css b/src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.css similarity index 100% rename from src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.css rename to src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.css diff --git a/src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.html b/src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.html similarity index 100% rename from src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.html rename to src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.html diff --git a/src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.ts b/src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.ts similarity index 67% rename from src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.ts rename to src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.ts index bb21f883..60864ee4 100644 --- a/src/frontend/app/ui/sorting-method-icon/sorting-method-icon.component.ts +++ b/src/frontend/app/ui/utils/sorting-method-icon/sorting-method-icon.component.ts @@ -1,5 +1,5 @@ import {Component, Input} from '@angular/core'; -import {GroupSortByTypes} from '../../../../common/entities/SortingMethods'; +import {GroupSortByTypes} from '../../../../../common/entities/SortingMethods'; @Component({ selector: 'app-sorting-method-icon', @@ -8,5 +8,5 @@ import {GroupSortByTypes} from '../../../../common/entities/SortingMethods'; }) export class SortingMethodIconComponent { @Input() method: number; - GroupSortByTypes = GroupSortByTypes; + public readonly GroupSortByTypes = GroupSortByTypes; } diff --git a/src/frontend/styles.css b/src/frontend/styles.css index 27bc467e..4025a8d4 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -58,7 +58,7 @@ ng-icon { font-size: 1.15em } -ng-icon svg { +ng-icon:not([strokeWidth]) svg { vertical-align: unset; --ng-icon__stroke-width: 40; }