Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CleanMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions CleanMac/CleanMacApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down
141 changes: 141 additions & 0 deletions CleanMac/Views/AboutView.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions CleanMac/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
10 changes: 10 additions & 0 deletions CleanMac/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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" = "Занято %@ из %@";
Expand Down
72 changes: 35 additions & 37 deletions contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions project-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
7 changes: 7 additions & 0 deletions trace.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading