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
7 changes: 3 additions & 4 deletions Scripts/build-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,10 @@ if [[ -d "$APP_ROOT" ]]; then
fi
mkdir -p "$OUTPUT_ROOT"
ditto --noextattr --noqtn "$STAGED_APP" "$APP_ROOT"
# Strip metadata in one pass immediately before verification. Removing
# File Provider attributes individually can cause FinderInfo to be re-applied
# between commands on synced workspace folders.
xattr -cr "$APP_ROOT"
# File Provider folders can immediately reapply bundle-level metadata after a
# copy. Remove those two attributes explicitly before the final verification.
xattr -d com.apple.FinderInfo "$APP_ROOT" 2>/dev/null || true
xattr -d 'com.apple.fileprovider.fpfs#P' "$APP_ROOT" 2>/dev/null || true
codesign --verify --deep --strict "$APP_ROOT"

echo "$APP_ROOT"
13 changes: 9 additions & 4 deletions Sources/CleanMyScreen/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,17 @@ struct ContentView: View {
.animation(.easeInOut(duration: 0.2), value: coordinator.selectedMode)
.animation(.easeInOut(duration: 0.2), value: coordinator.warningMessage)
.onChange(of: coordinator.sessionState) { _, state in
guard state == .active,
coordinator.selectedMode.hidesApplicationOnActivation
else {
return
let shouldHide = switch state {
case .idle:
false
case .countingDown:
coordinator.selectedMode.hidesApplicationWhenCountdownBegins
case .active:
coordinator.selectedMode.hidesApplicationOnActivation
}

guard shouldHide else { return }

// The app must keep running to enforce the lock, so hide it instead
// of terminating it. The menu-bar item remains available.
NSApp.hide(nil)
Expand Down
31 changes: 30 additions & 1 deletion Sources/CleanMyScreen/ModeContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct ModeContentView: View {
}

var body: some View {
VStack(spacing: 22) {
VStack(spacing: coordinator.selectedMode == .petKid ? 12 : 22) {
ModeHero(
mode: coordinator.selectedMode,
statusTitle: coordinator.statusTitle,
Expand All @@ -23,6 +23,10 @@ struct ModeContentView: View {
.disabled(!coordinator.sessionState.isIdle)
.opacity(coordinator.sessionState.isIdle ? 1 : 0.62)

if coordinator.selectedMode == .petKid {
PetVideoPreparationHint()
}

SessionActionView()

if coordinator.selectedMode == .selective {
Expand Down Expand Up @@ -144,6 +148,31 @@ struct ModeContentView: View {
}
}

private struct PetVideoPreparationHint: View {
var body: some View {
HStack(alignment: .center, spacing: 9) {
Image(systemName: "play.rectangle.fill")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(AppTheme.accent)
.frame(width: 21)

Text("After Start, this window hides. You’ll have 5 seconds to open or full-screen your video.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(maxWidth: 520, alignment: .leading)
.background(AppTheme.accent.opacity(0.055), in: RoundedRectangle(cornerRadius: 11, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 11, style: .continuous)
.stroke(AppTheme.accent.opacity(0.18), lineWidth: 1)
}
.accessibilityElement(children: .combine)
}
}

private struct ModeHero: View {
let mode: LockMode
let statusTitle: String
Expand Down
3 changes: 2 additions & 1 deletion Sources/CleanMyScreenKit/LockSessionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ public final class LockSessionCoordinator: ObservableObject {
}

countdownTask?.cancel()
let countdownSeconds = selectedMode.countdownSeconds
countdownTask = Task { [weak self] in
guard let self else { return }
for seconds in stride(from: 3, through: 1, by: -1) {
for seconds in stride(from: countdownSeconds, through: 1, by: -1) {
guard !Task.isCancelled else { return }
self.sessionState = .countingDown(seconds)
try? await Task.sleep(for: .seconds(1))
Expand Down
11 changes: 11 additions & 0 deletions Sources/CleanMyScreenKit/Models/LockMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ public enum LockMode: String, CaseIterable, Identifiable, Sendable {
public var hidesApplicationOnActivation: Bool {
self != .cleaning
}

public var hidesApplicationWhenCountdownBegins: Bool {
self == .petKid
}

public var countdownSeconds: Int {
switch self {
case .petKid: 5
case .cleaning, .selective: 3
}
}
}
4 changes: 2 additions & 2 deletions SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.2</string>
<string>0.1.3</string>
<key>CFBundleVersion</key>
<string>3</string>
<string>4</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSHighResolutionCapable</key>
Expand Down
19 changes: 19 additions & 0 deletions Tests/CleanMyScreenKitTests/CoordinatorFlowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ func permissionAllowsCountdown() async {
coordinator.cancelCountdown()
}

@MainActor
@Test("Pet and Kid mode provides a five-second preparation countdown")
func petKidPreparationCountdown() async {
let input = TestInputBlocker(hasMonitoringAccess: true)
let coordinator = LockSessionCoordinator(
inputBlocker: input,
hidBlocker: TestHIDBlocker(),
overlays: TestOverlayController(),
brightness: TestBrightnessController()
)
coordinator.selectedMode = .petKid

coordinator.startSelectedMode()
await Task.yield()

#expect(coordinator.sessionState == .countingDown(5))
coordinator.cancelCountdown()
}

private final class TestInputBlocker: InputBlocking, @unchecked Sendable {
let hasMonitoringAccess: Bool
private(set) var monitoringRequestCount = 0
Expand Down
6 changes: 6 additions & 0 deletions Tests/CleanMyScreenKitTests/LockConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ func exposesThreeModes() {
#expect(!LockMode.cleaning.hidesApplicationOnActivation)
#expect(LockMode.petKid.hidesApplicationOnActivation)
#expect(LockMode.selective.hidesApplicationOnActivation)
#expect(!LockMode.cleaning.hidesApplicationWhenCountdownBegins)
#expect(LockMode.petKid.hidesApplicationWhenCountdownBegins)
#expect(!LockMode.selective.hidesApplicationWhenCountdownBegins)
#expect(LockMode.cleaning.countdownSeconds == 3)
#expect(LockMode.petKid.countdownSeconds == 5)
#expect(LockMode.selective.countdownSeconds == 3)
}

@Test("Cleaning defaults match the selected prototype")
Expand Down
Loading