diff --git a/CleanMac.xcodeproj/project.pbxproj b/CleanMac.xcodeproj/project.pbxproj index 211418a..db400c2 100644 --- a/CleanMac.xcodeproj/project.pbxproj +++ b/CleanMac.xcodeproj/project.pbxproj @@ -261,7 +261,7 @@ CODE_SIGN_ENTITLEMENTS = CleanMac/CleanMac.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -275,7 +275,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2.0; + MARKETING_VERSION = 0.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.codex.cleanmac; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -296,7 +296,7 @@ CODE_SIGN_ENTITLEMENTS = CleanMac/CleanMac.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -310,7 +310,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2.0; + MARKETING_VERSION = 0.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.codex.cleanmac; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/CleanMac/CleanMacApp.swift b/CleanMac/CleanMacApp.swift index f5ff13d..fae3a5f 100644 --- a/CleanMac/CleanMacApp.swift +++ b/CleanMac/CleanMacApp.swift @@ -4,6 +4,7 @@ import SwiftUI @main struct CleanMacApp: App { @NSApplicationDelegateAdaptor(CleanMacAppDelegate.self) private var appDelegate + @Environment(\.openWindow) private var openWindow @AppStorage(CleanMacAppearance.storageKey) private var appearanceMode = CleanMacAppearance.defaultCode @AppStorage(CleanMacLanguage.storageKey) private var languageCode = CleanMacLanguage.defaultCode @@ -23,6 +24,22 @@ struct CleanMacApp: App { } .defaultSize(width: 980, height: 720) .windowResizability(.contentMinSize) + .commands { + CommandGroup(replacing: .appInfo) { + Button(L.t("about.menu")) { + openWindow(id: "about") + } + } + } + + Window(L.t("about.windowTitle"), id: "about") { + AboutView() + .environment(\.locale, language.locale) + .preferredColorScheme(appearance.colorScheme) + } + .defaultSize(width: 520, height: 440) + .defaultPosition(.center) + .windowResizability(.contentSize) MenuBarExtra("CleanMac", image: "MenuBarIcon") { StatusMenuView() diff --git a/CleanMac/Views/AboutView.swift b/CleanMac/Views/AboutView.swift new file mode 100644 index 0000000..e1d9c40 --- /dev/null +++ b/CleanMac/Views/AboutView.swift @@ -0,0 +1,141 @@ +import SwiftUI + +struct AboutView: View { + @Environment(\.dismiss) private var dismiss + @AppStorage(CleanMacLanguage.storageKey) private var languageCode = CleanMacLanguage.defaultCode + + private let repositoryURL = URL(string: "https://github.com/Dezoff-max/CleanMac")! + private let releasesURL = URL(string: "https://github.com/Dezoff-max/CleanMac/releases")! + private let licenseURL = URL(string: "https://github.com/Dezoff-max/CleanMac/blob/main/LICENSE")! + + private var version: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "—" + } + + private var build: String { + Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "—" + } + + var body: some View { + ZStack { + MainWindowBackground() + + LinearGradient( + colors: [ + Color.accentColor.opacity(0.1), + Color.clear, + Color(nsColor: .windowBackgroundColor).opacity(0.22) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .allowsHitTesting(false) + + VStack(spacing: 12) { + Image("BrandIcon") + .resizable() + .scaledToFit() + .frame(width: 88, height: 88) + .shadow(color: .black.opacity(0.2), radius: 16, y: 7) + .accessibilityLabel(L.t("app.name")) + + VStack(spacing: 5) { + Text(L.t("app.name")) + .font(.system(size: 30, weight: .bold, design: .rounded)) + + Text(L.t("about.tagline")) + .font(.callout) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Text(L.f("about.version", version, build)) + .font(.caption.weight(.semibold)) + .foregroundStyle(.tint) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(.tint.opacity(0.11), in: Capsule()) + .textSelection(.enabled) + } + + VStack(spacing: 0) { + AboutRow(title: L.t("about.developer"), value: "@rootoff") + Divider() + AboutRow(title: L.t("about.license"), value: "MIT") + } + .background( + Color(nsColor: .controlBackgroundColor).opacity(0.9), + in: RoundedRectangle(cornerRadius: 14, style: .continuous) + ) + .overlay { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .strokeBorder(.separator.opacity(0.55)) + } + + HStack(spacing: 10) { + AboutLink( + title: L.t("about.github"), + systemImage: "chevron.left.forwardslash.chevron.right", + destination: repositoryURL + ) + AboutLink( + title: L.t("about.releases"), + systemImage: "arrow.down.circle", + destination: releasesURL + ) + AboutLink( + title: L.t("about.license"), + systemImage: "doc.text", + destination: licenseURL + ) + } + + Text(L.t("about.safetyNote")) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Button(L.t("about.close")) { + dismiss() + } + .keyboardShortcut(.defaultAction) + } + .padding(.horizontal, 36) + .padding(.vertical, 20) + } + .frame(width: 520, height: 440) + .navigationTitle(L.t("about.windowTitle")) + .id(languageCode) + } +} + +private struct AboutRow: View { + let title: String + let value: String + + var body: some View { + LabeledContent { + Text(value) + .fontWeight(.semibold) + } label: { + Text(title) + } + .font(.callout) + .padding(.horizontal, 14) + .padding(.vertical, 8) + } +} + +private struct AboutLink: View { + let title: String + let systemImage: String + let destination: URL + + var body: some View { + Link(destination: destination) { + Label(title, systemImage: systemImage) + .frame(minWidth: 104) + } + .buttonStyle(.bordered) + } +} diff --git a/CleanMac/en.lproj/Localizable.strings b/CleanMac/en.lproj/Localizable.strings index 3f4a0b9..3f463d0 100644 --- a/CleanMac/en.lproj/Localizable.strings +++ b/CleanMac/en.lproj/Localizable.strings @@ -16,6 +16,16 @@ "menu.open" = "Open CleanMac"; "menu.open.short" = "Open"; "menu.quit" = "Quit"; +"about.menu" = "About CleanMac"; +"about.windowTitle" = "About CleanMac"; +"about.tagline" = "Safe, local-first cleanup for macOS"; +"about.version" = "Version %@ (%@)"; +"about.developer" = "Developer"; +"about.license" = "License"; +"about.github" = "GitHub"; +"about.releases" = "Releases"; +"about.safetyNote" = "CleanMac scans first and moves only confirmed items to Trash."; +"about.close" = "Close"; "status.disk.defaultName" = "Macintosh HD"; "status.disk.percent" = "%d%% used"; "status.disk.usedOfTotal" = "%@ of %@ used"; diff --git a/CleanMac/ru.lproj/Localizable.strings b/CleanMac/ru.lproj/Localizable.strings index 842b327..4c262c8 100644 --- a/CleanMac/ru.lproj/Localizable.strings +++ b/CleanMac/ru.lproj/Localizable.strings @@ -16,6 +16,16 @@ "menu.open" = "Открыть CleanMac"; "menu.open.short" = "Открыть"; "menu.quit" = "Выйти"; +"about.menu" = "О CleanMac"; +"about.windowTitle" = "О CleanMac"; +"about.tagline" = "Безопасная локальная очистка macOS"; +"about.version" = "Версия %@ (%@)"; +"about.developer" = "Разработчик"; +"about.license" = "Лицензия"; +"about.github" = "GitHub"; +"about.releases" = "Релизы"; +"about.safetyNote" = "CleanMac сначала сканирует и перемещает в Корзину только подтверждённые элементы."; +"about.close" = "Закрыть"; "status.disk.defaultName" = "Macintosh HD"; "status.disk.percent" = "занято %d%%"; "status.disk.usedOfTotal" = "Занято %@ из %@"; diff --git a/contract.md b/contract.md index adc1ae7..dd0bac9 100644 --- a/contract.md +++ b/contract.md @@ -2,77 +2,75 @@ ## Task -- ID: TASK-032 -- Title: Multi-select application removal +- ID: TASK-033 +- Title: Custom About window and v0.2.1 release - Mode: continue ## Planner Notes -- Why this task now: the user explicitly requested checkbox selection for removing several applications from the Applications screen. -- Expected value: the user can build one reviewed removal set instead of confirming every app separately. -- Main risk: mixing detail selection with removal selection, losing per-app leftover choices, or touching leftovers after an individual app move fails. -- UX constraint: use native macOS checkbox controls; keep the last interacted app visible in the detail panel; preserve mandatory confirmation and Trash-only removal. +- Why this task now: the user asked to replace the sparse system About panel with a polished window consistent with their other macOS apps, before publishing the pending v0.2.1 release. +- Expected value: About CleanMac clearly presents the product, exact installed version/build, local-first safety model, author, license, and project links. +- Main risk: creating duplicate windows, hardcoding a stale version, or using macOS APIs newer than the current macOS 14 deployment target. +- UX constraint: preserve the current RU/EN language and light/dark appearance selections; keep the window compact, fixed-size, and independently closable. ## Builder Scope - Allowed files: - - `CleanMac/Views/ApplicationsView.swift` + - `CleanMac/CleanMacApp.swift` + - `CleanMac/Views/AboutView.swift` - `CleanMac/en.lproj/Localizable.strings` - `CleanMac/ru.lproj/Localizable.strings` + - `CleanMac.xcodeproj/project.pbxproj` - `project-analysis.md` - `roadmap.md` - `contract.md` - `progress.md` + - `trace.md` - `verification.md` - Allowed commands: - - `plutil -lint CleanMac/en.lproj/Localizable.strings CleanMac/ru.lproj/Localizable.strings` + - localization/plist lint - `swift test --package-path CleanMacCore` - `./script/build_and_run.sh --verify` - - `git diff --check` + - `./script/package_release.sh` - non-destructive UI inspection and screenshots + - focused Git/GitHub release commands after verification - Out of scope: - - permanent deletion, Empty Trash, privilege escalation, or forced process termination; - - changing the application scanner or path allowlists; - - selecting leftovers automatically; - - removing a real installed application during verification; - - unrelated cleanup, permissions, packaging, or release changes. + - cleanup behavior, scanner rules, application removal, permissions, signing identity setup, or notarization; + - adding dependencies or changing the macOS deployment target; + - presenting an ad-hoc package as Apple-notarized. - Dependencies allowed: no -- Destructive actions allowed: yes, only after the existing dedicated in-app confirmation and only for explicitly checked applications. +- Destructive actions allowed: no ## Evaluator Checklist - Done criteria: - - Every application row has a native checkbox with a visible checked state. - - Multiple applications can remain checked while one app is shown in the detail panel. - - Exact leftover choices are stored separately for each checked application. - - The summary and destructive button show the selected application count and total reviewed size. - - Confirmation lists the number of apps, selected leftovers, and total size. - - Each app is independently replanned and moved before its leftovers; one app failure cannot touch that app's leftovers or block safe reporting for the remaining checked apps. - - Successfully moved apps leave the list; failed apps remain selected for review. - - Refresh preserves only selections that still exist. + - The app menu replaces the system About action with a localized `About CleanMac` action. + - A singleton About scene opens centered and does not create duplicate windows. + - The window presents the CleanMac icon, name, localized tagline, exact bundle version/build, developer, local-first mode, MIT license, and working GitHub/Release/License links. + - The selected RU/EN language and light/dark appearance apply to the About window. + - The window has a deliberate fixed content size and normal close behavior. + - Version 0.2.1 build 3 is packaged and published with an honest unsigned/ad-hoc distribution note. - Required verification: - - `plutil -lint CleanMac/en.lproj/Localizable.strings CleanMac/ru.lproj/Localizable.strings` + - localization lint - `swift test --package-path CleanMacCore` - `./script/build_and_run.sh --verify` + - `./script/package_release.sh` - `git diff --check` - Manual checks: - - Check at least two applications and confirm both checkboxes remain selected. - - Switch details and verify per-app leftover choices do not leak to another app. - - Open the batch confirmation and cancel it without removing anything. -- Evidence to collect: - - Build/test command exit status. - - Accessibility state showing multiple checked rows and batch confirmation. - - UI screenshot path if captured. + - Open About from the application menu and confirm its visual hierarchy in Russian. + - Switch to English and dark appearance and confirm the About content updates. + - Invoke About more than once and confirm only one About window exists. + - Inspect the packaged app Info.plist for version 0.2.1 build 3 and verify the ZIP checksum. ## Restart Signals Restart or shrink the task if: -- SwiftUI checkbox composition breaks row accessibility or detail selection; -- batch execution would require weakening the existing single-app planner/executor checks; -- UI verification would require accepting a removal confirmation. +- the SwiftUI `Window` scene does not behave as a singleton on macOS 14; +- replacing `.appInfo` removes required application-menu behavior; +- visual verification reveals clipping or a fixed-size window that does not fit localized content. ## Result -- Status: complete -- Verification result: passed. All 15 core tests, localization lint, `git diff --check`, Debug build, and `./script/build_and_run.sh --verify` pass. Live accessibility review showed two independently checked apps, isolated per-app leftover state, and the correct batch confirmation. -- Notes: screenshot saved at `/tmp/cleanmac-task32-multiselect.jpg`. The batch confirmation was cancelled; no installed application or real leftover was moved. +- Status: in progress +- Verification result: pending +- Notes: the current branch already contains the intended v0.2.1 build-number bump; no release has been published yet. diff --git a/project-analysis.md b/project-analysis.md index 57d2d15..cbe20c9 100644 --- a/project-analysis.md +++ b/project-analysis.md @@ -57,6 +57,7 @@ CleanMac is a macOS menu bar and windowed system cleanup utility. The project wa - Permissions UI checks live Full Disk Access status by probing protected metadata/readability and can refresh the result. - Finder Automation now shows the live Apple Events consent state, requests access only from an explicit button, and uses the permission to reveal selected items while preserving the NSWorkspace fallback. - English and Russian app localizations are included; macOS selects the language from system preferences. +- The standard About panel is replaced by a centered singleton CleanMac window with live version/build metadata, local-first safety details, project links, and immediate RU/EN plus light/dark updates. - The selected language override applies through the app's localizer, and the selected appearance applies to both the main window and menu bar popover. - The menu bar popover shows current disk usage, scan-in-progress state, last scan source/time, and last scan result summary with larger readable typography and rounded compact panels. - Settings can enable read-only auto scan while the app is running; it supports daily, hourly, and every-two-hours frequencies, uses the currently selected scan areas, and updates menu bar status. diff --git a/roadmap.md b/roadmap.md index ffca24e..783284e 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,5 +1,19 @@ # Roadmap +- [ ] ID: TASK-033 + Title: Custom About window and v0.2.1 release + Goal: Replace the default About panel with a polished localized CleanMac window, then publish the verified v0.2.1 build. + What to do: Add a singleton SwiftUI About scene, live bundle version/build data, project links, RU/EN copy, theme support, and complete the protected PR/release flow. + Files: `CleanMac/CleanMacApp.swift`, `CleanMac/Views/AboutView.swift`, `CleanMac/*/Localizable.strings`, version settings, Loop docs + Definition of done: One fixed-size About window opens from the app menu, matches the selected language/theme, shows accurate metadata and working links, and v0.2.1 is released with verified assets. + Verification: localization lint; `swift test --package-path CleanMacCore`; `./script/build_and_run.sh --verify`; `./script/package_release.sh`; visual review; GitHub CI + Priority: high + Impact: medium + Risk: low + Effort: small + Confidence: high + Score: medium impact / low risk / small + - [x] ID: TASK-032 Title: Multi-select application removal Goal: Let the user check several applications and remove the reviewed set through one mandatory confirmation. diff --git a/trace.md b/trace.md index 3b9b3f6..758944e 100644 --- a/trace.md +++ b/trace.md @@ -29,3 +29,10 @@ Append-only trace of failures, restarts, and judgment divergences. - Cause: Xcode's Debug product uses separate preview/debug dynamic libraries, while ad-hoc signatures have no stable Team ID for hardened library validation. The raw build bundle also retained extended attributes that codesign refuses. - Fix: sanitize only the generated bundle, build Debug with `ENABLE_DEBUG_DYLIB=NO` so it has one executable, then sign the app ad-hoc with hardened runtime and the Automation entitlement. Release remains hardened and is signed in two passes so only the outer app receives the Automation entitlement. Because File Provider can reattach Finder metadata to `dist/CleanMac.app`, packaging now strictly verifies a fresh extraction of the metadata-free ZIP. - Status: resolved; `./script/build_and_run.sh --verify` launches successfully with `adhoc,runtime`, and the extracted Release ZIP passes strict signature, usage-description, and entitlement inspection. + +## 2026-07-11 - TASK-033 - Invisible About metadata rows + +- Symptom: the first custom About layout exposed all metadata to accessibility, but the four custom `HStack` rows rendered as empty bands in the live light and dark windows. +- Cause: the bespoke label/value row did not receive a stable native macOS layout under the fixed auxiliary window, even after contrast and height adjustments. +- Fix: replaced the custom row layout with native `LabeledContent`, kept the compact fixed-size scene, and rechecked both languages and appearances in the running app. +- Status: resolved; the live Russian/light and English/dark passes rendered every remaining metadata value before the user simplified the final card to Developer and License only. diff --git a/verification.md b/verification.md index 508c1f7..69977d7 100644 --- a/verification.md +++ b/verification.md @@ -46,6 +46,7 @@ | Area | Command or check | When to run | Success signal | Fallback | | --- | --- | --- | --- | --- | | macOS app launch | `./script/build_and_run.sh --verify` | UI, app lifecycle, assets, or project changes | Build succeeds and `pgrep -x CleanMac` finds the app | Run the xcodebuild command from this file and inspect launch logs | +| About window | Open the app menu item, switch RU/EN and light/dark, then inspect the Window menu | About scene, metadata, localization, theme, or links change | One fixed-size About window shows the active language/theme, accurate bundle version/build, and GitHub/Releases/License links | Inspect the accessibility tree and capture a screenshot without activating external links | | Core package | `swift test --package-path CleanMacCore` | Core model, scanner, or package changes | All tests pass | Run `cd CleanMacCore && swift test` | | Application removal | Temporary fake `.app` fixtures with an injected Trash handler plus read-only UI review | Application scanner, removal policy, multi-selection, or Applications UI changes | App target is moved first; failed app move leaves leftovers untouched; outside/forged paths are rejected; multiple checkboxes and per-app leftovers stay isolated; no real app is removed | Run the focused `testApplication*` SwiftPM tests and inspect the batch confirmation without accepting it | | Release package | `./script/package_release.sh` | Packaging, release, or CI artifact changes | `dist/*.zip` and `.sha256` are created and checksum passes | Inspect `build/XcodeData/Build/Products/Release` |