diff --git a/Sources/ManagedFavsGenerator/AppKitTextField.swift b/Sources/ManagedFavsGenerator/AppKitTextField.swift
index 694310c..2dfa1f4 100644
--- a/Sources/ManagedFavsGenerator/AppKitTextField.swift
+++ b/Sources/ManagedFavsGenerator/AppKitTextField.swift
@@ -43,6 +43,14 @@ struct AppKitTextField: NSViewRepresentable {
// KRITISCH: TextField muss explizit First Responder werden können
textField.refusesFirstResponder = false
+ // WICHTIG: Komplett deaktiviere Drag & Drop für dieses TextField
+ textField.unregisterDraggedTypes()
+
+ // Disable text field's cell from accepting drops
+ if let cell = textField.cell as? NSTextFieldCell {
+ cell.isScrollable = true
+ }
+
return textField
}
@@ -96,15 +104,43 @@ struct AppKitTextField: NSViewRepresentable {
/// - Explizites `acceptsFirstResponder = true`
/// - Automatische Window-Aktivierung beim Focus-Wechsel
/// - Window-Aktivierung beim Hinzufügen zur View-Hierarchie
+/// - Blockiert Drag & Drop Operations komplett durch eigenen Field Editor
private class KeyboardReceivingTextField: NSTextField {
override var acceptsFirstResponder: Bool { true }
override var canBecomeKeyView: Bool { true }
+ // Custom field editor that never accepts drops
+ private lazy var customFieldEditor: NoDragTextView = {
+ let textContainer = NSTextContainer()
+ let editor = NoDragTextView(frame: .zero, textContainer: textContainer)
+ editor.isFieldEditor = true
+ editor.isRichText = false
+ editor.importsGraphics = false
+ editor.allowsUndo = true
+ editor.backgroundColor = .clear
+ editor.drawsBackground = false
+ return editor
+ }()
+
/// Stellt sicher, dass das Window key wird, wenn das TextField First Responder wird
override func becomeFirstResponder() -> Bool {
- let result = super.becomeFirstResponder()
+ // Get or create the window's field editor and disable drag & drop
+ if let window = self.window,
+ let fieldEditor = window.fieldEditor(true, for: self) as? NSTextView {
+ fieldEditor.unregisterDraggedTypes()
+
+ // Permanently block re-registration
+ DispatchQueue.main.async {
+ fieldEditor.unregisterDraggedTypes()
+ }
+
+ // Schedule periodic unregistration
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ fieldEditor.unregisterDraggedTypes()
+ }
+ }
- // Sicherstellen, dass das Window auch key ist
+ let result = super.becomeFirstResponder()
self.window?.makeKeyAndOrderFront(nil)
return result
@@ -122,4 +158,87 @@ private class KeyboardReceivingTextField: NSTextField {
}
}
}
+
+
+ // MARK: - Drag & Drop Protection
+
+ /// Blockiert alle Drag Operations - kein visuelles Feedback
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
+ return [] // Reject all drags - no cursor feedback
+ }
+
+ override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
+ return [] // Reject all drags - no cursor feedback
+ }
+
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ return false // Never accept drops
+ }
+
+ override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ return false // Don't even prepare
+ }
+
+ override func draggingExited(_ sender: NSDraggingInfo?) {
+ // Do nothing - ignore exit
+ }
+
+ override func wantsPeriodicDraggingUpdates() -> Bool {
+ return false // No periodic updates needed
+ }
+}
+
+/// Custom NSTextView that completely disables drag & drop
+private class NoDragTextView: NSTextView {
+
+ override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
+ super.init(frame: frameRect, textContainer: container)
+ setupNoDrag()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupNoDrag()
+ }
+
+ private func setupNoDrag() {
+ // Never register any drag types
+ self.unregisterDraggedTypes()
+
+ // Disable additional drag features
+ self.allowsImageEditing = false
+ self.isAutomaticQuoteSubstitutionEnabled = false
+ self.isAutomaticLinkDetectionEnabled = false
+ }
+
+ // MARK: - Block all drag operations
+
+ override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
+ return []
+ }
+
+ override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
+ return []
+ }
+
+ override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ return false
+ }
+
+ override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
+ return false
+ }
+
+ override func wantsPeriodicDraggingUpdates() -> Bool {
+ return false
+ }
+
+ override func draggingExited(_ sender: NSDraggingInfo?) {
+ // Do nothing
+ }
+
+ // Prevent registering drag types
+ override func registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]) {
+ // Do nothing - never register drag types
+ }
}
diff --git a/Sources/ManagedFavsGenerator/ContentView.swift b/Sources/ManagedFavsGenerator/ContentView.swift
index 5f72369..9b9c6c3 100644
--- a/Sources/ManagedFavsGenerator/ContentView.swift
+++ b/Sources/ManagedFavsGenerator/ContentView.swift
@@ -9,6 +9,39 @@ struct ContentView: View {
@State private var viewModel = FavoritesViewModel()
@Environment(\.openWindow) private var openWindow
+ /// Root level items (no parent)
+ private var rootLevelItems: [Favorite] {
+ favorites.filter { $0.parentID == nil }.sorted { $0.order < $1.order }
+ }
+
+ /// Get children of a folder
+ private func childrenOf(_ folder: Favorite) -> [Favorite] {
+ favorites.filter { $0.parentID == folder.id }.sorted { $0.order < $1.order }
+ }
+
+ /// Handle drop operation
+ private func handleDrop(droppedIds: [String], toParent parentID: UUID?, atIndex index: Int) {
+ guard let droppedId = droppedIds.first,
+ let droppedUUID = UUID(uuidString: droppedId),
+ let favorite = favorites.first(where: { $0.id == droppedUUID }) else {
+ return
+ }
+
+ // Don't allow dropping a folder into itself
+ if let parentID = parentID, parentID == favorite.id {
+ return
+ }
+
+ // Don't allow dropping folders into other folders (only 1 level deep)
+ if favorite.isFolder && parentID != nil {
+ return
+ }
+
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.moveFavorite(favorite, toParent: parentID, atIndex: index, allFavorites: favorites)
+ }
+ }
+
var body: some View {
ZStack {
// Hidden helper view um First Responder zu aktivieren
@@ -40,6 +73,17 @@ struct ContentView: View {
.keyboardShortcut("n", modifiers: [.command])
.help("Add a new favorite (⌘N)")
+ // Add Folder
+ Button {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.addFolder()
+ }
+ } label: {
+ Label("Add Folder", systemImage: "folder.badge.plus")
+ }
+ .keyboardShortcut("n", modifiers: [.command, .shift])
+ .help("Add a new folder (⌘⇧N)")
+
Divider()
// Copy JSON
@@ -101,37 +145,113 @@ struct ContentView: View {
private var inputSection: some View {
VStack(alignment: .leading, spacing: 16) {
- // Toplevel Name
- VStack(alignment: .leading, spacing: 8) {
- Text("Toplevel Name")
- .font(.headline)
- AppKitTextField(
- text: $viewModel.toplevelName,
- placeholder: "e.g., managedFavs"
- )
- .frame(height: 22)
- }
-
- Divider()
-
// Favorites List
- VStack(alignment: .leading, spacing: 8) {
- Text("Favorites")
- .font(.headline)
+ VStack(alignment: .leading, spacing: 20) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Favorites")
+ .font(.title2)
+ .fontWeight(.bold)
+ Text("Add and organize your favorites")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
ScrollView {
VStack(spacing: 12) {
- ForEach(favorites) { favorite in
- FavoriteRowView(
- favorite: favorite,
- onRemove: {
- withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
- viewModel.removeFavorite(favorite)
+ // Drop zone BEFORE first root item (always visible)
+ Color.clear
+ .frame(height: 20)
+ .dropDestination(for: String.self) { droppedIds, location in
+ handleDrop(droppedIds: droppedIds, toParent: nil, atIndex: 0)
+ return true
+ }
+
+ // Root level items (no parent)
+ ForEach(Array(rootLevelItems.enumerated()), id: \.element.id) { index, item in
+ if item.isFolder {
+ // Folder with children
+ VStack(spacing: 0) {
+ DisclosureGroup {
+ VStack(spacing: 12) {
+ // Drop zone at START of folder (for first position)
+ Color.clear
+ .frame(height: 20)
+ .dropDestination(for: String.self) { droppedIds, location in
+ handleDrop(droppedIds: droppedIds, toParent: item.id, atIndex: 0)
+ return true
+ }
+
+ ForEach(Array(childrenOf(item).enumerated()), id: \.element.id) { childIndex, child in
+ FavoriteRowView(
+ favorite: child,
+ onRemove: {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.removeFavorite(child)
+ }
+ }
+ )
+ .padding(.leading, 16)
+ .transition(.scale.combined(with: .opacity))
+ .dropDestination(for: String.self) { droppedIds, location in
+ // Drop AFTER this child
+ handleDrop(droppedIds: droppedIds, toParent: item.id, atIndex: childIndex + 1)
+ return true
+ }
+ }
+
+ // Drop zone at END of folder (when folder is empty or after last item)
+ if childrenOf(item).isEmpty {
+ Color.clear
+ .frame(height: 40)
+ .dropDestination(for: String.self) { droppedIds, location in
+ handleDrop(droppedIds: droppedIds, toParent: item.id, atIndex: 0)
+ return true
+ }
+ }
+ }
+ } label: {
+ FolderRowView(
+ folder: item,
+ onRemove: {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.removeFavorite(item)
+ }
+ },
+ onAddChild: {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.addFavorite(parentID: item.id)
+ }
+ }
+ )
+ }
+ .disclosureGroupStyle(.automatic)
+ }
+ } else {
+ // Regular favorite
+ FavoriteRowView(
+ favorite: item,
+ onRemove: {
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
+ viewModel.removeFavorite(item)
+ }
}
+ )
+ .transition(.scale.combined(with: .opacity))
+ .dropDestination(for: String.self) { droppedIds, location in
+ // Drop after this favorite
+ handleDrop(droppedIds: droppedIds, toParent: nil, atIndex: index + 1)
+ return true
}
- )
- .transition(.scale.combined(with: .opacity))
+ }
}
+
+ // Drop zone at end of root level
+ Color.clear
+ .frame(height: 40)
+ .dropDestination(for: String.self) { droppedIds, location in
+ handleDrop(droppedIds: droppedIds, toParent: nil, atIndex: rootLevelItems.count)
+ return true
+ }
}
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: favorites.count)
}
@@ -224,11 +344,13 @@ struct FavoriteRowView: View {
@Bindable var favorite: Favorite
let onRemove: () -> Void
@State private var isHovering = false
+ @State private var isDragging = false
/// Generates the favicon URL using Google's favicon service
private var faviconURL: URL? {
- guard !favorite.url.isEmpty,
- let url = URL(string: favorite.url),
+ guard let urlString = favorite.url,
+ !urlString.isEmpty,
+ let url = URL(string: urlString),
let domain = url.host else {
return nil
}
@@ -286,7 +408,10 @@ struct FavoriteRowView: View {
.frame(height: 22)
AppKitTextField(
- text: $favorite.url,
+ text: Binding(
+ get: { favorite.url ?? "" },
+ set: { favorite.url = $0.isEmpty ? nil : $0 }
+ ),
placeholder: "URL"
)
.frame(height: 22)
@@ -295,10 +420,34 @@ struct FavoriteRowView: View {
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(isHovering ? 0.12 : 0.08), radius: isHovering ? 12 : 8, y: isHovering ? 6 : 4)
.scaleEffect(isHovering ? 1.01 : 1.0)
+ .opacity(isDragging ? 0.5 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovering)
+ .animation(.easeInOut(duration: 0.2), value: isDragging)
.onHover { hovering in
isHovering = hovering
}
+ .draggable(favorite.id.uuidString) {
+ // Drag preview
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: "star.fill")
+ .foregroundStyle(.yellow)
+ Text(favorite.name)
+ .font(.headline)
+ }
+ if let url = favorite.url {
+ Text(url)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ }
+ .padding(12)
+ .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 8))
+ .shadow(radius: 8)
+ .onAppear { isDragging = true }
+ .onDisappear { isDragging = false }
+ }
}
}
diff --git a/Sources/ManagedFavsGenerator/Favorite.swift b/Sources/ManagedFavsGenerator/Favorite.swift
index a766aae..35948d2 100644
--- a/Sources/ManagedFavsGenerator/Favorite.swift
+++ b/Sources/ManagedFavsGenerator/Favorite.swift
@@ -1,19 +1,28 @@
import Foundation
import SwiftData
-/// SwiftData Model für Favoriten
+/// SwiftData Model für Favoriten und Ordner
// Note: @Model is a SwiftData macro, not a GitHub user mention
@Model
final class Favorite {
@Attribute(.unique) var id: UUID
var name: String
- var url: String
+ var url: String? // nil = Folder, String = Favorite
+ var parentID: UUID? // nil = Root level, UUID = Inside folder
+ var order: Int = 0 // For sorting within same level (default: 0)
var createdAt: Date
- init(id: UUID = UUID(), name: String = "", url: String = "") {
+ /// Computed property to check if this is a folder
+ var isFolder: Bool {
+ url == nil
+ }
+
+ init(id: UUID = UUID(), name: String = "", url: String? = "", parentID: UUID? = nil, order: Int = 0) {
self.id = id
self.name = name
self.url = url
+ self.parentID = parentID
+ self.order = order
self.createdAt = Date()
}
}
diff --git a/Sources/ManagedFavsGenerator/FavoritesViewModel.swift b/Sources/ManagedFavsGenerator/FavoritesViewModel.swift
index a8c598f..3aa36d7 100644
--- a/Sources/ManagedFavsGenerator/FavoritesViewModel.swift
+++ b/Sources/ManagedFavsGenerator/FavoritesViewModel.swift
@@ -45,13 +45,13 @@ class FavoritesViewModel {
// MARK: - Business Logic
- func addFavorite() {
+ func addFavorite(parentID: UUID? = nil) {
guard let modelContext = modelContext else {
logger.error("ModelContext nicht verfügbar")
return
}
- let favorite = Favorite()
+ let favorite = Favorite(parentID: parentID)
modelContext.insert(favorite)
do {
@@ -63,6 +63,25 @@ class FavoritesViewModel {
}
}
+ func addFolder() {
+ guard let modelContext = modelContext else {
+ logger.error("ModelContext nicht verfügbar")
+ return
+ }
+
+ // Folder = Favorite with url = nil
+ let folder = Favorite(name: "New Folder", url: nil)
+ modelContext.insert(folder)
+
+ do {
+ try modelContext.save()
+ logger.info("Ordner hinzugefügt und gespeichert")
+ } catch {
+ logger.error("Fehler beim Speichern: \(error.localizedDescription)")
+ handleError(error)
+ }
+ }
+
func removeFavorite(_ favorite: Favorite) {
guard let modelContext = modelContext else {
logger.error("ModelContext nicht verfügbar")
@@ -80,6 +99,41 @@ class FavoritesViewModel {
}
}
+ // MARK: - Drag & Drop
+
+ func moveFavorite(_ favorite: Favorite, toParent newParentID: UUID?, atIndex index: Int, allFavorites: [Favorite]) {
+ guard let modelContext = modelContext else {
+ logger.error("ModelContext nicht verfügbar")
+ return
+ }
+
+ // Update parentID
+ favorite.parentID = newParentID
+
+ // Reorder siblings at target location
+ let siblings = allFavorites
+ .filter { $0.parentID == newParentID && $0.id != favorite.id }
+ .sorted { $0.order < $1.order }
+
+ // Insert at new position
+ var reorderedSiblings = siblings
+ let targetIndex = min(index, reorderedSiblings.count)
+ reorderedSiblings.insert(favorite, at: targetIndex)
+
+ // Update order values
+ for (idx, item) in reorderedSiblings.enumerated() {
+ item.order = idx
+ }
+
+ do {
+ try modelContext.save()
+ logger.info("Favorit verschoben: parentID=\(newParentID?.uuidString ?? "root"), order=\(favorite.order)")
+ } catch {
+ logger.error("Fehler beim Verschieben: \(error.localizedDescription)")
+ handleError(error)
+ }
+ }
+
func copyToClipboard(_ text: String) {
do {
try clipboardService.copyToClipboard(text)
diff --git a/Sources/ManagedFavsGenerator/FolderRowView.swift b/Sources/ManagedFavsGenerator/FolderRowView.swift
new file mode 100644
index 0000000..783e2e7
--- /dev/null
+++ b/Sources/ManagedFavsGenerator/FolderRowView.swift
@@ -0,0 +1,65 @@
+import SwiftUI
+
+/// View für Ordner-Darstellung
+struct FolderRowView: View {
+ @Bindable var folder: Favorite
+ let onRemove: () -> Void
+ let onAddChild: () -> Void
+ @State private var isHovering = false
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 10) {
+ HStack {
+ // Folder Icon + Title
+ HStack(spacing: 8) {
+ Image(systemName: "folder.fill")
+ .foregroundStyle(.yellow)
+ .frame(width: 20, height: 20)
+ .imageScale(.medium)
+
+ Label("Folder", systemImage: "folder.fill")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundStyle(.secondary)
+ .imageScale(.small)
+ .labelStyle(.titleOnly)
+ }
+
+ Spacer()
+
+ // Add to Folder Button
+ Button(action: onAddChild) {
+ Image(systemName: "plus.circle.fill")
+ .foregroundStyle(.blue)
+ .imageScale(.medium)
+ }
+ .buttonStyle(.plain)
+ .opacity(isHovering ? 1.0 : 0.6)
+ .help("Add favorite to this folder")
+
+ Button(action: onRemove) {
+ Image(systemName: "trash")
+ .foregroundStyle(.red)
+ .imageScale(.medium)
+ }
+ .buttonStyle(.plain)
+ .opacity(isHovering ? 1.0 : 0.6)
+ .help("Delete this folder")
+ }
+
+ AppKitTextField(
+ text: $folder.name,
+ placeholder: "Folder Name"
+ )
+ .frame(height: 22)
+ }
+ .padding(16)
+ .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
+ .shadow(color: .black.opacity(isHovering ? 0.12 : 0.08), radius: isHovering ? 12 : 8, y: isHovering ? 6 : 4)
+ .scaleEffect(isHovering ? 1.01 : 1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovering)
+ .onHover { hovering in
+ isHovering = hovering
+ }
+ }
+}
diff --git a/Sources/ManagedFavsGenerator/FormatGenerator.swift b/Sources/ManagedFavsGenerator/FormatGenerator.swift
index fa5f3a1..8478142 100644
--- a/Sources/ManagedFavsGenerator/FormatGenerator.swift
+++ b/Sources/ManagedFavsGenerator/FormatGenerator.swift
@@ -4,24 +4,46 @@ enum FormatGenerator {
// JSON for GPO (Windows onPrem) and Intune Settings Catalog (Windows)
static func generateJSON(toplevelName: String, favorites: [Favorite]) -> String {
- var items: [[String: String]] = []
+ var items: [[String: Any]] = []
// First item: toplevel_name
items.append(["toplevel_name": toplevelName])
- // Favorites
- for favorite in favorites where !favorite.name.isEmpty && !favorite.url.isEmpty {
- items.append([
- "url": favorite.url,
- "name": favorite.name
- ])
- }
+ // Get root level items (no parent)
+ let rootItems = favorites.filter { $0.parentID == nil }.sorted { $0.order < $1.order }
- // Use JSONEncoder with .withoutEscapingSlashes to prevent https:// -> https:\/\/
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
+ for item in rootItems where !item.name.isEmpty {
+ if item.isFolder {
+ // Folder: name + children array
+ let children = favorites.filter { $0.parentID == item.id }.sorted { $0.order < $1.order }
+ var childrenArray: [[String: String]] = []
+
+ for child in children where !child.name.isEmpty {
+ if let url = child.url, !url.isEmpty {
+ childrenArray.append([
+ "name": child.name,
+ "url": url
+ ])
+ }
+ }
+
+ items.append([
+ "name": item.name,
+ "children": childrenArray
+ ])
+ } else {
+ // Regular favorite: name + url
+ if let url = item.url, !url.isEmpty {
+ items.append([
+ "name": item.name,
+ "url": url
+ ])
+ }
+ }
+ }
- guard let jsonData = try? encoder.encode(items),
+ // Use JSONSerialization for [String: Any] support
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: items, options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return "[]"
}
@@ -46,14 +68,43 @@ enum FormatGenerator {
plistLines.append("\t\t\t\(toplevelName.xmlEscaped)")
plistLines.append("\t\t")
- // Favorites
- for favorite in favorites where !favorite.name.isEmpty && !favorite.url.isEmpty {
- plistLines.append("\t\t")
- plistLines.append("\t\t\tname")
- plistLines.append("\t\t\t\(favorite.name.xmlEscaped)")
- plistLines.append("\t\t\turl")
- plistLines.append("\t\t\t\(favorite.url.xmlEscaped)")
- plistLines.append("\t\t")
+ // Get root level items (no parent)
+ let rootItems = favorites.filter { $0.parentID == nil }.sorted { $0.order < $1.order }
+
+ for item in rootItems where !item.name.isEmpty {
+ if item.isFolder {
+ // Folder: children array FIRST, then name (Microsoft format)
+ plistLines.append("\t\t")
+ plistLines.append("\t\t\tchildren")
+ plistLines.append("\t\t\t")
+
+ let children = favorites.filter { $0.parentID == item.id }.sorted { $0.order < $1.order }
+ for child in children where !child.name.isEmpty {
+ if let url = child.url, !url.isEmpty {
+ plistLines.append("\t\t\t\t")
+ plistLines.append("\t\t\t\t\tname")
+ plistLines.append("\t\t\t\t\t\(child.name.xmlEscaped)")
+ plistLines.append("\t\t\t\t\turl")
+ plistLines.append("\t\t\t\t\t\(url.xmlEscaped)")
+ plistLines.append("\t\t\t\t")
+ }
+ }
+
+ plistLines.append("\t\t\t")
+ plistLines.append("\t\t\tname")
+ plistLines.append("\t\t\t\(item.name.xmlEscaped)")
+ plistLines.append("\t\t")
+ } else {
+ // Regular favorite: name + url
+ if let url = item.url, !url.isEmpty {
+ plistLines.append("\t\t")
+ plistLines.append("\t\t\tname")
+ plistLines.append("\t\t\t\(item.name.xmlEscaped)")
+ plistLines.append("\t\t\turl")
+ plistLines.append("\t\t\t\(url.xmlEscaped)")
+ plistLines.append("\t\t")
+ }
+ }
}
plistLines.append("\t")
diff --git a/Sources/ManagedFavsGenerator/SettingsView.swift b/Sources/ManagedFavsGenerator/SettingsView.swift
index ce3c068..6e57e8a 100644
--- a/Sources/ManagedFavsGenerator/SettingsView.swift
+++ b/Sources/ManagedFavsGenerator/SettingsView.swift
@@ -2,11 +2,30 @@ import SwiftUI
/// Settings View für App-Einstellungen
struct SettingsView: View {
+ @State private var viewModel = FavoritesViewModel()
+
var body: some View {
Form {
+ Section {
+ LabeledContent("Toplevel Name") {
+ TextField("e.g., managedFavs", text: $viewModel.toplevelName)
+ .textFieldStyle(.roundedBorder)
+ .frame(width: 250)
+ }
+ .help("The toplevel name for your managed favorites structure")
+
+ Text("This name appears as the first entry in the exported JSON/Plist configuration.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ } header: {
+ Text("Configuration")
+ }
+
+ Divider()
+
Section {
LabeledContent("Version") {
- Text("1.0.0")
+ Text("1.1.0")
.foregroundStyle(.secondary)
}
@@ -19,7 +38,7 @@ struct SettingsView: View {
}
}
.formStyle(.grouped)
- .frame(width: 500, height: 200)
+ .frame(width: 550, height: 300)
}
}