diff --git a/common/entities/SortingMethods.ts b/common/entities/SortingMethods.ts index 9082178..dab4c96 100644 --- a/common/entities/SortingMethods.ts +++ b/common/entities/SortingMethods.ts @@ -1,3 +1,3 @@ export enum SortingMethods { - ascName = 1, descName = 2, ascDate = 3, descDate = 4 + ascName = 1, descName = 2, ascDate = 3, descDate = 4, random = 5 } diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index afe0b6b..3997732 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -73,6 +73,7 @@ import {FileSizePipe} from './pipes/FileSizePipe'; import {DuplicateService} from './duplicates/duplicates.service'; import {DuplicateComponent} from './duplicates/duplicates.component'; import {DuplicatesPhotoComponent} from './duplicates/photo/photo.duplicates.component'; +import {SeededRandomService} from './model/seededRandom.service'; @Injectable() @@ -189,6 +190,7 @@ export function translationsFactory(locale: string) { FullScreenService, NavigationService, SettingsService, + SeededRandomService, OverlayService, QueryService, DuplicateService, diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index b2a6b3e..3d82e58 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -10,12 +10,13 @@ import {SearchResultDTO} from '../../../common/entities/SearchResultDTO'; import {ShareService} from './share.service'; import {NavigationService} from '../model/navigation.service'; import {UserRoles} from '../../../common/entities/UserDTO'; -import {interval, Subscription, Observable} from 'rxjs'; +import {interval, Observable, Subscription} from 'rxjs'; import {ContentWrapper} from '../../../common/entities/ConentWrapper'; import {PageHelper} from '../model/page.helper'; import {SortingMethods} from '../../../common/entities/SortingMethods'; import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {QueryParams} from '../../../common/QueryParams'; +import {SeededRandomService} from '../model/seededRandom.service'; @Component({ selector: 'app-gallery', @@ -32,6 +33,9 @@ export class GalleryComponent implements OnInit, OnDestroy { public directories: DirectoryDTO[] = []; public isPhotoWithLocation = false; + public countDown: { day: number, hour: number, minute: number, second: number } = null; + public mapEnabled = true; + readonly SearchTypes: typeof SearchTypes; private $counter: Observable; private subscription: { [key: string]: Subscription } = { content: null, @@ -39,16 +43,14 @@ export class GalleryComponent implements OnInit, OnDestroy { timer: null, sorting: null }; - public countDown: { day: number, hour: number, minute: number, second: number } = null; - public mapEnabled = true; - readonly SearchTypes: typeof SearchTypes; constructor(public _galleryService: GalleryService, private _authService: AuthenticationService, private _router: Router, private shareService: ShareService, private _route: ActivatedRoute, - private _navigation: NavigationService) { + private _navigation: NavigationService, + private rndService: SeededRandomService) { this.mapEnabled = Config.Client.Map.enabled; this.SearchTypes = SearchTypes; @@ -70,6 +72,46 @@ export class GalleryComponent implements OnInit, OnDestroy { this.countDown.second = t % 60; } + ngOnDestroy() { + if (this.subscription.content !== null) { + this.subscription.content.unsubscribe(); + } + if (this.subscription.route !== null) { + this.subscription.route.unsubscribe(); + } + if (this.subscription.timer !== null) { + this.subscription.timer.unsubscribe(); + } + if (this.subscription.sorting !== null) { + this.subscription.sorting.unsubscribe(); + } + } + + async ngOnInit() { + await this.shareService.wait(); + if (!this._authService.isAuthenticated() && + (!this.shareService.isSharing() || + (this.shareService.isSharing() && Config.Client.Sharing.passwordProtected === true))) { + + return this._navigation.toLogin(); + } + this.showSearchBar = Config.Client.Search.enabled && this._authService.isAuthorized(UserRoles.Guest); + this.showShare = Config.Client.Sharing.enabled && this._authService.isAuthorized(UserRoles.User); + this.showRandomPhotoBuilder = Config.Client.RandomPhoto.enabled && this._authService.isAuthorized(UserRoles.Guest); + this.subscription.content = this._galleryService.content.subscribe(this.onContentChange); + this.subscription.route = this._route.params.subscribe(this.onRoute); + + if (this.shareService.isSharing()) { + this.$counter = interval(1000); + this.subscription.timer = this.$counter.subscribe((x) => this.updateTimer(x)); + } + + this.subscription.sorting = this._galleryService.sorting.subscribe(() => { + this.sortDirectories(); + }); + + } + private onRoute = async (params: Params) => { const searchText = params[QueryParams.gallery.searchText]; if (searchText && searchText !== '') { @@ -100,21 +142,6 @@ export class GalleryComponent implements OnInit, OnDestroy { }; - ngOnDestroy() { - if (this.subscription.content !== null) { - this.subscription.content.unsubscribe(); - } - if (this.subscription.route !== null) { - this.subscription.route.unsubscribe(); - } - if (this.subscription.timer !== null) { - this.subscription.timer.unsubscribe(); - } - if (this.subscription.sorting !== null) { - this.subscription.sorting.unsubscribe(); - } - } - private onContentChange = (content: ContentWrapper) => { const ascdirSorter = (a: DirectoryDTO, b: DirectoryDTO) => { return a.name.localeCompare(b.name); @@ -169,34 +196,24 @@ export class GalleryComponent implements OnInit, OnDestroy { return 0; }); break; + case SortingMethods.random: + this.rndService.setSeed(this.directories.length); + this.directories.sort((a: DirectoryDTO, b: DirectoryDTO) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return 1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return -1; + } + return 0; + }).sort(() => { + return this.rndService.get() - 0.5; + }); + break; + } } - async ngOnInit() { - await this.shareService.wait(); - if (!this._authService.isAuthenticated() && - (!this.shareService.isSharing() || - (this.shareService.isSharing() && Config.Client.Sharing.passwordProtected === true))) { - - return this._navigation.toLogin(); - } - this.showSearchBar = Config.Client.Search.enabled && this._authService.isAuthorized(UserRoles.Guest); - this.showShare = Config.Client.Sharing.enabled && this._authService.isAuthorized(UserRoles.User); - this.showRandomPhotoBuilder = Config.Client.RandomPhoto.enabled && this._authService.isAuthorized(UserRoles.Guest); - this.subscription.content = this._galleryService.content.subscribe(this.onContentChange); - this.subscription.route = this._route.params.subscribe(this.onRoute); - - if (this.shareService.isSharing()) { - this.$counter = interval(1000); - this.subscription.timer = this.$counter.subscribe((x) => this.updateTimer(x)); - } - - this.subscription.sorting = this._galleryService.sorting.subscribe(() => { - this.sortDirectories(); - }); - - } - } diff --git a/frontend/app/gallery/grid/grid.gallery.component.ts b/frontend/app/gallery/grid/grid.gallery.component.ts index 7c91882..e60f418 100644 --- a/frontend/app/gallery/grid/grid.gallery.component.ts +++ b/frontend/app/gallery/grid/grid.gallery.component.ts @@ -7,10 +7,10 @@ import { Input, OnChanges, OnDestroy, + OnInit, QueryList, ViewChild, - ViewChildren, - OnInit + ViewChildren } from '@angular/core'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {GridRowBuilder} from './GridRowBuilder'; @@ -27,7 +27,7 @@ import {GalleryService} from '../gallery.service'; import {SortingMethods} from '../../../../common/entities/SortingMethods'; import {MediaDTO} from '../../../../common/entities/MediaDTO'; import {QueryParams} from '../../../../common/QueryParams'; -import {Media} from '../Media'; +import {SeededRandomService} from '../../model/seededRandom.service'; @Component({ selector: 'app-gallery-grid', @@ -38,24 +38,13 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O @ViewChild('gridContainer') gridContainer: ElementRef; @ViewChildren(GalleryPhotoComponent) gridPhotoQL: QueryList; - private scrollListenerPhotos: GalleryPhotoComponent[] = []; - @Input() media: MediaDTO[]; @Input() lightbox: GalleryLightboxComponent; - photosToRender: Array = []; containerWidth = 0; screenHeight = 0; - public IMAGE_MARGIN = 2; - private TARGET_COL_COUNT = 5; - private MIN_ROW_COUNT = 2; - private MAX_ROW_COUNT = 5; - - private onScrollFired = false; - private helperTime: number = null; isAfterViewInit = false; - private renderedPhotoIndex = 0; subscriptions: { route: Subscription, sorting: Subscription @@ -64,13 +53,21 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O sorting: null }; delayedRenderUpToPhoto: string = null; + private scrollListenerPhotos: GalleryPhotoComponent[] = []; + private TARGET_COL_COUNT = 5; + private MIN_ROW_COUNT = 2; + private MAX_ROW_COUNT = 5; + private onScrollFired = false; + private helperTime: number = null; + private renderedPhotoIndex = 0; constructor(private overlayService: OverlayService, private changeDetector: ChangeDetectorRef, public queryService: QueryService, private router: Router, public galleryService: GalleryService, - private route: ActivatedRoute) { + private route: ActivatedRoute, + private rndService: SeededRandomService) { } ngOnInit() { @@ -159,17 +156,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O this.isAfterViewInit = true; } - - private renderUpToMedia(mediaStringId: string) { - const index = this.media.findIndex(p => this.queryService.getMediaStringId(p) === mediaStringId); - if (index === -1) { - this.router.navigate([], {queryParams: this.queryService.getParams()}); - return; - } - while (this.renderedPhotoIndex < index && this.renderARow()) { - } - } - public renderARow(): number { if (this.renderedPhotoIndex >= this.media.length || this.containerWidth === 0) { @@ -204,6 +190,37 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O return rowHeight; } + @HostListener('window:scroll') + onScroll() { + if (!this.onScrollFired && + // should we trigger this at all? + (this.renderedPhotoIndex < this.media.length || this.scrollListenerPhotos.length > 0)) { + window.requestAnimationFrame(() => { + this.renderPhotos(); + + if (Config.Client.Other.enableOnScrollThumbnailPrioritising === true) { + this.scrollListenerPhotos.forEach((pc: GalleryPhotoComponent) => { + pc.onScroll(); + }); + this.scrollListenerPhotos = this.scrollListenerPhotos.filter(pc => pc.ScrollListener); + } + + this.onScrollFired = false; + }); + this.onScrollFired = true; + } + } + + private renderUpToMedia(mediaStringId: string) { + const index = this.media.findIndex(p => this.queryService.getMediaStringId(p) === mediaStringId); + if (index === -1) { + this.router.navigate([], {queryParams: this.queryService.getParams()}); + return; + } + while (this.renderedPhotoIndex < index && this.renderARow()) { + } + } + private clearRenderedPhotos() { this.photosToRender = []; this.renderedPhotoIndex = 0; @@ -244,6 +261,20 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O return b.metadata.creationDate - a.metadata.creationDate; }); break; + case SortingMethods.random: + this.rndService.setSeed(this.media.length); + this.media.sort((a: PhotoDTO, b: PhotoDTO) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return -1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + return 0; + }).sort(() => { + return this.rndService.get() - 0.5; + }); + break; } @@ -281,7 +312,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O } - /** * Returns true, if scroll is >= 70% to render more images. * Or of onscroll rendering is off: return always to render all the images at once @@ -294,28 +324,6 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O || (document.body.clientHeight + offset) * 0.85 < window.innerHeight; } - - @HostListener('window:scroll') - onScroll() { - if (!this.onScrollFired && - // should we trigger this at all? - (this.renderedPhotoIndex < this.media.length || this.scrollListenerPhotos.length > 0)) { - window.requestAnimationFrame(() => { - this.renderPhotos(); - - if (Config.Client.Other.enableOnScrollThumbnailPrioritising === true) { - this.scrollListenerPhotos.forEach((pc: GalleryPhotoComponent) => { - pc.onScroll(); - }); - this.scrollListenerPhotos = this.scrollListenerPhotos.filter(pc => pc.ScrollListener); - } - - this.onScrollFired = false; - }); - this.onScrollFired = true; - } - } - private renderPhotos(numberOfPhotos: number = 0) { if (this.containerWidth === 0 || this.renderedPhotoIndex >= this.media.length || diff --git a/frontend/app/gallery/navigator/navigator.gallery.component.css b/frontend/app/gallery/navigator/navigator.gallery.component.css index d8bee09..c2bb567 100644 --- a/frontend/app/gallery/navigator/navigator.gallery.component.css +++ b/frontend/app/gallery/navigator/navigator.gallery.component.css @@ -24,7 +24,7 @@ ol { } .dropdown-menu { - min-width: 16rem; + min-width: 10rem; } .row { diff --git a/frontend/app/model/seededRandom.service.ts b/frontend/app/model/seededRandom.service.ts new file mode 100644 index 0000000..689e538 --- /dev/null +++ b/frontend/app/model/seededRandom.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from '@angular/core'; + +@Injectable() +export class SeededRandomService { + + private static readonly baseSeed = Math.random() * 2147483647; + private seed: number; + + constructor() { + this.setSeed(0); + + if (this.seed <= 0) { + this.seed += 2147483646; + } + } + + setSeed(seed: number) { + this.seed = (SeededRandomService.baseSeed + seed) % 2147483647; // shifting with 16 to the left + } + + get() { + this.seed = (this.seed * 16807 % 2147483647); + return this.seed / 2147483647; + } + +} diff --git a/frontend/app/pipes/IconizeSortingMethod.ts b/frontend/app/pipes/IconizeSortingMethod.ts index 409bfa3..9f2a249 100644 --- a/frontend/app/pipes/IconizeSortingMethod.ts +++ b/frontend/app/pipes/IconizeSortingMethod.ts @@ -14,6 +14,8 @@ export class IconizeSortingMethod implements PipeTransform { return ''; case SortingMethods.descDate: return ''; + case SortingMethods.random: + return ''; } } } diff --git a/frontend/app/pipes/StringifySortingMethod.ts b/frontend/app/pipes/StringifySortingMethod.ts index c095296..f27e3fa 100644 --- a/frontend/app/pipes/StringifySortingMethod.ts +++ b/frontend/app/pipes/StringifySortingMethod.ts @@ -18,6 +18,8 @@ export class StringifySortingMethod implements PipeTransform { return this.i18n('ascending date'); case SortingMethods.descDate: return this.i18n('descending date'); + case SortingMethods.random: + return this.i18n('random'); } } }