in kahuna/public/js/image/controller.js [87:347]
function ($rootScope,
$scope,
$element,
$state,
$stateParams,
$window,
$filter,
inject$,
storage,
image,
mediaApi,
optimisedImageUri,
lowResImageUri,
cropKey,
mediaCropper,
imageService,
imageUsagesService,
editsService,
scrollPosition,
keyboardShortcut,
cropSettings,
globalErrors) {
let ctrl = this;
keyboardShortcut.bindTo($scope)
.add({
combo: 'c',
description: 'Crop image',
callback: () => $state.go('crop', {imageId: ctrl.image.data.id})
})
.add({
combo: 'f',
description: 'Enter fullscreen',
callback: () => {
const imageEl = $element[0].querySelector('.easel__image');
// Fullscreen API has vendor prefixing https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API/Guide#Prefixing
const fullscreenElement = (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement
);
const exitFullscreen = (
document.exitFullscreen ||
document.webkitExitFullscreen ||
document.mozCancelFullScreen
);
const requestFullscreen = (
imageEl.requestFullscreen ||
imageEl.webkitRequestFullscreen ||
imageEl.mozRequestFullScreen
);
// `.call` to ensure `this` is bound correctly.
return fullscreenElement
? exitFullscreen.call(document)
: requestFullscreen.call(imageEl);
}
});
ctrl.tabs = [
{key: 'metadata', value: 'Metadata'},
{key: 'usages', value: `Usages`, disabled: true}
];
ctrl.selectedTab = 'metadata';
ctrl.image = image;
if (ctrl.image && ctrl.image.data.softDeletedMetadata !== undefined) { ctrl.isDeleted = true; }
ctrl.optimisedImageUri = optimisedImageUri;
ctrl.lowResImageUri = lowResImageUri;
ctrl.singleImageList = ctrl.image ? new List([ctrl.image]) : new List([]);
editsService.canUserEdit(ctrl.image).then(editable => {
ctrl.canUserEdit = editable;
});
const usages = imageUsagesService.getUsages(ctrl.image);
const usagesCount$ = usages.count$;
const recentUsages$ = usages.recentUsages$;
inject$($scope, usagesCount$, ctrl, 'usagesCount');
inject$($scope, recentUsages$, ctrl, 'recentUsages');
const freeUsageCountWatch = $scope.$watch('ctrl.usagesCount', value => {
const usageTab = ctrl.tabs.find(_ => _.key === 'usages');
usageTab.value = `Usages (${value > 0 ? value : 'None'})`;
usageTab.disabled = value === 0;
// stop watching
freeUsageCountWatch();
});
// TODO: we should be able to rely on ctrl.crop.id instead once
// all existing crops are migrated to have an id (they didn't
// initially)
ctrl.cropKey = cropKey;
ctrl.cropSelected = cropSelected;
ctrl.image.allCrops = [];
cropSettings.set($stateParams);
ctrl.cropType = cropSettings.getCropType();
ctrl.cropRatio = ctrl.cropType ?
cropSettings.getCropOptions().find(_ => _.key === ctrl.cropType).ratioString ?? '' :
'';
imageService(ctrl.image).states.canDelete.then(deletable => {
ctrl.canBeDeleted = deletable;
});
ctrl.allowCropSelection = (crop) => {
if (ctrl.cropType) {
const cropSpec = cropSettings.getCropOptions().find(_ => _.key === ctrl.cropType);
return crop.specification.aspectRatio === cropSpec.ratioString;
}
return true;
};
ctrl.shareImage = () => {
const sharedUrl = $window._clientConfig.rootUri + "/images/" + ctrl.image.data.id;
navigator.clipboard.writeText(sharedUrl);
globalErrors.trigger('clipboard', sharedUrl);
};
ctrl.onCropsDeleted = () => {
// a bit nasty - but it updates the state of the page better than trying to do that in
// the client.
$state.go('image', {imageId: ctrl.image.data.id, crop: undefined}, {reload: true});
};
ctrl.onLogoClick = () => {
mediaApi.getSession().then(session => {
const showPaid = session.user.permissions.showPaid ? session.user.permissions.showPaid : undefined;
const defaultNonFreeFilter = {
isDefault: true,
isNonFree: showPaid ? showPaid : false
};
storage.setJs("defaultNonFreeFilter", defaultNonFreeFilter, true);
window.dispatchEvent(new CustomEvent("logoClick", {
detail: {showPaid: defaultNonFreeFilter.isNonFree},
bubbles: true
}));
scrollPosition.resetToTop();
});
};
// TODO: move this to a more sensible place.
function getCropDimensions() {
return {
width: ctrl.crop.specification.bounds.width,
height: ctrl.crop.specification.bounds.height
};
}
// TODO: move this to a more sensible place.
function getImageDimensions() {
return ctrl.image.data.source.dimensions;
}
function getImageIdFromCropResource(cropsResource) {
const imageHref = cropsResource.links.find(link => link.rel == 'image')?.href;
const hrefTokens = imageHref.split('/');
const count = hrefTokens.length;
const imageId = hrefTokens[count - 1];
return imageId;
}
mediaCropper.getCropsFor(image).then(cropsResource => {
const s3Crops = cropsResource.data;
const esCrops = image.data.exports;
const crops = s3Crops.filter( (s3Crop) => {
return esCrops.find( (esCrop) => {
return s3Crop.id === esCrop.id && esCrop.assets.every( (esCropAsset)=> {
return s3Crop.assets.find( (s3CropAsset) => {
return s3CropAsset.dimensions !== undefined &&
esCropAsset.dimensions !== undefined &&
s3CropAsset.dimensions.width === esCropAsset.dimensions.width;
}) !== undefined;
});
}) !== undefined;
});
if ($window._clientConfig.canDownloadCrop) {
crops.forEach((crop) => {
crop.assets.forEach((asset) =>
asset.downloadLink = cropsResource.links.find(link => link.rel.includes(`crop-download-${crop.id}-${asset.dimensions.width}`))?.href
);
//set the download link of the crop to be the largest asset
//by ordering the assets in increasing dimension width and picking the last one
//this way largestAsset can never be undefined
//this will ensure that asset inconsistency in S3 will still result to a fallback download
if (crop.assets.length > 0) {
const largestAsset = crop.assets.sort((a, b) => (a.dimensions.width > b.dimensions.width) ? 1 : -1)[ crop.assets.length - 1];
const largestWidth = largestAsset.dimensions.width;
if (largestWidth != crop.master.dimensions.width) {
const imageId = getImageIdFromCropResource(cropsResource);
console.log('The largest cropped asset of ' + crop.id + ' available for image ' + imageId +
' does not have the same dimensions as the master. Using the next largest cropped asset with width ' + largestWidth +
'Please correct this inconsistency.');
}
crop.downloadLink = largestAsset.downloadLink;
}
});
}
ctrl.crop = crops.find(crop => crop.id === cropKey);
ctrl.fullCrop = crops.find(crop => crop.specification.type === 'full');
ctrl.crops = crops.filter(crop => crop.specification.type === 'crop');
ctrl.image.allCrops = ctrl.fullCrop ? [ctrl.fullCrop].concat(ctrl.crops) : ctrl.crops;
//boolean version for use in template
ctrl.hasFullCrop = angular.isDefined(ctrl.fullCrop);
ctrl.hasCrops = ctrl.crops.length > 0;
}).finally(() => {
ctrl.dimensions = angular.isDefined(ctrl.crop) ?
getCropDimensions() : getImageDimensions();
if (angular.isDefined(ctrl.crop)) {
ctrl.originalDimensions = getImageDimensions();
}
});
function cropSelected(crop) {
$rootScope.$emit('events:crop-selected', {
image: ctrl.image,
crop: crop
});
}
const freeImagesUpdateListener = $rootScope.$on('images-updated', (e, updatedImages) => {
const maybeUpdatedImage = updatedImages.find(updatedImage => ctrl.image.data.id === updatedImage.data.id);
if (maybeUpdatedImage) {
ctrl.image = maybeUpdatedImage;
}
});
const freeImageDeleteListener = $rootScope.$on('images-deleted', () => {
$state.go('search');
});
const freeImageDeleteFailListener = $rootScope.$on('image-delete-failure', (err, image) => {
if (err && err.body && err.body.errorMessage) {
$window.alert(err.body.errorMessage);
} else {
// Possibly not receiving a proper image object sometimes?
const imageId = image && image.data && image.data.id || 'Unknown ID';
$window.alert(`Failed to delete image ${imageId}`);
}
});
$scope.$on('$destroy', function () {
freeImagesUpdateListener();
freeImageDeleteListener();
freeImageDeleteFailListener();
});
}]);