From b47310125ed104c4e871cd6147fa9f0d87e1b706 Mon Sep 17 00:00:00 2001 From: bebricoOOOOOOf Date: Mon, 7 Sep 2026 22:11:59 +0900 Subject: [PATCH] feat: add first-launch welcome window and handle app relaunch This change introduces a standalone onboarding window on first launch to guide users to the macOS menu bar, and handles app relaunch events: - Show a standalone Welcome window centered on screen on the first launch on a device (tracked via UserDefaults). - Display a simulated macOS menu bar with a visual pointer pointing to the status item (displaying live free disk space) so users know where ClearDisk lives. - Provide advice for users with crowded menu bars or MacBooks with a camera notch. - Provide a 'Got it! Go to Menu Bar' button that transitions the app to .accessory mode and opens the menu bar popover. - Implement applicationShouldHandleReopen in AppDelegate to toggle the popover/interface whenever the app is reopened from Finder or Spotlight. - Add Turkish translations for the onboarding strings. - Stage code signing in a clean temporary directory in build_app.sh to avoid iCloud Drive com.apple.provenance detritus errors. --- Resources/tr.lproj/Localizable.strings | 10 ++ Sources/ClearDisk/ClearDiskApp.swift | 39 ++++- Sources/ClearDisk/WelcomeWindow.swift | 220 +++++++++++++++++++++++++ scripts/build_app.sh | 8 +- 4 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 Sources/ClearDisk/WelcomeWindow.swift diff --git a/Resources/tr.lproj/Localizable.strings b/Resources/tr.lproj/Localizable.strings index 3bd100b..95f550f 100644 --- a/Resources/tr.lproj/Localizable.strings +++ b/Resources/tr.lproj/Localizable.strings @@ -590,3 +590,13 @@ "The downloaded update payload, staging copy, updater state, and updater logs." = "İndirilen güncelleme paketi, hazırlık kopyası, güncelleyici durumu ve güncelleyici günlükleri."; "The installed app, its settings, accounts, and documents." = "Kurulu uygulama, ayarları, hesapları ve belgeleri."; "Your account, playlists, local files, and explicitly downloaded offline library." = "Hesabınız, çalma listeleriniz, yerel dosyalarınız ve bilerek indirdiğiniz çevrimdışı kitaplığınız."; + +/* MARK: Welcome / Onboarding */ +"Welcome to ClearDisk" = "ClearDisk'e Hoş Geldiniz"; +"Developer cache cleaner & disk space monitor" = "Geliştirici önbellek temizleyici ve disk alanı izleyici"; +"ClearDisk lives here in your menu bar!" = "ClearDisk menü çubuğunuzda burada yer alır!"; +"Runs in your Menu Bar" = "Menü Çubuğunuzda Çalışır"; +"ClearDisk sits quietly in your menu bar without filling your Dock. Click its icon anytime to inspect caches and clean disk space." = "ClearDisk, Dock'unuzu doldurmadan menü çubuğunuzda sessizce çalışır. Önbellekleri incelemek ve disk alanını temizlemek için simgesine dilediğiniz zaman tıklayın."; +"MacBook Notch & Hidden Icons" = "MacBook Çentiği ve Gizli Simgeler"; +"If your menu bar is crowded or hidden behind a camera notch, you can also launch ClearDisk from Finder or Spotlight anytime to open it." = "Menü çubuğunuz kalabalıksa veya kamera çentiğinin arkasında kalıyorsa, ClearDisk'i açmak için Finder veya Spotlight'tan dilediğiniz zaman tekrar başlatabilirsiniz."; +"Got it! Go to Menu Bar" = "Anladım! Menü Çubuğuna Git"; diff --git a/Sources/ClearDisk/ClearDiskApp.swift b/Sources/ClearDisk/ClearDiskApp.swift index 67cb6d7..5cba760 100644 --- a/Sources/ClearDisk/ClearDiskApp.swift +++ b/Sources/ClearDisk/ClearDiskApp.swift @@ -22,7 +22,12 @@ struct ClearDiskApp { static func main() { let app = NSApplication.shared app.delegate = delegate - app.setActivationPolicy(.accessory) // Menu bar only, no dock icon + let isFirstLaunch = !UserDefaults.standard.bool(forKey: AppDelegate.hasSeenWelcomeWindowKey) + if isFirstLaunch { + app.setActivationPolicy(.regular) // Standalone window with Dock icon during first-launch onboarding + } else { + app.setActivationPolicy(.accessory) // Menu bar only, no dock icon + } app.run() } } @@ -50,6 +55,8 @@ extension Notification.Name { // MARK: - App Delegate class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { + static let hasSeenWelcomeWindowKey = "ClearDisk.hasSeenWelcomeWindow" + var welcomeWindowController: WelcomeWindowController? var statusItem: NSStatusItem! var popover: NSPopover! var diskMonitor: DiskMonitor! @@ -157,6 +164,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { self?.diskMonitor.refreshDiskSpaceOnly() } } + + // Show welcome window on first launch + if !UserDefaults.standard.bool(forKey: Self.hasSeenWelcomeWindowKey) { + welcomeWindowController = WelcomeWindowController( + diskMonitor: diskMonitor, + onDismiss: { [weak self] in + self?.completeWelcome() + } + ) + welcomeWindowController?.show() + } } func updateMenuBarIcon() { @@ -274,4 +292,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { } NotificationCenter.default.post(name: .clearDiskPopoverDidClose, object: nil) } + + func completeWelcome() { + UserDefaults.standard.set(true, forKey: Self.hasSeenWelcomeWindowKey) + NSApp.setActivationPolicy(.accessory) + welcomeWindowController?.close() + welcomeWindowController = nil + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in + self?.togglePopover() + } + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + if !UserDefaults.standard.bool(forKey: Self.hasSeenWelcomeWindowKey) { + welcomeWindowController?.show() + } else { + togglePopover() + } + return true + } } diff --git a/Sources/ClearDisk/WelcomeWindow.swift b/Sources/ClearDisk/WelcomeWindow.swift new file mode 100644 index 0000000..a251630 --- /dev/null +++ b/Sources/ClearDisk/WelcomeWindow.swift @@ -0,0 +1,220 @@ +import AppKit +import SwiftUI + +// MARK: - Welcome Window Controller +final class WelcomeWindowController: NSObject, NSWindowDelegate { + private let window: NSWindow + var onDismiss: (() -> Void)? + private var isDismissed = false + + init(diskMonitor: DiskMonitor, onDismiss: @escaping () -> Void) { + self.onDismiss = onDismiss + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 540, height: 470), + styleMask: [.titled, .closable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.title = L("Welcome to ClearDisk") + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.isReleasedWhenClosed = false + window.center() + + self.window = window + super.init() + + let rootView = WelcomeView(diskMonitor: diskMonitor) { [weak self] in + self?.dismiss() + } + window.contentViewController = NSHostingController(rootView: rootView) + window.delegate = self + } + + func show() { + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(nil) + } + + func close() { + window.close() + } + + private func dismiss() { + guard !isDismissed else { return } + isDismissed = true + close() + onDismiss?() + } + + func windowWillClose(_ notification: Notification) { + guard !isDismissed else { return } + isDismissed = true + onDismiss?() + } +} + +// MARK: - Welcome View +struct WelcomeView: View { + @ObservedObject var diskMonitor: DiskMonitor + let onDismiss: () -> Void + @AppStorage(AppAppearance.storageKey) private var appearance: AppAppearance = .system + + private var freeSpaceString: String { + let freeGB = Double(diskMonitor.freeSpace) / 1_073_741_824 + if freeGB >= 1.0 { + return " \(String(format: "%.0f", freeGB))GB" + } + return " 158GB" + } + + var body: some View { + VStack(spacing: 18) { + // Header + HStack(spacing: 16) { + if let icon = NSImage(named: "AppIcon") { + Image(nsImage: icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 54, height: 54) + } else { + Image(systemName: "externaldrive.badge.checkmark") + .font(.system(size: 42)) + .foregroundColor(.accentColor) + } + + VStack(alignment: .leading, spacing: 3) { + Text(L("Welcome to ClearDisk")) + .font(.system(size: 22, weight: .bold)) + + Text(L("Developer cache cleaner & disk space monitor")) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + Spacer() + } + + // Menu Bar Mockup with pointer + VStack(spacing: 8) { + HStack(spacing: 10) { + Image(systemName: "apple.logo") + .font(.system(size: 11)) + .foregroundColor(.secondary) + Text("Finder") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + Text("File Edit View Go Window") + .font(.system(size: 10)) + .foregroundColor(.secondary.opacity(0.5)) + + Spacer() + + // Simulated system status icons + Image(systemName: "wifi") + .font(.system(size: 10)) + .foregroundColor(.secondary) + Image(systemName: "battery.100") + .font(.system(size: 10)) + .foregroundColor(.secondary) + + // ClearDisk Highlighted Status Item + HStack(spacing: 4) { + Image(systemName: "externaldrive.fill") + .font(.system(size: 10)) + Text(freeSpaceString) + .font(.system(size: 10, weight: .bold)) + } + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background( + RoundedRectangle(cornerRadius: 5) + .fill(Color.accentColor.opacity(0.2)) + .overlay( + RoundedRectangle(cornerRadius: 5) + .stroke(Color.accentColor, lineWidth: 1.5) + ) + ) + } + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 7) + .fill(Color(NSColor.controlBackgroundColor)) + ) + + // Callout pointer to the menu bar item + HStack { + Spacer() + HStack(spacing: 5) { + Image(systemName: "arrow.up") + .font(.system(size: 11, weight: .bold)) + Text(L("ClearDisk lives here in your menu bar!")) + .font(.system(size: 11, weight: .semibold)) + } + .foregroundColor(.accentColor) + .padding(.trailing, 6) + } + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 9) + .fill(Color(NSColor.windowBackgroundColor).opacity(0.5)) + .overlay( + RoundedRectangle(cornerRadius: 9) + .stroke(Color.secondary.opacity(0.18), lineWidth: 1) + ) + ) + + // Info rows + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .top, spacing: 12) { + Image(systemName: "menubar.rectangle") + .font(.system(size: 16)) + .foregroundColor(.accentColor) + .frame(width: 22) + VStack(alignment: .leading, spacing: 2) { + Text(L("Runs in your Menu Bar")) + .font(.system(size: 12, weight: .semibold)) + Text(L("ClearDisk sits quietly in your menu bar without filling your Dock. Click its icon anytime to inspect caches and clean disk space.")) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + + HStack(alignment: .top, spacing: 12) { + Image(systemName: "laptopcomputer") + .font(.system(size: 16)) + .foregroundColor(.orange) + .frame(width: 22) + VStack(alignment: .leading, spacing: 2) { + Text(L("MacBook Notch & Hidden Icons")) + .font(.system(size: 12, weight: .semibold)) + Text(L("If your menu bar is crowded or hidden behind a camera notch, you can also launch ClearDisk from Finder or Spotlight anytime to open it.")) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + + Spacer() + + // Footer action button + HStack { + Spacer() + Button(action: onDismiss) { + Text(L("Got it! Go to Menu Bar")) + .font(.system(size: 13, weight: .semibold)) + .frame(width: 220, height: 32) + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + Spacer() + } + } + .padding(24) + .frame(width: 540, height: 470) + .preferredColorScheme(appearance.colorScheme) + } +} diff --git a/scripts/build_app.sh b/scripts/build_app.sh index 98979a6..17ca1fb 100755 --- a/scripts/build_app.sh +++ b/scripts/build_app.sh @@ -172,7 +172,13 @@ cat > "$APP_BUNDLE/Contents/Info.plist" << EOF EOF # Ad-hoc code sign the entire bundle (better Gatekeeper handling than linker-signed) -codesign --force --deep -s - "$APP_BUNDLE" +STAGE_DIR="$(mktemp -d /tmp/cleardisk-sign.XXXXXX)" +cp -R "$APP_BUNDLE" "$STAGE_DIR/$APP_NAME.app" +xattr -cr "$STAGE_DIR/$APP_NAME.app" +codesign --force --deep -s - "$STAGE_DIR/$APP_NAME.app" +rm -rf "$APP_BUNDLE" +ditto "$STAGE_DIR/$APP_NAME.app" "$APP_BUNDLE" +rm -rf "$STAGE_DIR" echo "Code signed (ad-hoc)." echo "Done! App bundle created at: $APP_BUNDLE"