feat: add lottie for animations
This commit is contained in:
@@ -112,6 +112,7 @@
|
|||||||
);
|
);
|
||||||
name = Gaze;
|
name = Gaze;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||||
);
|
);
|
||||||
productName = Gaze;
|
productName = Gaze;
|
||||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||||
@@ -195,6 +196,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 27A21B332F0F69DC0018C4F3;
|
mainGroup = 27A21B332F0F69DC0018C4F3;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -573,6 +577,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.6.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
27AE10B12F10B1FC00E00DBC /* Lottie */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
|
productName = Lottie;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
};
|
};
|
||||||
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "6b3386dc9ff1f3a74f1534de9c41d47137eae0901cfe819ed442f1b241549359",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "lottie-spm",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/airbnb/lottie-spm.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "69faaefa7721fba9e434a52c16adf4329c9084db",
|
||||||
|
"version" : "4.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
132
Gaze/Animations/blink.json
Normal file
132
Gaze/Animations/blink.json
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"v": "5.7.4",
|
||||||
|
"fr": 30,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 60,
|
||||||
|
"w": 200,
|
||||||
|
"h": 200,
|
||||||
|
"nm": "Blink Animation",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Eye Container",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 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,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [80, 80], "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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"w": {"a": 0, "k": 8},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 2,
|
||||||
|
"nm": "Stroke"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {"a": 0, "k": [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": "Eye Shape"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 60,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Pupil",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 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,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [25, 25]},
|
||||||
|
"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": 0, "k": [0, 0]},
|
||||||
|
"a": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100]},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"o": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [100], "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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Pupil Shape"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 60,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": []
|
||||||
|
}
|
||||||
319
Gaze/Animations/look-away.json
Normal file
319
Gaze/Animations/look-away.json
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
{
|
||||||
|
"v": "5.7.4",
|
||||||
|
"fr": 30,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 120,
|
||||||
|
"w": 200,
|
||||||
|
"h": 200,
|
||||||
|
"nm": "Look Away Animation",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Face Circle",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 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,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [150, 150]},
|
||||||
|
"nm": "Face Ellipse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"w": {"a": 0, "k": 6},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 2,
|
||||||
|
"nm": "Stroke"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {"a": 0, "k": [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": "Face"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 120,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Left Eye",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"p": {"a": 0, "k": [70, 85, 0]},
|
||||||
|
"a": {"a": 0, "k": [0, 0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100, 100]}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [20, 20]},
|
||||||
|
"nm": "Eye Ellipse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"w": {"a": 0, "k": 4},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 2,
|
||||||
|
"nm": "Stroke"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 20, "s": [-15, 0], "h": 1},
|
||||||
|
{"t": 40, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 60, "s": [15, 0], "h": 1},
|
||||||
|
{"t": 80, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 100, "s": [0, -10], "h": 1},
|
||||||
|
{"t": 120, "s": [0, 0], "h": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"a": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100]},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"o": {"a": 0, "k": 100}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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], "h": 1},
|
||||||
|
{"t": 20, "s": [-15, 0], "h": 1},
|
||||||
|
{"t": 40, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 60, "s": [15, 0], "h": 1},
|
||||||
|
{"t": 80, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 100, "s": [0, -10], "h": 1},
|
||||||
|
{"t": 120, "s": [0, 0], "h": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"op": 120,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Right Eye",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"p": {"a": 0, "k": [130, 85, 0]},
|
||||||
|
"a": {"a": 0, "k": [0, 0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100, 100]}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [20, 20]},
|
||||||
|
"nm": "Eye Ellipse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"w": {"a": 0, "k": 4},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 2,
|
||||||
|
"nm": "Stroke"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 20, "s": [-15, 0], "h": 1},
|
||||||
|
{"t": 40, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 60, "s": [15, 0], "h": 1},
|
||||||
|
{"t": 80, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 100, "s": [0, -10], "h": 1},
|
||||||
|
{"t": 120, "s": [0, 0], "h": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"a": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100]},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"o": {"a": 0, "k": 100}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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], "h": 1},
|
||||||
|
{"t": 20, "s": [-15, 0], "h": 1},
|
||||||
|
{"t": 40, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 60, "s": [15, 0], "h": 1},
|
||||||
|
{"t": 80, "s": [0, 0], "h": 1},
|
||||||
|
{"t": 100, "s": [0, -10], "h": 1},
|
||||||
|
{"t": 120, "s": [0, 0], "h": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"op": 120,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Smile",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"r": {"a": 0, "k": 0},
|
||||||
|
"p": {"a": 0, "k": [100, 120, 0]},
|
||||||
|
"a": {"a": 0, "k": [0, 0, 0]},
|
||||||
|
"s": {"a": 0, "k": [100, 100, 100]}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "sh",
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [[0, 0], [-20, 10], [20, 10]],
|
||||||
|
"o": [[20, 10], [20, -10], [0, 0]],
|
||||||
|
"v": [[-30, 0], [0, 15], [30, 0]],
|
||||||
|
"c": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nm": "Smile Path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "st",
|
||||||
|
"c": {"a": 0, "k": [0, 0.478, 1, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"w": {"a": 0, "k": 4},
|
||||||
|
"lc": 2,
|
||||||
|
"lj": 2,
|
||||||
|
"nm": "Stroke"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {"a": 0, "k": [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": "Smile"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 120,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": []
|
||||||
|
}
|
||||||
171
Gaze/Animations/posture.json
Normal file
171
Gaze/Animations/posture.json
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"v": "5.7.4",
|
||||||
|
"fr": 30,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 90,
|
||||||
|
"w": 200,
|
||||||
|
"h": 400,
|
||||||
|
"nm": "Posture Arrow Animation",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Arrow",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [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},
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "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]},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "sh",
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [[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": [
|
||||||
|
[0, -40],
|
||||||
|
[-30, -10],
|
||||||
|
[-12, -10],
|
||||||
|
[-12, 40],
|
||||||
|
[12, 40],
|
||||||
|
[12, -10],
|
||||||
|
[30, -10]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nm": "Arrow Path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {"a": 0, "k": [0, 0, 0, 1]},
|
||||||
|
"o": {"a": 0, "k": 100},
|
||||||
|
"r": 1,
|
||||||
|
"nm": "Fill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {"a": 0, "k": [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": "Arrow Shape"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 90,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Circle Background",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [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},
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "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]},
|
||||||
|
"s": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{"t": 0, "s": [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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ty": "el",
|
||||||
|
"p": {"a": 0, "k": [0, 0]},
|
||||||
|
"s": {"a": 0, "k": [120, 120]},
|
||||||
|
"nm": "Circle Ellipse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {"a": 0, "k": [0.9, 0.9, 0.9, 1]},
|
||||||
|
"o": {"a": 0, "k": 30},
|
||||||
|
"r": 1,
|
||||||
|
"nm": "Fill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": {"a": 0, "k": [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": "Circle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 90,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": []
|
||||||
|
}
|
||||||
18
Gaze/Models/AnimationAsset.swift
Normal file
18
Gaze/Models/AnimationAsset.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// AnimationAsset.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum AnimationAsset: String {
|
||||||
|
case blink = "blink"
|
||||||
|
case lookAway = "look-away"
|
||||||
|
case posture = "posture"
|
||||||
|
|
||||||
|
var fileName: String {
|
||||||
|
return self.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Gaze/Views/Components/LottieView.swift
Normal file
67
Gaze/Views/Components/LottieView.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// LottieView.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Lottie
|
||||||
|
|
||||||
|
struct LottieView: NSViewRepresentable {
|
||||||
|
let animationName: String
|
||||||
|
let loopMode: LottieLoopMode
|
||||||
|
let animationSpeed: CGFloat
|
||||||
|
|
||||||
|
init(
|
||||||
|
animationName: String,
|
||||||
|
loopMode: LottieLoopMode = .playOnce,
|
||||||
|
animationSpeed: CGFloat = 1.0
|
||||||
|
) {
|
||||||
|
self.animationName = animationName
|
||||||
|
self.loopMode = loopMode
|
||||||
|
self.animationSpeed = animationSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> LottieAnimationView {
|
||||||
|
let animationView = LottieAnimationView()
|
||||||
|
animationView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
if let animation = LottieAnimation.named(animationName) {
|
||||||
|
animationView.animation = animation
|
||||||
|
animationView.loopMode = loopMode
|
||||||
|
animationView.animationSpeed = animationSpeed
|
||||||
|
animationView.backgroundBehavior = .pauseAndRestore
|
||||||
|
animationView.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
return animationView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: LottieAnimationView, context: Context) {
|
||||||
|
guard nsView.animation == nil || nsView.isAnimationPlaying == false else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let animation = LottieAnimation.named(animationName) {
|
||||||
|
nsView.animation = animation
|
||||||
|
nsView.loopMode = loopMode
|
||||||
|
nsView.animationSpeed = animationSpeed
|
||||||
|
nsView.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Lottie Preview") {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
LottieView(animationName: "blink")
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
|
LottieView(animationName: "look-away", loopMode: .loop)
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
|
LottieView(animationName: "posture")
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
}
|
||||||
|
.frame(width: 600, height: 800)
|
||||||
|
}
|
||||||
@@ -6,39 +6,26 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Lottie
|
||||||
|
|
||||||
struct BlinkReminderView: View {
|
struct BlinkReminderView: View {
|
||||||
var onDismiss: () -> Void
|
var onDismiss: () -> Void
|
||||||
|
|
||||||
@State private var opacity: Double = 0
|
@State private var opacity: Double = 0
|
||||||
@State private var scale: CGFloat = 0
|
@State private var scale: CGFloat = 0
|
||||||
@State private var blinkProgress: Double = 0
|
|
||||||
@State private var blinkCount = 0
|
|
||||||
|
|
||||||
private let screenHeight = NSScreen.main?.frame.height ?? 800
|
private let screenHeight = NSScreen.main?.frame.height ?? 800
|
||||||
private let screenWidth = NSScreen.main?.frame.width ?? 1200
|
private let screenWidth = NSScreen.main?.frame.width ?? 1200
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
// Custom eye design for more polished look
|
LottieView(
|
||||||
ZStack {
|
animationName: AnimationAsset.blink.fileName,
|
||||||
// Eye outline
|
loopMode: .playOnce,
|
||||||
Circle()
|
animationSpeed: 1.0
|
||||||
.stroke(Color.accentColor, lineWidth: 4)
|
)
|
||||||
.frame(width: scale * 1.2, height: scale * 1.2)
|
.frame(width: scale, height: scale)
|
||||||
|
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 2)
|
||||||
// Iris
|
|
||||||
Circle()
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
.frame(width: scale * 0.6, height: scale * 0.6)
|
|
||||||
|
|
||||||
// Pupil that moves with blink
|
|
||||||
Circle()
|
|
||||||
.fill(.black)
|
|
||||||
.frame(width: scale * 0.25, height: scale * 0.25)
|
|
||||||
.offset(y: blinkProgress * -scale * 0.1) // Vertical movement during blink
|
|
||||||
}
|
|
||||||
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
|
|
||||||
}
|
}
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
@@ -49,60 +36,25 @@ struct BlinkReminderView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startAnimation() {
|
private func startAnimation() {
|
||||||
// Fade in and grow with spring animation for natural feel
|
// Fade in and grow
|
||||||
withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
opacity = 1.0
|
opacity = 1.0
|
||||||
scale = screenWidth * 0.12
|
scale = screenWidth * 0.15
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start blinking after fade in
|
// Animation duration (2 seconds for double blink) + hold time
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.3) {
|
||||||
performBlinks()
|
fadeOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performBlinks() {
|
|
||||||
let blinkDuration = 0.15
|
|
||||||
let pauseBetweenBlinks = 0.2
|
|
||||||
|
|
||||||
func blink() {
|
|
||||||
// Close eyes with spring animation for natural movement
|
|
||||||
withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
|
|
||||||
blinkProgress = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open eyes
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + blinkDuration) {
|
|
||||||
withAnimation(.spring(duration: blinkDuration, bounce: 0.0)) {
|
|
||||||
blinkProgress = 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
blinkCount += 1
|
|
||||||
|
|
||||||
if blinkCount < 2 {
|
|
||||||
// Pause before next blink
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + pauseBetweenBlinks) {
|
|
||||||
blink()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fade out after all blinks with smooth spring animation
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
||||||
fadeOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blink()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fadeOut() {
|
private func fadeOut() {
|
||||||
withAnimation(.spring(duration: 0.5, bounce: 0.2)) {
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
opacity = 0
|
opacity = 0
|
||||||
scale = screenWidth * 0.08
|
scale = screenWidth * 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,3 +64,8 @@ struct BlinkReminderView: View {
|
|||||||
BlinkReminderView(onDismiss: {})
|
BlinkReminderView(onDismiss: {})
|
||||||
.frame(width: 800, height: 600)
|
.frame(width: 800, height: 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview("Blink Reminder") {
|
||||||
|
BlinkReminderView(onDismiss: {})
|
||||||
|
.frame(width: 800, height: 600)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Lottie
|
||||||
|
|
||||||
struct LookAwayReminderView: View {
|
struct LookAwayReminderView: View {
|
||||||
let countdownSeconds: Int
|
let countdownSeconds: Int
|
||||||
@@ -35,8 +36,13 @@ struct LookAwayReminderView: View {
|
|||||||
.font(.system(size: 28))
|
.font(.system(size: 28))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.white.opacity(0.9))
|
||||||
|
|
||||||
AnimatedFaceView(size: 200)
|
LottieView(
|
||||||
.padding(.vertical, 30)
|
animationName: AnimationAsset.lookAway.fileName,
|
||||||
|
loopMode: .loop,
|
||||||
|
animationSpeed: 1.0
|
||||||
|
)
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
.padding(.vertical, 30)
|
||||||
|
|
||||||
// Countdown display
|
// Countdown display
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
53
GazeTests/Models/AnimationAssetTests.swift
Normal file
53
GazeTests/Models/AnimationAssetTests.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// AnimationAssetTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class AnimationAssetTests: XCTestCase {
|
||||||
|
|
||||||
|
func testRawValues() {
|
||||||
|
XCTAssertEqual(AnimationAsset.blink.rawValue, "blink")
|
||||||
|
XCTAssertEqual(AnimationAsset.lookAway.rawValue, "look-away")
|
||||||
|
XCTAssertEqual(AnimationAsset.posture.rawValue, "posture")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileNames() {
|
||||||
|
XCTAssertEqual(AnimationAsset.blink.fileName, "blink")
|
||||||
|
XCTAssertEqual(AnimationAsset.lookAway.fileName, "look-away")
|
||||||
|
XCTAssertEqual(AnimationAsset.posture.fileName, "posture")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileNameMatchesRawValue() {
|
||||||
|
XCTAssertEqual(AnimationAsset.blink.fileName, AnimationAsset.blink.rawValue)
|
||||||
|
XCTAssertEqual(AnimationAsset.lookAway.fileName, AnimationAsset.lookAway.rawValue)
|
||||||
|
XCTAssertEqual(AnimationAsset.posture.fileName, AnimationAsset.posture.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitFromRawValue() {
|
||||||
|
XCTAssertEqual(AnimationAsset(rawValue: "blink"), .blink)
|
||||||
|
XCTAssertEqual(AnimationAsset(rawValue: "look-away"), .lookAway)
|
||||||
|
XCTAssertEqual(AnimationAsset(rawValue: "posture"), .posture)
|
||||||
|
XCTAssertNil(AnimationAsset(rawValue: "invalid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
XCTAssertEqual(AnimationAsset.blink, AnimationAsset.blink)
|
||||||
|
XCTAssertNotEqual(AnimationAsset.blink, AnimationAsset.lookAway)
|
||||||
|
XCTAssertNotEqual(AnimationAsset.lookAway, AnimationAsset.posture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllCasesExist() {
|
||||||
|
let blink = AnimationAsset.blink
|
||||||
|
let lookAway = AnimationAsset.lookAway
|
||||||
|
let posture = AnimationAsset.posture
|
||||||
|
|
||||||
|
XCTAssertNotNil(blink)
|
||||||
|
XCTAssertNotNil(lookAway)
|
||||||
|
XCTAssertNotNil(posture)
|
||||||
|
}
|
||||||
|
}
|
||||||
175
GazeTests/Models/AppSettingsTests.swift
Normal file
175
GazeTests/Models/AppSettingsTests.swift
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
//
|
||||||
|
// AppSettingsTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class AppSettingsTests: XCTestCase {
|
||||||
|
|
||||||
|
func testDefaultSettings() {
|
||||||
|
let settings = AppSettings.defaults
|
||||||
|
|
||||||
|
XCTAssertTrue(settings.lookAwayTimer.enabled)
|
||||||
|
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60)
|
||||||
|
XCTAssertEqual(settings.lookAwayCountdownSeconds, 20)
|
||||||
|
|
||||||
|
XCTAssertTrue(settings.blinkTimer.enabled)
|
||||||
|
XCTAssertEqual(settings.blinkTimer.intervalSeconds, 5 * 60)
|
||||||
|
|
||||||
|
XCTAssertTrue(settings.postureTimer.enabled)
|
||||||
|
XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60)
|
||||||
|
|
||||||
|
XCTAssertFalse(settings.hasCompletedOnboarding)
|
||||||
|
XCTAssertFalse(settings.launchAtLogin)
|
||||||
|
XCTAssertTrue(settings.playSounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
let settings1 = AppSettings.defaults
|
||||||
|
let settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
XCTAssertEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenLookAwayTimerDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.lookAwayTimer.enabled = false
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
|
||||||
|
settings2.lookAwayTimer.enabled = true
|
||||||
|
settings2.lookAwayTimer.intervalSeconds = 10 * 60
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenCountdownDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.lookAwayCountdownSeconds = 30
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenBlinkTimerDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.blinkTimer.enabled = false
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenPostureTimerDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.postureTimer.intervalSeconds = 60 * 60
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenOnboardingDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.hasCompletedOnboarding = true
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenLaunchAtLoginDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.launchAtLogin = true
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInequalityWhenPlaySoundsDiffers() {
|
||||||
|
var settings1 = AppSettings.defaults
|
||||||
|
var settings2 = AppSettings.defaults
|
||||||
|
|
||||||
|
settings2.playSounds = false
|
||||||
|
XCTAssertNotEqual(settings1, settings2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableEncoding() throws {
|
||||||
|
let settings = AppSettings.defaults
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(settings)
|
||||||
|
|
||||||
|
XCTAssertNotNil(data)
|
||||||
|
XCTAssertGreaterThan(data.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableDecoding() throws {
|
||||||
|
let settings = AppSettings.defaults
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(settings)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let decoded = try decoder.decode(AppSettings.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableRoundTripWithModifiedSettings() throws {
|
||||||
|
var settings = AppSettings.defaults
|
||||||
|
settings.lookAwayTimer.enabled = false
|
||||||
|
settings.lookAwayCountdownSeconds = 30
|
||||||
|
settings.blinkTimer.intervalSeconds = 10 * 60
|
||||||
|
settings.postureTimer.enabled = false
|
||||||
|
settings.hasCompletedOnboarding = true
|
||||||
|
settings.launchAtLogin = true
|
||||||
|
settings.playSounds = false
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(settings)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let decoded = try decoder.decode(AppSettings.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, settings)
|
||||||
|
XCTAssertFalse(decoded.lookAwayTimer.enabled)
|
||||||
|
XCTAssertEqual(decoded.lookAwayCountdownSeconds, 30)
|
||||||
|
XCTAssertEqual(decoded.blinkTimer.intervalSeconds, 10 * 60)
|
||||||
|
XCTAssertFalse(decoded.postureTimer.enabled)
|
||||||
|
XCTAssertTrue(decoded.hasCompletedOnboarding)
|
||||||
|
XCTAssertTrue(decoded.launchAtLogin)
|
||||||
|
XCTAssertFalse(decoded.playSounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMutability() {
|
||||||
|
var settings = AppSettings.defaults
|
||||||
|
|
||||||
|
settings.lookAwayTimer.enabled = false
|
||||||
|
XCTAssertFalse(settings.lookAwayTimer.enabled)
|
||||||
|
|
||||||
|
settings.lookAwayCountdownSeconds = 30
|
||||||
|
XCTAssertEqual(settings.lookAwayCountdownSeconds, 30)
|
||||||
|
|
||||||
|
settings.hasCompletedOnboarding = true
|
||||||
|
XCTAssertTrue(settings.hasCompletedOnboarding)
|
||||||
|
|
||||||
|
settings.launchAtLogin = true
|
||||||
|
XCTAssertTrue(settings.launchAtLogin)
|
||||||
|
|
||||||
|
settings.playSounds = false
|
||||||
|
XCTAssertFalse(settings.playSounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBoundaryValues() {
|
||||||
|
var settings = AppSettings.defaults
|
||||||
|
|
||||||
|
settings.lookAwayTimer.intervalSeconds = 0
|
||||||
|
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 0)
|
||||||
|
|
||||||
|
settings.lookAwayCountdownSeconds = 0
|
||||||
|
XCTAssertEqual(settings.lookAwayCountdownSeconds, 0)
|
||||||
|
|
||||||
|
settings.lookAwayTimer.intervalSeconds = Int.max
|
||||||
|
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, Int.max)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
GazeTests/Models/ReminderEventTests.swift
Normal file
115
GazeTests/Models/ReminderEventTests.swift
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// ReminderEventTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class ReminderEventTests: XCTestCase {
|
||||||
|
|
||||||
|
func testLookAwayTriggeredCreation() {
|
||||||
|
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||||
|
|
||||||
|
if case .lookAwayTriggered(let countdown) = event {
|
||||||
|
XCTAssertEqual(countdown, 20)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected lookAwayTriggered event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBlinkTriggeredCreation() {
|
||||||
|
let event = ReminderEvent.blinkTriggered
|
||||||
|
|
||||||
|
if case .blinkTriggered = event {
|
||||||
|
XCTAssertTrue(true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected blinkTriggered event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostureTriggeredCreation() {
|
||||||
|
let event = ReminderEvent.postureTriggered
|
||||||
|
|
||||||
|
if case .postureTriggered = event {
|
||||||
|
XCTAssertTrue(true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected postureTriggered event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTypePropertyForLookAway() {
|
||||||
|
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||||
|
XCTAssertEqual(event.type, .lookAway)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTypePropertyForBlink() {
|
||||||
|
let event = ReminderEvent.blinkTriggered
|
||||||
|
XCTAssertEqual(event.type, .blink)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTypePropertyForPosture() {
|
||||||
|
let event = ReminderEvent.postureTriggered
|
||||||
|
XCTAssertEqual(event.type, .posture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||||
|
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
|
||||||
|
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 30)
|
||||||
|
let event4 = ReminderEvent.blinkTriggered
|
||||||
|
let event5 = ReminderEvent.blinkTriggered
|
||||||
|
let event6 = ReminderEvent.postureTriggered
|
||||||
|
|
||||||
|
XCTAssertEqual(event1, event2)
|
||||||
|
XCTAssertNotEqual(event1, event3)
|
||||||
|
XCTAssertNotEqual(event1, event4)
|
||||||
|
XCTAssertEqual(event4, event5)
|
||||||
|
XCTAssertNotEqual(event4, event6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDifferentCountdownValues() {
|
||||||
|
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 0)
|
||||||
|
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 10)
|
||||||
|
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 60)
|
||||||
|
|
||||||
|
XCTAssertNotEqual(event1, event2)
|
||||||
|
XCTAssertNotEqual(event2, event3)
|
||||||
|
XCTAssertNotEqual(event1, event3)
|
||||||
|
|
||||||
|
XCTAssertEqual(event1.type, .lookAway)
|
||||||
|
XCTAssertEqual(event2.type, .lookAway)
|
||||||
|
XCTAssertEqual(event3.type, .lookAway)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNegativeCountdown() {
|
||||||
|
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: -5)
|
||||||
|
|
||||||
|
if case .lookAwayTriggered(let countdown) = event {
|
||||||
|
XCTAssertEqual(countdown, -5)
|
||||||
|
} else {
|
||||||
|
XCTFail("Expected lookAwayTriggered event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSwitchExhaustivenessWithAllCases() {
|
||||||
|
let events: [ReminderEvent] = [
|
||||||
|
.lookAwayTriggered(countdownSeconds: 20),
|
||||||
|
.blinkTriggered,
|
||||||
|
.postureTriggered
|
||||||
|
]
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
switch event {
|
||||||
|
case .lookAwayTriggered:
|
||||||
|
XCTAssertEqual(event.type, .lookAway)
|
||||||
|
case .blinkTriggered:
|
||||||
|
XCTAssertEqual(event.type, .blink)
|
||||||
|
case .postureTriggered:
|
||||||
|
XCTAssertEqual(event.type, .posture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
GazeTests/Models/TimerConfigurationTests.swift
Normal file
124
GazeTests/Models/TimerConfigurationTests.swift
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// TimerConfigurationTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class TimerConfigurationTests: XCTestCase {
|
||||||
|
|
||||||
|
func testInitialization() {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
|
||||||
|
XCTAssertTrue(config.enabled)
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitializationDisabled() {
|
||||||
|
let config = TimerConfiguration(enabled: false, intervalSeconds: 600)
|
||||||
|
|
||||||
|
XCTAssertFalse(config.enabled)
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIntervalMinutesGetter() {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
XCTAssertEqual(config.intervalMinutes, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIntervalMinutesSetter() {
|
||||||
|
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||||
|
config.intervalMinutes = 15
|
||||||
|
|
||||||
|
XCTAssertEqual(config.intervalMinutes, 15)
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 900)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIntervalMinutesConversion() {
|
||||||
|
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||||
|
|
||||||
|
config.intervalMinutes = 1
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 60)
|
||||||
|
|
||||||
|
config.intervalMinutes = 60
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 3600)
|
||||||
|
|
||||||
|
config.intervalMinutes = 0
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
let config1 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
let config2 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
let config3 = TimerConfiguration(enabled: false, intervalSeconds: 1200)
|
||||||
|
let config4 = TimerConfiguration(enabled: true, intervalSeconds: 600)
|
||||||
|
|
||||||
|
XCTAssertEqual(config1, config2)
|
||||||
|
XCTAssertNotEqual(config1, config3)
|
||||||
|
XCTAssertNotEqual(config1, config4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableEncoding() throws {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(config)
|
||||||
|
|
||||||
|
XCTAssertNotNil(data)
|
||||||
|
XCTAssertGreaterThan(data.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableDecoding() throws {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(config)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodableRoundTrip() throws {
|
||||||
|
let configs = [
|
||||||
|
TimerConfiguration(enabled: true, intervalSeconds: 300),
|
||||||
|
TimerConfiguration(enabled: false, intervalSeconds: 1200),
|
||||||
|
TimerConfiguration(enabled: true, intervalSeconds: 1800),
|
||||||
|
TimerConfiguration(enabled: false, intervalSeconds: 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
for config in configs {
|
||||||
|
let data = try encoder.encode(config)
|
||||||
|
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
|
||||||
|
XCTAssertEqual(decoded, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMutability() {
|
||||||
|
var config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
|
||||||
|
|
||||||
|
config.enabled = false
|
||||||
|
XCTAssertFalse(config.enabled)
|
||||||
|
|
||||||
|
config.intervalSeconds = 600
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 600)
|
||||||
|
XCTAssertEqual(config.intervalMinutes, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testZeroInterval() {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 0)
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 0)
|
||||||
|
XCTAssertEqual(config.intervalMinutes, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLargeInterval() {
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 86400)
|
||||||
|
XCTAssertEqual(config.intervalSeconds, 86400)
|
||||||
|
XCTAssertEqual(config.intervalMinutes, 1440)
|
||||||
|
}
|
||||||
|
}
|
||||||
93
GazeTests/Models/TimerStateTests.swift
Normal file
93
GazeTests/Models/TimerStateTests.swift
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// TimerStateTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class TimerStateTests: XCTestCase {
|
||||||
|
|
||||||
|
func testInitialization() {
|
||||||
|
let state = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||||
|
|
||||||
|
XCTAssertEqual(state.type, .lookAway)
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 1200)
|
||||||
|
XCTAssertFalse(state.isPaused)
|
||||||
|
XCTAssertTrue(state.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitializationWithPausedState() {
|
||||||
|
let state = TimerState(type: .blink, intervalSeconds: 300, isPaused: true)
|
||||||
|
|
||||||
|
XCTAssertEqual(state.type, .blink)
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 300)
|
||||||
|
XCTAssertTrue(state.isPaused)
|
||||||
|
XCTAssertTrue(state.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitializationWithInactiveState() {
|
||||||
|
let state = TimerState(type: .posture, intervalSeconds: 1800, isPaused: false, isActive: false)
|
||||||
|
|
||||||
|
XCTAssertEqual(state.type, .posture)
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 1800)
|
||||||
|
XCTAssertFalse(state.isPaused)
|
||||||
|
XCTAssertFalse(state.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMutability() {
|
||||||
|
var state = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||||
|
|
||||||
|
state.remainingSeconds = 600
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 600)
|
||||||
|
|
||||||
|
state.isPaused = true
|
||||||
|
XCTAssertTrue(state.isPaused)
|
||||||
|
|
||||||
|
state.isActive = false
|
||||||
|
XCTAssertFalse(state.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||||
|
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200)
|
||||||
|
let state3 = TimerState(type: .blink, intervalSeconds: 1200)
|
||||||
|
let state4 = TimerState(type: .lookAway, intervalSeconds: 600)
|
||||||
|
|
||||||
|
XCTAssertEqual(state1, state2)
|
||||||
|
XCTAssertNotEqual(state1, state3)
|
||||||
|
XCTAssertNotEqual(state1, state4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEqualityWithDifferentPausedState() {
|
||||||
|
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: false)
|
||||||
|
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: true)
|
||||||
|
|
||||||
|
XCTAssertNotEqual(state1, state2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEqualityWithDifferentActiveState() {
|
||||||
|
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: true)
|
||||||
|
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: false)
|
||||||
|
|
||||||
|
XCTAssertNotEqual(state1, state2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testZeroRemainingSeconds() {
|
||||||
|
let state = TimerState(type: .lookAway, intervalSeconds: 0)
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNegativeRemainingSeconds() {
|
||||||
|
var state = TimerState(type: .lookAway, intervalSeconds: 10)
|
||||||
|
state.remainingSeconds = -5
|
||||||
|
XCTAssertEqual(state.remainingSeconds, -5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLargeIntervalSeconds() {
|
||||||
|
let state = TimerState(type: .posture, intervalSeconds: 86400)
|
||||||
|
XCTAssertEqual(state.remainingSeconds, 86400)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
GazeTests/Models/TimerTypeTests.swift
Normal file
68
GazeTests/Models/TimerTypeTests.swift
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// TimerTypeTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class TimerTypeTests: XCTestCase {
|
||||||
|
|
||||||
|
func testAllCases() {
|
||||||
|
let allCases = TimerType.allCases
|
||||||
|
XCTAssertEqual(allCases.count, 3)
|
||||||
|
XCTAssertTrue(allCases.contains(.lookAway))
|
||||||
|
XCTAssertTrue(allCases.contains(.blink))
|
||||||
|
XCTAssertTrue(allCases.contains(.posture))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRawValues() {
|
||||||
|
XCTAssertEqual(TimerType.lookAway.rawValue, "lookAway")
|
||||||
|
XCTAssertEqual(TimerType.blink.rawValue, "blink")
|
||||||
|
XCTAssertEqual(TimerType.posture.rawValue, "posture")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisplayNames() {
|
||||||
|
XCTAssertEqual(TimerType.lookAway.displayName, "Look Away")
|
||||||
|
XCTAssertEqual(TimerType.blink.displayName, "Blink")
|
||||||
|
XCTAssertEqual(TimerType.posture.displayName, "Posture")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIconNames() {
|
||||||
|
XCTAssertEqual(TimerType.lookAway.iconName, "eye.fill")
|
||||||
|
XCTAssertEqual(TimerType.blink.iconName, "eye.circle")
|
||||||
|
XCTAssertEqual(TimerType.posture.iconName, "figure.stand")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIdentifiable() {
|
||||||
|
XCTAssertEqual(TimerType.lookAway.id, "lookAway")
|
||||||
|
XCTAssertEqual(TimerType.blink.id, "blink")
|
||||||
|
XCTAssertEqual(TimerType.posture.id, "posture")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCodable() throws {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
for timerType in TimerType.allCases {
|
||||||
|
let encoded = try encoder.encode(timerType)
|
||||||
|
let decoded = try decoder.decode(TimerType.self, from: encoded)
|
||||||
|
XCTAssertEqual(decoded, timerType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEquality() {
|
||||||
|
XCTAssertEqual(TimerType.lookAway, TimerType.lookAway)
|
||||||
|
XCTAssertNotEqual(TimerType.lookAway, TimerType.blink)
|
||||||
|
XCTAssertNotEqual(TimerType.blink, TimerType.posture)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitFromRawValue() {
|
||||||
|
XCTAssertEqual(TimerType(rawValue: "lookAway"), .lookAway)
|
||||||
|
XCTAssertEqual(TimerType(rawValue: "blink"), .blink)
|
||||||
|
XCTAssertEqual(TimerType(rawValue: "posture"), .posture)
|
||||||
|
XCTAssertNil(TimerType(rawValue: "invalid"))
|
||||||
|
}
|
||||||
|
}
|
||||||
71
GazeTests/Services/LaunchAtLoginManagerTests.swift
Normal file
71
GazeTests/Services/LaunchAtLoginManagerTests.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// LaunchAtLoginManagerTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class LaunchAtLoginManagerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testIsEnabledReturnsBool() {
|
||||||
|
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||||
|
XCTAssertNotNil(isEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsEnabledOnMacOS13AndLater() {
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||||
|
XCTAssert(isEnabled == true || isEnabled == false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIsEnabledOnOlderMacOS() {
|
||||||
|
if #unavailable(macOS 13.0) {
|
||||||
|
let isEnabled = LaunchAtLoginManager.isEnabled
|
||||||
|
XCTAssertFalse(isEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnableThrowsOnUnsupportedOS() {
|
||||||
|
if #unavailable(macOS 13.0) {
|
||||||
|
XCTAssertThrowsError(try LaunchAtLoginManager.enable()) { error in
|
||||||
|
XCTAssertTrue(error is LaunchAtLoginError)
|
||||||
|
if let launchError = error as? LaunchAtLoginError {
|
||||||
|
XCTAssertEqual(launchError, .unsupportedOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisableThrowsOnUnsupportedOS() {
|
||||||
|
if #unavailable(macOS 13.0) {
|
||||||
|
XCTAssertThrowsError(try LaunchAtLoginManager.disable()) { error in
|
||||||
|
XCTAssertTrue(error is LaunchAtLoginError)
|
||||||
|
if let launchError = error as? LaunchAtLoginError {
|
||||||
|
XCTAssertEqual(launchError, .unsupportedOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testToggleDoesNotCrash() {
|
||||||
|
LaunchAtLoginManager.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLaunchAtLoginErrorCases() {
|
||||||
|
let unsupportedError = LaunchAtLoginError.unsupportedOS
|
||||||
|
let registrationError = LaunchAtLoginError.registrationFailed
|
||||||
|
|
||||||
|
XCTAssertNotEqual(unsupportedError, registrationError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLaunchAtLoginErrorEquality() {
|
||||||
|
let error1 = LaunchAtLoginError.unsupportedOS
|
||||||
|
let error2 = LaunchAtLoginError.unsupportedOS
|
||||||
|
|
||||||
|
XCTAssertEqual(error1, error2)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
GazeTests/Services/MigrationManagerTests.swift
Normal file
137
GazeTests/Services/MigrationManagerTests.swift
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
//
|
||||||
|
// MigrationManagerTests.swift
|
||||||
|
// GazeTests
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import Gaze
|
||||||
|
|
||||||
|
final class MigrationManagerTests: XCTestCase {
|
||||||
|
|
||||||
|
var migrationManager: MigrationManager!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
migrationManager = MigrationManager()
|
||||||
|
UserDefaults.standard.removeObject(forKey: "app_version")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "app_version")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "gazeAppSettings")
|
||||||
|
UserDefaults.standard.removeObject(forKey: "gazeAppSettings_backup")
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetCurrentVersionDefaultsToZero() {
|
||||||
|
let version = migrationManager.getCurrentVersion()
|
||||||
|
XCTAssertEqual(version, "0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSetCurrentVersion() {
|
||||||
|
migrationManager.setCurrentVersion("1.2.3")
|
||||||
|
let version = migrationManager.getCurrentVersion()
|
||||||
|
XCTAssertEqual(version, "1.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMigrateSettingsReturnsNilWhenNoSettings() throws {
|
||||||
|
let result = try migrationManager.migrateSettingsIfNeeded()
|
||||||
|
XCTAssertNil(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMigrateSettingsReturnsExistingDataWhenUpToDate() throws {
|
||||||
|
let testData: [String: Any] = ["test": "value"]
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: testData)
|
||||||
|
UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings")
|
||||||
|
|
||||||
|
if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
|
||||||
|
migrationManager.setCurrentVersion(bundleVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try migrationManager.migrateSettingsIfNeeded()
|
||||||
|
XCTAssertNotNil(result)
|
||||||
|
XCTAssertEqual(result?["test"] as? String, "value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMigrationErrorTypes() {
|
||||||
|
let migrationFailed = MigrationError.migrationFailed("test")
|
||||||
|
let invalidData = MigrationError.invalidDataStructure
|
||||||
|
let versionMismatch = MigrationError.versionMismatch
|
||||||
|
let noBackup = MigrationError.noBackupAvailable
|
||||||
|
|
||||||
|
switch migrationFailed {
|
||||||
|
case .migrationFailed(let message):
|
||||||
|
XCTAssertEqual(message, "test")
|
||||||
|
default:
|
||||||
|
XCTFail("Expected migrationFailed error")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNotNil(invalidData.errorDescription)
|
||||||
|
XCTAssertNotNil(versionMismatch.errorDescription)
|
||||||
|
XCTAssertNotNil(noBackup.errorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMigrationErrorDescriptions() {
|
||||||
|
let errors: [MigrationError] = [
|
||||||
|
.migrationFailed("test message"),
|
||||||
|
.invalidDataStructure,
|
||||||
|
.versionMismatch,
|
||||||
|
.noBackupAvailable
|
||||||
|
]
|
||||||
|
|
||||||
|
for error in errors {
|
||||||
|
XCTAssertNotNil(error.errorDescription)
|
||||||
|
XCTAssertFalse(error.errorDescription!.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVersion101MigrationTargetVersion() {
|
||||||
|
let migration = Version101Migration()
|
||||||
|
XCTAssertEqual(migration.targetVersion, "1.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVersion101MigrationPreservesData() throws {
|
||||||
|
let migration = Version101Migration()
|
||||||
|
let testData: [String: Any] = ["key1": "value1", "key2": 42]
|
||||||
|
|
||||||
|
let result = try migration.migrate(testData)
|
||||||
|
|
||||||
|
XCTAssertEqual(result["key1"] as? String, "value1")
|
||||||
|
XCTAssertEqual(result["key2"] as? Int, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMigrationThrowsOnInvalidData() {
|
||||||
|
UserDefaults.standard.set(Data("invalid json".utf8), forKey: "gazeAppSettings")
|
||||||
|
migrationManager.setCurrentVersion("0.0.0")
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try migrationManager.migrateSettingsIfNeeded()) { error in
|
||||||
|
XCTAssertTrue(error is MigrationError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVersionComparison() throws {
|
||||||
|
migrationManager.setCurrentVersion("1.0.0")
|
||||||
|
let current = migrationManager.getCurrentVersion()
|
||||||
|
XCTAssertEqual(current, "1.0.0")
|
||||||
|
|
||||||
|
migrationManager.setCurrentVersion("1.2.3")
|
||||||
|
let updated = migrationManager.getCurrentVersion()
|
||||||
|
XCTAssertEqual(updated, "1.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBackupIsCreatedDuringMigration() throws {
|
||||||
|
let testData: [String: Any] = ["test": "backup"]
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: testData)
|
||||||
|
UserDefaults.standard.set(jsonData, forKey: "gazeAppSettings")
|
||||||
|
migrationManager.setCurrentVersion("0.0.0")
|
||||||
|
|
||||||
|
_ = try? migrationManager.migrateSettingsIfNeeded()
|
||||||
|
|
||||||
|
let backupData = UserDefaults.standard.data(forKey: "gazeAppSettings_backup")
|
||||||
|
XCTAssertNotNil(backupData)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,4 +122,101 @@ final class SettingsManagerTests: XCTestCase {
|
|||||||
config.intervalMinutes = 20
|
config.intervalMinutes = 20
|
||||||
XCTAssertEqual(config.intervalSeconds, 1200)
|
XCTAssertEqual(config.intervalSeconds, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSettingsAutoSaveOnChange() {
|
||||||
|
var settings = AppSettings.defaults
|
||||||
|
settings.playSounds = false
|
||||||
|
|
||||||
|
settingsManager.settings = settings
|
||||||
|
|
||||||
|
let savedData = UserDefaults.standard.data(forKey: "gazeAppSettings")
|
||||||
|
XCTAssertNotNil(savedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultipleTimerConfigurationUpdates() {
|
||||||
|
let config1 = TimerConfiguration(enabled: false, intervalSeconds: 600)
|
||||||
|
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config1)
|
||||||
|
|
||||||
|
let config2 = TimerConfiguration(enabled: true, intervalSeconds: 900)
|
||||||
|
settingsManager.updateTimerConfiguration(for: .blink, configuration: config2)
|
||||||
|
|
||||||
|
let config3 = TimerConfiguration(enabled: false, intervalSeconds: 2400)
|
||||||
|
settingsManager.updateTimerConfiguration(for: .posture, configuration: config3)
|
||||||
|
|
||||||
|
XCTAssertEqual(settingsManager.timerConfiguration(for: .lookAway).intervalSeconds, 600)
|
||||||
|
XCTAssertEqual(settingsManager.timerConfiguration(for: .blink).intervalSeconds, 900)
|
||||||
|
XCTAssertEqual(settingsManager.timerConfiguration(for: .posture).intervalSeconds, 2400)
|
||||||
|
|
||||||
|
XCTAssertFalse(settingsManager.timerConfiguration(for: .lookAway).enabled)
|
||||||
|
XCTAssertTrue(settingsManager.timerConfiguration(for: .blink).enabled)
|
||||||
|
XCTAssertFalse(settingsManager.timerConfiguration(for: .posture).enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSettingsPersistenceAcrossReloads() {
|
||||||
|
var settings = AppSettings.defaults
|
||||||
|
settings.lookAwayCountdownSeconds = 45
|
||||||
|
settings.playSounds = false
|
||||||
|
|
||||||
|
settingsManager.settings = settings
|
||||||
|
settingsManager.load()
|
||||||
|
|
||||||
|
XCTAssertEqual(settingsManager.settings.lookAwayCountdownSeconds, 45)
|
||||||
|
XCTAssertFalse(settingsManager.settings.playSounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidDataDoesNotCrashLoad() {
|
||||||
|
UserDefaults.standard.set(Data("invalid".utf8), forKey: "gazeAppSettings")
|
||||||
|
|
||||||
|
settingsManager.load()
|
||||||
|
|
||||||
|
XCTAssertEqual(settingsManager.settings, .defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllTimerTypesHaveConfiguration() {
|
||||||
|
for timerType in TimerType.allCases {
|
||||||
|
let config = settingsManager.timerConfiguration(for: timerType)
|
||||||
|
XCTAssertNotNil(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateTimerConfigurationPersists() {
|
||||||
|
let newConfig = TimerConfiguration(enabled: false, intervalSeconds: 7200)
|
||||||
|
settingsManager.updateTimerConfiguration(for: .posture, configuration: newConfig)
|
||||||
|
|
||||||
|
settingsManager.load()
|
||||||
|
|
||||||
|
let retrieved = settingsManager.timerConfiguration(for: .posture)
|
||||||
|
XCTAssertEqual(retrieved.intervalSeconds, 7200)
|
||||||
|
XCTAssertFalse(retrieved.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResetToDefaultsClearsAllChanges() {
|
||||||
|
settingsManager.settings.lookAwayTimer.enabled = false
|
||||||
|
settingsManager.settings.lookAwayCountdownSeconds = 60
|
||||||
|
settingsManager.settings.blinkTimer.intervalSeconds = 10 * 60
|
||||||
|
settingsManager.settings.postureTimer.enabled = false
|
||||||
|
settingsManager.settings.hasCompletedOnboarding = true
|
||||||
|
settingsManager.settings.launchAtLogin = true
|
||||||
|
settingsManager.settings.playSounds = false
|
||||||
|
|
||||||
|
settingsManager.resetToDefaults()
|
||||||
|
|
||||||
|
let defaults = AppSettings.defaults
|
||||||
|
XCTAssertEqual(settingsManager.settings, defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConcurrentAccessDoesNotCrash() {
|
||||||
|
let expectation = XCTestExpectation(description: "Concurrent access")
|
||||||
|
expectation.expectedFulfillmentCount = 10
|
||||||
|
|
||||||
|
for i in 0..<10 {
|
||||||
|
Task { @MainActor in
|
||||||
|
let config = TimerConfiguration(enabled: true, intervalSeconds: 300 * (i + 1))
|
||||||
|
settingsManager.updateTimerConfiguration(for: .lookAway, configuration: config)
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wait(for: [expectation], timeout: 2.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user