You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-11-25 22:32:52 +02:00
Add metadata filters and always show options to media buttons #743
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pigallery2-extension-kit",
|
"name": "pigallery2-extension-kit",
|
||||||
"version": "2.5.0-edge8",
|
"version": "2.6.0-edge",
|
||||||
"description": "Interfaces for developing extensions for pigallery2",
|
"description": "Interfaces for developing extensions for pigallery2",
|
||||||
"author": "Patrik J. Braun",
|
"author": "Patrik J. Braun",
|
||||||
"homepage": "https://github.com/bpatrik/pigallery2",
|
"homepage": "https://github.com/bpatrik/pigallery2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pigallery2",
|
"name": "pigallery2",
|
||||||
"version": "2.5.0-edge",
|
"version": "2.6.0-edge",
|
||||||
"description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)",
|
"description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)",
|
||||||
"author": "Patrik J. Braun",
|
"author": "Patrik J. Braun",
|
||||||
"homepage": "https://github.com/bpatrik/pigallery2",
|
"homepage": "https://github.com/bpatrik/pigallery2",
|
||||||
|
|||||||
@@ -237,10 +237,10 @@ export interface IUIExtension<C> {
|
|||||||
* Adds a new button on to UI to all media (photo, video).
|
* Adds a new button on to UI to all media (photo, video).
|
||||||
* Implement the server-side click action in the serverSB function.
|
* Implement the server-side click action in the serverSB function.
|
||||||
* @param buttonConfig
|
* @param buttonConfig
|
||||||
* @param serverSB
|
* @param serverSB If not set the button will be a fake button (i.e.: only show up not clickable)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
addMediaButton(buttonConfig: IClientMediaButtonConfig, serverSB: (params: ParamsDictionary, body: any, user: UserDTO, media: MediaEntity, repository: Repository<MediaEntity>) => Promise<void>): void;
|
addMediaButton(buttonConfig: IClientMediaButtonConfig, serverSB?: (params: ParamsDictionary, body: any, user: UserDTO, media: MediaEntity, repository: Repository<MediaEntity>) => Promise<void>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExtensionConfigInit<C> {
|
export interface IExtensionConfigInit<C> {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface IClientSVGIconConfig {
|
|||||||
|
|
||||||
export interface IClientMediaButtonPopupFields {
|
export interface IClientMediaButtonPopupFields {
|
||||||
/**
|
/**
|
||||||
* Id of the field. This how it will listed in the body.
|
* Id of the field. This how it will be listed in the body.
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
/**
|
||||||
@@ -70,20 +70,35 @@ export interface IClientMediaButtonConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
* Icon of the button. This will be shown in the gallery and also in the lightbox.
|
* Icon of the button. This will be shown in the gallery and also in the lightbox.
|
||||||
|
* You can look for icons here: https://fontawesome.com
|
||||||
*/
|
*/
|
||||||
svgIcon: IClientSVGIconConfig;
|
svgIcon: IClientSVGIconConfig;
|
||||||
/**
|
/**
|
||||||
* If true, the button will be hidden if on photos
|
* If true, the button will be hidden on photos
|
||||||
*/
|
*/
|
||||||
skipPhotos?: boolean;
|
skipPhotos?: boolean;
|
||||||
/**
|
/**
|
||||||
* If true, the button will be hidden if on videos
|
* If true, the button will be hidden on videos
|
||||||
*/
|
*/
|
||||||
skipVideos?: boolean;
|
skipVideos?: boolean;
|
||||||
/**
|
/**
|
||||||
* Path to the server side function that will be called when the button is clicked.
|
* If set, the button will only be shown if the metadata matches this filter.
|
||||||
|
* example values:
|
||||||
|
* [
|
||||||
|
* {field: 'rating', comparator: '==', value: 4}, // matches if rating is 4
|
||||||
|
* {field: 'size.width', comparator: '>=', value: 400} // matches if width is >= 400px
|
||||||
|
* ]
|
||||||
*/
|
*/
|
||||||
apiPath: string;
|
metadataFilter?: { field: string, comparator: '>=' | '<=' | '==', value: string | number }[];
|
||||||
|
/**
|
||||||
|
* If true, the button will always be visible, not only on hover.
|
||||||
|
*/
|
||||||
|
alwaysVisible?: boolean;
|
||||||
|
/**
|
||||||
|
* Path to the server side function that will be called when the button is clicked.
|
||||||
|
* If it is not set, it will be a fake button. (i.e.: only shows up but does not do anything)
|
||||||
|
*/
|
||||||
|
apiPath?: string;
|
||||||
/**
|
/**
|
||||||
* Popup config. IF you want conformation before calling the apiPath or want to change media metedata, set this.
|
* Popup config. IF you want conformation before calling the apiPath or want to change media metedata, set this.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ export class MediaButtonModalService {
|
|||||||
|
|
||||||
async executeButtonAction(button: IClientMediaButtonConfigWithBaseApiPath, media: GridMedia, formData?: any): Promise<void> {
|
async executeButtonAction(button: IClientMediaButtonConfigWithBaseApiPath, media: GridMedia, formData?: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
if (!button.apiPath) {
|
||||||
|
return; // this is a fake button, nothing to call
|
||||||
|
}
|
||||||
// Construct the full API path using base path and button's API path
|
// Construct the full API path using base path and button's API path
|
||||||
const apiPath = Utils.concatUrls(button.extensionBasePath, button.apiPath);
|
const apiPath = Utils.concatUrls(button.extensionBasePath, button.apiPath);
|
||||||
|
|
||||||
|
|||||||
@@ -194,11 +194,15 @@ a {
|
|||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-container:hover .media-buttons {
|
.photo-container:hover .media-button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-buttons {
|
.media-button {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .3s ease-out;
|
transition: opacity .3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-button.always-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<div class="media-buttons" *ngIf="mediaButtons.length > 0">
|
<div class="media-buttons" *ngIf="mediaButtons.length > 0">
|
||||||
<button *ngFor="let button of mediaButtons"
|
<button *ngFor="let button of mediaButtons"
|
||||||
class="media-button"
|
class="media-button"
|
||||||
|
[class.always-visible]="button.alwaysVisible"
|
||||||
[title]="button.name"
|
[title]="button.name"
|
||||||
(click)="onMediaButtonClick(button, $event)">
|
(click)="onMediaButtonClick(button, $event)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -109,8 +109,51 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
|||||||
if (this.gridMedia.isPhoto() && button.skipPhotos) {
|
if (this.gridMedia.isPhoto() && button.skipPhotos) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check metadataFilter
|
||||||
|
if (button.metadataFilter && button.metadataFilter.length > 0) {
|
||||||
|
return this.matchesMetadataFilter(button.metadataFilter);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// move always visible buttons to the front
|
||||||
|
this.mediaButtons = [...this.mediaButtons.filter(b => b.alwaysVisible), ...this.mediaButtons.filter(b => !b.alwaysVisible)];
|
||||||
|
}
|
||||||
|
|
||||||
|
matchesMetadataFilter(filters: { field: string, comparator: '>=' | '<=' | '==', value: string | number }[]): boolean {
|
||||||
|
const metadata = this.gridMedia.media.metadata;
|
||||||
|
|
||||||
|
// All filters must match (AND logic)
|
||||||
|
return filters.every(filter => {
|
||||||
|
// Get the value from metadata using the field path (e.g., 'rating' or 'size.width')
|
||||||
|
const fieldParts = filter.field.split('.');
|
||||||
|
let fieldValue: any = metadata;
|
||||||
|
|
||||||
|
for (const part of fieldParts) {
|
||||||
|
if (fieldValue === undefined || fieldValue === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fieldValue = fieldValue[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldValue === undefined || fieldValue === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare based on comparator
|
||||||
|
switch (filter.comparator) {
|
||||||
|
case '>=':
|
||||||
|
return fieldValue >= filter.value;
|
||||||
|
case '<=':
|
||||||
|
return fieldValue <= filter.value;
|
||||||
|
case '==':
|
||||||
|
return fieldValue == filter.value; // Use == for loose equality
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -223,6 +266,10 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
if(!button.apiPath){
|
||||||
|
return; // this is a fake button, nothing to call
|
||||||
|
}
|
||||||
|
|
||||||
if (button.popup) {
|
if (button.popup) {
|
||||||
this.modalService.showModal(button, this.gridMedia);
|
this.modalService.showModal(button, this.gridMedia);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user