feat: better links and animation
This commit is contained in:
@@ -192,10 +192,45 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"s": {
|
"s": {
|
||||||
"a": 0,
|
"a": 1,
|
||||||
"k": [
|
"k": [
|
||||||
20,
|
{
|
||||||
20
|
"t": 0,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"i": {
|
||||||
|
"x": 0.4,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"x": 0.6,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 70,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"i": {
|
||||||
|
"x": 0.4,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"x": 0.6,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 75,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nm": "Eye Ellipse"
|
"nm": "Eye Ellipse"
|
||||||
@@ -382,205 +417,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nm": "Eye"
|
"nm": "Eye"
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "gr",
|
|
||||||
"it": [
|
|
||||||
{
|
|
||||||
"ty": "el",
|
|
||||||
"p": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
8,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nm": "Pupil Ellipse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "fl",
|
|
||||||
"c": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0.478,
|
|
||||||
1,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 100
|
|
||||||
},
|
|
||||||
"r": 1,
|
|
||||||
"nm": "Fill"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "tr",
|
|
||||||
"p": {
|
|
||||||
"a": 1,
|
|
||||||
"k": [
|
|
||||||
{
|
|
||||||
"t": 0,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 20,
|
|
||||||
"s": [
|
|
||||||
-20,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 40,
|
|
||||||
"s": [
|
|
||||||
-25,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 60,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 80,
|
|
||||||
"s": [
|
|
||||||
25,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 100,
|
|
||||||
"s": [
|
|
||||||
20,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 120,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
-15
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 140,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 150,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"a": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
100,
|
|
||||||
100
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"r": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 0
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nm": "Pupil"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ip": 0,
|
"ip": 0,
|
||||||
@@ -643,10 +479,45 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"s": {
|
"s": {
|
||||||
"a": 0,
|
"a": 1,
|
||||||
"k": [
|
"k": [
|
||||||
20,
|
{
|
||||||
20
|
"t": 0,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"i": {
|
||||||
|
"x": 0.4,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"x": 0.6,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 70,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"i": {
|
||||||
|
"x": 0.4,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"x": 0.6,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 75,
|
||||||
|
"s": [
|
||||||
|
20,
|
||||||
|
20
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nm": "Eye Ellipse"
|
"nm": "Eye Ellipse"
|
||||||
@@ -833,205 +704,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nm": "Eye"
|
"nm": "Eye"
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "gr",
|
|
||||||
"it": [
|
|
||||||
{
|
|
||||||
"ty": "el",
|
|
||||||
"p": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
8,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nm": "Pupil Ellipse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "fl",
|
|
||||||
"c": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0.478,
|
|
||||||
1,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 100
|
|
||||||
},
|
|
||||||
"r": 1,
|
|
||||||
"nm": "Fill"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ty": "tr",
|
|
||||||
"p": {
|
|
||||||
"a": 1,
|
|
||||||
"k": [
|
|
||||||
{
|
|
||||||
"t": 0,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 20,
|
|
||||||
"s": [
|
|
||||||
-20,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 40,
|
|
||||||
"s": [
|
|
||||||
-25,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 60,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 80,
|
|
||||||
"s": [
|
|
||||||
25,
|
|
||||||
5
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 100,
|
|
||||||
"s": [
|
|
||||||
20,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 120,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
-15
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 140,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
-8
|
|
||||||
],
|
|
||||||
"i": {
|
|
||||||
"x": 0.4,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"x": 0.6,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"t": 150,
|
|
||||||
"s": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"a": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"s": {
|
|
||||||
"a": 0,
|
|
||||||
"k": [
|
|
||||||
100,
|
|
||||||
100
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"r": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 0
|
|
||||||
},
|
|
||||||
"o": {
|
|
||||||
"a": 0,
|
|
||||||
"k": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nm": "Pupil"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ip": 0,
|
"ip": 0,
|
||||||
|
|||||||
@@ -201,6 +201,34 @@ private func showReminderWindow(_ content: AnyView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public method to reopen onboarding window
|
||||||
|
func openOnboarding() {
|
||||||
|
// Post notification to close menu bar popover
|
||||||
|
NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil)
|
||||||
|
|
||||||
|
// Small delay to allow menu bar to close before opening onboarding
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||||
|
guard let self = self, let settingsManager = self.settingsManager else { return }
|
||||||
|
|
||||||
|
let window = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 700, height: 700),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
window.title = "Gaze Onboarding"
|
||||||
|
window.center()
|
||||||
|
window.isReleasedWhenClosed = true
|
||||||
|
window.contentView = NSHostingView(
|
||||||
|
rootView: OnboardingContainerView(settingsManager: settingsManager)
|
||||||
|
)
|
||||||
|
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func openSettingsWindow(tab: Int) {
|
private func openSettingsWindow(tab: Int) {
|
||||||
// If window already exists, switch to the tab and bring it to front
|
// If window already exists, switch to the tab and bring it to front
|
||||||
if let existingWindow = settingsWindowController?.window {
|
if let existingWindow = settingsWindowController?.window {
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ struct GazeApp: App {
|
|||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
onQuit: { NSApplication.shared.terminate(nil) },
|
onQuit: { NSApplication.shared.terminate(nil) },
|
||||||
onOpenSettings: { appDelegate.openSettings() },
|
onOpenSettings: { appDelegate.openSettings() },
|
||||||
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) }
|
onOpenSettingsTab: { tab in appDelegate.openSettings(tab: tab) },
|
||||||
|
onOpenOnboarding: { appDelegate.openOnboarding() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
|||||||
var title: String
|
var title: String
|
||||||
var type: UserTimerType
|
var type: UserTimerType
|
||||||
var timeOnScreenSeconds: Int
|
var timeOnScreenSeconds: Int
|
||||||
|
var intervalMinutes: Int
|
||||||
var message: String?
|
var message: String?
|
||||||
var colorHex: String
|
var colorHex: String
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
@@ -23,6 +24,7 @@ struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
|||||||
title: String? = nil,
|
title: String? = nil,
|
||||||
type: UserTimerType = .subtle,
|
type: UserTimerType = .subtle,
|
||||||
timeOnScreenSeconds: Int = 30,
|
timeOnScreenSeconds: Int = 30,
|
||||||
|
intervalMinutes: Int = 15,
|
||||||
message: String? = nil,
|
message: String? = nil,
|
||||||
colorHex: String? = nil,
|
colorHex: String? = nil,
|
||||||
enabled: Bool = true
|
enabled: Bool = true
|
||||||
@@ -31,6 +33,7 @@ struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
|||||||
self.title = title ?? "User Reminder"
|
self.title = title ?? "User Reminder"
|
||||||
self.type = type
|
self.type = type
|
||||||
self.timeOnScreenSeconds = timeOnScreenSeconds
|
self.timeOnScreenSeconds = timeOnScreenSeconds
|
||||||
|
self.intervalMinutes = intervalMinutes
|
||||||
self.message = message
|
self.message = message
|
||||||
self.colorHex = colorHex ?? UserTimer.defaultColors[0]
|
self.colorHex = colorHex ?? UserTimer.defaultColors[0]
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
@@ -38,8 +41,8 @@ struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
|||||||
|
|
||||||
static func == (lhs: UserTimer, rhs: UserTimer) -> Bool {
|
static func == (lhs: UserTimer, rhs: UserTimer) -> Bool {
|
||||||
lhs.id == rhs.id && lhs.title == rhs.title && lhs.type == rhs.type
|
lhs.id == rhs.id && lhs.title == rhs.title && lhs.type == rhs.type
|
||||||
&& lhs.timeOnScreenSeconds == rhs.timeOnScreenSeconds && lhs.message == rhs.message
|
&& lhs.timeOnScreenSeconds == rhs.timeOnScreenSeconds && lhs.intervalMinutes == rhs.intervalMinutes
|
||||||
&& lhs.colorHex == rhs.colorHex && lhs.enabled == rhs.enabled
|
&& lhs.message == rhs.message && lhs.colorHex == rhs.colorHex && lhs.enabled == rhs.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default color palette for user timers
|
// Default color palette for user timers
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ struct MenuBarContentView: View {
|
|||||||
var onQuit: () -> Void
|
var onQuit: () -> Void
|
||||||
var onOpenSettings: () -> Void
|
var onOpenSettings: () -> Void
|
||||||
var onOpenSettingsTab: (Int) -> Void
|
var onOpenSettingsTab: (Int) -> Void
|
||||||
|
var onOpenOnboarding: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
@@ -116,6 +117,23 @@ struct MenuBarContentView: View {
|
|||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
|
// Show "Complete Onboarding" button if not completed
|
||||||
|
if !settingsManager.settings.hasCompletedOnboarding {
|
||||||
|
Button(action: {
|
||||||
|
onOpenOnboarding()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text("Complete Onboarding")
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
}
|
||||||
|
.buttonStyle(MenuBarHoverButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if timerEngine.timerStates.values.first?.isPaused == true {
|
if timerEngine.timerStates.values.first?.isPaused == true {
|
||||||
timerEngine.resume()
|
timerEngine.resume()
|
||||||
@@ -420,6 +438,7 @@ struct UserTimerStatusRow: View {
|
|||||||
settingsManager: settingsManager,
|
settingsManager: settingsManager,
|
||||||
onQuit: {},
|
onQuit: {},
|
||||||
onOpenSettings: {},
|
onOpenSettings: {},
|
||||||
onOpenSettingsTab: { _ in }
|
onOpenSettingsTab: { _ in },
|
||||||
|
onOpenOnboarding: {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -27,18 +27,14 @@ struct BlinkSetupView: View {
|
|||||||
|
|
||||||
// Vertically centered content
|
// Vertically centered content
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 30) {
|
|
||||||
Text("Keep your eyes hydrated")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
// InfoBox with link functionality
|
VStack(spacing: 30) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if let url = URL(
|
if let url = URL(
|
||||||
string: "https://www.healthline.com/health/eye-health/eye-strain#symptoms")
|
string:
|
||||||
{
|
"https://www.aao.org/eye-health/tips-prevention/computer-usage#:~:text=Humans normally blink about 15 times in one minute. However, studies show that we only blink about 5 to 7 times in a minute while using computers and other digital screen devices."
|
||||||
|
) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
@@ -50,7 +46,7 @@ struct BlinkSetupView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
}.buttonStyle(.plain)
|
}.buttonStyle(.plain)
|
||||||
Text(
|
Text(
|
||||||
"We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes"
|
"We blink much less when focusing on screens. Regular blink reminders help prevent dry eyes."
|
||||||
)
|
)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -99,7 +95,7 @@ struct BlinkSetupView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ struct LookAwaySetupView: View {
|
|||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if let url = URL(
|
if let url = URL(
|
||||||
string: "https://www.healthline.com/health/eye-health/20-20-20-rule")
|
string:
|
||||||
{
|
"https://journals.co.za/doi/abs/10.4102/aveh.v79i1.554#:~:text=the 20/20/20 rule induces significant changes in dry eye symptoms and tear film and some limited changes for ocular surface integrity."
|
||||||
|
) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import SwiftUI
|
|||||||
struct PostureSetupView: View {
|
struct PostureSetupView: View {
|
||||||
@Binding var enabled: Bool
|
@Binding var enabled: Bool
|
||||||
@Binding var intervalMinutes: Int
|
@Binding var intervalMinutes: Int
|
||||||
|
|
||||||
|
@State private var isPreviewShowing = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -29,16 +31,12 @@ struct PostureSetupView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 30) {
|
||||||
Text("Maintain proper ergonomics")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
// InfoBox with link functionality
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if let url = URL(
|
if let url = URL(
|
||||||
string: "https://www.healthline.com/health/ergonomic-workspace")
|
string:
|
||||||
{
|
"https://pubmed.ncbi.nlm.nih.gov/40111906/#:~:text=For studies exploring sitting posture, seven found a relationship with LBP. Regarding studies on sitting behavior, only one showed no relationship between LBP prevalence, while twelve indicated a relationship."
|
||||||
|
) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
@@ -98,6 +96,29 @@ struct PostureSetupView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preview button
|
||||||
|
Button(action: {
|
||||||
|
isPreviewShowing = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "eye")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Text("Preview Reminder")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(.blue)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $isPreviewShowing) {
|
||||||
|
PostureReminderView(sizePercentage: 10.0, onDismiss: {
|
||||||
|
isPreviewShowing = false
|
||||||
|
})
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(.black.opacity(0.85))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ struct UserTimerRow: View {
|
|||||||
var onEdit: () -> Void
|
var onEdit: () -> Void
|
||||||
var onDelete: () -> Void
|
var onDelete: () -> Void
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
|
@State private var showingDeleteConfirmation = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -155,7 +156,7 @@ struct UserTimerRow: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
Text("\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen")
|
Text("\(timer.type.displayName) • \(timer.timeOnScreenSeconds)s on screen • \(timer.intervalMinutes) min interval")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@@ -175,12 +176,20 @@ struct UserTimerRow: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
Button(action: onDelete) {
|
Button(action: { showingDeleteConfirmation = true }) {
|
||||||
Image(systemName: "trash.circle.fill")
|
Image(systemName: "trash.circle.fill")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.confirmationDialog("Delete Timer", isPresented: $showingDeleteConfirmation) {
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("Are you sure you want to delete this timer? This action cannot be undone.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -200,10 +209,11 @@ struct UserTimerEditSheet: View {
|
|||||||
var onSave: (UserTimer) -> Void
|
var onSave: (UserTimer) -> Void
|
||||||
var onCancel: () -> Void
|
var onCancel: () -> Void
|
||||||
|
|
||||||
@State private var title: String
|
@State private var title: String
|
||||||
@State private var message: String
|
@State private var message: String
|
||||||
@State private var type: UserTimerType
|
@State private var type: UserTimerType
|
||||||
@State private var timeOnScreen: Int
|
@State private var timeOnScreen: Int
|
||||||
|
@State private var intervalMinutes: Int
|
||||||
@State private var selectedColorHex: String
|
@State private var selectedColorHex: String
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -222,6 +232,7 @@ struct UserTimerEditSheet: View {
|
|||||||
_message = State(initialValue: timer?.message ?? "")
|
_message = State(initialValue: timer?.message ?? "")
|
||||||
_type = State(initialValue: timer?.type ?? .subtle)
|
_type = State(initialValue: timer?.type ?? .subtle)
|
||||||
_timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30)
|
_timeOnScreen = State(initialValue: timer?.timeOnScreenSeconds ?? 30)
|
||||||
|
_intervalMinutes = State(initialValue: timer?.intervalMinutes ?? 15)
|
||||||
_selectedColorHex = State(
|
_selectedColorHex = State(
|
||||||
initialValue: timer?.colorHex
|
initialValue: timer?.colorHex
|
||||||
?? UserTimer.defaultColors[existingTimersCount % UserTimer.defaultColors.count])
|
?? UserTimer.defaultColors[existingTimersCount % UserTimer.defaultColors.count])
|
||||||
@@ -311,6 +322,27 @@ struct UserTimerEditSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Interval")
|
||||||
|
.font(.headline)
|
||||||
|
HStack {
|
||||||
|
Slider(
|
||||||
|
value: Binding(
|
||||||
|
get: { Double(intervalMinutes) },
|
||||||
|
set: { intervalMinutes = Int($0) }
|
||||||
|
),
|
||||||
|
in: 1...120,
|
||||||
|
step: 1
|
||||||
|
)
|
||||||
|
Text("\(intervalMinutes) min")
|
||||||
|
.frame(width: 60, alignment: .trailing)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
Text("How often this reminder will appear (in minutes)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Message (Optional)")
|
Text("Message (Optional)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@@ -334,6 +366,7 @@ struct UserTimerEditSheet: View {
|
|||||||
title: title,
|
title: title,
|
||||||
type: type,
|
type: type,
|
||||||
timeOnScreenSeconds: timeOnScreen,
|
timeOnScreenSeconds: timeOnScreen,
|
||||||
|
intervalMinutes: intervalMinutes,
|
||||||
message: message.isEmpty ? nil : message,
|
message: message.isEmpty ? nil : message,
|
||||||
colorHex: selectedColorHex,
|
colorHex: selectedColorHex,
|
||||||
enabled: timer?.enabled ?? true
|
enabled: timer?.enabled ?? true
|
||||||
@@ -358,10 +391,10 @@ struct UserTimerEditSheet: View {
|
|||||||
userTimers: .constant([
|
userTimers: .constant([
|
||||||
UserTimer(
|
UserTimer(
|
||||||
id: "1", title: "User Reminder 1", type: .subtle, timeOnScreenSeconds: 30,
|
id: "1", title: "User Reminder 1", type: .subtle, timeOnScreenSeconds: 30,
|
||||||
message: "Take a break", colorHex: "9B59B6"),
|
intervalMinutes: 15, message: "Take a break", colorHex: "9B59B6"),
|
||||||
UserTimer(
|
UserTimer(
|
||||||
id: "2", title: "User Reminder 2", type: .overlay, timeOnScreenSeconds: 60,
|
id: "2", title: "User Reminder 2", type: .overlay, timeOnScreenSeconds: 60,
|
||||||
message: "Stretch your legs", colorHex: "3498DB"),
|
intervalMinutes: 30, message: "Stretch your legs", colorHex: "3498DB"),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,4 +99,38 @@ final class MenuBarUITests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCompleteOnboardingButtonVisibleWhenOnboardingIncomplete() throws {
|
||||||
|
// Relaunch app without skip-onboarding flag
|
||||||
|
app.terminate()
|
||||||
|
let newApp = XCUIApplication()
|
||||||
|
newApp.launchArguments.append("--reset-onboarding")
|
||||||
|
newApp.launch()
|
||||||
|
|
||||||
|
let menuBar = newApp.menuBarItems.firstMatch
|
||||||
|
if menuBar.waitForExistence(timeout: 5) {
|
||||||
|
menuBar.click()
|
||||||
|
|
||||||
|
let completeOnboardingButton = newApp.buttons["Complete Onboarding"]
|
||||||
|
XCTAssertTrue(
|
||||||
|
completeOnboardingButton.waitForExistence(timeout: 2),
|
||||||
|
"Complete Onboarding button should be visible when onboarding is incomplete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
newApp.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCompleteOnboardingButtonNotVisibleWhenOnboardingComplete() throws {
|
||||||
|
let menuBar = app.menuBarItems.firstMatch
|
||||||
|
if menuBar.waitForExistence(timeout: 5) {
|
||||||
|
menuBar.click()
|
||||||
|
|
||||||
|
let completeOnboardingButton = app.buttons["Complete Onboarding"]
|
||||||
|
XCTAssertFalse(
|
||||||
|
completeOnboardingButton.exists,
|
||||||
|
"Complete Onboarding button should not be visible when onboarding is complete"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user