function()

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();
    });
  }]);