Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Optional coffee icon click animation with a preferences toggle.

### Changed

- Improved Ukrainian translation.
- Coffee icon animation now respects the macOS Reduce Motion accessibility setting and turns off automatically when Reduce Motion is enabled.

### Fixed

Expand Down
1 change: 1 addition & 0 deletions src/Caffeine/Classes/ViewModels/CaffeineViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,5 @@ enum PreferenceKeys {
static let suppressLaunchMessage = "CASuppressLaunchMessage"
static let deactivateOnManualSleep = "CADeactivateOnManualSleep"
static let keepAppsActive = "CAKeepAppsActive"
static let animateCoffeeIcon = "CAAnimateCoffeeIcon"
}
111 changes: 110 additions & 1 deletion src/Caffeine/Classes/Views/MenuBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Cocoa
import Combine
import QuartzCore
import Sparkle
import SwiftUI

Expand All @@ -16,7 +17,13 @@ class MenuBarController: NSObject {
private var viewModel: CaffeineViewModel
private var preferencesWindow: NSWindow?
private var cancellables = Set<AnyCancellable>()
private var coffeeFillTimer: Timer?
private var coffeeFillAnimationStartTime: TimeInterval?
private var coffeeFillInactiveImage: NSImage?
private var coffeeFillActiveImage: NSImage?
private var isAnimatingCoffeeFill = false
private let updaterController: SPUStandardUpdaterController
private let coffeeFillAnimationDuration: TimeInterval = 0.42

init(updaterController: SPUStandardUpdaterController) {
self.updaterController = updaterController
Expand All @@ -31,6 +38,7 @@ class MenuBarController: NSObject {
}

func cleanup() {
self.cancelCoffeeFillAnimation()
self.viewModel.deactivate()
if let statusItem {
NSStatusBar.system.removeStatusItem(statusItem)
Expand Down Expand Up @@ -70,23 +78,124 @@ class MenuBarController: NSObject {
private func updateIcon() {
guard let button = statusItem?.button else { return }

if self.isAnimatingCoffeeFill, self.viewModel.isActive {
return
}

self.cancelCoffeeFillAnimation()

let imageName = self.viewModel.isActive ? "active" : "inactive"
if let image = NSImage(named: NSImage.Name(imageName)) {
button.image = image
}
}

@objc
private func statusItemClicked(_: NSStatusBarButton) {
private func statusItemClicked(_ button: NSStatusBarButton) {
guard let event = NSApp.currentEvent else { return }

if event.type == .rightMouseUp || (event.type == .leftMouseUp && event.modifierFlags.contains(.control)) {
self.showContextMenu()
} else {
let shouldAnimateCoffeeFill = !self.viewModel.isActive && self.isCoffeeAnimationEnabled
if shouldAnimateCoffeeFill {
self.startCoffeeFillAnimation(on: button)
} else {
self.cancelCoffeeFillAnimation()
}

self.viewModel.toggleActive()
}
}

private func startCoffeeFillAnimation(on button: NSStatusBarButton) {
guard
let inactiveImage = NSImage(named: NSImage.Name("inactive")),
let activeImage = NSImage(named: NSImage.Name("active"))
else {
return
}

self.coffeeFillTimer?.invalidate()
self.coffeeFillInactiveImage = inactiveImage
self.coffeeFillActiveImage = activeImage
self.isAnimatingCoffeeFill = true
self.coffeeFillAnimationStartTime = CACurrentMediaTime()
button.image = self.coffeeImage(inactiveImage: inactiveImage, activeImage: activeImage, fillProgress: 0)

self.coffeeFillTimer = Timer.scheduledTimer(
timeInterval: 1.0 / 60.0,
target: self,
selector: #selector(self.updateCoffeeFillAnimationFrame(_:)),
userInfo: nil,
repeats: true
)
}

@objc
private func updateCoffeeFillAnimationFrame(_ timer: Timer) {
guard
let button = statusItem?.button,
let inactiveImage = coffeeFillInactiveImage,
let activeImage = coffeeFillActiveImage,
let startTime = coffeeFillAnimationStartTime
else {
self.cancelCoffeeFillAnimation()
return
}

let elapsed = CACurrentMediaTime() - startTime
let rawProgress = min(1.0, elapsed / self.coffeeFillAnimationDuration)
let fillProgress = self.easeOutQuint(rawProgress)
button.image = self.coffeeImage(inactiveImage: inactiveImage, activeImage: activeImage, fillProgress: fillProgress)

if rawProgress >= 1.0 {
timer.invalidate()
self.coffeeFillTimer = nil
self.coffeeFillAnimationStartTime = nil
self.coffeeFillInactiveImage = nil
self.coffeeFillActiveImage = nil
self.isAnimatingCoffeeFill = false
self.updateIcon()
}
}

private func cancelCoffeeFillAnimation() {
self.coffeeFillTimer?.invalidate()
self.coffeeFillTimer = nil
self.coffeeFillAnimationStartTime = nil
self.coffeeFillInactiveImage = nil
self.coffeeFillActiveImage = nil
self.isAnimatingCoffeeFill = false
}

private func coffeeImage(inactiveImage: NSImage, activeImage: NSImage, fillProgress: Double) -> NSImage {
let imageSize = inactiveImage.size
let image = NSImage(size: imageSize)
let fillHeight = imageSize.height * fillProgress

image.lockFocus()
inactiveImage.draw(in: NSRect(origin: .zero, size: imageSize))

NSGraphicsContext.current?.saveGraphicsState()
NSBezierPath(rect: NSRect(x: 0, y: 0, width: imageSize.width, height: fillHeight)).addClip()
activeImage.draw(in: NSRect(origin: .zero, size: imageSize))
NSGraphicsContext.current?.restoreGraphicsState()

image.unlockFocus()
image.isTemplate = true
return image
}

private func easeOutQuint(_ progress: Double) -> Double {
1.0 - pow(1.0 - progress, 5.0)
}

private var isCoffeeAnimationEnabled: Bool {
let userPreference = UserDefaults.standard.object(forKey: PreferenceKeys.animateCoffeeIcon) as? Bool ?? true
return userPreference && !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
}

private func showContextMenu() {
let menu = NSMenu()

Expand Down
22 changes: 22 additions & 0 deletions src/Caffeine/Classes/Views/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ struct PreferencesView: View {
@AppStorage(PreferenceKeys.suppressLaunchMessage) private var suppressLaunchMessage = false
@AppStorage(PreferenceKeys.deactivateOnManualSleep) private var deactivateOnManualSleep = false
@AppStorage(PreferenceKeys.keepAppsActive) private var keepAppsActive = false
@AppStorage(PreferenceKeys.animateCoffeeIcon) private var animateCoffeeIcon = true
@State private var reduceMotionEnabled = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion

var body: some View {
VStack(alignment: .leading, spacing: 0) {
Expand Down Expand Up @@ -79,6 +81,15 @@ struct PreferencesView: View {
))
.font(.system(size: 13))

Toggle("Animate coffee icon when clicked", isOn: self.$animateCoffeeIcon)
.font(.system(size: 13))
.disabled(self.reduceMotionEnabled)

Text(self.coffeeAnimationDescription)
.font(.system(size: 11))
.foregroundColor(.secondary)
.padding(.leading, 20)

Divider()
.padding(.vertical, 4)

Expand Down Expand Up @@ -121,6 +132,17 @@ struct PreferencesView: View {
.padding(.horizontal, 20)
.frame(width: 640)
.fixedSize(horizontal: false, vertical: true)
.onReceive(NotificationCenter.default.publisher(for: NSWorkspace.accessibilityDisplayOptionsDidChangeNotification)) { _ in
self.reduceMotionEnabled = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
}
}

private var coffeeAnimationDescription: String {
if self.reduceMotionEnabled {
return String(localized: "Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings.")
}

return String(localized: "Adds a short rising animation when you click the menu bar icon.")
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/Caffeine/Resources/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Caffeine beim Start aktivieren";
"Deactivate when device goes to sleep manually" = "Beim manuellen Ruhezustand deaktivieren";
"Show this message when starting Caffeine" = "Diese Nachricht beim Start von Caffeine anzeigen";
"Animate coffee icon when clicked" = "Kaffeesymbol beim Klicken animieren";
"Keep apps active" = "Apps aktiv halten";
"Adds a short rising animation when you click the menu bar icon." = "Fügt eine kurze Aufwärtsanimation hinzu, wenn du auf das Menüleistensymbol klickst.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "Die Kaffee-Animation ist deaktiviert, solange „Bewegung reduzieren“ in den macOS-Bedienungshilfen aktiviert ist.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Verhindert, dass Apps inaktiv werden und der Bildschirmschoner startet.";
"Close" = "Schließen";

Expand All @@ -42,4 +45,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine verhindert den Ruhezustand";

4 changes: 3 additions & 1 deletion src/Caffeine/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Activate when starting Caffeine";
"Deactivate when device goes to sleep manually" = "Deactivate when device goes to sleep manually";
"Show this message when starting Caffeine" = "Show this message when starting Caffeine";
"Animate coffee icon when clicked" = "Animate coffee icon when clicked";
"Keep apps active" = "Keep apps active";
"Adds a short rising animation when you click the menu bar icon." = "Adds a short rising animation when you click the menu bar icon.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Prevents apps from becoming inactive and the screen saver from starting.";
"Close" = "Close";

Expand All @@ -42,4 +45,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine prevents sleep";

4 changes: 3 additions & 1 deletion src/Caffeine/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Activar al iniciar Caffeine";
"Deactivate when device goes to sleep manually" = "Desactivar cuando el dispositivo entra en reposo manualmente";
"Show this message when starting Caffeine" = "Mostrar este mensaje al iniciar Caffeine";
"Animate coffee icon when clicked" = "Animar el icono del café al hacer clic";
"Keep apps active" = "Mantener apps activas";
"Adds a short rising animation when you click the menu bar icon." = "Añade una breve animación ascendente al hacer clic en el icono de la barra de menús.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "La animación del café se desactiva mientras Reducir movimiento esté activado en los ajustes de Accesibilidad de macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Evita que las apps se vuelvan inactivas y que se inicie el salvapantallas.";
"Close" = "Cerrar";

Expand All @@ -42,4 +45,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine impide el reposo";

4 changes: 3 additions & 1 deletion src/Caffeine/Resources/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Activer au lancement de Caffeine";
"Deactivate when device goes to sleep manually" = "Désactiver lorsque l’appareil se met en veille manuellement";
"Show this message when starting Caffeine" = "Afficher ce message au lancement de Caffeine";
"Animate coffee icon when clicked" = "Animer l’icône du café au clic";
"Keep apps active" = "Garder les apps actives";
"Adds a short rising animation when you click the menu bar icon." = "Ajoute une courte animation ascendante lorsque vous cliquez sur l’icône de la barre de menus.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "L’animation du café est désactivée lorsque l’option Réduire les animations est activée dans les réglages d’accessibilité de macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Empêche les apps de devenir inactives et l'économiseur d'écran de démarrer.";
"Close" = "Fermer";

Expand All @@ -42,4 +45,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine empêche la mise en veille";

3 changes: 3 additions & 0 deletions src/Caffeine/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Attiva all'avvio di Caffeine";
"Deactivate when device goes to sleep manually" = "Disattiva quando il dispositivo va in stop manualmente";
"Show this message when starting Caffeine" = "Mostra questo messaggio all'avvio di Caffeine";
"Animate coffee icon when clicked" = "Anima l’icona del caffè al clic";
"Keep apps active" = "Mantieni le app attive";
"Adds a short rising animation when you click the menu bar icon." = "Aggiunge una breve animazione verso l’alto quando fai clic sull’icona della barra dei menu.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "L’animazione del caffè è disattivata mentre Riduci movimento è abilitato nelle impostazioni di Accessibilità di macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Impedisce alle app di diventare inattive e l'avvio del salvaschermo.";
"Close" = "Chiudi";

Expand Down
4 changes: 3 additions & 1 deletion src/Caffeine/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Caffeine 起動時に有効化";
"Deactivate when device goes to sleep manually" = "手動でスリープしたときに無効化";
"Show this message when starting Caffeine" = "Caffeine 起動時にこのメッセージを表示";
"Animate coffee icon when clicked" = "クリック時にコーヒーアイコンをアニメーション表示";
"Keep apps active" = "アプリをアクティブに保つ";
"Adds a short rising animation when you click the menu bar icon." = "メニューバーアイコンをクリックしたときに、短い上昇アニメーションを追加します。";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "macOS のアクセシビリティ設定で視差効果を減らすが有効な間は、コーヒーのアニメーションはオフになります。";
"Prevents apps from becoming inactive and the screen saver from starting." = "アプリが非アクティブになるのを防ぎ、スクリーンセーバーの起動を防止します。";
"Close" = "閉じる";

Expand All @@ -42,4 +45,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine がスリープを防ぎます";

3 changes: 3 additions & 0 deletions src/Caffeine/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Caffeine 시작 시 활성화";
"Deactivate when device goes to sleep manually" = "수동으로 잠자기에 들어가면 비활성화";
"Show this message when starting Caffeine" = "Caffeine 시작 시 이 메시지 표시";
"Animate coffee icon when clicked" = "클릭할 때 커피 아이콘 애니메이션";
"Keep apps active" = "앱 활성 상태 유지";
"Adds a short rising animation when you click the menu bar icon." = "메뉴 막대 아이콘을 클릭할 때 짧게 떠오르는 애니메이션을 추가합니다.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "macOS 손쉬운 사용 설정에서 동작 줄이기가 켜져 있는 동안에는 커피 애니메이션이 꺼집니다.";
"Prevents apps from becoming inactive and the screen saver from starting." = "앱이 비활성 상태가 되거나 화면 보호기가 시작되는 것을 방지합니다.";
"Close" = "닫기";

Expand Down
3 changes: 3 additions & 0 deletions src/Caffeine/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Caffeine bij starten inschakelen";
"Deactivate when device goes to sleep manually" = "Uitschakelen wanneer het apparaat handmatig in sluimerstand gaat";
"Show this message when starting Caffeine" = "Dit bericht tonen bij het starten van Caffeine";
"Animate coffee icon when clicked" = "Koffiepictogram animeren bij klikken";
"Keep apps active" = "Apps actief houden";
"Adds a short rising animation when you click the menu bar icon." = "Voegt een korte opstijgende animatie toe wanneer je op het menubalkpictogram klikt.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "De koffie-animatie staat uit zolang Verminder beweging is ingeschakeld in de toegankelijkheidsinstellingen van macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Voorkomt dat apps inactief worden en dat de schermbeveiliging start.";
"Close" = "Sluiten";

Expand Down
3 changes: 3 additions & 0 deletions src/Caffeine/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Ativar ao iniciar o Caffeine";
"Deactivate when device goes to sleep manually" = "Desativar quando o dispositivo entrar em repouso manualmente";
"Show this message when starting Caffeine" = "Mostrar esta mensagem ao iniciar o Caffeine";
"Animate coffee icon when clicked" = "Animar o ícone do café ao clicar";
"Keep apps active" = "Manter apps ativos";
"Adds a short rising animation when you click the menu bar icon." = "Adiciona uma curta animação de subida quando você clica no ícone da barra de menus.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "A animação do café fica desativada enquanto Reduzir Movimento estiver ativado nos ajustes de Acessibilidade do macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Impede que os apps fiquem inativos e que a proteção de tela seja iniciada.";
"Close" = "Fechar";

Expand Down
3 changes: 3 additions & 0 deletions src/Caffeine/Resources/pt.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Ativar ao iniciar o Caffeine";
"Deactivate when device goes to sleep manually" = "Desativar quando o dispositivo entra em repouso manualmente";
"Show this message when starting Caffeine" = "Mostrar esta mensagem ao iniciar o Caffeine";
"Animate coffee icon when clicked" = "Animar o ícone do café ao clicar";
"Keep apps active" = "Manter apps ativas";
"Adds a short rising animation when you click the menu bar icon." = "Adiciona uma breve animação ascendente quando clica no ícone da barra de menus.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "A animação do café fica desativada enquanto Reduzir movimento estiver ativado nas definições de Acessibilidade do macOS.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Impede que as apps fiquem inativas e que a proteção de ecrã seja iniciada.";
"Close" = "Fechar";

Expand Down
3 changes: 3 additions & 0 deletions src/Caffeine/Resources/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"Activate when starting Caffeine" = "Активировать при запуске Caffeine";
"Deactivate when device goes to sleep manually" = "Деактивировать при ручном переводе устройства в сон";
"Show this message when starting Caffeine" = "Показывать это сообщение при запуске Caffeine";
"Animate coffee icon when clicked" = "Анимировать значок кофе при нажатии";
"Keep apps active" = "Поддерживать активность приложений";
"Adds a short rising animation when you click the menu bar icon." = "Добавляет короткую анимацию подъёма при нажатии на значок в строке меню.";
"Coffee animation is turned off while Reduce Motion is enabled in macOS Accessibility settings." = "Анимация кофе отключается, пока в настройках специальных возможностей macOS включено уменьшение движения.";
"Prevents apps from becoming inactive and the screen saver from starting." = "Предотвращает переход приложений в неактивный режим и запуск заставки.";
"Close" = "Закрыть";

Expand Down
Loading