]> source.dussan.org Git - gitea.git/commitdiff
Add Image Diff options in Pull Request Diff view (#14450)
authorKN4CK3R <KN4CK3R@users.noreply.github.com>
Sat, 27 Feb 2021 17:25:00 +0000 (18:25 +0100)
committerGitHub <noreply@github.com>
Sat, 27 Feb 2021 17:25:00 +0000 (18:25 +0100)
Implemented GitHub style image diff

options/locale/locale_en-US.ini
templates/repo/diff/image_diff.tmpl
web_src/js/features/imagediff.js [new file with mode: 0644]
web_src/js/index.js
web_src/less/features/imagediff.less [new file with mode: 0644]
web_src/less/index.less

index 8245df754a70e1751c64359ab2ae94e562a49175..ee8a7673e9b6b9e5e79b72cf3eb32e4a16a44797 100644 (file)
@@ -1854,6 +1854,9 @@ diff.review.approve = Approve
 diff.review.reject = Request changes
 diff.committed_by = committed by
 diff.protected = Protected
+diff.image.side_by_side = Side by Side
+diff.image.swipe = Swipe
+diff.image.overlay = Overlay
 
 releases.desc = Track project versions and downloads.
 release.releases = Releases
index eda208d7446a3cf6162b690a643f96345fd5143e..01f7e3f8e8f4a5d3a25fe1ca2d59232203513fbc 100644 (file)
 {{ $imagePathOld := printf "%s/%s" .root.BeforeRawPath (EscapePound .file.OldName)  }}
 {{ $imagePathNew := printf "%s/%s" .root.RawPath (EscapePound .file.Name)  }}
-
-<tr>
-       <th class="halfwidth center pl-3 pr-2">
-               {{.root.i18n.Tr "repo.diff.file_before"}}
-       </th>
-       <th class="halfwidth center pl-2 pr-3">
-               {{.root.i18n.Tr "repo.diff.file_after"}}
-       </th>
-</tr>
-<tr>
-       <td class="halfwidth center pl-3 pr-2">
-           {{if or .file.IsDeleted (not .file.IsCreated)}}
-            <a href="{{$imagePathOld}}" target="_blank">
-                <img src="{{$imagePathOld}}" class="border red" />
-            </a>
-           {{end}}
-       </td>
-       <td class="halfwidth center pl-2 pr-3">
-           {{if or .file.IsCreated (not .file.IsDeleted)}}
-                       <a href="{{$imagePathNew}}" target="_blank">
-                               <img src="{{$imagePathNew}}" class="border green" />
-                       </a>
-           {{end}}
-       </td>
-</tr>
 {{ $imageInfoBase := (call .root.ImageInfoBase .file.OldName) }}
 {{ $imageInfoHead := (call .root.ImageInfo .file.Name) }}
-{{if or $imageInfoBase $imageInfoHead }}
+{{if or $imageInfoBase $imageInfoHead}}
 <tr>
-       <td class="halfwidth center pl-3 pr-2">
-       {{if $imageInfoBase }}
-               {{ $classWidth := "" }}
-               {{ $classHeight := "" }}
-               {{ $classByteSize := "" }}
-               {{if $imageInfoHead}}
-                       {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}
-                               {{ $classWidth = "red" }}
-                       {{end}}
-                       {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}
-                               {{ $classHeight = "red" }}
-                       {{end}}
-                       {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}
-                               {{ $classByteSize = "red" }}
-                       {{end}}
-               {{end}}
-               {{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoBase.Width}}</span>
-               &nbsp;|&nbsp;
-           {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span>
-               &nbsp;|&nbsp;
-           {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoBase.ByteSize}}</span>
-       {{end}}
-       </td>
-       <td class="halfwidth center pl-2 pr-3">
-       {{if $imageInfoHead }}
-               {{ $classWidth := "" }}
-               {{ $classHeight := "" }}
-               {{ $classByteSize := "" }}
-               {{if $imageInfoBase}}
-                       {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}
-                               {{ $classWidth = "green" }}
-                       {{end}}
-                       {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}
-                               {{ $classHeight = "green" }}
-                       {{end}}
-                       {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}
-                               {{ $classByteSize = "green" }}
-                       {{end}}
-               {{end}}
-               {{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoHead.Width}}</span>
-               &nbsp;|&nbsp;
-           {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoHead.Height}}</span>
-               &nbsp;|&nbsp;
-           {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoHead.ByteSize}}</span>
-       {{end}}
-       </td>
- </tr>
-{{end}}
+       <td colspan="2">
+               <div class="image-diff" data-path-before="{{$imagePathOld}}" data-path-after="{{$imagePathNew}}">
+                       <div class="ui secondary pointing tabular top attached borderless menu stackable new-menu">
+                               <div class="new-menu-inner">
+                                       <a class="item active" data-tab="diff-side-by-side">{{.root.i18n.Tr "repo.diff.image.side_by_side"}}</a>
+                                       {{if and $imageInfoBase $imageInfoHead}}
+                                       <a class="item" data-tab="diff-swipe">{{.root.i18n.Tr "repo.diff.image.swipe"}}</a>
+                                       <a class="item" data-tab="diff-overlay">{{.root.i18n.Tr "repo.diff.image.overlay"}}</a>
+                                       {{end}}
+                               </div>
+                       </div>
+                       <div class="hide">
+                               <div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side">
+                                       <div class="diff-side-by-side">
+                                               {{if $imageInfoBase }}
+                                               <span class="side">
+                                                       <p class="side-header">{{.root.i18n.Tr "repo.diff.file_before"}}</p>
+                                                       <span class="before-container"><img class="image-before" /></span>
+                                                       <p>
+                                                               {{ $classWidth := "" }}
+                                                               {{ $classHeight := "" }}
+                                                               {{ $classByteSize := "" }}
+                                                               {{if $imageInfoHead}}
+                                                                       {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}
+                                                                               {{ $classWidth = "red" }}
+                                                                       {{end}}
+                                                                       {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}
+                                                                               {{ $classHeight = "red" }}
+                                                                       {{end}}
+                                                                       {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}
+                                                                               {{ $classByteSize = "red" }}
+                                                                       {{end}}
+                                                               {{end}}
+                                                               {{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoBase.Width}}</span>
+                                                               &nbsp;|&nbsp;
+                                                               {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span>
+                                                               &nbsp;|&nbsp;
+                                                               {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoBase.ByteSize}}</span>
+                                                       </p>
+                                               </span>
+                                               {{end}}
+                                               {{if $imageInfoHead }}
+                                               <span class="side">
+                                                       <p class="side-header">{{.root.i18n.Tr "repo.diff.file_after"}}</p>
+                                                       <span class="after-container"><img class="image-after" /></span>
+                                                       <p>
+                                                               {{ $classWidth := "" }}
+                                                               {{ $classHeight := "" }}
+                                                               {{ $classByteSize := "" }}
+                                                               {{if $imageInfoBase}}
+                                                                       {{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}}
+                                                                               {{ $classWidth = "green" }}
+                                                                       {{end}}
+                                                                       {{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}}
+                                                                               {{ $classHeight = "green" }}
+                                                                       {{end}}
+                                                                       {{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}}
+                                                                               {{ $classByteSize = "green" }}
+                                                                       {{end}}
+                                                               {{end}}
+                                                               {{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoHead.Width}}</span>
+                                                               &nbsp;|&nbsp;
+                                                               {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoHead.Height}}</span>
+                                                               &nbsp;|&nbsp;
+                                                               {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoHead.ByteSize}}</span>
+                                                       </p>
+                                               </span>
+                                               {{end}}
+                                       </div>
+                               </div>
+                               {{if and $imageInfoBase $imageInfoHead}}
+                               <div class="ui bottom attached tab image-diff-container" data-tab="diff-swipe">
+                                       <div class="diff-swipe">
+                                               <div class="swipe-frame">
+                                                       <span class="before-container"><img class="image-before" /></span>
+                                                       <span class="swipe-container">
+                                                               <span class="after-container"><img class="image-after" /></span>
+                                                       </span>
+                                                       <span class="swipe-bar">
+                                                               <span class="handle top-handle"></span>
+                                                               <span class="handle bottom-handle"></span>
+                                                       </span>
+                                               </div>
+                                       </div>
+                               </div>
+                               <div class="ui bottom attached tab image-diff-container" data-tab="diff-overlay">
+                                       <div class="diff-overlay">
+                                               <div class="overlay-frame">
+                                                       <div class="ui centered">
+                                                               <input type="range" min="0" max="100" value="50" />
+                                                       </div>
+                                                       <span class="before-container"><img class="image-before"/></span>
+                                                       <span class="after-container"><img class="image-after" /></span>
+                                               </div>
+                                       </div>
+                               </div>
+                               {{end}}
+                       </div>
+                       <div class="ui active centered inline loader"></div>
+               </div>
+       </td>
+</tr>
+{{end}}
\ No newline at end of file
diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js
new file mode 100644 (file)
index 0000000..ce7ce8d
--- /dev/null
@@ -0,0 +1,206 @@
+export default async function initImageDiff() {
+  function createContext(image1, image2) {
+    const size1 = {
+      width: image1 && image1.width || 0,
+      height: image1 && image1.height || 0
+    };
+    const size2 = {
+      width: image2 && image2.width || 0,
+      height: image2 && image2.height || 0
+    };
+    const max = {
+      width: Math.max(size2.width, size1.width),
+      height: Math.max(size2.height, size1.height)
+    };
+
+    return {
+      image1: $(image1),
+      image2: $(image2),
+      size1,
+      size2,
+      max,
+      ratio: [
+        Math.floor(max.width - size1.width) / 2,
+        Math.floor(max.height - size1.height) / 2,
+        Math.floor(max.width - size2.width) / 2,
+        Math.floor(max.height - size2.height) / 2
+      ]
+    };
+  }
+
+  $('.image-diff').each(function() {
+    const $container = $(this);
+    const pathAfter = $container.data('path-after');
+    const pathBefore = $container.data('path-before');
+
+    const imageInfos = [{
+      loaded: false,
+      path: pathAfter,
+      $image: $container.find('img.image-after')
+    }, {
+      loaded: false,
+      path: pathBefore,
+      $image: $container.find('img.image-before')
+    }];
+
+    for (const info of imageInfos) {
+      if (info.$image.length > 0) {
+        info.$image.on('load', () => {
+          info.loaded = true;
+          setReadyIfLoaded();
+        });
+        info.$image.attr('src', info.path);
+      } else {
+        info.loaded = true;
+        setReadyIfLoaded();
+      }
+    }
+
+    const diffContainerWidth = $container.width() - 300;
+
+    function setReadyIfLoaded() {
+      if (imageInfos[0].loaded && imageInfos[1].loaded) {
+        initViews(imageInfos[0].$image, imageInfos[1].$image);
+      }
+    }
+
+    function initViews($imageAfter, $imageBefore) {
+      initSideBySide(createContext($imageAfter[0], $imageBefore[0]));
+      if ($imageAfter.length > 0 && $imageBefore.length > 0) {
+        initSwipe(createContext($imageAfter[1], $imageBefore[1]));
+        initOverlay(createContext($imageAfter[2], $imageBefore[2]));
+      }
+
+      $container.find('> .loader').hide();
+      $container.find('> .hide').removeClass('hide');
+    }
+
+    function initSideBySide(sizes) {
+      let factor = 1;
+      if (sizes.max.width > (diffContainerWidth - 24) / 2) {
+        factor = (diffContainerWidth - 24) / 2 / sizes.max.width;
+      }
+
+      sizes.image1.css({
+        width: sizes.size1.width * factor,
+        height: sizes.size1.height * factor
+      });
+      sizes.image1.parent().css({
+        margin: `${sizes.ratio[1] * factor + 15}px ${sizes.ratio[0] * factor}px ${sizes.ratio[1] * factor}px`,
+        width: sizes.size1.width * factor + 2,
+        height: sizes.size1.height * factor + 2
+      });
+      sizes.image2.css({
+        width: sizes.size2.width * factor,
+        height: sizes.size2.height * factor
+      });
+      sizes.image2.parent().css({
+        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
+        width: sizes.size2.width * factor + 2,
+        height: sizes.size2.height * factor + 2
+      });
+    }
+
+    function initSwipe(sizes) {
+      let factor = 1;
+      if (sizes.max.width > diffContainerWidth - 12) {
+        factor = (diffContainerWidth - 12) / sizes.max.width;
+      }
+
+      sizes.image1.css({
+        width: sizes.size1.width * factor,
+        height: sizes.size1.height * factor
+      });
+      sizes.image1.parent().css({
+        margin: `0px ${sizes.ratio[0] * factor}px`,
+        width: sizes.size1.width * factor + 2,
+        height: sizes.size1.height * factor + 2
+      });
+      sizes.image1.parent().parent().css({
+        padding: `${sizes.ratio[1] * factor}px 0 0 0`,
+        width: sizes.max.width * factor + 2
+      });
+      sizes.image2.css({
+        width: sizes.size2.width * factor,
+        height: sizes.size2.height * factor
+      });
+      sizes.image2.parent().css({
+        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
+        width: sizes.size2.width * factor + 2,
+        height: sizes.size2.height * factor + 2
+      });
+      sizes.image2.parent().parent().css({
+        width: sizes.max.width * factor + 2,
+        height: sizes.max.height * factor + 2
+      });
+      $container.find('.diff-swipe').css({
+        width: sizes.max.width * factor + 2,
+        height: sizes.max.height * factor + 4
+      });
+      $container.find('.swipe-bar').on('mousedown', function(e) {
+        e.preventDefault();
+
+        const $swipeBar = $(this);
+        const $swipeFrame = $swipeBar.parent();
+        const width = $swipeFrame.width() - $swipeBar.width() - 2;
+
+        $(document).on('mousemove.diff-swipe', (e2) => {
+          e2.preventDefault();
+
+          const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width));
+
+          $swipeBar.css({
+            left: value
+          });
+          $container.find('.swipe-container').css({
+            width: $swipeFrame.width() - value
+          });
+          $(document).on('mouseup.diff-swipe', () => {
+            $(document).off('.diff-swipe');
+          });
+        });
+      });
+    }
+
+    function initOverlay(sizes) {
+      let factor = 1;
+      if (sizes.max.width > diffContainerWidth - 12) {
+        factor = (diffContainerWidth - 12) / sizes.max.width;
+      }
+
+      sizes.image1.css({
+        width: sizes.size1.width * factor,
+        height: sizes.size1.height * factor
+      });
+      sizes.image2.css({
+        width: sizes.size2.width * factor,
+        height: sizes.size2.height * factor
+      });
+      sizes.image1.parent().css({
+        margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`,
+        width: sizes.size1.width * factor + 2,
+        height: sizes.size1.height * factor + 2
+      });
+      sizes.image2.parent().css({
+        margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`,
+        width: sizes.size2.width * factor + 2,
+        height: sizes.size2.height * factor + 2
+      });
+      sizes.image2.parent().parent().css({
+        width: sizes.max.width * factor + 2,
+        height: sizes.max.height * factor + 2
+      });
+      $container.find('.onion-skin').css({
+        width: sizes.max.width * factor + 2,
+        height: sizes.max.height * factor + 4
+      });
+
+      const $range = $container.find("input[type='range'");
+      const onInput = () => sizes.image1.parent().css({
+        opacity: $range.val() / 100
+      });
+      $range.on('input', onInput);
+      onInput();
+    }
+  });
+}
index b65291a2665268dd1c811cbafb0dab6885d48c7f..30af5dea15c22dc284532a994ff0d782d9fc8a05 100644 (file)
@@ -20,6 +20,7 @@ import attachTribute from './features/tribute.js';
 import createColorPicker from './features/colorpicker.js';
 import createDropzone from './features/dropzone.js';
 import initTableSort from './features/tablesort.js';
+import initImageDiff from './features/imagediff.js';
 import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
 import {initNotificationsTable, initNotificationCount} from './features/notification.js';
 import {initStopwatch} from './features/stopwatch.js';
@@ -2693,6 +2694,7 @@ $(document).ready(async () => {
     initStopwatch(),
     renderMarkdownContent(),
     initGithook(),
+    initImageDiff(),
   ]);
 });
 
diff --git a/web_src/less/features/imagediff.less b/web_src/less/features/imagediff.less
new file mode 100644 (file)
index 0000000..f38ea98
--- /dev/null
@@ -0,0 +1,105 @@
+.image-diff-container {
+  text-align: center;
+  padding: 30px 0;
+
+  img {
+    border: 1px solid var(--color-primary-light-7);
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC) right bottom var(--color-primary-light-7);
+  }
+
+  .before-container {
+    border: 1px solid var(--color-red);
+    display: block;
+  }
+
+  .after-container {
+    border: 1px solid var(--color-green);
+    display: block;
+  }
+
+  .diff-side-by-side {
+    .side {
+      display: inline-block;
+      line-height: 0;
+      vertical-align: top;
+
+      .side-header {
+        font-weight: bold;
+      }
+    }
+  }
+
+  .diff-swipe {
+    margin: auto;
+
+    .swipe-frame {
+      position: absolute;
+
+      .before-container {
+        position: absolute;
+      }
+
+      .swipe-container {
+        position: absolute;
+        right: 0;
+        display: block;
+        border-left: 2px solid var(--color-secondary-dark-8);
+        height: 100%;
+        overflow: hidden;
+
+        .after-container {
+          position: absolute;
+          right: 0;
+        }
+      }
+
+      .swipe-bar {
+        z-index: 100;
+        position: absolute;
+        height: 100%;
+        top: 0;
+        left: 0;
+
+        .handle {
+          background: var(--color-secondary-dark-8);
+          left: -5px;
+          height: 12px;
+          width: 12px;
+          position: absolute;
+          transform: rotate(45deg);
+          box-sizing: border-box;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          cursor: pointer;
+        }
+
+        .top-handle {
+          top: -12px;
+        }
+
+        .bottom-handle {
+          bottom: -14px;
+        }
+      }
+    }
+  }
+
+  .diff-overlay {
+    margin: 0 auto;
+
+    .overlay-frame {
+      margin: 0 auto;
+      position: relative;
+    }
+
+    .before-container,
+    .after-container {
+      position: absolute;
+    }
+
+    input {
+      width: 300px;
+    }
+  }
+}
index 5986930859c6058f5d2ed0b73a0d59d7c785f75a..cd70eedefdedb532b2a9afd0eae46780880c0911 100644 (file)
@@ -5,6 +5,7 @@
 @import "./features/gitgraph.less";
 @import "./features/animations.less";
 @import "./features/heatmap.less";
+@import "./features/imagediff.less";
 @import "./markdown/mermaid.less";
 
 @import "./chroma/base.less";