Repository navigation
feat: Add image comparison modes for diff view #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
deefc09
feat: Add image comparison modes for diff view
Tranthanh98 dc29a94
feat: Add dark mode logo asset and app icon configuration
Tranthanh98 f4b8ec3
feat: Add ImagePreviewViewport and single-image diff preview
Tranthanh98 a4b3fc6
fix: address image comparison review findings
Tranthanh98 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import Foundation | ||
|
|
||
| nonisolated enum ImageComparisonMode: String, CaseIterable, Identifiable { | ||
| case sideBySide | ||
| case overlay | ||
| var id: String { rawValue } | ||
| var title: String { self == .overlay ? "Overlay Slider" : "Side by Side" } | ||
| var icon: String { self == .overlay ? "slider.horizontal.below.rectangle" : "rectangle.split.2x1" } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import Foundation | ||
|
|
||
| nonisolated struct ImageComparisonRequest: Hashable, Sendable { | ||
| enum Source: Hashable, Sendable { | ||
| case revision(String, String) | ||
| case index(String) | ||
| case indexWithFallback(String, String) | ||
| case disk(String) | ||
| } | ||
| let before: Source? | ||
| let after: Source? | ||
|
|
||
| static func working(_ file: StatusFile, staged: Bool) -> Self { | ||
| Self(before: file.status == .untracked || file.status == .added ? nil : | ||
| (staged ? .revision("HEAD", file.originalPath ?? file.path) : | ||
| (file.status == .renamed ? .indexWithFallback(file.path, file.originalPath ?? file.path) : .index(file.path))), | ||
| after: file.status == .deleted ? nil : (staged ? .index(file.path) : .disk(file.path))) | ||
| } | ||
|
|
||
| static func commit(path: String, oldPath: String?, status: CommitFileStatus?, ref: String) -> Self { | ||
| Self(before: status == .added ? nil : .revision("\(ref)^1", oldPath ?? path), | ||
| after: status == .deleted ? nil : .revision(ref, path)) | ||
| } | ||
| } | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| { | ||
| "fill" : { | ||
| "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" | ||
| }, | ||
| "groups" : [ | ||
| { | ||
| "layers" : [ | ||
| { | ||
| "fill" : { | ||
| "linear-gradient" : [ | ||
| "extended-srgb:1.00000,0.21961,0.23529,1.00000", | ||
| "extended-srgb:1.00000,0.55294,0.15686,1.00000" | ||
| ], | ||
| "orientation" : { | ||
| "start" : { | ||
| "x" : 0.5, | ||
| "y" : 0 | ||
| }, | ||
| "stop" : { | ||
| "x" : 0.5, | ||
| "y" : 0.7 | ||
| } | ||
| } | ||
| }, | ||
| "glass" : true, | ||
| "image-name" : "Branch (1).png", | ||
| "name" : "Branch (1)", | ||
| "position" : { | ||
| "scale" : 1.62, | ||
| "translation-in-points" : [ | ||
| 95.921875, | ||
| -75.0703125 | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "blend-mode" : "lighten", | ||
| "fill" : "automatic", | ||
| "glass" : true, | ||
| "image-name" : "Commit (1).png", | ||
| "name" : "Commit (1)", | ||
| "position" : { | ||
| "scale" : 1.35, | ||
| "translation-in-points" : [ | ||
| -108.4921875, | ||
| 0 | ||
| ] | ||
| } | ||
| } | ||
| ], | ||
| "shadow" : { | ||
| "kind" : "neutral", | ||
| "opacity" : 0.5 | ||
| }, | ||
| "translucency" : { | ||
| "enabled" : true, | ||
| "value" : 0.5 | ||
| } | ||
| } | ||
| ], | ||
| "supported-platforms" : { | ||
| "circles" : [ | ||
| "watchOS" | ||
| ], | ||
| "squares" : "shared" | ||
| } | ||
| } |
Binary file modified
BIN
+2.36 KB
(120%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+9.18 KB
(130%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+11 Bytes
(100%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+88 Bytes
(100%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+9.18 KB
(130%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+39.3 KB
(130%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+88 Bytes
(100%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+551 Bytes
(110%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+39.3 KB
(130%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+301 KB
(130%)
macgit/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+147 KB
macgit/Resources/Assets.xcassets/commitplus-logo.imageset/logo-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified
BIN
+39.3 KB
(130%)
macgit/Resources/Assets.xcassets/commitplus-logo.imageset/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import Foundation | ||
|
|
||
| extension GitStatusService { | ||
| func imageData(from source: ImageComparisonRequest.Source, in repositoryURL: URL) async throws -> Data { | ||
| switch source { | ||
| case let .revision(ref, path): | ||
| return try await showFile(at: path, ref: ref, in: repositoryURL) | ||
| case let .index(path): | ||
| return try await indexFile(at: path, in: repositoryURL) | ||
| case let .indexWithFallback(path, originalPath): | ||
| // Paired worktree renames may have either path in the index. | ||
| do { | ||
| return try await indexFile(at: path, in: repositoryURL) | ||
| } catch { | ||
| try Task.checkCancellation() | ||
| guard path != originalPath else { throw error } | ||
| return try await indexFile(at: originalPath, in: repositoryURL) | ||
| } | ||
| case let .disk(path): | ||
| return try await Task.detached(priority: .userInitiated) { | ||
| let root = repositoryURL.standardizedFileURL.resolvingSymlinksInPath() | ||
| let file = repositoryURL.appendingPathComponent(path).standardizedFileURL.resolvingSymlinksInPath() | ||
| let rootComponents = root.pathComponents | ||
| guard file.pathComponents.count > rootComponents.count, | ||
| Array(file.pathComponents.prefix(rootComponents.count)) == rootComponents else { | ||
| throw GitError.commandFailed("Image preview paths must remain inside the repository.") | ||
| } | ||
| return try Data(contentsOf: file) | ||
| }.value | ||
|
Tranthanh98 marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import SwiftUI | ||
|
|
||
| struct ImageComparisonModeButton: View { | ||
| @Binding var mode: ImageComparisonMode | ||
|
|
||
| private var nextMode: ImageComparisonMode { | ||
| mode == .overlay ? .sideBySide : .overlay | ||
| } | ||
|
|
||
| var body: some View { | ||
| Button { | ||
| mode = nextMode | ||
| } label: { | ||
| Image(systemName: mode.icon) | ||
| .frame(width: 24, height: 24) | ||
| .contentShape(Rectangle()) | ||
| } | ||
| .buttonStyle(.plain) | ||
| .fixedSize() | ||
| .help("Switch to \(nextMode.title)") | ||
| .accessibilityLabel("Switch image comparison to \(nextMode.title)") | ||
| .accessibilityValue(mode.title) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import SwiftUI | ||
|
|
||
| struct ImageComparisonOverlay: View { | ||
| let before: NSImage | ||
| let after: NSImage | ||
| @Binding var fraction: Double | ||
|
|
||
| var body: some View { | ||
| GeometryReader { geometry in | ||
| let size = geometry.size | ||
| let width = max(before.size.width, after.size.width, 1) | ||
| let height = max(before.size.height, after.size.height, 1) | ||
| let scale = max(0, min(size.width / width, size.height / height)) | ||
| let imageWidth = width * scale | ||
| let imageOffset = (size.width - imageWidth) / 2 | ||
| let dividerX = imageOffset + imageWidth * fraction | ||
| ZStack { | ||
| Color(nsColor: .textBackgroundColor) | ||
| image(after, scale: scale) | ||
| .frame(width: width * scale, height: height * scale, alignment: .topLeading) | ||
| ZStack { | ||
| Color(nsColor: .textBackgroundColor) | ||
| image(before, scale: scale) | ||
| .frame(width: width * scale, height: height * scale, alignment: .topLeading) | ||
| } | ||
| .frame(width: size.width, height: size.height) | ||
| .mask(alignment: .leading) { | ||
| Rectangle().frame(width: dividerX) | ||
| } | ||
| Rectangle().fill(.primary).frame(width: 2) | ||
| .position(x: dividerX, y: size.height / 2) | ||
| Image(systemName: "arrow.left.and.right") | ||
| .font(.caption.bold()).padding(8) | ||
| .background(.regularMaterial, in: Circle()) | ||
| .position(x: dividerX, y: size.height / 2) | ||
| } | ||
| .clipped() | ||
| .contentShape(Rectangle()) | ||
| .gesture(DragGesture(minimumDistance: 0).onChanged { value in | ||
| fraction = min(1, max(0, (value.location.x - imageOffset) / max(imageWidth, 1))) | ||
| }) | ||
| .accessibilityElement(children: .ignore) | ||
| .accessibilityLabel("Before and after image comparison") | ||
| }.frame(maxWidth: .infinity, maxHeight: .infinity) | ||
| } | ||
|
|
||
| private func image(_ image: NSImage, scale: CGFloat) -> some View { | ||
| Image(nsImage: image).resizable() | ||
| .frame(width: image.size.width * scale, height: image.size.height * scale) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
| import SwiftUI | ||
|
|
||
| struct ImageComparisonSide: View { | ||
| let title: String? | ||
| let image: NSImage? | ||
| let error: String? | ||
| let isMissing: Bool | ||
| var body: some View { | ||
| VStack(spacing: 0) { | ||
| if let title { | ||
| Text(title).font(.caption.weight(.semibold)).padding(10) | ||
| Divider() | ||
| } | ||
| if let image { | ||
| ImagePreviewViewport { | ||
| Image(nsImage: image) | ||
| .resizable() | ||
| .aspectRatio(contentMode: .fit) | ||
| .frame(maxWidth: .infinity, maxHeight: .infinity) | ||
| .padding(12) | ||
| } | ||
| } else { | ||
| EmptyStateView(icon: "photo", message: isMissing ? "No image" : "Unable to preview image", detail: error) | ||
| } | ||
| }.frame(maxWidth: .infinity, maxHeight: .infinity) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.