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
{{ $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>
- |
- {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span>
- |
- {{.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>
- |
- {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoHead.Height}}</span>
- |
- {{.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>
+ |
+ {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span>
+ |
+ {{.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>
+ |
+ {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoHead.Height}}</span>
+ |
+ {{.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
--- /dev/null
+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();
+ }
+ });
+}
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';
initStopwatch(),
renderMarkdownContent(),
initGithook(),
+ initImageDiff(),
]);
});
--- /dev/null
+.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;
+ }
+ }
+}
@import "./features/gitgraph.less";
@import "./features/animations.less";
@import "./features/heatmap.less";
+@import "./features/imagediff.less";
@import "./markdown/mermaid.less";
@import "./chroma/base.less";