diff options
-rw-r--r-- | options/locale/locale_en-US.ini | 3 | ||||
-rw-r--r-- | templates/repo/diff/image_diff.tmpl | 178 | ||||
-rw-r--r-- | web_src/js/features/imagediff.js | 206 | ||||
-rw-r--r-- | web_src/js/index.js | 2 | ||||
-rw-r--r-- | web_src/less/features/imagediff.less | 105 | ||||
-rw-r--r-- | web_src/less/index.less | 1 |
6 files changed, 421 insertions, 74 deletions
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8245df754a..ee8a7673e9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl index eda208d744..01f7e3f8e8 100644 --- a/templates/repo/diff/image_diff.tmpl +++ b/templates/repo/diff/image_diff.tmpl @@ -1,79 +1,109 @@ {{ $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 diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js new file mode 100644 index 0000000000..ce7ce8d2af --- /dev/null +++ b/web_src/js/features/imagediff.js @@ -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(); + } + }); +} diff --git a/web_src/js/index.js b/web_src/js/index.js index b65291a266..30af5dea15 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -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 index 0000000000..f38ea98d7d --- /dev/null +++ b/web_src/less/features/imagediff.less @@ -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; + } + } +} diff --git a/web_src/less/index.less b/web_src/less/index.less index 5986930859..cd70eedefd 100644 --- a/web_src/less/index.less +++ b/web_src/less/index.less @@ -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"; |