Implemented GitHub style image difftags/v1.15.0-dev
@@ -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 |
@@ -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}} |
@@ -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(); | |||
} | |||
}); | |||
} |
@@ -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(), | |||
]); | |||
}); | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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"; |