feat: begin work on user control & update server

This commit is contained in:
Michael Freno
2026-01-09 11:56:51 -05:00
parent 3d7044050e
commit b8f995b2e9
7 changed files with 1676 additions and 186 deletions

View File

@@ -8,6 +8,7 @@
"nm": "Blink Animation", "nm": "Blink Animation",
"ddd": 0, "ddd": 0,
"assets": [], "assets": [],
"date": "2026-01-09T00:00:00Z",
"layers": [ "layers": [
{ {
"ddd": 0, "ddd": 0,
@@ -16,11 +17,38 @@
"nm": "Eye Container", "nm": "Eye Container",
"sr": 1, "sr": 1,
"ks": { "ks": {
"o": {"a": 0, "k": 100}, "o": {
"r": {"a": 0, "k": 0}, "a": 0,
"p": {"a": 0, "k": [100, 100, 0]}, "k": 100
"a": {"a": 0, "k": [0, 0, 0]}, },
"s": {"a": 0, "k": [100, 100, 100]} "r": {
"a": 0,
"k": 0
},
"p": {
"a": 0,
"k": [
100,
100,
0
]
},
"a": {
"a": 0,
"k": [
0,
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100,
100
]
}
}, },
"ao": 0, "ao": 0,
"shapes": [ "shapes": [
@@ -29,37 +57,130 @@
"it": [ "it": [
{ {
"ty": "el", "ty": "el",
"p": {"a": 0, "k": [0, 0]}, "p": {
"a": 0,
"k": [
0,
0
]
},
"s": { "s": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [80, 80], "h": 1}, {
{"t": 15, "s": [80, 80], "h": 1}, "t": 0,
{"t": 20, "s": [80, 10], "h": 1}, "s": [
{"t": 25, "s": [80, 80], "h": 1}, 80,
{"t": 35, "s": [80, 80], "h": 1}, 80
{"t": 40, "s": [80, 10], "h": 1}, ],
{"t": 45, "s": [80, 80], "h": 1} "h": 1
},
{
"t": 15,
"s": [
80,
80
],
"h": 1
},
{
"t": 20,
"s": [
80,
10
],
"h": 1
},
{
"t": 25,
"s": [
80,
80
],
"h": 1
},
{
"t": 35,
"s": [
80,
80
],
"h": 1
},
{
"t": 40,
"s": [
80,
10
],
"h": 1
},
{
"t": 45,
"s": [
80,
80
],
"h": 1
}
] ]
}, },
"nm": "Eye Ellipse" "nm": "Eye Ellipse"
}, },
{ {
"ty": "st", "ty": "st",
"c": {"a": 0, "k": [0, 0.478, 1, 1]}, "c": {
"o": {"a": 0, "k": 100}, "a": 0,
"w": {"a": 0, "k": 8}, "k": [
0,
0.478,
1,
1
]
},
"o": {
"a": 0,
"k": 100
},
"w": {
"a": 0,
"k": 8
},
"lc": 2, "lc": 2,
"lj": 2, "lj": 2,
"nm": "Stroke" "nm": "Stroke"
}, },
{ {
"ty": "tr", "ty": "tr",
"p": {"a": 0, "k": [0, 0]}, "p": {
"a": {"a": 0, "k": [0, 0]}, "a": 0,
"s": {"a": 0, "k": [100, 100]}, "k": [
"r": {"a": 0, "k": 0}, 0,
"o": {"a": 0, "k": 100} 0
]
},
"a": {
"a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100
]
},
"r": {
"a": 0,
"k": 0
},
"o": {
"a": 0,
"k": 100
}
} }
], ],
"nm": "Eye Shape" "nm": "Eye Shape"
@@ -77,11 +198,38 @@
"nm": "Pupil", "nm": "Pupil",
"sr": 1, "sr": 1,
"ks": { "ks": {
"o": {"a": 0, "k": 100}, "o": {
"r": {"a": 0, "k": 0}, "a": 0,
"p": {"a": 0, "k": [100, 100, 0]}, "k": 100
"a": {"a": 0, "k": [0, 0, 0]}, },
"s": {"a": 0, "k": [100, 100, 100]} "r": {
"a": 0,
"k": 0
},
"p": {
"a": 0,
"k": [
100,
100,
0
]
},
"a": {
"a": 0,
"k": [
0,
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100,
100
]
}
}, },
"ao": 0, "ao": 0,
"shapes": [ "shapes": [
@@ -90,31 +238,105 @@
"it": [ "it": [
{ {
"ty": "el", "ty": "el",
"p": {"a": 0, "k": [0, 0]}, "p": {
"s": {"a": 0, "k": [25, 25]}, "a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
25,
25
]
},
"nm": "Pupil Ellipse" "nm": "Pupil Ellipse"
}, },
{ {
"ty": "fl", "ty": "fl",
"c": {"a": 0, "k": [0, 0.478, 1, 1]}, "c": {
"o": {"a": 0, "k": 100}, "a": 0,
"k": [
0,
0.478,
1,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1, "r": 1,
"nm": "Fill" "nm": "Fill"
}, },
{ {
"ty": "tr", "ty": "tr",
"p": {"a": 0, "k": [0, 0]}, "p": {
"a": {"a": 0, "k": [0, 0]}, "a": 0,
"s": {"a": 0, "k": [100, 100]}, "k": [
"r": {"a": 0, "k": 0}, 0,
0
]
},
"a": {
"a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100
]
},
"r": {
"a": 0,
"k": 0
},
"o": { "o": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [100], "h": 1}, {
{"t": 20, "s": [0], "h": 1}, "t": 0,
{"t": 25, "s": [100], "h": 1}, "s": [
{"t": 40, "s": [0], "h": 1}, 100
{"t": 45, "s": [100], "h": 1} ],
"h": 1
},
{
"t": 20,
"s": [
0
],
"h": 1
},
{
"t": 25,
"s": [
100
],
"h": 1
},
{
"t": 40,
"s": [
0
],
"h": 1
},
{
"t": 45,
"s": [
100
],
"h": 1
}
] ]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"nm": "Posture Arrow Animation", "nm": "Posture Arrow Animation",
"ddd": 0, "ddd": 0,
"assets": [], "assets": [],
"date": "2026-01-09T00:00:00Z",
"layers": [ "layers": [
{ {
"ddd": 0, "ddd": 0,
@@ -19,29 +20,119 @@
"o": { "o": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [0], "h": 1}, {
{"t": 10, "s": [100], "h": 1}, "t": 0,
{"t": 60, "s": [100], "h": 1}, "s": [
{"t": 70, "s": [0], "h": 1} 0
],
"h": 1
},
{
"t": 10,
"s": [
100
],
"h": 1
},
{
"t": 60,
"s": [
100
],
"h": 1
},
{
"t": 70,
"s": [
0
],
"h": 1
}
] ]
}, },
"r": {"a": 0, "k": 0}, "r": {
"a": 0,
"k": 0
},
"p": { "p": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [100, 200, 0], "h": 1}, {
{"t": 60, "s": [100, 200, 0], "h": 1}, "t": 0,
{"t": 90, "s": [100, -100, 0], "h": 1} "s": [
100,
200,
0
],
"h": 1
},
{
"t": 60,
"s": [
100,
200,
0
],
"h": 1
},
{
"t": 90,
"s": [
100,
-100,
0
],
"h": 1
}
]
},
"a": {
"a": 0,
"k": [
0,
0,
0
] ]
}, },
"a": {"a": 0, "k": [0, 0, 0]},
"s": { "s": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [0, 0, 100], "h": 1}, {
{"t": 10, "s": [100, 100, 100], "h": 1}, "t": 0,
{"t": 60, "s": [100, 100, 100], "h": 1}, "s": [
{"t": 70, "s": [50, 50, 100], "h": 1} 0,
0,
100
],
"h": 1
},
{
"t": 10,
"s": [
100,
100,
100
],
"h": 1
},
{
"t": 60,
"s": [
100,
100,
100
],
"h": 1
},
{
"t": 70,
"s": [
50,
50,
100
],
"h": 1
}
] ]
} }
}, },
@@ -55,16 +146,95 @@
"ks": { "ks": {
"a": 0, "a": 0,
"k": { "k": {
"i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], "i": [
"o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], [
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"o": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
]
],
"v": [ "v": [
[0, -40], [
[-30, -10], 0,
[-12, -10], -40
[-12, 40], ],
[12, 40], [
[12, -10], -30,
[30, -10] -10
],
[
-12,
-10
],
[
-12,
40
],
[
12,
40
],
[
12,
-10
],
[
30,
-10
]
], ],
"c": true "c": true
} }
@@ -73,18 +243,53 @@
}, },
{ {
"ty": "fl", "ty": "fl",
"c": {"a": 0, "k": [0, 0, 0, 1]}, "c": {
"o": {"a": 0, "k": 100}, "a": 0,
"k": [
0,
0,
0,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1, "r": 1,
"nm": "Fill" "nm": "Fill"
}, },
{ {
"ty": "tr", "ty": "tr",
"p": {"a": 0, "k": [0, 0]}, "p": {
"a": {"a": 0, "k": [0, 0]}, "a": 0,
"s": {"a": 0, "k": [100, 100]}, "k": [
"r": {"a": 0, "k": 0}, 0,
"o": {"a": 0, "k": 100} 0
]
},
"a": {
"a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100
]
},
"r": {
"a": 0,
"k": 0
},
"o": {
"a": 0,
"k": 100
}
} }
], ],
"nm": "Arrow Shape" "nm": "Arrow Shape"
@@ -105,29 +310,119 @@
"o": { "o": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [0], "h": 1}, {
{"t": 10, "s": [100], "h": 1}, "t": 0,
{"t": 60, "s": [100], "h": 1}, "s": [
{"t": 70, "s": [0], "h": 1} 0
],
"h": 1
},
{
"t": 10,
"s": [
100
],
"h": 1
},
{
"t": 60,
"s": [
100
],
"h": 1
},
{
"t": 70,
"s": [
0
],
"h": 1
}
] ]
}, },
"r": {"a": 0, "k": 0}, "r": {
"a": 0,
"k": 0
},
"p": { "p": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [100, 200, 0], "h": 1}, {
{"t": 60, "s": [100, 200, 0], "h": 1}, "t": 0,
{"t": 90, "s": [100, -100, 0], "h": 1} "s": [
100,
200,
0
],
"h": 1
},
{
"t": 60,
"s": [
100,
200,
0
],
"h": 1
},
{
"t": 90,
"s": [
100,
-100,
0
],
"h": 1
}
]
},
"a": {
"a": 0,
"k": [
0,
0,
0
] ]
}, },
"a": {"a": 0, "k": [0, 0, 0]},
"s": { "s": {
"a": 1, "a": 1,
"k": [ "k": [
{"t": 0, "s": [0, 0, 100], "h": 1}, {
{"t": 10, "s": [100, 100, 100], "h": 1}, "t": 0,
{"t": 60, "s": [100, 100, 100], "h": 1}, "s": [
{"t": 70, "s": [50, 50, 100], "h": 1} 0,
0,
100
],
"h": 1
},
{
"t": 10,
"s": [
100,
100,
100
],
"h": 1
},
{
"t": 60,
"s": [
100,
100,
100
],
"h": 1
},
{
"t": 70,
"s": [
50,
50,
100
],
"h": 1
}
] ]
} }
}, },
@@ -138,24 +433,71 @@
"it": [ "it": [
{ {
"ty": "el", "ty": "el",
"p": {"a": 0, "k": [0, 0]}, "p": {
"s": {"a": 0, "k": [120, 120]}, "a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
120,
120
]
},
"nm": "Circle Ellipse" "nm": "Circle Ellipse"
}, },
{ {
"ty": "fl", "ty": "fl",
"c": {"a": 0, "k": [0.9, 0.9, 0.9, 1]}, "c": {
"o": {"a": 0, "k": 30}, "a": 0,
"k": [
0.9,
0.9,
0.9,
1
]
},
"o": {
"a": 0,
"k": 30
},
"r": 1, "r": 1,
"nm": "Fill" "nm": "Fill"
}, },
{ {
"ty": "tr", "ty": "tr",
"p": {"a": 0, "k": [0, 0]}, "p": {
"a": {"a": 0, "k": [0, 0]}, "a": 0,
"s": {"a": 0, "k": [100, 100]}, "k": [
"r": {"a": 0, "k": 0}, 0,
"o": {"a": 0, "k": 100} 0
]
},
"a": {
"a": 0,
"k": [
0,
0
]
},
"s": {
"a": 0,
"k": [
100,
100
]
},
"r": {
"a": 0,
"k": 0
},
"o": {
"a": 0,
"k": 100
}
} }
], ],
"nm": "Circle" "nm": "Circle"

View File

@@ -7,11 +7,23 @@
import Foundation import Foundation
// MARK: - Centralized Configuration System
/// Unified configuration class that manages all app settings in a centralized way
struct AppSettings: Codable, Equatable { struct AppSettings: Codable, Equatable {
// Timer configurations
var lookAwayTimer: TimerConfiguration var lookAwayTimer: TimerConfiguration
var lookAwayCountdownSeconds: Int var lookAwayCountdownSeconds: Int
var blinkTimer: TimerConfiguration var blinkTimer: TimerConfiguration
var postureTimer: TimerConfiguration var postureTimer: TimerConfiguration
// User-defined timers (up to 3)
var userTimers: [UserTimer]
// UI and display settings
var subtleReminderSizePercentage: Double // 2-35% of screen width
// App state and behavior
var hasCompletedOnboarding: Bool var hasCompletedOnboarding: Bool
var launchAtLogin: Bool var launchAtLogin: Bool
var playSounds: Bool var playSounds: Bool
@@ -20,8 +32,10 @@ struct AppSettings: Codable, Equatable {
AppSettings( AppSettings(
lookAwayTimer: TimerConfiguration(enabled: true, intervalSeconds: 20 * 60), lookAwayTimer: TimerConfiguration(enabled: true, intervalSeconds: 20 * 60),
lookAwayCountdownSeconds: 20, lookAwayCountdownSeconds: 20,
blinkTimer: TimerConfiguration(enabled: true, intervalSeconds: 5 * 60), blinkTimer: TimerConfiguration(enabled: false, intervalSeconds: 7 * 60),
postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60), postureTimer: TimerConfiguration(enabled: true, intervalSeconds: 30 * 60),
userTimers: [],
subtleReminderSizePercentage: 5.0,
hasCompletedOnboarding: false, hasCompletedOnboarding: false,
launchAtLogin: false, launchAtLogin: false,
playSounds: true playSounds: true
@@ -33,6 +47,8 @@ struct AppSettings: Codable, Equatable {
lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds && lhs.lookAwayCountdownSeconds == rhs.lookAwayCountdownSeconds &&
lhs.blinkTimer == rhs.blinkTimer && lhs.blinkTimer == rhs.blinkTimer &&
lhs.postureTimer == rhs.postureTimer && lhs.postureTimer == rhs.postureTimer &&
lhs.userTimers == rhs.userTimers &&
lhs.subtleReminderSizePercentage == rhs.subtleReminderSizePercentage &&
lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding && lhs.hasCompletedOnboarding == rhs.hasCompletedOnboarding &&
lhs.launchAtLogin == rhs.launchAtLogin && lhs.launchAtLogin == rhs.launchAtLogin &&
lhs.playSounds == rhs.playSounds lhs.playSounds == rhs.playSounds

View File

@@ -0,0 +1,51 @@
//
// UserTimer.swift
// Gaze
//
// Created by Mike Freno on 1/9/26.
//
import Foundation
/// Represents a user-defined timer with customizable properties
struct UserTimer: Codable, Equatable {
let id: String
var type: UserTimerType
var timeOnScreenSeconds: Int
var message: String?
init(
id: String = UUID().uuidString,
type: UserTimerType = .subtle,
timeOnScreenSeconds: Int = 30,
message: String? = nil
) {
self.id = id
self.type = type
self.timeOnScreenSeconds = timeOnScreenSeconds
self.message = message
}
static func == (lhs: UserTimer, rhs: UserTimer) -> Bool {
lhs.id == rhs.id && lhs.type == rhs.type
&& lhs.timeOnScreenSeconds == rhs.timeOnScreenSeconds && lhs.message == rhs.message
}
}
/// Type of user timer - subtle or overlay
enum UserTimerType: String, Codable, CaseIterable, Identifiable {
case subtle
case overlay
var id: String { rawValue }
var displayName: String {
switch self {
case .subtle:
return "Subtle"
case .overlay:
return "Overlay"
}
}
}

View File

@@ -0,0 +1,68 @@
//
// AnimationService.swift
// Gaze
//
// Created by Mike Freno on 1/9/26.
//
import Foundation
@MainActor
class AnimationService {
static let shared = AnimationService()
private init() {}
struct RemoteAnimation: Codable {
let name: String
let version: String
let date: String // ISO 8601 formatted date string
enum CodingKeys: String, CodingKey {
case name, version, date
}
}
struct RemoteAnimationsResponse: Codable {
let animations: [RemoteAnimation]
}
// MARK: - Public Methods
func fetchRemoteAnimations() async throws -> [RemoteAnimation] {
guard let url = URL(string: "https://freno.me/api/Gaze/animations") else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard 200...299 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
do {
let decoder = JSONDecoder()
let remoteAnimations = try decoder.decode(RemoteAnimationsResponse.self, from: data)
return remoteAnimations.animations
} catch {
throw error
}
}
func updateLocalAnimationsIfNeeded(remoteAnimations: [RemoteAnimation]) async throws {
// For now, just validate the API response structure.
// In a real implementation, this would:
// 1. Compare dates of local vs remote animations
// 2. Update local files if newer versions exist
// 3. Tag local files with date fields in ISO 8601 format
for animation in remoteAnimations {
print("Remote animation: \(animation.name) - \(animation.version) - \(animation.date)")
}
}
}

View File

@@ -0,0 +1,17 @@
//
// AnimationServiceTests.swift
// GazeTests
//
// Created by Mike Freno on 1/9/26.
//
@testable import Gaze
final class AnimationServiceTests {
// Test cases can be added here as needed
func testRemoteAnimationDecoding() {
// This will be implemented when we have a testable implementation
}
}