Compare commits
17
Commits
ecbae7cd21
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
188b952be7 | ||
|
|
8d6c2213ad | ||
|
|
519706dd41 | ||
|
|
0fce3e6d28 | ||
|
|
4267650739 | ||
|
|
052663b356 | ||
|
|
827af0b239 | ||
|
|
d878e8e333 | ||
|
|
b55947e536 | ||
|
|
d50a1d550a | ||
|
|
e2c3ef7f37 | ||
|
|
7a78176183 | ||
|
|
8c73c5fbc5 | ||
|
|
78d6809445 | ||
|
|
59b3f8d2a1 | ||
|
|
f6e90047a8 | ||
|
|
062285abbf |
@@ -0,0 +1,119 @@
|
||||
# SnipMC
|
||||
|
||||
**macOS Menu Bar Screenshot Tool mit integriertem Bildeditor**
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
SnipMC ist ein schlankes Screenshot-Tool für macOS, das in der Menüleiste lebt. Es bietet drei Aufnahme-Modi, einen integrierten Bildeditor für Annotationen und ein URL-Schema zur Steuerung über externe Tools wie Logitech Options+, Kurzbefehle oder Stream Deck.
|
||||
|
||||
## Features
|
||||
|
||||
- **Screenshot-Modi**: Ganzer Bildschirm, Fenster oder frei wählbarer Bereich
|
||||
- **Bildeditor**: Pfeile, Rechtecke, Ellipsen und Text-Annotationen
|
||||
- **Flexibler Zugriff**: Letzte Aufnahme bearbeiten, Bild aus Zwischenablage, Datei öffnen
|
||||
- **URL-Schema** (`snipmc://`): Externe Steuerung per Tastenkürzel, Stream Deck etc.
|
||||
- **Einstellungen**: Speicherort, Bildformat (PNG/JPG), konfigurierbare Tastenkürzel
|
||||
- **Menüleisten-App**: Kein Dock-Icon, läuft dezent im Hintergrund
|
||||
|
||||
## Installation
|
||||
|
||||
1. `SnipMC.dmg` aus dem [aktuellen Release](https://github.com/mwende/SnipMC/releases/latest) herunterladen
|
||||
2. DMG öffnen und `SnipMC.app` nach `/Programme` ziehen
|
||||
3. Gatekeeper-Sperre aufheben (da die App nicht mit einem Apple Developer Zertifikat signiert ist):
|
||||
```bash
|
||||
xattr -cr /Applications/SnipMC.app
|
||||
```
|
||||
4. App starten — das Kamera-Icon erscheint in der Menüleiste
|
||||
5. Beim ersten Start die Berechtigung **Bildschirmaufnahme** erteilen:
|
||||
*Systemeinstellungen → Datenschutz & Sicherheit → Bildschirmaufnahme*
|
||||
|
||||
## Screenshot-Modi
|
||||
|
||||
| Modus | Beschreibung |
|
||||
|---|---|
|
||||
| Ganzer Bildschirm | Nimmt den gesamten Bildschirm auf |
|
||||
| Fenster | Klicke auf ein Fenster, um es aufzunehmen |
|
||||
| Bereich auswählen | Ziehe einen Rahmen um den gewünschten Bereich |
|
||||
|
||||
## Bildeditor
|
||||
|
||||
Der integrierte Editor öffnet sich nicht automatisch nach jeder Aufnahme, sondern nur auf expliziten Wunsch — über das Menü, per URL-Schema oder über die Tastenkombination.
|
||||
|
||||
### Werkzeuge
|
||||
|
||||
| Werkzeug | Beschreibung |
|
||||
|---|---|
|
||||
| Pfeil | Linie mit Pfeilspitze zeichnen |
|
||||
| Rechteck | Rahmen um einen Bereich zeichnen |
|
||||
| Ellipse | Kreis oder Oval zeichnen |
|
||||
| Text | Klicke auf eine Stelle und tippe Text ein |
|
||||
|
||||
### Tastenkürzel im Editor
|
||||
|
||||
| Kürzel | Aktion |
|
||||
|---|---|
|
||||
| ⌘Z | Rückgängig |
|
||||
| ⌘⇧Z | Wiederholen |
|
||||
| ⌫ / Entf | Ausgewählte Annotation löschen |
|
||||
| ⌘S | Speichern |
|
||||
|
||||
## URL-Schema
|
||||
|
||||
SnipMC registriert das URL-Schema `snipmc://` und kann so von externen Tools gesteuert werden.
|
||||
|
||||
| URL | Aktion |
|
||||
|---|---|
|
||||
| `snipmc://fullscreen` | Vollbild-Screenshot |
|
||||
| `snipmc://window` | Fenster-Screenshot |
|
||||
| `snipmc://region` | Bereich-Screenshot |
|
||||
| `snipmc://region?edit=true` | Bereich aufnehmen + Editor öffnen |
|
||||
| `snipmc://fullscreen?edit=true` | Vollbild aufnehmen + Editor öffnen |
|
||||
| `snipmc://edit` | Bild aus Datei im Editor öffnen |
|
||||
| `snipmc://editlast` | Letzten Screenshot im Editor bearbeiten |
|
||||
| `snipmc://clipboard` | Bild aus Zwischenablage bearbeiten |
|
||||
|
||||
### Einrichtung per macOS Kurzbefehle
|
||||
|
||||
1. Kurzbefehle-App öffnen
|
||||
2. Neuer Kurzbefehl → Aktion **URL öffnen**
|
||||
3. URL eintragen, z. B.: `snipmc://region`
|
||||
4. Optional: Tastaturkurzbefehl zuweisen
|
||||
|
||||
### Einrichtung per Terminal
|
||||
|
||||
```bash
|
||||
open snipmc://region
|
||||
open "snipmc://region?edit=true"
|
||||
open snipmc://editlast
|
||||
```
|
||||
|
||||
## Einstellungen
|
||||
|
||||
- **Ausgabe**: Nur speichern, nur Zwischenablage, oder beides
|
||||
- **Bildformat**: PNG oder JPG
|
||||
- **Speicherort**: Standard `~/Bilder/Screenshots`, frei wählbar
|
||||
- **Tastenkürzel**: Für jeden Aufnahme-Modus individuell konfigurierbar
|
||||
|
||||
## Systemvoraussetzungen
|
||||
|
||||
- macOS 14.0 (Sonoma) oder neuer
|
||||
- Berechtigung **Bildschirmaufnahme** erforderlich
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
# Voraussetzung: Xcode + XcodeGen
|
||||
brew install xcodegen
|
||||
|
||||
# Projekt generieren und bauen
|
||||
xcodegen generate
|
||||
xcodebuild -project SnipMC.xcodeproj -scheme SnipMC -configuration Release build
|
||||
```
|
||||
|
||||
## Autor
|
||||
|
||||
**Marco Wende — [Wende.IT](https://wende.it)**
|
||||
|
||||
Freeware — kostenlos nutzbar.
|
||||
@@ -0,0 +1,395 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
130D45C3838D4526C57BAE83 /* EditorWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35857E1B1439529A1C6932B7 /* EditorWindowController.swift */; };
|
||||
1BDB0D69E351CA5C221B94EA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081CCD02ACD35FC227278E48 /* SettingsView.swift */; };
|
||||
1D8FC537A5AC329C446C4218 /* SnippingToolApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */; };
|
||||
24C10B8D3D9AD133DE9920BC /* HotKeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275CE51E9539509ACD429507 /* HotKeyCombo.swift */; };
|
||||
2919075053BD6EBD40F49085 /* KeyCaptureRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */; };
|
||||
30D358DCC80875B5AE8E7CC9 /* CaptureMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */; };
|
||||
34615E753CFF584C1561040F /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF14927359614F91B42F70A0 /* SettingsWindowController.swift */; };
|
||||
449C4A7698D1DA7F8CD8B6BC /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */; };
|
||||
475DE4CC16A7A1EA786348F0 /* AnnotationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2048886E3C9DB924A3F618C /* AnnotationItem.swift */; };
|
||||
4B8591A347CDB31A5866A532 /* AboutWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6120576595AA8DB5C96DD665 /* AboutWindowController.swift */; };
|
||||
54D578ADAA0EA4B1B4E977BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CE7E1EFC83123C2285157A /* AppDelegate.swift */; };
|
||||
6FBABC7A889F4B1C0553ECEB /* KeyCodeSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */; };
|
||||
735EDBCCACD86A435C087FCA /* MenuBarContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */; };
|
||||
7CF577EBE926EBE85AD18CFF /* AnnotationDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */; };
|
||||
7FD8CD2B11ECC27CF23976B1 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F76602A1109742C19A7F88 /* ImageFormat.swift */; };
|
||||
8DCD5BBF7C77AD494D4296F4 /* OutputAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56887E8312DA185054D0C9B2 /* OutputAction.swift */; };
|
||||
AC0CAFD6D28E8B65BDB17976 /* HotKeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */; };
|
||||
B68ACECC44BB1E5907336177 /* AnnotationRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */; };
|
||||
BE94EAB3F172FA6B73752298 /* EditorToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */; };
|
||||
CB4219E3C65E1700F5D38149 /* HotKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */; };
|
||||
E0DDAD12E91158BF18D77A94 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0122BC6EC2818009EA7F3D53 /* Assets.xcassets */; };
|
||||
EE6381E9BC6F06BAA563A376 /* AnnotationCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0122BC6EC2818009EA7F3D53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
081CCD02ACD35FC227278E48 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
275CE51E9539509ACD429507 /* HotKeyCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyCombo.swift; sourceTree = "<group>"; };
|
||||
27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyManager.swift; sourceTree = "<group>"; };
|
||||
35857E1B1439529A1C6932B7 /* EditorWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorWindowController.swift; sourceTree = "<group>"; };
|
||||
415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeyRecorderView.swift; sourceTree = "<group>"; };
|
||||
47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCodeSymbols.swift; sourceTree = "<group>"; };
|
||||
56887E8312DA185054D0C9B2 /* OutputAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputAction.swift; sourceTree = "<group>"; };
|
||||
5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||
6120576595AA8DB5C96DD665 /* AboutWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindowController.swift; sourceTree = "<group>"; };
|
||||
61F76602A1109742C19A7F88 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = "<group>"; };
|
||||
7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarContentView.swift; sourceTree = "<group>"; };
|
||||
829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCaptureRepresentable.swift; sourceTree = "<group>"; };
|
||||
83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureMode.swift; sourceTree = "<group>"; };
|
||||
97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationCanvasView.swift; sourceTree = "<group>"; };
|
||||
A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationDocument.swift; sourceTree = "<group>"; };
|
||||
A2048886E3C9DB924A3F618C /* AnnotationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationItem.swift; sourceTree = "<group>"; };
|
||||
B4CE7E1EFC83123C2285157A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
DC94AA0BA2B93F0EC38D151B /* SnipMC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SnipMC.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnotationRenderer.swift; sourceTree = "<group>"; };
|
||||
DF14927359614F91B42F70A0 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
||||
E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnippingToolApp.swift; sourceTree = "<group>"; };
|
||||
F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorToolbarView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27F119FCBEB53FC15BA69005 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B94195006D6A63C034170E68 /* Sources */,
|
||||
627FB11FD8CF42D3297E06E1 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
627FB11FD8CF42D3297E06E1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DC94AA0BA2B93F0EC38D151B /* SnipMC.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
68AAE9FD7418A399448CE045 /* Editor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97BAF28ACE4ACF8932BE7C1C /* AnnotationCanvasView.swift */,
|
||||
A1DCECF6E7B7AD8486238262 /* AnnotationDocument.swift */,
|
||||
A2048886E3C9DB924A3F618C /* AnnotationItem.swift */,
|
||||
DD3AD2AE94290AF996796300 /* AnnotationRenderer.swift */,
|
||||
F1E46DCD8FA452BBCA2F0A47 /* EditorToolbarView.swift */,
|
||||
35857E1B1439529A1C6932B7 /* EditorWindowController.swift */,
|
||||
);
|
||||
path = Editor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B94195006D6A63C034170E68 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6120576595AA8DB5C96DD665 /* AboutWindowController.swift */,
|
||||
5F9EEA6BED296D9B4406E764 /* AppCoordinator.swift */,
|
||||
B4CE7E1EFC83123C2285157A /* AppDelegate.swift */,
|
||||
0122BC6EC2818009EA7F3D53 /* Assets.xcassets */,
|
||||
83137FC977A1F59EDC7C3DE3 /* CaptureMode.swift */,
|
||||
275CE51E9539509ACD429507 /* HotKeyCombo.swift */,
|
||||
27FAE65DDB9045BF6E0CE619 /* HotKeyManager.swift */,
|
||||
415C4A01A46C8F4A35C88ADB /* HotKeyRecorderView.swift */,
|
||||
61F76602A1109742C19A7F88 /* ImageFormat.swift */,
|
||||
829BB71510BB50F5C8CDB09B /* KeyCaptureRepresentable.swift */,
|
||||
47883B3A344C0BD1143DDD27 /* KeyCodeSymbols.swift */,
|
||||
7510AE1AF6818D4FE6428E9F /* MenuBarContentView.swift */,
|
||||
56887E8312DA185054D0C9B2 /* OutputAction.swift */,
|
||||
081CCD02ACD35FC227278E48 /* SettingsView.swift */,
|
||||
DF14927359614F91B42F70A0 /* SettingsWindowController.swift */,
|
||||
E00A19A40DF5025E03B556FD /* SnippingToolApp.swift */,
|
||||
68AAE9FD7418A399448CE045 /* Editor */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
EAED69AFD4EF2FC3C57FA1B8 /* SnipMC */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 99C7A42A282B29A64B2B7688 /* Build configuration list for PBXNativeTarget "SnipMC" */;
|
||||
buildPhases = (
|
||||
117C9BBC9123725FE6AD71C0 /* Sources */,
|
||||
C24E14B384D8A0D263EED133 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SnipMC;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SnipMC;
|
||||
productReference = DC94AA0BA2B93F0EC38D151B /* SnipMC.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
E5D0ACF0D9F5E10B2B3D9BB2 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7F1515B2EDB59F08F4421C1E /* Build configuration list for PBXProject "SnipMC" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 27F119FCBEB53FC15BA69005;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 627FB11FD8CF42D3297E06E1 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
EAED69AFD4EF2FC3C57FA1B8 /* SnipMC */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
C24E14B384D8A0D263EED133 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E0DDAD12E91158BF18D77A94 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
117C9BBC9123725FE6AD71C0 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4B8591A347CDB31A5866A532 /* AboutWindowController.swift in Sources */,
|
||||
EE6381E9BC6F06BAA563A376 /* AnnotationCanvasView.swift in Sources */,
|
||||
7CF577EBE926EBE85AD18CFF /* AnnotationDocument.swift in Sources */,
|
||||
475DE4CC16A7A1EA786348F0 /* AnnotationItem.swift in Sources */,
|
||||
B68ACECC44BB1E5907336177 /* AnnotationRenderer.swift in Sources */,
|
||||
449C4A7698D1DA7F8CD8B6BC /* AppCoordinator.swift in Sources */,
|
||||
54D578ADAA0EA4B1B4E977BB /* AppDelegate.swift in Sources */,
|
||||
30D358DCC80875B5AE8E7CC9 /* CaptureMode.swift in Sources */,
|
||||
BE94EAB3F172FA6B73752298 /* EditorToolbarView.swift in Sources */,
|
||||
130D45C3838D4526C57BAE83 /* EditorWindowController.swift in Sources */,
|
||||
24C10B8D3D9AD133DE9920BC /* HotKeyCombo.swift in Sources */,
|
||||
CB4219E3C65E1700F5D38149 /* HotKeyManager.swift in Sources */,
|
||||
AC0CAFD6D28E8B65BDB17976 /* HotKeyRecorderView.swift in Sources */,
|
||||
7FD8CD2B11ECC27CF23976B1 /* ImageFormat.swift in Sources */,
|
||||
2919075053BD6EBD40F49085 /* KeyCaptureRepresentable.swift in Sources */,
|
||||
6FBABC7A889F4B1C0553ECEB /* KeyCodeSymbols.swift in Sources */,
|
||||
735EDBCCACD86A435C087FCA /* MenuBarContentView.swift in Sources */,
|
||||
8DCD5BBF7C77AD494D4296F4 /* OutputAction.swift in Sources */,
|
||||
1BDB0D69E351CA5C221B94EA /* SettingsView.swift in Sources */,
|
||||
34615E753CFF584C1561040F /* SettingsWindowController.swift in Sources */,
|
||||
1D8FC537A5AC329C446C4218 /* SnippingToolApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
A8AD90B7DF485B4FE48C4DA2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
AAF2F2D5D59F12E7F040E326 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Generated/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
||||
PRODUCT_NAME = SnipMC;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF982956FBC79F78D1D42A71 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
EB9AB62157886891881D482F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Generated/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = it.wende.snipmc;
|
||||
PRODUCT_NAME = SnipMC;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7F1515B2EDB59F08F4421C1E /* Build configuration list for PBXProject "SnipMC" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A8AD90B7DF485B4FE48C4DA2 /* Debug */,
|
||||
BF982956FBC79F78D1D42A71 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
99C7A42A282B29A64B2B7688 /* Build configuration list for PBXNativeTarget "SnipMC" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
EB9AB62157886891881D482F /* Debug */,
|
||||
AAF2F2D5D59F12E7F040E326 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = E5D0ACF0D9F5E10B2B3D9BB2 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
|
||||
BuildableName = "SnipMC.app"
|
||||
BlueprintName = "SnipMC"
|
||||
ReferencedContainer = "container:SnipMC.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
|
||||
BuildableName = "SnipMC.app"
|
||||
BlueprintName = "SnipMC"
|
||||
ReferencedContainer = "container:SnipMC.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
|
||||
BuildableName = "SnipMC.app"
|
||||
BlueprintName = "SnipMC"
|
||||
ReferencedContainer = "container:SnipMC.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EAED69AFD4EF2FC3C57FA1B8"
|
||||
BuildableName = "SnipMC.app"
|
||||
BlueprintName = "SnipMC"
|
||||
ReferencedContainer = "container:SnipMC.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,168 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
final class AboutWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = AboutWindowController()
|
||||
|
||||
private convenience init() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 480, height: 520),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "SnipMC"
|
||||
window.isReleasedWhenClosed = false
|
||||
window.center()
|
||||
self.init(window: window)
|
||||
window.delegate = self
|
||||
}
|
||||
|
||||
func show() {
|
||||
window?.contentView = NSHostingView(rootView: AboutAndHelpView())
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
let otherVisible = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
|
||||
if otherVisible.isEmpty {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutAndHelpView: View {
|
||||
@State private var selectedTab = 0
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
AboutTab()
|
||||
.tabItem { Label("Info", systemImage: "info.circle") }
|
||||
.tag(0)
|
||||
HelpTab()
|
||||
.tabItem { Label("Dokumentation", systemImage: "book") }
|
||||
.tag(1)
|
||||
}
|
||||
.frame(width: 480, height: 500)
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutTab: View {
|
||||
private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
|
||||
private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
if let appIcon = NSImage(named: NSImage.applicationIconName) {
|
||||
Image(nsImage: appIcon)
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
}
|
||||
|
||||
Text("SnipMC")
|
||||
.font(.title.bold())
|
||||
|
||||
Text("Version \(version) (\(build))")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout)
|
||||
|
||||
Text("Freeware")
|
||||
.font(.callout)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.background(.quaternary)
|
||||
.clipShape(Capsule())
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("Marco Wende \u{2014} Wende.IT")
|
||||
.font(.body)
|
||||
Link("wende.it", destination: URL(string: "https://wende.it")!)
|
||||
.font(.body)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct HelpTab: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
helpSection("Aufnahme-Modi") {
|
||||
helpRow("Ganzer Bildschirm", "Nimmt den gesamten Bildschirm auf")
|
||||
helpRow("Fenster", "Klicke auf ein Fenster, um es aufzunehmen")
|
||||
helpRow("Bereich", "Ziehe einen Rahmen um den gew\u{00fc}nschten Bereich")
|
||||
}
|
||||
|
||||
helpSection("Bildeditor") {
|
||||
helpRow("Pfeil", "Linie mit Pfeilspitze zeichnen")
|
||||
helpRow("Rechteck", "Rahmen um einen Bereich zeichnen")
|
||||
helpRow("Ellipse", "Kreis oder Oval zeichnen")
|
||||
helpRow("Text", "Klicke auf eine Stelle und tippe Text ein")
|
||||
helpRow("\u{2318}Z / \u{2318}\u{21e7}Z", "R\u{00fc}ckg\u{00e4}ngig / Wiederholen")
|
||||
helpRow("\u{232b} / Entf", "Ausgew\u{00e4}hlte Annotation l\u{00f6}schen")
|
||||
}
|
||||
|
||||
helpSection("URL-Schema") {
|
||||
Text("Zum Ausl\u{00f6}sen per Logitech Options+, Kurzb efehle, Stream Deck o.\u{202f}\u{00e4}.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
urlRow("snipmc://fullscreen", "Vollbild-Screenshot")
|
||||
urlRow("snipmc://window", "Fenster-Screenshot")
|
||||
urlRow("snipmc://region", "Bereich-Screenshot")
|
||||
urlRow("snipmc://region?edit=true", "Bereich + Editor")
|
||||
urlRow("snipmc://fullscreen?edit=true", "Vollbild + Editor")
|
||||
urlRow("snipmc://edit", "Bild aus Datei bearbeiten")
|
||||
urlRow("snipmc://editlast", "Letzten Screenshot bearbeiten")
|
||||
urlRow("snipmc://clipboard", "Zwischenablage bearbeiten")
|
||||
}
|
||||
|
||||
helpSection("Hinweise") {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("\u{2022} Berechtigung Bildschirmaufnahme erforderlich (Systemeinstellungen \u{2192} Datenschutz & Sicherheit)")
|
||||
Text("\u{2022} Speicherort: ~/Bilder/Screenshots (\u{00e4}nderbar in Einstellungen)")
|
||||
Text("\u{2022} Tastenk\u{00fc}rzel frei konfigurierbar in den Einstellungen")
|
||||
}
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
|
||||
private func helpSection(_ title: String, @ViewBuilder content: () -> some View) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
private func helpRow(_ label: String, _ description: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text(label)
|
||||
.fontWeight(.medium)
|
||||
.frame(width: 120, alignment: .leading)
|
||||
Text(description)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
private func urlRow(_ url: String, _ description: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text(url)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.frame(width: 260, alignment: .leading)
|
||||
Text(description)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
-22
@@ -36,6 +36,9 @@ final class AppCoordinator: ObservableObject {
|
||||
didSet { persistAndRegister(mode: .region, combo: hotKeyRegion) }
|
||||
}
|
||||
|
||||
@Published var lastCapturedURL: URL?
|
||||
private var editorController: EditorWindowController?
|
||||
|
||||
init() {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
@@ -94,42 +97,62 @@ final class AppCoordinator: ObservableObject {
|
||||
|
||||
// MARK: - External triggers (URL scheme)
|
||||
|
||||
/// Handles `snippingtool://<mode>` URLs so external tools (Logitech
|
||||
/// Options, Shortcuts, Stream Deck, `open` from the command line, …)
|
||||
/// can trigger a specific capture mode instead of just launching the app.
|
||||
/// Accepts the mode either as the host (`snippingtool://region`) or as
|
||||
/// a query item (`snippingtool://capture?mode=region`).
|
||||
func handle(url: URL) {
|
||||
guard url.scheme?.caseInsensitiveCompare("snippingtool") == .orderedSame else { return }
|
||||
guard url.scheme?.caseInsensitiveCompare("snipmc") == .orderedSame else { return }
|
||||
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
let queryItems = components?.queryItems ?? []
|
||||
|
||||
let modeString = url.host?.lowercased()
|
||||
?? URLComponents(url: url, resolvingAgainstBaseURL: false)?
|
||||
.queryItems?
|
||||
.first(where: { $0.name.caseInsensitiveCompare("mode") == .orderedSame })?
|
||||
.value?
|
||||
.lowercased()
|
||||
?? queryItems.first(where: { $0.name.caseInsensitiveCompare("mode") == .orderedSame })?
|
||||
.value?.lowercased()
|
||||
|
||||
let wantsEditor = queryItems.first(where: { $0.name.caseInsensitiveCompare("edit") == .orderedSame })?
|
||||
.value?.lowercased() ?? "false"
|
||||
let openEditor = ["true", "1", "yes"].contains(wantsEditor)
|
||||
|
||||
if modeString == "edit" {
|
||||
openEditorForFile()
|
||||
return
|
||||
}
|
||||
|
||||
if modeString == "editlast" || modeString == "edit-last" || modeString == "lastedit" {
|
||||
openEditorForLastCapture()
|
||||
return
|
||||
}
|
||||
|
||||
if modeString == "paste" || modeString == "clipboard" || modeString == "editclipboard" {
|
||||
openEditorFromClipboard()
|
||||
return
|
||||
}
|
||||
|
||||
let mode: CaptureMode?
|
||||
switch modeString {
|
||||
case "fullscreen", "full", "full-screen", "screen":
|
||||
capture(mode: .fullScreen)
|
||||
mode = .fullScreen
|
||||
case "window":
|
||||
capture(mode: .window)
|
||||
mode = .window
|
||||
case "region", "area", "selection":
|
||||
capture(mode: .region)
|
||||
mode = .region
|
||||
default:
|
||||
NSLog("SnippingTool: unrecognized URL trigger \(url.absoluteString)")
|
||||
mode = nil
|
||||
NSLog("SnipMC: unrecognized URL trigger \(url.absoluteString)")
|
||||
}
|
||||
|
||||
if let mode {
|
||||
capture(mode: mode, openEditor: openEditor)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Capture
|
||||
|
||||
func capture(mode: CaptureMode) {
|
||||
func capture(mode: CaptureMode, openEditor: Bool = false) {
|
||||
let saves = outputAction.savesToFile
|
||||
let copies = outputAction.copiesToClipboard
|
||||
let ext = imageFormat.fileExtension
|
||||
|
||||
let targetURL: URL
|
||||
if saves {
|
||||
if saves || openEditor {
|
||||
try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true)
|
||||
targetURL = saveFolder.appendingPathComponent(Self.filename(ext: ext))
|
||||
} else {
|
||||
@@ -145,20 +168,24 @@ final class AppCoordinator: ObservableObject {
|
||||
process.arguments = arguments
|
||||
process.terminationHandler = { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
self?.handleCaptured(at: targetURL, saves: saves, copies: copies)
|
||||
self?.handleCaptured(at: targetURL, saves: saves, copies: copies, openEditor: openEditor)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
NSLog("SnippingTool: failed to launch screencapture: \(error)")
|
||||
NSLog("SnipMC: failed to launch screencapture: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCaptured(at url: URL, saves: Bool, copies: Bool) {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
// User cancelled the interactive capture (Esc).
|
||||
private func handleCaptured(at url: URL, saves: Bool, copies: Bool, openEditor: Bool = false) {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else { return }
|
||||
|
||||
lastCapturedURL = url
|
||||
|
||||
if openEditor, let image = NSImage(contentsOf: url) {
|
||||
self.openEditor(image: image, sourceURL: url)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,6 +200,94 @@ final class AppCoordinator: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Editor
|
||||
|
||||
func openEditorForLastCapture() {
|
||||
guard let url = lastCapturedURL,
|
||||
FileManager.default.fileExists(atPath: url.path),
|
||||
let image = NSImage(contentsOf: url) else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.openEditor(image: image, sourceURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
func openEditorFromClipboard() {
|
||||
guard let pasteboard = NSPasteboard.general.readObjects(forClasses: [NSImage.self], options: nil),
|
||||
let image = pasteboard.first as? NSImage else { return }
|
||||
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent(Self.filename(ext: imageFormat.fileExtension))
|
||||
DispatchQueue.main.async {
|
||||
self.openEditor(image: image, sourceURL: tempURL)
|
||||
}
|
||||
}
|
||||
|
||||
func openEditorForFile() {
|
||||
DispatchQueue.main.async {
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
|
||||
let panel = NSOpenPanel()
|
||||
panel.allowedContentTypes = [.png, .jpeg, .tiff, .bmp]
|
||||
panel.canChooseDirectories = false
|
||||
panel.allowsMultipleSelection = false
|
||||
if panel.runModal() == .OK, let url = panel.url,
|
||||
let image = NSImage(contentsOf: url) {
|
||||
self.openEditor(image: image, sourceURL: url)
|
||||
} else {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openEditor(image: NSImage, sourceURL: URL) {
|
||||
let controller = EditorWindowController(image: image, sourceURL: sourceURL) { [weak self] annotatedImage in
|
||||
guard let self, let annotatedImage else { return }
|
||||
let targetURL: URL
|
||||
if sourceURL.path.hasPrefix(NSTemporaryDirectory()) {
|
||||
try? FileManager.default.createDirectory(at: saveFolder, withIntermediateDirectories: true)
|
||||
targetURL = saveFolder.appendingPathComponent(Self.filename(ext: imageFormat.fileExtension))
|
||||
} else {
|
||||
targetURL = sourceURL
|
||||
}
|
||||
self.saveAnnotatedImage(annotatedImage, to: targetURL)
|
||||
}
|
||||
editorController = controller
|
||||
controller.showEditor()
|
||||
}
|
||||
|
||||
private func saveAnnotatedImage(_ image: NSImage, to url: URL) {
|
||||
let bitmap: NSBitmapImageRep
|
||||
if let rep = image.representations.first as? NSBitmapImageRep {
|
||||
bitmap = rep
|
||||
} else if let cgRef = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
||||
bitmap = NSBitmapImageRep(cgImage: cgRef)
|
||||
} else {
|
||||
NSLog("SnipMC: could not create bitmap for saving")
|
||||
return
|
||||
}
|
||||
let fileType: NSBitmapImageRep.FileType = imageFormat == .jpg ? .jpeg : .png
|
||||
let properties: [NSBitmapImageRep.PropertyKey: Any] = fileType == .jpeg
|
||||
? [.compressionFactor: 0.9] : [:]
|
||||
|
||||
guard let data = bitmap.representation(using: fileType, properties: properties) else {
|
||||
NSLog("SnipMC: could not encode image as \(imageFormat.rawValue)")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try data.write(to: url, options: .atomic)
|
||||
NSLog("SnipMC: saved to \(url.path)")
|
||||
} catch {
|
||||
NSLog("SnipMC: failed to save annotated image: \(error)")
|
||||
}
|
||||
|
||||
if outputAction.copiesToClipboard {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.writeObjects([image])
|
||||
}
|
||||
}
|
||||
|
||||
private static func filename(ext: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd 'um' HH.mm.ss"
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
final class AnnotationCanvasView: NSView {
|
||||
|
||||
var baseImage: NSImage? { didSet { needsDisplay = true } }
|
||||
var document: AnnotationDocument? { didSet { needsDisplay = true } }
|
||||
var onDocumentChanged: (() -> Void)?
|
||||
|
||||
private enum Interaction {
|
||||
case idle
|
||||
case creating(AnnotationKind, startPoint: CGPoint)
|
||||
case dragging(UUID, lastPoint: CGPoint)
|
||||
}
|
||||
|
||||
private var interaction: Interaction = .idle
|
||||
private var inProgressItem: AnnotationItem?
|
||||
private var textField: NSTextField?
|
||||
|
||||
override var isFlipped: Bool { true }
|
||||
override var acceptsFirstResponder: Bool { true }
|
||||
|
||||
// MARK: - Coordinate transform
|
||||
|
||||
private var imageRect: CGRect {
|
||||
guard let image = baseImage else { return bounds }
|
||||
let imageSize = image.size
|
||||
let scale = min(bounds.width / imageSize.width, bounds.height / imageSize.height)
|
||||
let scaledSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
|
||||
return CGRect(
|
||||
x: (bounds.width - scaledSize.width) / 2,
|
||||
y: (bounds.height - scaledSize.height) / 2,
|
||||
width: scaledSize.width,
|
||||
height: scaledSize.height
|
||||
)
|
||||
}
|
||||
|
||||
private var viewToImageScale: CGFloat {
|
||||
guard let image = baseImage else { return 1 }
|
||||
return image.size.width / imageRect.width
|
||||
}
|
||||
|
||||
private func viewToImage(_ point: CGPoint) -> CGPoint {
|
||||
let rect = imageRect
|
||||
let scale = viewToImageScale
|
||||
return CGPoint(
|
||||
x: (point.x - rect.origin.x) * scale,
|
||||
y: (point.y - rect.origin.y) * scale
|
||||
)
|
||||
}
|
||||
|
||||
private func imageToView(_ point: CGPoint) -> CGPoint {
|
||||
let rect = imageRect
|
||||
let scale = viewToImageScale
|
||||
return CGPoint(
|
||||
x: point.x / scale + rect.origin.x,
|
||||
y: point.y / scale + rect.origin.y
|
||||
)
|
||||
}
|
||||
|
||||
private func imageRectToView(_ r: CGRect) -> CGRect {
|
||||
let origin = imageToView(r.origin)
|
||||
let scale = viewToImageScale
|
||||
return CGRect(x: origin.x, y: origin.y,
|
||||
width: r.width / scale, height: r.height / scale)
|
||||
}
|
||||
|
||||
// MARK: - Drawing
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return }
|
||||
let bgColor: CGColor = NSColor.windowBackgroundColor.cgColor
|
||||
context.setFillColor(bgColor)
|
||||
context.fill(bounds)
|
||||
|
||||
guard let image = baseImage else { return }
|
||||
|
||||
let imgRect = imageRect
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
image.draw(in: imgRect, from: .zero, operation: .sourceOver, fraction: 1, respectFlipped: true, hints: nil)
|
||||
|
||||
context.saveGState()
|
||||
context.translateBy(x: imgRect.origin.x, y: imgRect.origin.y)
|
||||
let scale = 1.0 / viewToImageScale
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
|
||||
guard let doc = document else {
|
||||
context.restoreGState()
|
||||
return
|
||||
}
|
||||
|
||||
for item in doc.items {
|
||||
AnnotationRenderer.draw(item, in: context)
|
||||
|
||||
if item.id == doc.selectedItemID {
|
||||
drawSelectionIndicator(for: item, in: context)
|
||||
}
|
||||
}
|
||||
|
||||
if let inProgress = inProgressItem {
|
||||
AnnotationRenderer.draw(inProgress, in: context)
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
private func drawSelectionIndicator(for item: AnnotationItem, in context: CGContext) {
|
||||
let handleRect: CGRect
|
||||
if item.kind == .arrow {
|
||||
let start = item.rect.origin
|
||||
let end = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||
y: item.rect.origin.y + item.rect.size.height)
|
||||
handleRect = CGRect(x: min(start.x, end.x), y: min(start.y, end.y),
|
||||
width: abs(end.x - start.x), height: abs(end.y - start.y))
|
||||
} else if item.kind == .text {
|
||||
guard let text = item.text, !text.isEmpty else { return }
|
||||
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
|
||||
let size = (text as NSString).size(withAttributes: [.font: font])
|
||||
handleRect = CGRect(origin: item.rect.origin, size: size)
|
||||
} else {
|
||||
handleRect = item.rect
|
||||
}
|
||||
|
||||
context.saveGState()
|
||||
let dashPattern: [CGFloat] = [6, 4]
|
||||
context.setLineDash(phase: 0, lengths: dashPattern)
|
||||
context.setStrokeColor(NSColor.controlAccentColor.cgColor)
|
||||
context.setLineWidth(1.5)
|
||||
context.stroke(handleRect.insetBy(dx: -4, dy: -4))
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
// MARK: - Mouse events
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
commitTextField()
|
||||
window?.makeFirstResponder(self)
|
||||
|
||||
let viewPoint = convert(event.locationInWindow, from: nil)
|
||||
let imgPoint = viewToImage(viewPoint)
|
||||
guard let doc = document else { return }
|
||||
|
||||
// Hit-test existing items (reverse order = topmost first)
|
||||
for item in doc.items.reversed() {
|
||||
if AnnotationRenderer.hitTest(point: imgPoint, item: item) {
|
||||
doc.selectedItemID = item.id
|
||||
interaction = .dragging(item.id, lastPoint: imgPoint)
|
||||
needsDisplay = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
doc.selectedItemID = nil
|
||||
interaction = .creating(doc.currentTool, startPoint: imgPoint)
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
let viewPoint = convert(event.locationInWindow, from: nil)
|
||||
let imgPoint = viewToImage(viewPoint)
|
||||
guard let doc = document else { return }
|
||||
|
||||
switch interaction {
|
||||
case .creating(let kind, let start):
|
||||
if kind == .text { return }
|
||||
let rect = CGRect(x: start.x, y: start.y,
|
||||
width: imgPoint.x - start.x,
|
||||
height: imgPoint.y - start.y)
|
||||
inProgressItem = AnnotationItem(
|
||||
kind: kind,
|
||||
rect: rect,
|
||||
color: doc.currentColor,
|
||||
lineWidth: doc.currentLineWidth
|
||||
)
|
||||
needsDisplay = true
|
||||
|
||||
case .dragging(let id, let lastPoint):
|
||||
let delta = CGSize(width: imgPoint.x - lastPoint.x,
|
||||
height: imgPoint.y - lastPoint.y)
|
||||
doc.moveSelected(by: delta)
|
||||
interaction = .dragging(id, lastPoint: imgPoint)
|
||||
needsDisplay = true
|
||||
|
||||
case .idle:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
let viewPoint = convert(event.locationInWindow, from: nil)
|
||||
let imgPoint = viewToImage(viewPoint)
|
||||
guard let doc = document else { return }
|
||||
|
||||
switch interaction {
|
||||
case .creating(let kind, let start):
|
||||
if kind == .text {
|
||||
showTextField(at: viewPoint, imagePoint: imgPoint)
|
||||
} else if let item = inProgressItem {
|
||||
let dx = abs(item.rect.width)
|
||||
let dy = abs(item.rect.height)
|
||||
if dx > 4 || dy > 4 {
|
||||
let normalized = normalizedRect(from: start, to: imgPoint, kind: kind)
|
||||
var finalItem = item
|
||||
finalItem.rect = normalized
|
||||
doc.addItem(finalItem)
|
||||
onDocumentChanged?()
|
||||
}
|
||||
}
|
||||
inProgressItem = nil
|
||||
|
||||
case .dragging(_, _):
|
||||
doc.pushUndo()
|
||||
onDocumentChanged?()
|
||||
|
||||
case .idle:
|
||||
break
|
||||
}
|
||||
|
||||
interaction = .idle
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
private func normalizedRect(from start: CGPoint, to end: CGPoint, kind: AnnotationKind) -> CGRect {
|
||||
if kind == .arrow {
|
||||
return CGRect(x: start.x, y: start.y,
|
||||
width: end.x - start.x, height: end.y - start.y)
|
||||
}
|
||||
return CGRect(
|
||||
x: min(start.x, end.x), y: min(start.y, end.y),
|
||||
width: abs(end.x - start.x), height: abs(end.y - start.y)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Text editing
|
||||
|
||||
private func showTextField(at viewPoint: CGPoint, imagePoint: CGPoint) {
|
||||
let field = NSTextField(frame: NSRect(x: viewPoint.x, y: viewPoint.y, width: 200, height: 30))
|
||||
field.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
field.textColor = document?.currentColor ?? .systemRed
|
||||
field.backgroundColor = .white.withAlphaComponent(0.9)
|
||||
field.isBordered = true
|
||||
field.focusRingType = .exterior
|
||||
field.placeholderString = "Text eingeben…"
|
||||
field.target = self
|
||||
field.action = #selector(textFieldCommitted(_:))
|
||||
field.tag = Int(imagePoint.x) // store image coordinates in tag
|
||||
addSubview(field)
|
||||
window?.makeFirstResponder(field)
|
||||
|
||||
// Store image Y in field's identifier
|
||||
field.identifier = NSUserInterfaceItemIdentifier("\(imagePoint.x),\(imagePoint.y)")
|
||||
textField = field
|
||||
}
|
||||
|
||||
@objc private func textFieldCommitted(_ sender: NSTextField) {
|
||||
commitTextField()
|
||||
}
|
||||
|
||||
private func commitTextField() {
|
||||
guard let field = textField, let doc = document else { return }
|
||||
let text = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if !text.isEmpty, let identifier = field.identifier?.rawValue {
|
||||
let parts = identifier.split(separator: ",")
|
||||
if parts.count == 2,
|
||||
let x = Double(parts[0]),
|
||||
let y = Double(parts[1]) {
|
||||
let item = AnnotationItem(
|
||||
kind: .text,
|
||||
rect: CGRect(x: x, y: y, width: 0, height: 0),
|
||||
color: doc.currentColor,
|
||||
lineWidth: doc.currentLineWidth,
|
||||
text: text,
|
||||
fontSize: 24 * viewToImageScale
|
||||
)
|
||||
doc.addItem(item)
|
||||
onDocumentChanged?()
|
||||
needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
field.removeFromSuperview()
|
||||
textField = nil
|
||||
}
|
||||
|
||||
// MARK: - Keyboard
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if event.modifierFlags.contains(.command) {
|
||||
if event.charactersIgnoringModifiers == "z" {
|
||||
if event.modifierFlags.contains(.shift) {
|
||||
document?.redo()
|
||||
} else {
|
||||
document?.undo()
|
||||
}
|
||||
needsDisplay = true
|
||||
onDocumentChanged?()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if event.keyCode == 51 || event.keyCode == 117 { // Backspace or Delete
|
||||
document?.deleteSelected()
|
||||
needsDisplay = true
|
||||
onDocumentChanged?()
|
||||
return
|
||||
}
|
||||
|
||||
super.keyDown(with: event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
@Observable
|
||||
final class AnnotationDocument {
|
||||
var items: [AnnotationItem] = []
|
||||
var selectedItemID: UUID?
|
||||
var currentTool: AnnotationKind = .arrow
|
||||
var currentColor: NSColor = .systemRed
|
||||
var currentLineWidth: CGFloat = 3
|
||||
|
||||
private var undoStack: [[AnnotationItem]] = []
|
||||
private var redoStack: [[AnnotationItem]] = []
|
||||
|
||||
var canUndo: Bool { !undoStack.isEmpty }
|
||||
var canRedo: Bool { !redoStack.isEmpty }
|
||||
|
||||
func pushUndo() {
|
||||
undoStack.append(items)
|
||||
redoStack.removeAll()
|
||||
}
|
||||
|
||||
func undo() {
|
||||
guard let previous = undoStack.popLast() else { return }
|
||||
redoStack.append(items)
|
||||
items = previous
|
||||
selectedItemID = nil
|
||||
}
|
||||
|
||||
func redo() {
|
||||
guard let next = redoStack.popLast() else { return }
|
||||
undoStack.append(items)
|
||||
items = next
|
||||
selectedItemID = nil
|
||||
}
|
||||
|
||||
func addItem(_ item: AnnotationItem) {
|
||||
pushUndo()
|
||||
items.append(item)
|
||||
selectedItemID = item.id
|
||||
}
|
||||
|
||||
func deleteSelected() {
|
||||
guard let id = selectedItemID else { return }
|
||||
pushUndo()
|
||||
items.removeAll { $0.id == id }
|
||||
selectedItemID = nil
|
||||
}
|
||||
|
||||
func moveSelected(by delta: CGSize) {
|
||||
guard let id = selectedItemID,
|
||||
let index = items.firstIndex(where: { $0.id == id }) else { return }
|
||||
items[index].rect.origin.x += delta.width
|
||||
items[index].rect.origin.y += delta.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
enum AnnotationKind: String, CaseIterable, Codable {
|
||||
case arrow
|
||||
case rectangle
|
||||
case ellipse
|
||||
case text
|
||||
}
|
||||
|
||||
struct AnnotationItem: Identifiable {
|
||||
let id: UUID
|
||||
var kind: AnnotationKind
|
||||
var rect: CGRect
|
||||
var color: NSColor
|
||||
var lineWidth: CGFloat
|
||||
var text: String?
|
||||
var fontSize: CGFloat
|
||||
|
||||
init(
|
||||
kind: AnnotationKind,
|
||||
rect: CGRect,
|
||||
color: NSColor = .systemRed,
|
||||
lineWidth: CGFloat = 3,
|
||||
text: String? = nil,
|
||||
fontSize: CGFloat = 24
|
||||
) {
|
||||
self.id = UUID()
|
||||
self.kind = kind
|
||||
self.rect = rect
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.text = text
|
||||
self.fontSize = fontSize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
enum AnnotationRenderer {
|
||||
|
||||
static func draw(_ item: AnnotationItem, in context: CGContext) {
|
||||
context.saveGState()
|
||||
context.setStrokeColor(item.color.cgColor)
|
||||
context.setFillColor(item.color.cgColor)
|
||||
context.setLineWidth(item.lineWidth)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
|
||||
switch item.kind {
|
||||
case .arrow:
|
||||
drawArrow(item, in: context)
|
||||
case .rectangle:
|
||||
drawRectangle(item, in: context)
|
||||
case .ellipse:
|
||||
drawEllipse(item, in: context)
|
||||
case .text:
|
||||
drawText(item)
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
static func render(annotations: [AnnotationItem], onto image: NSImage) -> NSImage {
|
||||
guard let srcCG = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return image }
|
||||
let w = srcCG.width
|
||||
let h = srcCG.height
|
||||
let size = image.size
|
||||
|
||||
guard let cs = srcCG.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB),
|
||||
let ctx = CGContext(
|
||||
data: nil, width: w, height: h,
|
||||
bitsPerComponent: 8, bytesPerRow: 0, space: cs,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
) else { return image }
|
||||
|
||||
ctx.draw(srcCG, in: CGRect(x: 0, y: 0, width: w, height: h))
|
||||
|
||||
ctx.translateBy(x: 0, y: CGFloat(h))
|
||||
let sx = CGFloat(w) / size.width
|
||||
let sy = CGFloat(h) / size.height
|
||||
ctx.scaleBy(x: sx, y: -sy)
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(cgContext: ctx, flipped: true)
|
||||
|
||||
for item in annotations {
|
||||
draw(item, in: ctx)
|
||||
}
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard let resultCG = ctx.makeImage() else { return image }
|
||||
let bitmapRep = NSBitmapImageRep(cgImage: resultCG)
|
||||
bitmapRep.size = size
|
||||
let result = NSImage(size: size)
|
||||
result.addRepresentation(bitmapRep)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Shape drawing
|
||||
|
||||
private static func drawArrow(_ item: AnnotationItem, in context: CGContext) {
|
||||
let start = item.rect.origin
|
||||
let end = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||
y: item.rect.origin.y + item.rect.size.height)
|
||||
|
||||
context.move(to: start)
|
||||
context.addLine(to: end)
|
||||
context.strokePath()
|
||||
|
||||
let angle = atan2(end.y - start.y, end.x - start.x)
|
||||
let headLength = max(item.lineWidth * 4, 14)
|
||||
let headAngle: CGFloat = .pi / 6
|
||||
|
||||
let left = CGPoint(
|
||||
x: end.x - headLength * cos(angle - headAngle),
|
||||
y: end.y - headLength * sin(angle - headAngle)
|
||||
)
|
||||
let right = CGPoint(
|
||||
x: end.x - headLength * cos(angle + headAngle),
|
||||
y: end.y - headLength * sin(angle + headAngle)
|
||||
)
|
||||
|
||||
context.move(to: end)
|
||||
context.addLine(to: left)
|
||||
context.addLine(to: right)
|
||||
context.closePath()
|
||||
context.fillPath()
|
||||
}
|
||||
|
||||
private static func drawRectangle(_ item: AnnotationItem, in context: CGContext) {
|
||||
context.stroke(item.rect, width: item.lineWidth)
|
||||
}
|
||||
|
||||
private static func drawEllipse(_ item: AnnotationItem, in context: CGContext) {
|
||||
context.strokeEllipse(in: item.rect)
|
||||
}
|
||||
|
||||
private static func drawText(_ item: AnnotationItem) {
|
||||
guard let text = item.text, !text.isEmpty else { return }
|
||||
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: font,
|
||||
.foregroundColor: item.color,
|
||||
]
|
||||
let string = NSAttributedString(string: text, attributes: attributes)
|
||||
string.draw(at: item.rect.origin)
|
||||
}
|
||||
|
||||
// MARK: - Hit testing
|
||||
|
||||
static func hitTest(point: CGPoint, item: AnnotationItem, tolerance: CGFloat = 6) -> Bool {
|
||||
switch item.kind {
|
||||
case .arrow:
|
||||
let arrowEnd = CGPoint(x: item.rect.origin.x + item.rect.size.width,
|
||||
y: item.rect.origin.y + item.rect.size.height)
|
||||
return distanceToLine(point: point,
|
||||
from: item.rect.origin,
|
||||
to: arrowEnd) < tolerance + item.lineWidth
|
||||
case .rectangle:
|
||||
let outer = item.rect.insetBy(dx: -(tolerance + item.lineWidth),
|
||||
dy: -(tolerance + item.lineWidth))
|
||||
let inner = item.rect.insetBy(dx: tolerance + item.lineWidth,
|
||||
dy: tolerance + item.lineWidth)
|
||||
return outer.contains(point) && !inner.contains(point)
|
||||
case .ellipse:
|
||||
let center = CGPoint(x: item.rect.midX, y: item.rect.midY)
|
||||
let rx = item.rect.width / 2
|
||||
let ry = item.rect.height / 2
|
||||
guard rx > 0, ry > 0 else { return false }
|
||||
let dx = (point.x - center.x) / rx
|
||||
let dy = (point.y - center.y) / ry
|
||||
let dist = sqrt(dx * dx + dy * dy)
|
||||
return abs(dist - 1.0) < (tolerance + item.lineWidth) / min(rx, ry)
|
||||
case .text:
|
||||
guard let text = item.text, !text.isEmpty else { return false }
|
||||
let font = NSFont.systemFont(ofSize: item.fontSize, weight: .semibold)
|
||||
let size = (text as NSString).size(withAttributes: [.font: font])
|
||||
let textRect = CGRect(origin: item.rect.origin, size: size)
|
||||
return textRect.insetBy(dx: -tolerance, dy: -tolerance).contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
private static func distanceToLine(point: CGPoint, from a: CGPoint, to b: CGPoint) -> CGFloat {
|
||||
let dx = b.x - a.x
|
||||
let dy = b.y - a.y
|
||||
let lengthSq = dx * dx + dy * dy
|
||||
guard lengthSq > 0 else { return hypot(point.x - a.x, point.y - a.y) }
|
||||
|
||||
var t = ((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSq
|
||||
t = max(0, min(1, t))
|
||||
|
||||
let projX = a.x + t * dx
|
||||
let projY = a.y + t * dy
|
||||
return hypot(point.x - projX, point.y - projY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EditorToolbarView: View {
|
||||
@Bindable var document: AnnotationDocument
|
||||
var onSave: () -> Void
|
||||
var onCopyToClipboard: () -> Void
|
||||
var onCancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
toolButtons
|
||||
Divider().frame(height: 24)
|
||||
colorPicker
|
||||
lineWidthPicker
|
||||
Divider().frame(height: 24)
|
||||
undoRedoButtons
|
||||
Spacer()
|
||||
actionButtons
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(.bar)
|
||||
}
|
||||
|
||||
private var toolButtons: some View {
|
||||
HStack(spacing: 4) {
|
||||
toolButton(.arrow, icon: "arrow.up.right", label: "Pfeil")
|
||||
toolButton(.rectangle, icon: "rectangle", label: "Rechteck")
|
||||
toolButton(.ellipse, icon: "circle", label: "Ellipse")
|
||||
toolButton(.text, icon: "textformat", label: "Text")
|
||||
}
|
||||
}
|
||||
|
||||
private func toolButton(_ kind: AnnotationKind, icon: String, label: String) -> some View {
|
||||
Button {
|
||||
document.currentTool = kind
|
||||
} label: {
|
||||
Image(systemName: icon)
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(document.currentTool == kind ? .accentColor : nil)
|
||||
.help(label)
|
||||
}
|
||||
|
||||
private var colorPicker: some View {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(presetColors, id: \.self) { color in
|
||||
Circle()
|
||||
.fill(Color(nsColor: color))
|
||||
.frame(width: 20, height: 20)
|
||||
.overlay {
|
||||
if document.currentColor == color {
|
||||
Circle().strokeBorder(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
.shadow(radius: document.currentColor == color ? 2 : 0)
|
||||
.onTapGesture { document.currentColor = color }
|
||||
}
|
||||
|
||||
ColorPicker("", selection: Binding(
|
||||
get: { Color(nsColor: document.currentColor) },
|
||||
set: { document.currentColor = NSColor($0) }
|
||||
))
|
||||
.labelsHidden()
|
||||
.frame(width: 28)
|
||||
}
|
||||
}
|
||||
|
||||
private var presetColors: [NSColor] {
|
||||
[.systemRed, .systemBlue, .systemGreen, .systemYellow, .black, .white]
|
||||
}
|
||||
|
||||
private var lineWidthPicker: some View {
|
||||
Picker("", selection: $document.currentLineWidth) {
|
||||
Text("Dünn").tag(CGFloat(2))
|
||||
Text("Mittel").tag(CGFloat(3))
|
||||
Text("Dick").tag(CGFloat(5))
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 160)
|
||||
.labelsHidden()
|
||||
}
|
||||
|
||||
private var undoRedoButtons: some View {
|
||||
HStack(spacing: 4) {
|
||||
Button {
|
||||
document.undo()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.backward")
|
||||
}
|
||||
.disabled(!document.canUndo)
|
||||
.help("Widerrufen (⌘Z)")
|
||||
|
||||
Button {
|
||||
document.redo()
|
||||
} label: {
|
||||
Image(systemName: "arrow.uturn.forward")
|
||||
}
|
||||
.disabled(!document.canRedo)
|
||||
.help("Wiederholen (⌘⇧Z)")
|
||||
}
|
||||
}
|
||||
|
||||
private var actionButtons: some View {
|
||||
HStack(spacing: 8) {
|
||||
Button("Abbrechen", role: .cancel) { onCancel() }
|
||||
.keyboardShortcut(.escape)
|
||||
Button("In Zwischenablage") { onCopyToClipboard() }
|
||||
Button("Speichern") { onSave() }
|
||||
.keyboardShortcut("s")
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
private final class FlippedContainerView: NSView {
|
||||
override var isFlipped: Bool { true }
|
||||
}
|
||||
|
||||
final class EditorWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
private let annotationDoc = AnnotationDocument()
|
||||
private let baseImage: NSImage
|
||||
private let sourceURL: URL?
|
||||
private let completion: (NSImage?) -> Void
|
||||
private let canvasView = AnnotationCanvasView()
|
||||
|
||||
init(image: NSImage, sourceURL: URL?, completion: @escaping (NSImage?) -> Void) {
|
||||
self.baseImage = image
|
||||
self.sourceURL = sourceURL
|
||||
self.completion = completion
|
||||
|
||||
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1280, height: 800)
|
||||
let maxWidth = screenFrame.width * 0.8
|
||||
let maxHeight = screenFrame.height * 0.8
|
||||
let scale = min(maxWidth / image.size.width, maxHeight / image.size.height, 1.0)
|
||||
let windowWidth = max(image.size.width * scale, 600)
|
||||
let windowHeight = max(image.size.height * scale, 400) + 52
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: windowWidth, height: windowHeight),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "Bildeditor"
|
||||
window.isReleasedWhenClosed = false
|
||||
window.center()
|
||||
window.minSize = NSSize(width: 500, height: 400)
|
||||
|
||||
super.init(window: window)
|
||||
window.delegate = self
|
||||
|
||||
canvasView.baseImage = image
|
||||
canvasView.document = annotationDoc
|
||||
canvasView.onDocumentChanged = { [weak self] in
|
||||
self?.canvasView.needsDisplay = true
|
||||
}
|
||||
|
||||
let toolbarHosting = NSHostingView(rootView: EditorToolbarView(
|
||||
document: annotationDoc,
|
||||
onSave: { [weak self] in self?.save() },
|
||||
onCopyToClipboard: { [weak self] in self?.copyToClipboard() },
|
||||
onCancel: { [weak self] in self?.cancel() }
|
||||
))
|
||||
toolbarHosting.translatesAutoresizingMaskIntoConstraints = false
|
||||
canvasView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let container = FlippedContainerView()
|
||||
container.addSubview(toolbarHosting)
|
||||
container.addSubview(canvasView)
|
||||
window.contentView = container
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
toolbarHosting.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
toolbarHosting.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
toolbarHosting.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
toolbarHosting.heightAnchor.constraint(equalToConstant: 52),
|
||||
|
||||
canvasView.topAnchor.constraint(equalTo: toolbarHosting.bottomAnchor),
|
||||
canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
func showEditor() {
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
window?.makeFirstResponder(canvasView)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func renderFinal() -> NSImage {
|
||||
AnnotationRenderer.render(annotations: annotationDoc.items, onto: baseImage)
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let image = renderFinal()
|
||||
completion(image)
|
||||
close()
|
||||
}
|
||||
|
||||
private func copyToClipboard() {
|
||||
let image = renderFinal()
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.writeObjects([image])
|
||||
close()
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion(nil)
|
||||
close()
|
||||
}
|
||||
|
||||
// MARK: - NSWindowDelegate
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
let visibleWindows = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
|
||||
if visibleWindows.isEmpty {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,30 @@ struct MenuBarContentView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Letzte Aufnahme bearbeiten…") {
|
||||
coordinator.openEditorForLastCapture()
|
||||
}
|
||||
.disabled(coordinator.lastCapturedURL == nil)
|
||||
|
||||
Button("Aus Zwischenablage bearbeiten") {
|
||||
coordinator.openEditorFromClipboard()
|
||||
}
|
||||
|
||||
Button("Bild bearbeiten…") {
|
||||
coordinator.openEditorForFile()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Einstellungen…") {
|
||||
SettingsWindowController.shared.show(coordinator: coordinator)
|
||||
}
|
||||
.keyboardShortcut(",")
|
||||
|
||||
Button("Über SnipMC…") {
|
||||
AboutWindowController.shared.show()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Beenden") {
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import AppKit
|
||||
import ServiceManagement
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var coordinator: AppCoordinator
|
||||
@State private var launchAtLogin = SMAppService.mainApp.status == .enabled
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Allgemein") {
|
||||
Toggle("Mit dem System starten", isOn: $launchAtLogin)
|
||||
.onChange(of: launchAtLogin) { _, newValue in
|
||||
do {
|
||||
if newValue {
|
||||
try SMAppService.mainApp.register()
|
||||
} else {
|
||||
try SMAppService.mainApp.unregister()
|
||||
}
|
||||
} catch {
|
||||
launchAtLogin = SMAppService.mainApp.status == .enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Nach der Aufnahme") {
|
||||
Picker("Aktion:", selection: $coordinator.outputAction) {
|
||||
ForEach(OutputAction.allCases) { action in
|
||||
|
||||
@@ -32,7 +32,9 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
// Menu bar-only apps should not keep a Dock icon after the window closes.
|
||||
let otherVisible = NSApplication.shared.windows.filter { $0.isVisible && $0 != window }
|
||||
if otherVisible.isEmpty {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SnippingToolApp: App {
|
||||
struct SnipMCApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
var body: some Scene {
|
||||
MenuBarExtra("Snipping Tool", systemImage: "camera.viewfinder") {
|
||||
MenuBarExtra("SnipMC", systemImage: "camera.viewfinder") {
|
||||
MenuBarContentView()
|
||||
.environmentObject(appDelegate.coordinator)
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,11 +1,11 @@
|
||||
name: SnippingTool
|
||||
name: SnipMC
|
||||
options:
|
||||
bundleIdPrefix: it.wende
|
||||
deploymentTarget:
|
||||
macOS: "14.0"
|
||||
createIntermediateGroups: true
|
||||
targets:
|
||||
SnippingTool:
|
||||
SnipMC:
|
||||
type: application
|
||||
platform: macOS
|
||||
deploymentTarget: "14.0"
|
||||
@@ -14,29 +14,29 @@ targets:
|
||||
info:
|
||||
path: Generated/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: Snipping Tool
|
||||
CFBundleName: Snipping Tool
|
||||
CFBundleDisplayName: SnipMC
|
||||
CFBundleName: SnipMC
|
||||
LSUIElement: true
|
||||
NSHumanReadableCopyright: "Copyright © 2026"
|
||||
CFBundleURLTypes:
|
||||
- CFBundleURLName: it.wende.snippingtool
|
||||
- CFBundleURLName: it.wende.snipmc
|
||||
CFBundleURLSchemes:
|
||||
- snippingtool
|
||||
- snipmc
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snippingtool
|
||||
PRODUCT_NAME: Snipping Tool
|
||||
MARKETING_VERSION: "1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
PRODUCT_BUNDLE_IDENTIFIER: it.wende.snipmc
|
||||
PRODUCT_NAME: SnipMC
|
||||
MARKETING_VERSION: "1.0.2"
|
||||
CURRENT_PROJECT_VERSION: "3"
|
||||
SWIFT_VERSION: "5.0"
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
ENABLE_HARDENED_RUNTIME: false
|
||||
CODE_SIGN_IDENTITY: "-"
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
schemes:
|
||||
SnippingTool:
|
||||
SnipMC:
|
||||
build:
|
||||
targets:
|
||||
SnippingTool: all
|
||||
SnipMC: all
|
||||
run:
|
||||
config: Debug
|
||||
|
||||
Reference in New Issue
Block a user