Compare commits

...

10 commits
1.0 ... main

Author SHA1 Message Date
Paweł Orzech
524f3790c9
build: bump marketing version to 1.0.3 2026-01-18 21:59:18 +00:00
Paweł Orzech
d3252d8599
chore: remove release artifact from repo 2026-01-18 03:31:41 +00:00
Paweł Orzech
84f413fdcf
build: bump marketing version to 1.0.2 2026-01-18 03:29:55 +00:00
Paweł Orzech
bf916bf636
feat: Implement custom text view with placeholder, disable focus rings, and add Cmd+W to close the note window. 2026-01-18 03:27:30 +00:00
Paweł Orzech
3d710b8eeb
chore: release v1.0.1 with CLAUDE.md documentation
Update release artifact to 1.0.1 and add Claude Code guidance file.
2026-01-18 03:05:16 +00:00
Paweł Orzech
87e5c34fe3
build: bump marketing version to 1.0.1 2026-01-18 02:42:59 +00:00
Paweł Orzech
1f2a4bab1c
feat: Add "Quit App" button to the menu bar. 2026-01-18 02:42:04 +00:00
Paweł Orzech
9ea4974123
feat: Set up initial macOS application bundle structure and icon assets. 2026-01-18 02:41:42 +00:00
Paweł Orzech
5d271e417c
chore: Update FastMemos 1.0.0 release artifact. 2026-01-18 02:39:52 +00:00
Paweł Orzech
d7db888aad
feat: implement launch at login functionality with settings UI. 2026-01-18 02:39:36 +00:00
16 changed files with 581 additions and 74 deletions

65
CLAUDE.md Normal file
View file

@ -0,0 +1,65 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Development
This is a native macOS SwiftUI app. Open `FastMemos.xcodeproj` in Xcode and build with `⌘B`.
From the command line:
```bash
xcodebuild -project FastMemos.xcodeproj -scheme FastMemos -configuration Debug build
```
To run:
```bash
xcodebuild -project FastMemos.xcodeproj -scheme FastMemos -configuration Debug build
open ./build/Debug/FastMemos.app
```
**Requirements:** macOS 13.0+ (Ventura), Xcode 15+
## Architecture
FastMemos is a menubar-only macOS app for quickly capturing notes to a self-hosted [Memos](https://github.com/usememos/memos) server.
### Key Components
**Entry Point (`FastMemosApp.swift`):**
- `AppDelegate` sets up the menubar status item and popover
- App runs as `.accessory` (no dock icon)
- Global hotkey (⌘⇧M) triggers `showNoteWindow()`
- `NotePanel` is a floating `NSPanel` for the note capture UI
**State Management:**
- `AppState` (in `ViewModels/`) is the single source of truth, passed to all views
- Publishes: `isLoggedIn`, `serverURL`, `defaultVisibility`, `isLoading`, `lastError`, `launchAtLogin`
- Handles authentication flow and memo creation
**Services (in `Services/`):**
- `MemosAPIService` - Communicates with Memos v1 API (`/api/v1/memos`, `/api/v1/auth/status`)
- `KeychainService` - Stores access tokens securely in macOS Keychain (service: `me.orzech.FastMemos`)
- `ShortcutService` - Registers global hotkey using Carbon Events API
**Views (in `Views/`):**
- `MenuBarView` - Main popover shown from menubar icon
- `NoteWindowView` - Floating note capture panel with visibility picker
- `LoginView` - Access token authentication flow
- `SettingsView` - App preferences (visibility, launch at login)
**Models (in `Models/`):**
- `Memo`, `CreateMemoRequest` - API request/response structures
- `MemoVisibility` - Enum: `.private`, `.protected`, `.public`
### Data Flow
1. User triggers hotkey → `AppDelegate.showNoteWindow()``NotePanel` appears
2. User writes note, selects visibility, presses ⌘Enter
3. `NoteWindowView.submitMemo()``AppState.createMemo()``MemosAPIService.createMemo()`
4. Token from `KeychainService` is used for Bearer auth
### Settings Storage
- `UserDefaults`: `serverURL`, `defaultVisibility`, `shortcutKeyCode`, `shortcutModifiers`
- Keychain: `accessToken`, `username`
- `SMAppService.mainApp` for launch at login

Binary file not shown.

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>25B78</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>FastMemos</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>me.orzech.FastMemos</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>FastMemos</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>25C57</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>26.2</string>
<key>DTSDKBuild</key>
<string>25C57</string>
<key>DTSDKName</key>
<string>macosx26.2</string>
<key>DTXcode</key>
<string>2620</string>
<key>DTXcodeBuild</key>
<string>17C52</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key>
<string>26.0</string>
<key>LSUIElement</key>
<true/>
<key>NSAccentColorName</key>
<string>AccentColor</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Paweł Orzech. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

Binary file not shown.

View file

@ -0,0 +1 @@
APPL????

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/AppIcon.icns</key>
<data>
ORGbkp1S899rWbEjZ/tNkVVLdro=
</data>
<key>Resources/Assets.car</key>
<data>
cksUAC98EafZIfKsBaoS+aP805o=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/AppIcon.icns</key>
<dict>
<key>hash2</key>
<data>
ME58VQGz3AD9X5q7Xj654HeiGHgQ4D2k8mSLplwLvc4=
</data>
</dict>
<key>Resources/Assets.car</key>
<dict>
<key>hash2</key>
<data>
m3hT/zd8OdcdoapvIxDo+aPRvDvSej90Wfiy+aU6GlE=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View file

@ -343,7 +343,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = me.orzech.FastMemos; PRODUCT_BUNDLE_IDENTIFIER = me.orzech.FastMemos;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -372,7 +372,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = me.orzech.FastMemos; PRODUCT_BUNDLE_IDENTIFIER = me.orzech.FastMemos;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;

View file

@ -3,52 +3,62 @@
{ {
"idiom": "mac", "idiom": "mac",
"scale": "1x", "scale": "1x",
"size": "16x16" "size": "16x16",
"filename": "icon_16x16.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "2x", "scale": "2x",
"size": "16x16" "size": "16x16",
"filename": "icon_16x16@2x.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "1x", "scale": "1x",
"size": "32x32" "size": "32x32",
"filename": "icon_32x32.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "2x", "scale": "2x",
"size": "32x32" "size": "32x32",
"filename": "icon_32x32@2x.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "1x", "scale": "1x",
"size": "128x128" "size": "128x128",
"filename": "icon_128x128.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "2x", "scale": "2x",
"size": "128x128" "size": "128x128",
"filename": "icon_128x128@2x.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "1x", "scale": "1x",
"size": "256x256" "size": "256x256",
"filename": "icon_256x256.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "2x", "scale": "2x",
"size": "256x256" "size": "256x256",
"filename": "icon_256x256@2x.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "1x", "scale": "1x",
"size": "512x512" "size": "512x512",
"filename": "icon_512x512.png"
}, },
{ {
"idiom": "mac", "idiom": "mac",
"scale": "2x", "scale": "2x",
"size": "512x512" "size": "512x512",
"filename": "icon_512x512@2x.png"
} }
], ],
"info": { "info": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,001 B

View file

@ -2,37 +2,37 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>AppIcon</string> <string>AppIcon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.0</string> <string>1.0.2</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string> <string>public.app-category.productivity</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<true/> <true/>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Paweł Orzech. All rights reserved.</string> <string>Copyright © 2026 Paweł Orzech. All rights reserved.</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>
<string></string> <string></string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
</dict> </dict>
</plist> </plist>

View file

@ -1,5 +1,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import ServiceManagement
/// Global app state that persists settings and manages authentication /// Global app state that persists settings and manages authentication
class AppState: ObservableObject { class AppState: ObservableObject {
@ -10,11 +11,23 @@ class AppState: ObservableObject {
@Published var isLoading: Bool = false @Published var isLoading: Bool = false
@Published var lastError: String? @Published var lastError: String?
@Published var launchAtLogin: Bool = false {
didSet {
if launchAtLogin {
try? SMAppService.mainApp.register()
} else {
try? SMAppService.mainApp.unregister()
}
}
}
private let keychainService = KeychainService() private let keychainService = KeychainService()
private lazy var apiService = MemosAPIService() private lazy var apiService = MemosAPIService()
init() { init() {
loadSettings() loadSettings()
// Check current launch at login status
launchAtLogin = SMAppService.mainApp.status == .enabled
} }
// MARK: - Settings Persistence // MARK: - Settings Persistence

View file

@ -127,6 +127,13 @@ struct MenuBarView: View {
.font(.caption2) .font(.caption2)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Spacer() Spacer()
Button(action: { NSApplication.shared.terminate(nil) }) {
Text("Quit App")
.font(.caption2)
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
} }
} }
.padding() .padding()

View file

@ -1,9 +1,40 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
/// Custom NSHostingView that never draws focus ring
class NoFocusRingHostingView<Content: View>: NSHostingView<Content> {
override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
override func drawFocusRingMask() {
// Don't draw focus ring
}
override var focusRingMaskBounds: NSRect {
.zero
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
// Disable focus ring on all subviews after view hierarchy is set up
DispatchQueue.main.async {
self.disableFocusRingsRecursively(in: self)
}
}
private func disableFocusRingsRecursively(in view: NSView) {
view.focusRingType = .none
for subview in view.subviews {
disableFocusRingsRecursively(in: subview)
}
}
}
/// Floating panel for quick note capture /// Floating panel for quick note capture
class NotePanel: NSPanel { class NotePanel: NSPanel {
private var hostingView: NSHostingView<NoteWindowView>? private var hostingView: NoFocusRingHostingView<NoteWindowView>?
private let appState: AppState private let appState: AppState
override var canBecomeKey: Bool { true } override var canBecomeKey: Bool { true }
@ -30,8 +61,8 @@ class NotePanel: NSPanel {
self.backgroundColor = .clear self.backgroundColor = .clear
self.isOpaque = false self.isOpaque = false
self.hasShadow = true self.hasShadow = true
self.autorecalculatesKeyViewLoop = false
// Set minimum size // Set minimum size
self.minSize = NSSize(width: 400, height: 200) self.minSize = NSSize(width: 400, height: 200)
self.maxSize = NSSize(width: 800, height: 600) self.maxSize = NSSize(width: 800, height: 600)
@ -43,7 +74,7 @@ class NotePanel: NSPanel {
self?.orderOut(nil) self?.orderOut(nil)
}) })
hostingView = NSHostingView(rootView: contentView) hostingView = NoFocusRingHostingView(rootView: contentView)
self.contentView = hostingView self.contentView = hostingView
} }
@ -53,10 +84,180 @@ class NotePanel: NSPanel {
self?.orderOut(nil) self?.orderOut(nil)
}) })
hostingView?.rootView = contentView hostingView?.rootView = contentView
self.center() self.center()
self.makeKeyAndOrderFront(nil) self.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
// Disable focus rings on all subviews
DispatchQueue.main.async { [weak self] in
if let contentView = self?.contentView {
Self.disableFocusRingsRecursively(in: contentView)
}
}
}
private static func disableFocusRingsRecursively(in view: NSView) {
view.focusRingType = .none
for subview in view.subviews {
disableFocusRingsRecursively(in: subview)
}
}
}
/// Custom NSTextView with placeholder support and no focus ring
struct PlaceholderTextView: NSViewRepresentable {
@Binding var text: String
let placeholder: String
let font: NSFont
var onSubmit: (() -> Void)?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NoFocusRingScrollView()
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autohidesScrollers = true
scrollView.borderType = .noBorder
scrollView.drawsBackground = false
scrollView.focusRingType = .none
scrollView.contentView.focusRingType = .none
let textView = PlaceholderNSTextView()
textView.delegate = context.coordinator
textView.isRichText = false
textView.allowsUndo = true
textView.font = font
textView.backgroundColor = .clear
textView.drawsBackground = false
textView.isEditable = true
textView.isSelectable = true
textView.focusRingType = .none
textView.textContainerInset = NSSize(width: 0, height: 0)
textView.textContainer?.lineFragmentPadding = 0
// Placeholder setup
textView.placeholderString = placeholder
textView.placeholderFont = font
textView.placeholderColor = NSColor.secondaryLabelColor
// Auto-resize
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
textView.autoresizingMask = [.width]
textView.textContainer?.containerSize = NSSize(width: scrollView.contentSize.width, height: .greatestFiniteMagnitude)
textView.textContainer?.widthTracksTextView = true
scrollView.documentView = textView
// Focus the text view and disable focus rings in hierarchy
DispatchQueue.main.async {
textView.window?.makeFirstResponder(textView)
Self.disableFocusRingsRecursively(in: scrollView)
}
return scrollView
}
private static func disableFocusRingsRecursively(in view: NSView) {
view.focusRingType = .none
for subview in view.subviews {
disableFocusRingsRecursively(in: subview)
}
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? PlaceholderNSTextView else { return }
if textView.string != text {
textView.string = text
}
textView.needsDisplay = true
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: PlaceholderTextView
init(_ parent: PlaceholderTextView) {
self.parent = parent
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
parent.text = textView.string
}
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
// Handle Cmd+Enter for submit
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
if NSEvent.modifierFlags.contains(.command) {
parent.onSubmit?()
return true
}
}
return false
}
}
}
/// Custom NSScrollView that never draws focus ring
class NoFocusRingScrollView: NSScrollView {
override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
override func drawFocusRingMask() {
// Don't draw focus ring
}
override var focusRingMaskBounds: NSRect {
.zero
}
}
/// Custom NSTextView that draws placeholder text
class PlaceholderNSTextView: NSTextView {
var placeholderString: String = ""
var placeholderFont: NSFont = NSFont.systemFont(ofSize: 15)
var placeholderColor: NSColor = NSColor.secondaryLabelColor
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Draw placeholder if text is empty
if string.isEmpty {
let attributes: [NSAttributedString.Key: Any] = [
.font: placeholderFont,
.foregroundColor: placeholderColor
]
let placeholderRect = NSRect(
x: textContainerInset.width + (textContainer?.lineFragmentPadding ?? 0),
y: textContainerInset.height,
width: bounds.width - textContainerInset.width * 2,
height: bounds.height - textContainerInset.height * 2
)
placeholderString.draw(in: placeholderRect, withAttributes: attributes)
}
}
override var needsPanelToBecomeKey: Bool { true }
override var acceptsFirstResponder: Bool { true }
override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
override func drawFocusRingMask() {
// Don't draw focus ring
}
override var focusRingMaskBounds: NSRect {
.zero
} }
} }
@ -64,38 +265,25 @@ class NotePanel: NSPanel {
struct NoteWindowView: View { struct NoteWindowView: View {
@ObservedObject var appState: AppState @ObservedObject var appState: AppState
let closeWindow: () -> Void let closeWindow: () -> Void
@State private var content: String = "" @State private var content: String = ""
@State private var visibility: MemoVisibility = .private @State private var visibility: MemoVisibility = .private
@State private var isSending = false @State private var isSending = false
@State private var showSuccess = false @State private var showSuccess = false
@State private var errorMessage: String? @State private var errorMessage: String?
@FocusState private var isTextEditorFocused: Bool
private let placeholder = "What's on your mind?" private let placeholder = "What's on your mind?"
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Main text area // Main text area
ZStack(alignment: .topLeading) { PlaceholderTextView(
// Placeholder text: $content,
if content.isEmpty { placeholder: placeholder,
Text(placeholder) font: NSFont.systemFont(ofSize: 15),
.foregroundColor(.secondary) onSubmit: submitMemo
.padding(.horizontal, 5) )
.padding(.vertical, 8) .frame(minHeight: 150)
.allowsHitTesting(false)
}
// Text editor
TextEditor(text: $content)
.font(.system(size: 15))
.focused($isTextEditorFocused)
.scrollContentBackground(.hidden)
.background(Color.clear)
.frame(minHeight: 150)
}
.padding(16) .padding(16)
// Bottom bar // Bottom bar
@ -182,15 +370,25 @@ struct NoteWindowView: View {
.padding(.vertical, 12) .padding(.vertical, 12)
} }
.padding(4) .padding(4)
.glassEffect(.regular, in: .rect(cornerRadius: 20)) .background(
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
)
.clipShape(RoundedRectangle(cornerRadius: 20))
.focusable(false)
.focusEffectDisabled()
.frame(minWidth: 400, minHeight: 200) .frame(minWidth: 400, minHeight: 200)
.onAppear { .onAppear {
visibility = appState.defaultVisibility visibility = appState.defaultVisibility
isTextEditorFocused = true
} }
.onExitCommand { .onExitCommand {
closeWindow() closeWindow()
} }
.background(
Button("") { closeWindow() }
.keyboardShortcut("w", modifiers: .command)
.hidden()
)
.animation(Animation.easeInOut(duration: 0.2), value: showSuccess) .animation(Animation.easeInOut(duration: 0.2), value: showSuccess)
.animation(Animation.easeInOut(duration: 0.2), value: errorMessage) .animation(Animation.easeInOut(duration: 0.2), value: errorMessage)
} }

View file

@ -25,6 +25,18 @@ struct SettingsView: View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
// General Section
VStack(alignment: .leading, spacing: 8) {
Label("General", systemImage: "gearshape")
.font(.subheadline)
.fontWeight(.medium)
Toggle("Launch at login", isOn: $appState.launchAtLogin)
.toggleStyle(.switch)
}
Divider()
// Default Visibility Section // Default Visibility Section
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Label("Default Visibility", systemImage: "eye") Label("Default Visibility", systemImage: "eye")