mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Add Image Diff options in Pull Request Diff view (#14450)
Implemented GitHub style image diff
This commit is contained in:
		| @@ -1854,6 +1854,9 @@ diff.review.approve = Approve | |||||||
| diff.review.reject = Request changes | diff.review.reject = Request changes | ||||||
| diff.committed_by = committed by | diff.committed_by = committed by | ||||||
| diff.protected = Protected | 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. | releases.desc = Track project versions and downloads. | ||||||
| release.releases = Releases | release.releases = Releases | ||||||
|   | |||||||
| @@ -1,79 +1,109 @@ | |||||||
| {{ $imagePathOld := printf "%s/%s" .root.BeforeRawPath (EscapePound .file.OldName)  }} | {{ $imagePathOld := printf "%s/%s" .root.BeforeRawPath (EscapePound .file.OldName)  }} | ||||||
| {{ $imagePathNew := printf "%s/%s" .root.RawPath (EscapePound .file.Name)  }} | {{ $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) }} | {{ $imageInfoBase := (call .root.ImageInfoBase .file.OldName) }} | ||||||
| {{ $imageInfoHead := (call .root.ImageInfo .file.Name) }} | {{ $imageInfoHead := (call .root.ImageInfo .file.Name) }} | ||||||
| {{if or $imageInfoBase $imageInfoHead }} | {{if or $imageInfoBase $imageInfoHead}} | ||||||
| <tr> | <tr> | ||||||
|  	<td class="halfwidth center pl-3 pr-2"> | 	<td colspan="2"> | ||||||
|  	{{if $imageInfoBase }} | 		<div class="image-diff" data-path-before="{{$imagePathOld}}" data-path-after="{{$imagePathNew}}"> | ||||||
|  		{{ $classWidth := "" }} | 			<div class="ui secondary pointing tabular top attached borderless menu stackable new-menu"> | ||||||
|  		{{ $classHeight := "" }} | 				<div class="new-menu-inner"> | ||||||
|  		{{ $classByteSize := "" }} | 					<a class="item active" data-tab="diff-side-by-side">{{.root.i18n.Tr "repo.diff.image.side_by_side"}}</a> | ||||||
|  		{{if $imageInfoHead}} | 					{{if and $imageInfoBase $imageInfoHead}} | ||||||
| 			{{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}} | 					<a class="item" data-tab="diff-swipe">{{.root.i18n.Tr "repo.diff.image.swipe"}}</a> | ||||||
| 				{{ $classWidth = "red" }} | 					<a class="item" data-tab="diff-overlay">{{.root.i18n.Tr "repo.diff.image.overlay"}}</a> | ||||||
| 			{{end}} | 					{{end}} | ||||||
| 			{{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}} | 				</div> | ||||||
| 				{{ $classHeight = "red" }} | 			</div> | ||||||
| 			{{end}} | 			<div class="hide"> | ||||||
| 			{{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}} | 				<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side"> | ||||||
| 				{{ $classByteSize = "red" }} | 					<div class="diff-side-by-side"> | ||||||
| 			{{end}} | 						{{if $imageInfoBase }} | ||||||
|  		{{end}} | 						<span class="side"> | ||||||
|  		{{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoBase.Width}}</span> | 							<p class="side-header">{{.root.i18n.Tr "repo.diff.file_before"}}</p> | ||||||
|  		 |  | 							<span class="before-container"><img class="image-before" /></span> | ||||||
|  	    {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span> | 							<p> | ||||||
|  		 |  | 								{{ $classWidth := "" }} | ||||||
|  	    {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoBase.ByteSize}}</span> | 								{{ $classHeight := "" }} | ||||||
|  	{{end}} | 								{{ $classByteSize := "" }} | ||||||
|  	</td> | 								{{if $imageInfoHead}} | ||||||
|  	<td class="halfwidth center pl-2 pr-3"> | 									{{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}} | ||||||
|  	{{if $imageInfoHead }} | 										{{ $classWidth = "red" }} | ||||||
|  		{{ $classWidth := "" }} | 									{{end}} | ||||||
|  		{{ $classHeight := "" }} | 									{{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}} | ||||||
|  		{{ $classByteSize := "" }} | 										{{ $classHeight = "red" }} | ||||||
|  		{{if $imageInfoBase}} | 									{{end}} | ||||||
| 			{{if not (eq $imageInfoBase.Width $imageInfoHead.Width)}} | 									{{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}} | ||||||
| 				{{ $classWidth = "green" }} | 										{{ $classByteSize = "red" }} | ||||||
| 			{{end}} | 									{{end}} | ||||||
| 			{{if not (eq $imageInfoBase.Height $imageInfoHead.Height)}} | 								{{end}} | ||||||
| 				{{ $classHeight = "green" }} | 								{{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoBase.Width}}</span> | ||||||
| 			{{end}} | 								 |  | ||||||
| 			{{if not (eq $imageInfoBase.ByteSize $imageInfoHead.ByteSize)}} | 								{{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoBase.Height}}</span> | ||||||
| 				{{ $classByteSize = "green" }} | 								 |  | ||||||
| 			{{end}} | 								{{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoBase.ByteSize}}</span> | ||||||
|  		{{end}} | 							</p> | ||||||
|  		{{.root.i18n.Tr "repo.diff.file_image_width"}}: <span class="text {{$classWidth}}">{{$imageInfoHead.Width}}</span> | 						</span> | ||||||
|  		 |  | 						{{end}} | ||||||
|  	    {{.root.i18n.Tr "repo.diff.file_image_height"}}: <span class="text {{$classHeight}}">{{$imageInfoHead.Height}}</span> | 						{{if $imageInfoHead }} | ||||||
|  		 |  | 						<span class="side"> | ||||||
|  	    {{.root.i18n.Tr "repo.diff.file_byte_size"}}: <span class="text {{$classByteSize}}">{{FileSize $imageInfoHead.ByteSize}}</span> | 							<p class="side-header">{{.root.i18n.Tr "repo.diff.file_after"}}</p> | ||||||
|  	{{end}} | 							<span class="after-container"><img class="image-after" /></span> | ||||||
|  	</td> | 							<p> | ||||||
|  </tr> | 								{{ $classWidth := "" }} | ||||||
| {{end}} | 								{{ $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}} | ||||||
							
								
								
									
										206
									
								
								web_src/js/features/imagediff.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								web_src/js/features/imagediff.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 createColorPicker from './features/colorpicker.js'; | ||||||
| import createDropzone from './features/dropzone.js'; | import createDropzone from './features/dropzone.js'; | ||||||
| import initTableSort from './features/tablesort.js'; | import initTableSort from './features/tablesort.js'; | ||||||
|  | import initImageDiff from './features/imagediff.js'; | ||||||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | ||||||
| import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | import {initNotificationsTable, initNotificationCount} from './features/notification.js'; | ||||||
| import {initStopwatch} from './features/stopwatch.js'; | import {initStopwatch} from './features/stopwatch.js'; | ||||||
| @@ -2693,6 +2694,7 @@ $(document).ready(async () => { | |||||||
|     initStopwatch(), |     initStopwatch(), | ||||||
|     renderMarkdownContent(), |     renderMarkdownContent(), | ||||||
|     initGithook(), |     initGithook(), | ||||||
|  |     initImageDiff(), | ||||||
|   ]); |   ]); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										105
									
								
								web_src/less/features/imagediff.less
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								web_src/less/features/imagediff.less
									
									
									
									
									
										Normal file
									
								
							| @@ -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/gitgraph.less"; | ||||||
| @import "./features/animations.less"; | @import "./features/animations.less"; | ||||||
| @import "./features/heatmap.less"; | @import "./features/heatmap.less"; | ||||||
|  | @import "./features/imagediff.less"; | ||||||
| @import "./markdown/mermaid.less"; | @import "./markdown/mermaid.less"; | ||||||
|  |  | ||||||
| @import "./chroma/base.less"; | @import "./chroma/base.less"; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user