Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions SnipSnaps.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,19 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SnipSnaps/SnipSnaps.entitlements;
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = SnipSnaps/SnipSnaps-macOS.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = W2W286Y75F;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SnipSnaps/Info.plist;
"INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*]" = "public.app-category.utilities";
"INFOPLIST_KEY_NSHumanReadableCopyright[sdk=macosx*]" = "Copyright © 2026 Kyter, LLC. All rights reserved.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand All @@ -307,7 +310,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
MACOSX_DEPLOYMENT_TARGET = 15.0;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -320,16 +324,19 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SnipSnaps/SnipSnaps.entitlements;
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = SnipSnaps/SnipSnaps-macOS.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = W2W286Y75F;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SnipSnaps/Info.plist;
"INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*]" = "public.app-category.utilities";
"INFOPLIST_KEY_NSHumanReadableCopyright[sdk=macosx*]" = "Copyright © 2026 Kyter, LLC. All rights reserved.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand All @@ -347,7 +354,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
MACOSX_DEPLOYMENT_TARGET = 15.0;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
10 changes: 10 additions & 0 deletions SnipSnaps/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,51 +31,61 @@
"size" : "1024x1024"
},
{
"filename" : "mac_16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "mac_32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "mac_32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "mac_64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "mac_128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "mac_256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "mac_256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "mac_512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "mac_512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "mac_1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
Expand Down
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions SnipSnaps/Commands/AppCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#if os(macOS)
import SwiftUI
import AppKit

// Native macOS menu-bar commands. The Help menu is retargeted at SnipSnaps' real
// support/legal URLs (the default SwiftUI Help menu is empty). The Review menu
// and Edit ▸ Undo are wired to the active review surface via focused values.
struct AppCommands: Commands {
var body: some Commands {
// Edit ▸ Undo routed to whichever review owns the focus, so ⌘Z works
// regardless of which control is focused (and on the summary screen).
CommandGroup(replacing: .undoRedo) {
FocusedReviewUndoButton()
}

// File ▸ Add Folder… for the Files surface.
CommandGroup(after: .newItem) {
FocusedAddFolderButton()
}

CommandMenu("Review") {
FocusedReviewActionButtons()
}

CommandGroup(replacing: .help) {
Button("SnipSnaps Support") { Self.open("https://kyter.com/snipsnaps/support/") }
Button("GitHub Repository") { Self.open("https://github.com/Kyter-com/SnipSnaps") }
Button("Send Feedback…") { Self.open("mailto:dev@kyter.com?subject=SnipSnaps%20App%20Feedback") }
Divider()
Button("Privacy Policy") { Self.open("https://kyter.com/snipsnaps/privacy/") }
Button("Terms & Conditions") { Self.open("https://kyter.com/snipsnaps/terms/") }
}
}

static func open(_ string: String) {
if let url = URL(string: string) {
NSWorkspace.shared.open(url)
}
}
}

// MARK: - Focused review bridge

// A review screen publishes this so the menu bar can drive Keep / Delete / Undo /
// Skip on the currently-active review, with items auto-disabling elsewhere.
struct ReviewActions {
var keep: (() -> Void)?
var delete: (() -> Void)?
var undo: (() -> Void)?
var skipGroup: (() -> Void)?
var canUndo: Bool = false
}

struct ReviewActionsFocusedValueKey: FocusedValueKey {
typealias Value = ReviewActions
}

// Files surface publishes its "add folder" hook so File ▸ Add Folder… works.
struct AddFolderFocusedValueKey: FocusedValueKey {
typealias Value = () -> Void
}

extension FocusedValues {
var reviewActions: ReviewActions? {
get { self[ReviewActionsFocusedValueKey.self] }
set { self[ReviewActionsFocusedValueKey.self] = newValue }
}

var addFolderAction: (() -> Void)? {
get { self[AddFolderFocusedValueKey.self] }
set { self[AddFolderFocusedValueKey.self] = newValue }
}
}

private struct FocusedReviewUndoButton: View {
@FocusedValue(\.reviewActions) private var actions

var body: some View {
Button("Undo") { actions?.undo?() }
.keyboardShortcut("z", modifiers: .command)
.disabled(!(actions?.canUndo ?? false))
}
}

private struct FocusedAddFolderButton: View {
@FocusedValue(\.addFolderAction) private var addFolder

var body: some View {
Button("Add Folder…") { addFolder?() }
.keyboardShortcut("o", modifiers: [.command, .shift])
.disabled(addFolder == nil)
}
}

// Click-only menu items (discoverability + VoiceOver). The matching ←/→/Delete/s
// keys are owned by in-view buttons on the active review, so these intentionally
// carry NO keyboardShortcut to avoid a double-owned key equivalent.
private struct FocusedReviewActionButtons: View {
@FocusedValue(\.reviewActions) private var actions

var body: some View {
Button("Keep →") { actions?.keep?() }
.disabled(actions?.keep == nil)
Button("Move to Trash ←") { actions?.delete?() }
.disabled(actions?.delete == nil)
Button("Skip Group S") { actions?.skipGroup?() }
.disabled(actions?.skipGroup == nil)
}
}
#endif
91 changes: 84 additions & 7 deletions SnipSnaps/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,34 @@
import SwiftUI
Comment thread
NickReisenauer marked this conversation as resolved.

struct ContentView: View {
@State private var selectedTab = 0

var body: some View {
#if DEBUG
if let screenshotScreen = ProcessInfo.processInfo.environment["SNIPSNAPS_SCREENSHOT_SCREEN"] {
ScreenshotDemoView(screen: screenshotScreen)
} else {
tabView
shell
}
#else
shell
#endif
}

@ViewBuilder
private var shell: some View {
#if os(macOS)
// Mac uses a native sidebar instead of the iOS bottom tab bar; Settings lives
// in the ⌘, Settings scene (see SnipSnapsApp), so it's not a sidebar row.
MacSidebarShell()
.tint(AppColor.primary)
.frame(minWidth: 720, minHeight: 480)
#else
tabView
#endif
}

#if os(iOS)
@State private var selectedTab = 0

private var tabView: some View {
TabView(selection: $selectedTab) {
HomeView()
Expand All @@ -35,10 +49,73 @@ struct ContentView: View {
}
.tag(1)
}
.accentColor(AppColor.primary)
.onChange(of: selectedTab) {
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
.tint(AppColor.primary)
.onChange(of: selectedTab) {
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
}
}
#endif
}

#if os(macOS)
enum SidebarItem: String, Hashable, CaseIterable, Identifiable {
case photos
case files

var id: String { rawValue }

var title: String {
switch self {
case .photos: return "Photos"
case .files: return "Files"
}
}

var systemImage: String {
switch self {
case .photos: return "photo.on.rectangle.angled"
case .files: return "folder"
}
}
}

private struct MacSidebarShell: View {
// Persist the selection across launches; never let it resolve to nil so the
// detail pane is always populated.
@SceneStorage("sidebarSelection") private var selectionRaw: String = SidebarItem.photos.rawValue

private var selection: SidebarItem {
SidebarItem(rawValue: selectionRaw) ?? .photos
}

private var selectionBinding: Binding<SidebarItem?> {
Binding(
get: { selection },
set: { newValue in
if let newValue { selectionRaw = newValue.rawValue }
}
)
}

var body: some View {
NavigationSplitView {
List(selection: selectionBinding) {
Section("Clean Up") {
ForEach(SidebarItem.allCases) { item in
Label(item.title, systemImage: item.systemImage)
.tag(item)
}
}
}
.navigationSplitViewColumnWidth(min: 188, ideal: 210, max: 280)
.navigationTitle("SnipSnaps")
} detail: {
switch selection {
case .photos: HomeView()
case .files: FilesView()
}
}
}
}
#endif
Loading