Compare commits
10 Commits
c1d56de620
...
4bd80245cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bd80245cd | ||
|
|
1e20283afc | ||
|
|
b725f9cfd7 | ||
|
|
a992bc8374 | ||
|
|
4b446db817 | ||
|
|
cbd60fdd08 | ||
|
|
6e41c4059c | ||
|
|
7d6e51a183 | ||
|
|
0b6dd3f903 | ||
|
|
7a23ae9bad |
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
||||||
|
27CF3CCB2F2D266600D67058 /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 27CF3CCA2F2D266600D67058 /* MacroVisionKit */; };
|
||||||
27SPARKLE00000000003 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27SPARKLE00000000002 /* Sparkle */; };
|
27SPARKLE00000000003 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27SPARKLE00000000002 /* Sparkle */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
27CF3CCB2F2D266600D67058 /* MacroVisionKit in Frameworks */,
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
||||||
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -134,6 +136,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||||
27SPARKLE00000000002 /* Sparkle */,
|
27SPARKLE00000000002 /* Sparkle */,
|
||||||
|
27CF3CCA2F2D266600D67058 /* MacroVisionKit */,
|
||||||
);
|
);
|
||||||
productName = Gaze;
|
productName = Gaze;
|
||||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||||
@@ -220,6 +223,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||||
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
|
27CF3CC92F2D266600D67058 /* XCRemoteSwiftPackageReference "MacroVisionKit" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||||
@@ -424,7 +428,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -439,7 +443,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -462,7 +466,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -477,7 +481,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -496,11 +500,11 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -517,11 +521,11 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -537,10 +541,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -556,10 +560,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 0.4.1;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -621,6 +625,14 @@
|
|||||||
minimumVersion = 4.6.0;
|
minimumVersion = 4.6.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
27CF3CC92F2D266600D67058 /* XCRemoteSwiftPackageReference "MacroVisionKit" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/TheBoredTeam/MacroVisionKit.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.1.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||||
@@ -637,6 +649,11 @@
|
|||||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||||
productName = Lottie;
|
productName = Lottie;
|
||||||
};
|
};
|
||||||
|
27CF3CCA2F2D266600D67058 /* MacroVisionKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 27CF3CC92F2D266600D67058 /* XCRemoteSwiftPackageReference "MacroVisionKit" */;
|
||||||
|
productName = MacroVisionKit;
|
||||||
|
};
|
||||||
27SPARKLE00000000002 /* Sparkle */ = {
|
27SPARKLE00000000002 /* Sparkle */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
package = 27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
|||||||
@@ -1,646 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 27AE10B12F10B1FC00E00DBC /* Lottie */; };
|
|
||||||
27SPARKLE00000000003 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 27SPARKLE00000000002 /* Sparkle */; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
27A21B4A2F0F69DD0018C4F3 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 27A21B3B2F0F69DC0018C4F3;
|
|
||||||
remoteInfo = Gaze;
|
|
||||||
};
|
|
||||||
27A21B542F0F69DD0018C4F3 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 27A21B3B2F0F69DC0018C4F3;
|
|
||||||
remoteInfo = Gaze;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
27A21B3C2F0F69DC0018C4F3 /* Gaze.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gaze.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
27A21B492F0F69DD0018C4F3 /* GazeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
27A21B532F0F69DD0018C4F3 /* GazeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GazeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
||||||
270D22E92F1474F1008BCE42 /* Exceptions for "Gaze" folder in "Gaze" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
Info.plist,
|
|
||||||
);
|
|
||||||
target = 27A21B3B2F0F69DC0018C4F3 /* Gaze */;
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
27A21B3E2F0F69DC0018C4F3 /* Gaze */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
270D22E92F1474F1008BCE42 /* Exceptions for "Gaze" folder in "Gaze" target */,
|
|
||||||
);
|
|
||||||
path = Gaze;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
27A21B4C2F0F69DD0018C4F3 /* GazeTests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = GazeTests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
27A21B562F0F69DD0018C4F3 /* GazeUITests */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = GazeUITests;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
27A21B392F0F69DC0018C4F3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
|
||||||
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B462F0F69DD0018C4F3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B502F0F69DD0018C4F3 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
27A21B332F0F69DC0018C4F3 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
27A21B3E2F0F69DC0018C4F3 /* Gaze */,
|
|
||||||
27A21B4C2F0F69DD0018C4F3 /* GazeTests */,
|
|
||||||
27A21B562F0F69DD0018C4F3 /* GazeUITests */,
|
|
||||||
27A21B3D2F0F69DC0018C4F3 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
27A21B3D2F0F69DC0018C4F3 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
27A21B3C2F0F69DC0018C4F3 /* Gaze.app */,
|
|
||||||
27A21B492F0F69DD0018C4F3 /* GazeTests.xctest */,
|
|
||||||
27A21B532F0F69DD0018C4F3 /* GazeUITests.xctest */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
27A21B3B2F0F69DC0018C4F3 /* Gaze */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 27A21B5D2F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "Gaze" */;
|
|
||||||
buildPhases = (
|
|
||||||
27A21B382F0F69DC0018C4F3 /* Sources */,
|
|
||||||
27A21B392F0F69DC0018C4F3 /* Frameworks */,
|
|
||||||
27A21B3A2F0F69DC0018C4F3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
27A21B3E2F0F69DC0018C4F3 /* Gaze */,
|
|
||||||
);
|
|
||||||
name = Gaze;
|
|
||||||
packageProductDependencies = (
|
|
||||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
|
||||||
27SPARKLE00000000002 /* Sparkle */,
|
|
||||||
);
|
|
||||||
productName = Gaze;
|
|
||||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
27A21B482F0F69DD0018C4F3 /* GazeTests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 27A21B602F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "GazeTests" */;
|
|
||||||
buildPhases = (
|
|
||||||
27A21B452F0F69DD0018C4F3 /* Sources */,
|
|
||||||
27A21B462F0F69DD0018C4F3 /* Frameworks */,
|
|
||||||
27A21B472F0F69DD0018C4F3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
27A21B4B2F0F69DD0018C4F3 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
27A21B4C2F0F69DD0018C4F3 /* GazeTests */,
|
|
||||||
);
|
|
||||||
name = GazeTests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = GazeTests;
|
|
||||||
productReference = 27A21B492F0F69DD0018C4F3 /* GazeTests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
|
||||||
};
|
|
||||||
27A21B522F0F69DD0018C4F3 /* GazeUITests */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 27A21B632F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "GazeUITests" */;
|
|
||||||
buildPhases = (
|
|
||||||
27A21B4F2F0F69DD0018C4F3 /* Sources */,
|
|
||||||
27A21B502F0F69DD0018C4F3 /* Frameworks */,
|
|
||||||
27A21B512F0F69DD0018C4F3 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
27A21B552F0F69DD0018C4F3 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
27A21B562F0F69DD0018C4F3 /* GazeUITests */,
|
|
||||||
);
|
|
||||||
name = GazeUITests;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = GazeUITests;
|
|
||||||
productReference = 27A21B532F0F69DD0018C4F3 /* GazeUITests.xctest */;
|
|
||||||
productType = "com.apple.product-type.bundle.ui-testing";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
27A21B342F0F69DC0018C4F3 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 2620;
|
|
||||||
LastUpgradeCheck = 2620;
|
|
||||||
TargetAttributes = {
|
|
||||||
27A21B3B2F0F69DC0018C4F3 = {
|
|
||||||
CreatedOnToolsVersion = 26.2;
|
|
||||||
};
|
|
||||||
27A21B482F0F69DD0018C4F3 = {
|
|
||||||
CreatedOnToolsVersion = 26.2;
|
|
||||||
TestTargetID = 27A21B3B2F0F69DC0018C4F3;
|
|
||||||
};
|
|
||||||
27A21B522F0F69DD0018C4F3 = {
|
|
||||||
CreatedOnToolsVersion = 26.2;
|
|
||||||
TestTargetID = 27A21B3B2F0F69DC0018C4F3;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = 27A21B372F0F69DC0018C4F3 /* Build configuration list for PBXProject "Gaze" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = 27A21B332F0F69DC0018C4F3;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
packageReferences = (
|
|
||||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
|
||||||
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
|
||||||
);
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
27A21B3B2F0F69DC0018C4F3 /* Gaze */,
|
|
||||||
27A21B482F0F69DD0018C4F3 /* GazeTests */,
|
|
||||||
27A21B522F0F69DD0018C4F3 /* GazeUITests */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
27A21B3A2F0F69DC0018C4F3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B472F0F69DD0018C4F3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B512F0F69DD0018C4F3 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
27A21B382F0F69DC0018C4F3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B452F0F69DD0018C4F3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
27A21B4F2F0F69DD0018C4F3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
27A21B4B2F0F69DD0018C4F3 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 27A21B3B2F0F69DC0018C4F3 /* Gaze */;
|
|
||||||
targetProxy = 27A21B4A2F0F69DD0018C4F3 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
27A21B552F0F69DD0018C4F3 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 27A21B3B2F0F69DC0018C4F3 /* Gaze */;
|
|
||||||
targetProxy = 27A21B542F0F69DD0018C4F3 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
27A21B5B2F0F69DD0018C4F3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
27A21B5C2F0F69DD0018C4F3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = macosx;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
27A21B5E2F0F69DD0018C4F3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
ENABLE_APP_SANDBOX = YES;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = Gaze/Info.plist;
|
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
REGISTER_APP_GROUPS = YES;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
27A21B5F2F0F69DD0018C4F3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Gaze;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
ENABLE_APP_SANDBOX = YES;
|
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
|
||||||
INFOPLIST_FILE = Gaze/Info.plist;
|
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/../Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
OTHER_SWIFT_FLAGS = "-D APPSTORE";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
REGISTER_APP_GROUPS = YES;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
27A21B612F0F69DD0018C4F3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gaze.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Gaze";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
27A21B622F0F69DD0018C4F3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Gaze.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Gaze";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
27A21B642F0F69DD0018C4F3 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_TARGET_NAME = Gaze;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
27A21B652F0F69DD0018C4F3 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
MARKETING_VERSION = 0.4.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TEST_TARGET_NAME = Gaze;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
27A21B372F0F69DC0018C4F3 /* Build configuration list for PBXProject "Gaze" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
27A21B5B2F0F69DD0018C4F3 /* Debug */,
|
|
||||||
27A21B5C2F0F69DD0018C4F3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
27A21B5D2F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "Gaze" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
27A21B5E2F0F69DD0018C4F3 /* Debug */,
|
|
||||||
27A21B5F2F0F69DD0018C4F3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
27A21B602F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "GazeTests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
27A21B612F0F69DD0018C4F3 /* Debug */,
|
|
||||||
27A21B622F0F69DD0018C4F3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
27A21B632F0F69DD0018C4F3 /* Build configuration list for PBXNativeTarget "GazeUITests" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
27A21B642F0F69DD0018C4F3 /* Debug */,
|
|
||||||
27A21B652F0F69DD0018C4F3 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* 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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
|
||||||
requirement = {
|
|
||||||
kind = exactVersion;
|
|
||||||
version = 2.8.1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
27AE10B12F10B1FC00E00DBC /* Lottie */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
|
||||||
productName = Lottie;
|
|
||||||
};
|
|
||||||
27SPARKLE00000000002 /* Sparkle */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
|
||||||
productName = Sparkle;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
};
|
|
||||||
rootObject = 27A21B342F0F69DC0018C4F3 /* Project object */;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "513d974fbede884a919977d3446360023f6e3239ac314f4fbd9657e80aca7560",
|
"originHash" : "83c4b4b69555e54712e60721606a120fe3f01308b1af84957cd0941e93e64f8a",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "lottie-spm",
|
"identity" : "lottie-spm",
|
||||||
@@ -10,6 +10,15 @@
|
|||||||
"version" : "4.6.0"
|
"version" : "4.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "macrovisionkit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TheBoredTeam/MacroVisionKit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "da481a6be8d8b1bf7fcb218507a72428bbcae7b0",
|
||||||
|
"version" : "0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "sparkle",
|
"identity" : "sparkle",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -7,11 +7,35 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum PauseReason: Codable, Equatable, Hashable {
|
enum PauseReason: nonisolated Codable, nonisolated Sendable, nonisolated Equatable, nonisolated Hashable {
|
||||||
case manual
|
case manual
|
||||||
case fullscreen
|
case fullscreen
|
||||||
case idle
|
case idle
|
||||||
case system
|
case system
|
||||||
|
|
||||||
|
nonisolated static func == (lhs: PauseReason, rhs: PauseReason) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.manual, .manual),
|
||||||
|
(.fullscreen, .fullscreen),
|
||||||
|
(.idle, .idle),
|
||||||
|
(.system, .system):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .manual:
|
||||||
|
hasher.combine(0)
|
||||||
|
case .fullscreen:
|
||||||
|
hasher.combine(1)
|
||||||
|
case .idle:
|
||||||
|
hasher.combine(2)
|
||||||
|
case .system:
|
||||||
|
hasher.combine(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PauseReason: Sendable {}
|
|
||||||
|
|||||||
16
Gaze/Models/SetupPresentation.swift
Normal file
16
Gaze/Models/SetupPresentation.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// SetupPresentation.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum SetupPresentation {
|
||||||
|
case window
|
||||||
|
case card
|
||||||
|
|
||||||
|
var isWindow: Bool { self == .window }
|
||||||
|
var isCard: Bool { self == .card }
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Unified identifier for both built-in and user-defined timers
|
/// Unified identifier for both built-in and user-defined timers
|
||||||
enum TimerIdentifier: Hashable, Codable {
|
enum TimerIdentifier: Hashable, Codable, Sendable {
|
||||||
case builtIn(TimerType)
|
case builtIn(TimerType)
|
||||||
case user(id: String)
|
case user(id: String)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TimerState: Equatable, Hashable {
|
struct TimerState: Equatable, Hashable, Sendable {
|
||||||
let identifier: TimerIdentifier
|
let identifier: TimerIdentifier
|
||||||
var remainingSeconds: Int
|
var remainingSeconds: Int
|
||||||
var isPaused: Bool
|
var isPaused: Bool
|
||||||
@@ -45,7 +45,7 @@ struct TimerState: Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimerStateBuilder {
|
enum TimerStateBuilder: Sendable {
|
||||||
static func make(
|
static func make(
|
||||||
identifier: TimerIdentifier,
|
identifier: TimerIdentifier,
|
||||||
intervalSeconds: Int,
|
intervalSeconds: Int,
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import Combine
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol CameraSessionDelegate: AnyObject {
|
protocol CameraSessionDelegate: AnyObject {
|
||||||
nonisolated func cameraSession(
|
@MainActor func cameraSession(
|
||||||
_ manager: CameraSessionManager,
|
_ manager: CameraSessionManager,
|
||||||
didOutput pixelBuffer: CVPixelBuffer,
|
didOutput pixelBuffer: CVPixelBuffer,
|
||||||
imageSize: CGSize
|
imageSize: CGSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct PixelBufferBox: @unchecked Sendable {
|
||||||
|
let buffer: CVPixelBuffer
|
||||||
|
}
|
||||||
|
|
||||||
final class CameraSessionManager: NSObject, ObservableObject {
|
final class CameraSessionManager: NSObject, ObservableObject {
|
||||||
@Published private(set) var isRunning = false
|
@Published private(set) var isRunning = false
|
||||||
weak var delegate: CameraSessionDelegate?
|
nonisolated(unsafe) weak var delegate: CameraSessionDelegate?
|
||||||
|
|
||||||
private var captureSession: AVCaptureSession?
|
private var captureSession: AVCaptureSession?
|
||||||
private var videoOutput: AVCaptureVideoDataOutput?
|
private var videoOutput: AVCaptureVideoDataOutput?
|
||||||
@@ -116,6 +120,11 @@ extension CameraSessionManager: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|||||||
height: CVPixelBufferGetHeight(pixelBuffer)
|
height: CVPixelBufferGetHeight(pixelBuffer)
|
||||||
)
|
)
|
||||||
|
|
||||||
delegate?.cameraSession(self, didOutput: pixelBuffer, imageSize: size)
|
let bufferBox = PixelBufferBox(buffer: pixelBuffer)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self, bufferBox] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.delegate?.cameraSession(self, didOutput: bufferBox.buffer, imageSize: size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension EyeTrackingService: CameraSessionDelegate {
|
extension EyeTrackingService: CameraSessionDelegate {
|
||||||
nonisolated func cameraSession(
|
@MainActor func cameraSession(
|
||||||
_ manager: CameraSessionManager,
|
_ manager: CameraSessionManager,
|
||||||
didOutput pixelBuffer: CVPixelBuffer,
|
didOutput pixelBuffer: CVPixelBuffer,
|
||||||
imageSize: CGSize
|
imageSize: CGSize
|
||||||
@@ -174,7 +174,6 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
if let leftRatio = result.leftPupilRatio,
|
if let leftRatio = result.leftPupilRatio,
|
||||||
let rightRatio = result.rightPupilRatio,
|
let rightRatio = result.rightPupilRatio,
|
||||||
let faceWidth = result.faceWidthRatio {
|
let faceWidth = result.faceWidthRatio {
|
||||||
Task { @MainActor in
|
|
||||||
guard CalibratorService.shared.isCalibrating else { return }
|
guard CalibratorService.shared.isCalibrating else { return }
|
||||||
CalibratorService.shared.submitSampleToBridge(
|
CalibratorService.shared.submitSampleToBridge(
|
||||||
leftRatio: leftRatio,
|
leftRatio: leftRatio,
|
||||||
@@ -184,10 +183,7 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
faceWidthRatio: faceWidth
|
faceWidthRatio: faceWidth
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Task { @MainActor [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
self.faceDetected = result.faceDetected
|
self.faceDetected = result.faceDetected
|
||||||
self.isEyesClosed = result.isEyesClosed
|
self.isEyesClosed = result.isEyesClosed
|
||||||
self.userLookingAtScreen = result.userLookingAtScreen
|
self.userLookingAtScreen = result.userLookingAtScreen
|
||||||
@@ -196,7 +192,6 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
self.syncDebugState()
|
self.syncDebugState()
|
||||||
self.updateGazeConfiguration()
|
self.updateGazeConfiguration()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Error Handling
|
// MARK: - Error Handling
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Vision
|
@preconcurrency import Vision
|
||||||
import simd
|
import simd
|
||||||
|
|
||||||
struct EyeTrackingProcessingResult: Sendable {
|
struct EyeTrackingProcessingResult: Sendable {
|
||||||
@@ -54,19 +54,19 @@ final class GazeDetector: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var configuration: Configuration
|
private nonisolated(unsafe) var configuration: Configuration
|
||||||
|
|
||||||
init(configuration: Configuration) {
|
nonisolated init(configuration: Configuration) {
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateConfiguration(_ configuration: Configuration) {
|
nonisolated func updateConfiguration(_ configuration: Configuration) {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated func process(
|
func process(
|
||||||
analysis: VisionPipeline.FaceAnalysis,
|
analysis: VisionPipeline.FaceAnalysis,
|
||||||
pixelBuffer: CVPixelBuffer
|
pixelBuffer: CVPixelBuffer
|
||||||
) -> EyeTrackingProcessingResult {
|
) -> EyeTrackingProcessingResult {
|
||||||
@@ -75,7 +75,7 @@ final class GazeDetector: @unchecked Sendable {
|
|||||||
config = configuration
|
config = configuration
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
|
|
||||||
guard analysis.faceDetected, let face = analysis.face else {
|
guard analysis.faceDetected, let face = analysis.face?.value else {
|
||||||
return EyeTrackingProcessingResult(
|
return EyeTrackingProcessingResult(
|
||||||
faceDetected: false,
|
faceDetected: false,
|
||||||
isEyesClosed: false,
|
isEyesClosed: false,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import Accelerate
|
|||||||
import CoreImage
|
import CoreImage
|
||||||
import ImageIO
|
import ImageIO
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import Vision
|
@preconcurrency import Vision
|
||||||
|
|
||||||
struct PupilPosition: Equatable, Sendable {
|
struct PupilPosition: Equatable, Sendable {
|
||||||
let x: CGFloat
|
let x: CGFloat
|
||||||
|
|||||||
@@ -6,17 +6,21 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Vision
|
@preconcurrency import Vision
|
||||||
|
|
||||||
final class VisionPipeline: @unchecked Sendable {
|
final class VisionPipeline: @unchecked Sendable {
|
||||||
struct FaceAnalysis: Sendable {
|
struct FaceAnalysis: Sendable {
|
||||||
let faceDetected: Bool
|
let faceDetected: Bool
|
||||||
let face: VNFaceObservation?
|
let face: NonSendableFaceObservation?
|
||||||
let imageSize: CGSize
|
let imageSize: CGSize
|
||||||
let debugYaw: Double?
|
let debugYaw: Double?
|
||||||
let debugPitch: Double?
|
let debugPitch: Double?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NonSendableFaceObservation: @unchecked Sendable {
|
||||||
|
nonisolated(unsafe) let value: VNFaceObservation
|
||||||
|
}
|
||||||
|
|
||||||
nonisolated func analyze(
|
nonisolated func analyze(
|
||||||
pixelBuffer: CVPixelBuffer,
|
pixelBuffer: CVPixelBuffer,
|
||||||
imageSize: CGSize
|
imageSize: CGSize
|
||||||
@@ -46,7 +50,7 @@ final class VisionPipeline: @unchecked Sendable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let face = (request.results as? [VNFaceObservation])?.first else {
|
guard let face = request.results?.first else {
|
||||||
return FaceAnalysis(
|
return FaceAnalysis(
|
||||||
faceDetected: false,
|
faceDetected: false,
|
||||||
face: nil,
|
face: nil,
|
||||||
@@ -58,7 +62,7 @@ final class VisionPipeline: @unchecked Sendable {
|
|||||||
|
|
||||||
return FaceAnalysis(
|
return FaceAnalysis(
|
||||||
faceDetected: true,
|
faceDetected: true,
|
||||||
face: face,
|
face: NonSendableFaceObservation(value: face),
|
||||||
imageSize: imageSize,
|
imageSize: imageSize,
|
||||||
debugYaw: face.yaw?.doubleValue,
|
debugYaw: face.yaw?.doubleValue,
|
||||||
debugPitch: face.pitch?.doubleValue
|
debugPitch: face.pitch?.doubleValue
|
||||||
|
|||||||
@@ -7,139 +7,45 @@
|
|||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreGraphics
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import MacroVisionKit
|
||||||
public struct FullscreenWindowDescriptor: Equatable {
|
|
||||||
public let ownerPID: pid_t
|
|
||||||
public let layer: Int
|
|
||||||
public let bounds: CGRect
|
|
||||||
|
|
||||||
public init(ownerPID: pid_t, layer: Int, bounds: CGRect) {
|
|
||||||
self.ownerPID = ownerPID
|
|
||||||
self.layer = layer
|
|
||||||
self.bounds = bounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol FullscreenEnvironmentProviding {
|
|
||||||
func frontmostProcessIdentifier() -> pid_t?
|
|
||||||
func windowDescriptors() -> [FullscreenWindowDescriptor]
|
|
||||||
func screenFrames() -> [CGRect]
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding {
|
|
||||||
func frontmostProcessIdentifier() -> pid_t? {
|
|
||||||
NSWorkspace.shared.frontmostApplication?.processIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
func windowDescriptors() -> [FullscreenWindowDescriptor] {
|
|
||||||
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
|
||||||
guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return windowList.compactMap { window in
|
|
||||||
guard let ownerPID = window[kCGWindowOwnerPID as String] as? pid_t,
|
|
||||||
let layer = window[kCGWindowLayer as String] as? Int,
|
|
||||||
let boundsDict = window[kCGWindowBounds as String] as? [String: CGFloat] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let bounds = CGRect(
|
|
||||||
x: boundsDict["X"] ?? 0,
|
|
||||||
y: boundsDict["Y"] ?? 0,
|
|
||||||
width: boundsDict["Width"] ?? 0,
|
|
||||||
height: boundsDict["Height"] ?? 0
|
|
||||||
)
|
|
||||||
|
|
||||||
return FullscreenWindowDescriptor(ownerPID: ownerPID, layer: layer, bounds: bounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func screenFrames() -> [CGRect] {
|
|
||||||
NSScreen.screens.map(\.frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class FullscreenDetectionService: ObservableObject {
|
final class FullscreenDetectionService: ObservableObject {
|
||||||
@Published private(set) var isFullscreenActive = false
|
@Published private(set) var isFullscreenActive = false
|
||||||
|
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var fullscreenTask: Task<Void, Never>?
|
||||||
private var frontmostAppObserver: AnyCancellable?
|
|
||||||
private let permissionManager: ScreenCapturePermissionManaging
|
private let permissionManager: ScreenCapturePermissionManaging
|
||||||
private let environmentProvider: FullscreenEnvironmentProviding
|
#if canImport(MacroVisionKit)
|
||||||
private let windowMatcher = FullscreenWindowMatcher()
|
private let monitor = FullScreenMonitor.shared
|
||||||
|
#endif
|
||||||
|
|
||||||
init(
|
init(
|
||||||
permissionManager: ScreenCapturePermissionManaging,
|
permissionManager: ScreenCapturePermissionManaging
|
||||||
environmentProvider: FullscreenEnvironmentProviding
|
|
||||||
) {
|
) {
|
||||||
self.permissionManager = permissionManager
|
self.permissionManager = permissionManager
|
||||||
self.environmentProvider = environmentProvider
|
startMonitoring()
|
||||||
setupObservers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience initializer using default services
|
/// Convenience initializer using default services
|
||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(
|
self.init(
|
||||||
permissionManager: ScreenCapturePermissionManager.shared,
|
permissionManager: ScreenCapturePermissionManager.shared
|
||||||
environmentProvider: SystemFullscreenEnvironmentProvider()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory method to safely create instances from non-main actor contexts
|
// Factory method to safely create instances from non-main actor contexts
|
||||||
static func create(
|
static func create(
|
||||||
permissionManager: ScreenCapturePermissionManaging? = nil,
|
permissionManager: ScreenCapturePermissionManaging? = nil
|
||||||
environmentProvider: FullscreenEnvironmentProviding? = nil
|
|
||||||
) async -> FullscreenDetectionService {
|
) async -> FullscreenDetectionService {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
return FullscreenDetectionService(
|
return FullscreenDetectionService(
|
||||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
|
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared
|
||||||
environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
let notificationCenter = NSWorkspace.shared.notificationCenter
|
fullscreenTask?.cancel()
|
||||||
observers.forEach { notificationCenter.removeObserver($0) }
|
|
||||||
frontmostAppObserver?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupObservers() {
|
|
||||||
let workspace = NSWorkspace.shared
|
|
||||||
let notificationCenter = workspace.notificationCenter
|
|
||||||
|
|
||||||
let stateChangeHandler: (Notification) -> Void = { [weak self] _ in
|
|
||||||
self?.checkFullscreenState()
|
|
||||||
}
|
|
||||||
|
|
||||||
let notifications: [(NSNotification.Name, Any?)] = [
|
|
||||||
(NSWorkspace.activeSpaceDidChangeNotification, workspace),
|
|
||||||
(NSApplication.didChangeScreenParametersNotification, nil),
|
|
||||||
(NSWindow.willEnterFullScreenNotification, nil),
|
|
||||||
(NSWindow.willExitFullScreenNotification, nil),
|
|
||||||
]
|
|
||||||
|
|
||||||
observers = notifications.map { notification, object in
|
|
||||||
notificationCenter.addObserver(
|
|
||||||
forName: notification,
|
|
||||||
object: object,
|
|
||||||
queue: .main,
|
|
||||||
using: stateChangeHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
frontmostAppObserver = NotificationCenter.default.publisher(
|
|
||||||
for: NSWorkspace.didActivateApplicationNotification,
|
|
||||||
object: workspace
|
|
||||||
)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.checkFullscreenState()
|
|
||||||
}
|
|
||||||
|
|
||||||
checkFullscreenState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canReadWindowInfo() -> Bool {
|
private func canReadWindowInfo() -> Bool {
|
||||||
@@ -151,25 +57,17 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkFullscreenState() {
|
private func startMonitoring() {
|
||||||
guard canReadWindowInfo() else { return }
|
fullscreenTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else {
|
let stream = await monitor.spaceChanges()
|
||||||
setFullscreenState(false)
|
for await spaces in stream {
|
||||||
return
|
guard self.canReadWindowInfo() else { continue }
|
||||||
}
|
self.setFullscreenState(!spaces.isEmpty)
|
||||||
|
|
||||||
let windows = environmentProvider.windowDescriptors()
|
|
||||||
let screens = environmentProvider.screenFrames()
|
|
||||||
|
|
||||||
for window in windows where window.ownerPID == frontmostPID && window.layer == 0 {
|
|
||||||
if windowMatcher.isFullscreen(windowBounds: window.bounds, screenFrames: screens) {
|
|
||||||
setFullscreenState(true)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFullscreenState(false)
|
forceUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setFullscreenState(_ isActive: Bool) {
|
fileprivate func setFullscreenState(_ isActive: Bool) {
|
||||||
@@ -179,7 +77,12 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func forceUpdate() {
|
func forceUpdate() {
|
||||||
checkFullscreenState()
|
Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.canReadWindowInfo() else { return }
|
||||||
|
let spaces = await monitor.detectFullscreenApps()
|
||||||
|
self.setFullscreenState(!spaces.isEmpty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -188,16 +91,3 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FullscreenWindowMatcher {
|
|
||||||
func isFullscreen(windowBounds: CGRect, screenFrames: [CGRect], tolerance: CGFloat = 1) -> Bool {
|
|
||||||
screenFrames.contains { matches(windowBounds, screenFrame: $0, tolerance: tolerance) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func matches(_ windowBounds: CGRect, screenFrame: CGRect, tolerance: CGFloat) -> Bool {
|
|
||||||
abs(windowBounds.width - screenFrame.width) < tolerance
|
|
||||||
&& abs(windowBounds.height - screenFrame.height) < tolerance
|
|
||||||
&& abs(windowBounds.origin.x - screenFrame.origin.x) < tolerance
|
|
||||||
&& abs(windowBounds.origin.y - screenFrame.origin.y) < tolerance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
552
Gaze/Views/Components/EnforceModeSetupContent.swift
Normal file
552
Gaze/Views/Components/EnforceModeSetupContent.swift
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
//
|
||||||
|
// EnforceModeSetupContent.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct EnforceModeSetupContent: View {
|
||||||
|
@Bindable var settingsManager: SettingsManager
|
||||||
|
@ObservedObject var cameraService = CameraAccessService.shared
|
||||||
|
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
|
||||||
|
@ObservedObject var enforceModeService = EnforceModeService.shared
|
||||||
|
@ObservedObject var calibratorService = CalibratorService.shared
|
||||||
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
|
let presentation: SetupPresentation
|
||||||
|
@Binding var isTestModeActive: Bool
|
||||||
|
@Binding var cachedPreviewLayer: AVCaptureVideoPreviewLayer?
|
||||||
|
@Binding var showAdvancedSettings: Bool
|
||||||
|
@Binding var showCalibrationWindow: Bool
|
||||||
|
@Binding var isViewActive: Bool
|
||||||
|
let isProcessingToggle: Bool
|
||||||
|
let handleEnforceModeToggle: (Bool) -> Void
|
||||||
|
|
||||||
|
private var cameraHardwareAvailable: Bool {
|
||||||
|
cameraService.hasCameraHardware
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionCornerRadius: CGFloat {
|
||||||
|
presentation.isCard ? 10 : 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionPadding: CGFloat {
|
||||||
|
presentation.isCard ? 10 : 16
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headerFont: Font {
|
||||||
|
presentation.isCard ? .subheadline : .headline
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconSize: CGFloat {
|
||||||
|
presentation.isCard ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: presentation.isCard ? 10 : 24) {
|
||||||
|
if presentation.isCard {
|
||||||
|
Image(systemName: "video.fill")
|
||||||
|
.font(.system(size: iconSize))
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
|
||||||
|
Text("Enforce Mode")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Use your camera to ensure you take breaks")
|
||||||
|
.font(presentation.isCard ? .subheadline : (isCompact ? .subheadline : .title3))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if presentation.isCard {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: presentation.isCard ? 10 : 20) {
|
||||||
|
enforceModeToggleView
|
||||||
|
cameraStatusView
|
||||||
|
if enforceModeService.isEnforceModeEnabled {
|
||||||
|
testModeButton
|
||||||
|
}
|
||||||
|
if isTestModeActive && enforceModeService.isCameraActive {
|
||||||
|
testModePreviewView
|
||||||
|
trackingConstantsView
|
||||||
|
} else if enforceModeService.isCameraActive && !isTestModeActive {
|
||||||
|
eyeTrackingStatusView
|
||||||
|
trackingConstantsView
|
||||||
|
}
|
||||||
|
privacyInfoView
|
||||||
|
}
|
||||||
|
|
||||||
|
if presentation.isCard {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showCalibrationWindow) {
|
||||||
|
EyeTrackingCalibrationView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var testModeButton: some View {
|
||||||
|
Button(action: {
|
||||||
|
Task { @MainActor in
|
||||||
|
if isTestModeActive {
|
||||||
|
enforceModeService.stopTestMode()
|
||||||
|
isTestModeActive = false
|
||||||
|
cachedPreviewLayer = nil
|
||||||
|
} else {
|
||||||
|
await enforceModeService.startTestMode()
|
||||||
|
isTestModeActive = enforceModeService.isCameraActive
|
||||||
|
if isTestModeActive {
|
||||||
|
cachedPreviewLayer = eyeTrackingService.previewLayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: isTestModeActive ? "stop.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.title3)
|
||||||
|
Text(isTestModeActive ? "Stop Test" : "Test Tracking")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(presentation.isCard ? .regular : .large)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var calibrationSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "target")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Text("Eye Tracking Calibration")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
|
if calibratorService.calibrationData.isComplete {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(calibratorService.getCalibrationSummary())
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if calibratorService.needsRecalibration() {
|
||||||
|
Label(
|
||||||
|
"Calibration expired - recalibration recommended",
|
||||||
|
systemImage: "exclamationmark.triangle.fill"
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else {
|
||||||
|
Label("Calibration active and valid", systemImage: "checkmark.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Not calibrated - using default thresholds")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
showCalibrationWindow = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "target")
|
||||||
|
Text(
|
||||||
|
calibratorService.calibrationData.isComplete
|
||||||
|
? "Recalibrate" : "Run Calibration")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.regular)
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(
|
||||||
|
GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: sectionCornerRadius)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var testModePreviewView: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
let lookingAway = !eyeTrackingService.userLookingAtScreen
|
||||||
|
let borderColor: NSColor = lookingAway ? .systemGreen : .systemRed
|
||||||
|
|
||||||
|
let previewLayer = eyeTrackingService.previewLayer ?? cachedPreviewLayer
|
||||||
|
|
||||||
|
if let layer = previewLayer {
|
||||||
|
ZStack {
|
||||||
|
CameraPreviewView(previewLayer: layer, borderColor: borderColor)
|
||||||
|
PupilOverlayView(eyeTrackingService: eyeTrackingService)
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
GazeOverlayView(eyeTrackingService: eyeTrackingService)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: presentation.isCard ? 180 : (isCompact ? 200 : 300))
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
.onAppear {
|
||||||
|
if cachedPreviewLayer == nil {
|
||||||
|
cachedPreviewLayer = eyeTrackingService.previewLayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cameraStatusView: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Camera Access")
|
||||||
|
.font(headerFont)
|
||||||
|
|
||||||
|
if cameraService.isCameraAuthorized {
|
||||||
|
Label("Authorized", systemImage: "checkmark.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
} else if let error = cameraService.cameraError {
|
||||||
|
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else {
|
||||||
|
Label("Not authorized", systemImage: "xmark.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if !cameraService.isCameraAuthorized {
|
||||||
|
Button("Request Access") {
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
try await cameraService.requestCameraAccess()
|
||||||
|
} catch {
|
||||||
|
print("⚠️ Camera access failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(presentation.isCard ? .small : .regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var eyeTrackingStatusView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Eye Tracking Status")
|
||||||
|
.font(headerFont)
|
||||||
|
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
statusIndicator(
|
||||||
|
title: "Face Detected",
|
||||||
|
isActive: eyeTrackingService.faceDetected,
|
||||||
|
icon: "person.fill"
|
||||||
|
)
|
||||||
|
|
||||||
|
statusIndicator(
|
||||||
|
title: "Looking Away",
|
||||||
|
isActive: !eyeTrackingService.userLookingAtScreen,
|
||||||
|
icon: "arrow.turn.up.right"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func statusIndicator(title: String, isActive: Bool, icon: String) -> some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(isActive ? .green : .secondary)
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var privacyInfoView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "lock.shield.fill")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Text("Privacy Information")
|
||||||
|
.font(headerFont)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
privacyBullet("All processing happens on-device")
|
||||||
|
privacyBullet("No images are stored or transmitted")
|
||||||
|
privacyBullet("Camera only active during lookaway reminders (3 second window)")
|
||||||
|
privacyBullet("You can always force quit with cmd+q")
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(
|
||||||
|
GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: sectionCornerRadius)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func privacyBullet(_ text: String) -> some View {
|
||||||
|
HStack(alignment: .top, spacing: 8) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var enforceModeToggleView: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Enable Enforce Mode")
|
||||||
|
.font(headerFont)
|
||||||
|
if !cameraHardwareAvailable {
|
||||||
|
Text("No camera hardware detected")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
} else {
|
||||||
|
Text("Camera activates 3 seconds before lookaway reminders")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Toggle(
|
||||||
|
"",
|
||||||
|
isOn: Binding(
|
||||||
|
get: {
|
||||||
|
settingsManager.isTimerEnabled(for: .lookAway)
|
||||||
|
|| settingsManager.isTimerEnabled(for: .blink)
|
||||||
|
|| settingsManager.isTimerEnabled(for: .posture)
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
guard !isProcessingToggle else { return }
|
||||||
|
handleEnforceModeToggle(newValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.labelsHidden()
|
||||||
|
.disabled(isProcessingToggle || !cameraHardwareAvailable)
|
||||||
|
.controlSize(presentation.isCard ? .small : (isCompact ? .small : .regular))
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trackingConstantsView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
HStack {
|
||||||
|
Text("Tracking Sensitivity")
|
||||||
|
.font(headerFont)
|
||||||
|
Spacer()
|
||||||
|
Button(action: {
|
||||||
|
eyeTrackingService.enableDebugLogging.toggle()
|
||||||
|
}) {
|
||||||
|
Image(
|
||||||
|
systemName: eyeTrackingService.enableDebugLogging
|
||||||
|
? "ant.circle.fill" : "ant.circle"
|
||||||
|
)
|
||||||
|
.foregroundStyle(eyeTrackingService.enableDebugLogging ? .orange : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Toggle console debug logging")
|
||||||
|
|
||||||
|
Button(showAdvancedSettings ? "Hide Settings" : "Show Settings") {
|
||||||
|
withAnimation {
|
||||||
|
showAdvancedSettings.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Live Values:")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if let leftRatio = eyeTrackingService.debugLeftPupilRatio,
|
||||||
|
let rightRatio = eyeTrackingService.debugRightPupilRatio
|
||||||
|
{
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Left Pupil: \(String(format: "%.3f", leftRatio))")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(
|
||||||
|
!EyeTrackingConstants.minPupilEnabled
|
||||||
|
&& !EyeTrackingConstants.maxPupilEnabled
|
||||||
|
? .secondary
|
||||||
|
: (leftRatio < EyeTrackingConstants.minPupilRatio
|
||||||
|
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
||||||
|
? Color.orange : Color.green
|
||||||
|
)
|
||||||
|
Text("Right Pupil: \(String(format: "%.3f", rightRatio))")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(
|
||||||
|
!EyeTrackingConstants.minPupilEnabled
|
||||||
|
&& !EyeTrackingConstants.maxPupilEnabled
|
||||||
|
? .secondary
|
||||||
|
: (rightRatio < EyeTrackingConstants.minPupilRatio
|
||||||
|
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
||||||
|
? Color.orange : Color.green
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
|
Text(
|
||||||
|
"Range: \(String(format: "%.2f", EyeTrackingConstants.minPupilRatio)) - \(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))"
|
||||||
|
)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
let bothEyesOut =
|
||||||
|
(leftRatio < EyeTrackingConstants.minPupilRatio
|
||||||
|
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
||||||
|
&& (rightRatio < EyeTrackingConstants.minPupilRatio
|
||||||
|
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
||||||
|
Text(bothEyesOut ? "Both Out ⚠️" : "In Range ✓")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(bothEyesOut ? .orange : .green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Pupil data unavailable")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let yaw = eyeTrackingService.debugYaw,
|
||||||
|
let pitch = eyeTrackingService.debugPitch
|
||||||
|
{
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Yaw: \(String(format: "%.3f", yaw))")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(
|
||||||
|
!EyeTrackingConstants.yawEnabled
|
||||||
|
? .secondary
|
||||||
|
: abs(yaw) > EyeTrackingConstants.yawThreshold
|
||||||
|
? Color.orange : Color.green
|
||||||
|
)
|
||||||
|
Text("Pitch: \(String(format: "%.3f", pitch))")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(
|
||||||
|
!EyeTrackingConstants.pitchUpEnabled
|
||||||
|
&& !EyeTrackingConstants.pitchDownEnabled
|
||||||
|
? .secondary
|
||||||
|
: (pitch > EyeTrackingConstants.pitchUpThreshold
|
||||||
|
|| pitch < EyeTrackingConstants.pitchDownThreshold)
|
||||||
|
? Color.orange : Color.green
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
|
Text(
|
||||||
|
"Yaw Max: \(String(format: "%.2f", EyeTrackingConstants.yawThreshold))"
|
||||||
|
)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(
|
||||||
|
"Pitch: \(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) to \(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold))"
|
||||||
|
)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
|
|
||||||
|
if showAdvancedSettings {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Current Threshold Values:")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Yaw Threshold:")
|
||||||
|
Spacer()
|
||||||
|
Text("\(String(format: "%.2f", EyeTrackingConstants.yawThreshold)) rad")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Pitch Up Threshold:")
|
||||||
|
Spacer()
|
||||||
|
Text(
|
||||||
|
"\(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold)) rad"
|
||||||
|
)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Pitch Down Threshold:")
|
||||||
|
Spacer()
|
||||||
|
Text(
|
||||||
|
"\(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) rad"
|
||||||
|
)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Min Pupil Ratio:")
|
||||||
|
Spacer()
|
||||||
|
Text("\(String(format: "%.2f", EyeTrackingConstants.minPupilRatio))")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Max Pupil Ratio:")
|
||||||
|
Spacer()
|
||||||
|
Text("\(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Eye Closed Threshold:")
|
||||||
|
Spacer()
|
||||||
|
Text(
|
||||||
|
"\(String(format: "%.3f", EyeTrackingConstants.eyeClosedThreshold))"
|
||||||
|
)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
}
|
||||||
201
Gaze/Views/Components/SmartModeSetupContent.swift
Normal file
201
Gaze/Views/Components/SmartModeSetupContent.swift
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
//
|
||||||
|
// SmartModeSetupContent.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SmartModeSetupContent: View {
|
||||||
|
@Bindable var settingsManager: SettingsManager
|
||||||
|
@State private var permissionManager = ScreenCapturePermissionManager.shared
|
||||||
|
let presentation: SetupPresentation
|
||||||
|
|
||||||
|
private var iconSize: CGFloat {
|
||||||
|
presentation.isCard ? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionCornerRadius: CGFloat {
|
||||||
|
presentation.isCard ? 10 : 12
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionPadding: CGFloat {
|
||||||
|
presentation.isCard ? 10 : 16
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sectionSpacing: CGFloat {
|
||||||
|
presentation.isCard ? 8 : 12
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: presentation.isCard ? 10 : 24) {
|
||||||
|
if presentation.isCard {
|
||||||
|
Image(systemName: "brain.fill")
|
||||||
|
.font(.system(size: iconSize))
|
||||||
|
.foregroundStyle(.purple)
|
||||||
|
|
||||||
|
Text("Smart Mode")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Automatically manage timers based on your activity")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
if presentation.isCard {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: sectionSpacing) {
|
||||||
|
fullscreenSection
|
||||||
|
idleSection
|
||||||
|
#if DEBUG
|
||||||
|
usageTrackingSection
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.frame(maxWidth: presentation.isCard ? .infinity : 600)
|
||||||
|
|
||||||
|
if presentation.isCard {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fullscreenSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Text("Auto-pause on Fullscreen")
|
||||||
|
.font(presentation.isCard ? .subheadline : .headline)
|
||||||
|
}
|
||||||
|
Text("Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnFullscreen)
|
||||||
|
.labelsHidden()
|
||||||
|
.controlSize(presentation.isCard ? .small : .regular)
|
||||||
|
.onChange(of: settingsManager.settings.smartMode.autoPauseOnFullscreen) { _, newValue in
|
||||||
|
if newValue {
|
||||||
|
permissionManager.requestAuthorizationIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settingsManager.settings.smartMode.autoPauseOnFullscreen,
|
||||||
|
permissionManager.authorizationStatus != .authorized
|
||||||
|
{
|
||||||
|
permissionWarningView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var permissionWarningView: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Label(
|
||||||
|
permissionManager.authorizationStatus == .denied
|
||||||
|
? "Screen Recording permission required"
|
||||||
|
: "Grant Screen Recording access",
|
||||||
|
systemImage: "exclamationmark.shield"
|
||||||
|
)
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
|
||||||
|
Text("macOS requires Screen Recording permission to detect other apps in fullscreen.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Grant Access") {
|
||||||
|
permissionManager.requestAuthorizationIfNeeded()
|
||||||
|
permissionManager.openSystemSettings()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(presentation.isCard ? .small : .regular)
|
||||||
|
|
||||||
|
Button("Open Settings") {
|
||||||
|
permissionManager.openSystemSettings()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var idleSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "moon.zzz.fill")
|
||||||
|
.foregroundStyle(.indigo)
|
||||||
|
Text("Auto-pause on Idle")
|
||||||
|
.font(presentation.isCard ? .subheadline : .headline)
|
||||||
|
}
|
||||||
|
Text("Timers will pause when you're inactive for more than the threshold below")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnIdle)
|
||||||
|
.labelsHidden()
|
||||||
|
.controlSize(presentation.isCard ? .small : .regular)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settingsManager.settings.smartMode.autoPauseOnIdle {
|
||||||
|
ThresholdSlider(
|
||||||
|
label: "Idle Threshold:",
|
||||||
|
value: $settingsManager.settings.smartMode.idleThresholdMinutes,
|
||||||
|
range: 1...30,
|
||||||
|
unit: "min"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var usageTrackingSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "chart.line.uptrend.xyaxis")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
Text("Track Usage Statistics")
|
||||||
|
.font(presentation.isCard ? .subheadline : .headline)
|
||||||
|
}
|
||||||
|
Text("Monitor active and idle time, with automatic reset after the specified duration")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Toggle("", isOn: $settingsManager.settings.smartMode.trackUsage)
|
||||||
|
.labelsHidden()
|
||||||
|
.controlSize(presentation.isCard ? .small : .regular)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settingsManager.settings.smartMode.trackUsage {
|
||||||
|
ThresholdSlider(
|
||||||
|
label: "Reset After:",
|
||||||
|
value: $settingsManager.settings.smartMode.usageResetAfterMinutes,
|
||||||
|
range: 15...240,
|
||||||
|
step: 15,
|
||||||
|
unit: "min"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(sectionPadding)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Gaze/Views/Components/ThresholdSlider.swift
Normal file
39
Gaze/Views/Components/ThresholdSlider.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// ThresholdSlider.swift
|
||||||
|
// Gaze
|
||||||
|
//
|
||||||
|
// Created by Mike Freno on 1/30/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ThresholdSlider: View {
|
||||||
|
let label: String
|
||||||
|
@Binding var value: Int
|
||||||
|
let range: ClosedRange<Int>
|
||||||
|
var step: Int = 1
|
||||||
|
let unit: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.font(.subheadline)
|
||||||
|
Spacer()
|
||||||
|
Text("\(value) \(unit)")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value: Binding(
|
||||||
|
get: { Double(value) },
|
||||||
|
set: { value = Int($0) }
|
||||||
|
),
|
||||||
|
in: Double(range.lowerBound)...Double(range.upperBound),
|
||||||
|
step: Double(step)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
// Created by Mike Freno on 1/18/26.
|
// Created by Mike Freno on 1/18/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AdditionalModifiersView: View {
|
struct AdditionalModifiersView: View {
|
||||||
@@ -12,6 +13,13 @@ struct AdditionalModifiersView: View {
|
|||||||
@State private var frontCardIndex: Int = 0
|
@State private var frontCardIndex: Int = 0
|
||||||
@State private var dragOffset: CGFloat = 0
|
@State private var dragOffset: CGFloat = 0
|
||||||
@State private var isDragging: Bool = false
|
@State private var isDragging: Bool = false
|
||||||
|
@State private var isTestModeActive = false
|
||||||
|
@State private var cachedPreviewLayer: AVCaptureVideoPreviewLayer?
|
||||||
|
@State private var showAdvancedSettings = false
|
||||||
|
@State private var showCalibrationWindow = false
|
||||||
|
@State private var isViewActive = false
|
||||||
|
@State private var isProcessingToggle = false
|
||||||
|
@ObservedObject var cameraService = CameraAccessService.shared
|
||||||
@Environment(\.isCompactLayout) private var isCompact
|
@Environment(\.isCompactLayout) private var isCompact
|
||||||
|
|
||||||
private var backCardOffset: CGFloat { isCompact ? 20 : AdaptiveLayout.Card.backOffset }
|
private var backCardOffset: CGFloat { isCompact ? 20 : AdaptiveLayout.Card.backOffset }
|
||||||
@@ -50,15 +58,41 @@ struct AdditionalModifiersView: View {
|
|||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
cardView(for: 0, width: cardWidth, height: cardHeight)
|
setupCard(
|
||||||
.zIndex(zIndex(for: 0))
|
presentation: .card,
|
||||||
.scaleEffect(scale(for: 0))
|
content:
|
||||||
.offset(x: xOffset(for: 0), y: yOffset(for: 0))
|
EnforceModeSetupContent(
|
||||||
|
settingsManager: settingsManager,
|
||||||
|
presentation: .card,
|
||||||
|
isTestModeActive: $isTestModeActive,
|
||||||
|
cachedPreviewLayer: $cachedPreviewLayer,
|
||||||
|
showAdvancedSettings: $showAdvancedSettings,
|
||||||
|
showCalibrationWindow: $showCalibrationWindow,
|
||||||
|
isViewActive: $isViewActive,
|
||||||
|
isProcessingToggle: isProcessingToggle,
|
||||||
|
handleEnforceModeToggle: { enabled in
|
||||||
|
if enabled {
|
||||||
|
Task { @MainActor in
|
||||||
|
try await cameraService.requestCameraAccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
width: cardWidth,
|
||||||
|
height: cardHeight,
|
||||||
|
index: 0
|
||||||
|
)
|
||||||
#endif
|
#endif
|
||||||
cardView(for: 1, width: cardWidth, height: cardHeight)
|
setupCard(
|
||||||
.zIndex(zIndex(for: 1))
|
presentation: .card,
|
||||||
.scaleEffect(scale(for: 1))
|
content: SmartModeSetupContent(
|
||||||
.offset(x: xOffset(for: 1), y: yOffset(for: 1))
|
settingsManager: settingsManager,
|
||||||
|
presentation: .card
|
||||||
|
),
|
||||||
|
width: cardWidth,
|
||||||
|
height: cardHeight,
|
||||||
|
index: 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.padding(isCompact ? 12 : 20)
|
.padding(isCompact ? 12 : 20)
|
||||||
.gesture(dragGesture)
|
.gesture(dragGesture)
|
||||||
@@ -198,226 +232,28 @@ struct AdditionalModifiersView: View {
|
|||||||
// MARK: - Card Views
|
// MARK: - Card Views
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func cardView(for index: Int, width: CGFloat, height: CGFloat) -> some View {
|
private func setupCard(
|
||||||
|
presentation: SetupPresentation,
|
||||||
|
content: some View,
|
||||||
|
width: CGFloat,
|
||||||
|
height: CGFloat,
|
||||||
|
index: Int
|
||||||
|
) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(Color(NSColor.windowBackgroundColor))
|
.fill(Color(NSColor.windowBackgroundColor))
|
||||||
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
||||||
|
|
||||||
Group {
|
ScrollView {
|
||||||
if index == 0 {
|
content
|
||||||
enforceModeContent
|
|
||||||
} else {
|
|
||||||
smartModeContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(isCompact ? 12 : 20)
|
.padding(isCompact ? 12 : 20)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
}
|
.zIndex(zIndex(for: index))
|
||||||
|
.scaleEffect(scale(for: index))
|
||||||
@ObservedObject var cameraService = CameraAccessService.shared
|
.offset(x: xOffset(for: index), y: yOffset(for: index))
|
||||||
|
|
||||||
private var enforceModeContent: some View {
|
|
||||||
VStack(spacing: isCompact ? 10 : 16) {
|
|
||||||
Image(systemName: "video.fill")
|
|
||||||
.font(
|
|
||||||
.system(
|
|
||||||
size: isCompact
|
|
||||||
? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)
|
|
||||||
)
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
|
|
||||||
Text("Enforce Mode")
|
|
||||||
.font(isCompact ? .headline : .title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
if !cameraService.hasCameraHardware {
|
|
||||||
Text("Camera hardware not detected")
|
|
||||||
.font(isCompact ? .caption : .subheadline)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
} else {
|
|
||||||
Text("Use your camera to ensure you take breaks")
|
|
||||||
.font(isCompact ? .caption : .subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(spacing: isCompact ? 10 : 16) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("Enable Enforce Mode")
|
|
||||||
.font(isCompact ? .subheadline : .headline)
|
|
||||||
if !cameraService.hasCameraHardware {
|
|
||||||
Text("No camera hardware detected")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Text("Camera activates before lookaway reminders")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle(
|
|
||||||
"",
|
|
||||||
isOn: Binding(
|
|
||||||
get: {
|
|
||||||
settingsManager.isTimerEnabled(for: .lookAway)
|
|
||||||
|| settingsManager.isTimerEnabled(for: .blink)
|
|
||||||
|| settingsManager.isTimerEnabled(for: .posture)
|
|
||||||
},
|
|
||||||
set: { newValue in
|
|
||||||
if newValue {
|
|
||||||
Task { @MainActor in
|
|
||||||
try await cameraService.requestCameraAccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.labelsHidden()
|
|
||||||
.disabled(!cameraService.hasCameraHardware)
|
|
||||||
.controlSize(isCompact ? .small : .regular)
|
|
||||||
}
|
|
||||||
.padding(isCompact ? 10 : 16)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("Camera Access")
|
|
||||||
.font(isCompact ? .subheadline : .headline)
|
|
||||||
|
|
||||||
if !cameraService.hasCameraHardware {
|
|
||||||
Label("No camera", systemImage: "xmark.circle.fill")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else if cameraService.isCameraAuthorized {
|
|
||||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
} else if let error = cameraService.cameraError {
|
|
||||||
Label(
|
|
||||||
error.localizedDescription,
|
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
|
||||||
)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Label("Not authorized", systemImage: "xmark.circle.fill")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if !cameraService.isCameraAuthorized {
|
|
||||||
Button("Request Access") {
|
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
|
||||||
try await cameraService.requestCameraAccess()
|
|
||||||
} catch {
|
|
||||||
print("Camera access failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(isCompact ? .small : .regular)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(isCompact ? 10 : 16)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var smartModeContent: some View {
|
|
||||||
VStack(spacing: isCompact ? 10 : 16) {
|
|
||||||
Image(systemName: "brain.fill")
|
|
||||||
.font(
|
|
||||||
.system(
|
|
||||||
size: isCompact
|
|
||||||
? AdaptiveLayout.Font.cardIconSmall : AdaptiveLayout.Font.cardIcon)
|
|
||||||
)
|
|
||||||
.foregroundStyle(.purple)
|
|
||||||
|
|
||||||
Text("Smart Mode")
|
|
||||||
.font(isCompact ? .headline : .title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
|
|
||||||
Text("Automatically manage timers based on activity")
|
|
||||||
.font(isCompact ? .caption : .subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(spacing: isCompact ? 8 : 12) {
|
|
||||||
smartModeToggle(
|
|
||||||
icon: "arrow.up.left.and.arrow.down.right",
|
|
||||||
iconColor: .blue,
|
|
||||||
title: "Auto-pause on Fullscreen",
|
|
||||||
subtitle: "Pause during videos, games, presentations",
|
|
||||||
isOn: $settingsManager.settings.smartMode.autoPauseOnFullscreen
|
|
||||||
)
|
|
||||||
|
|
||||||
smartModeToggle(
|
|
||||||
icon: "moon.zzz.fill",
|
|
||||||
iconColor: .indigo,
|
|
||||||
title: "Auto-pause on Idle",
|
|
||||||
subtitle: "Pause when you're inactive",
|
|
||||||
isOn: $settingsManager.settings.smartMode.autoPauseOnIdle
|
|
||||||
)
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
smartModeToggle(
|
|
||||||
icon: "chart.line.uptrend.xyaxis",
|
|
||||||
iconColor: .green,
|
|
||||||
title: "Track Usage Statistics",
|
|
||||||
subtitle: "Monitor active and idle time",
|
|
||||||
isOn: $settingsManager.settings.smartMode.trackUsage
|
|
||||||
)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func smartModeToggle(
|
|
||||||
icon: String, iconColor: Color, title: String, subtitle: String, isOn: Binding<Bool>
|
|
||||||
) -> some View {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.foregroundStyle(iconColor)
|
|
||||||
.frame(width: isCompact ? 20 : 24)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
|
||||||
Text(title)
|
|
||||||
.font(isCompact ? .caption : .subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
Text(subtitle)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Toggle("", isOn: isOn)
|
|
||||||
.labelsHidden()
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, isCompact ? 8 : 12)
|
|
||||||
.padding(.vertical, isCompact ? 6 : 10)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Gestures & Navigation
|
// MARK: - Gestures & Navigation
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ final class MenuBarGuideOverlayPresenter {
|
|||||||
private func startCheckTimer() {
|
private func startCheckTimer() {
|
||||||
checkTimer?.invalidate()
|
checkTimer?.invalidate()
|
||||||
checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
|
checkTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
|
||||||
self?.checkWindowFrame()
|
guard let self else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
self.checkWindowFrame()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +132,10 @@ final class MenuBarGuideOverlayPresenter {
|
|||||||
// Set up KVO for window frame changes
|
// Set up KVO for window frame changes
|
||||||
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
|
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
|
||||||
[weak self] _, _ in
|
[weak self] _, _ in
|
||||||
self?.checkWindowFrame()
|
guard let self else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
self.checkWindowFrame()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add observer for when the onboarding window is closed
|
// Add observer for when the onboarding window is closed
|
||||||
@@ -145,7 +151,10 @@ final class MenuBarGuideOverlayPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide the overlay when onboarding window closes
|
// Hide the overlay when onboarding window closes
|
||||||
self?.hide()
|
guard let self else { return }
|
||||||
|
Task { @MainActor in
|
||||||
|
self.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ struct SettingsWindowView: View {
|
|||||||
BlinkSetupView(settingsManager: settingsManager)
|
BlinkSetupView(settingsManager: settingsManager)
|
||||||
case .posture:
|
case .posture:
|
||||||
PostureSetupView(settingsManager: settingsManager)
|
PostureSetupView(settingsManager: settingsManager)
|
||||||
#if ENFORCE_READY
|
#if DEBUG
|
||||||
case .enforceMode:
|
case .enforceMode:
|
||||||
EnforceModeSetupView(settingsManager: settingsManager)
|
EnforceModeSetupView(settingsManager: settingsManager)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,15 +6,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EnforceModeSetupView: View {
|
struct EnforceModeSetupView: View {
|
||||||
@Bindable var settingsManager: SettingsManager
|
@Bindable var settingsManager: SettingsManager
|
||||||
@ObservedObject var cameraService = CameraAccessService.shared
|
@ObservedObject var cameraService = CameraAccessService.shared
|
||||||
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
|
|
||||||
@ObservedObject var enforceModeService = EnforceModeService.shared
|
@ObservedObject var enforceModeService = EnforceModeService.shared
|
||||||
@Environment(\.isCompactLayout) private var isCompact
|
|
||||||
|
|
||||||
@State private var isProcessingToggle = false
|
@State private var isProcessingToggle = false
|
||||||
@State private var isTestModeActive = false
|
@State private var isTestModeActive = false
|
||||||
@@ -23,7 +20,6 @@ struct EnforceModeSetupView: View {
|
|||||||
@State private var isViewActive = false
|
@State private var isViewActive = false
|
||||||
@State private var showAdvancedSettings = false
|
@State private var showAdvancedSettings = false
|
||||||
@State private var showCalibrationWindow = false
|
@State private var showCalibrationWindow = false
|
||||||
@ObservedObject var calibratorService = CalibratorService.shared
|
|
||||||
|
|
||||||
private var cameraHardwareAvailable: Bool {
|
private var cameraHardwareAvailable: Bool {
|
||||||
cameraService.hasCameraHardware
|
cameraService.hasCameraHardware
|
||||||
@@ -33,72 +29,27 @@ struct EnforceModeSetupView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
||||||
|
|
||||||
Spacer()
|
EnforceModeSetupContent(
|
||||||
|
settingsManager: settingsManager,
|
||||||
VStack(spacing: isCompact ? 16 : 30) {
|
presentation: .window,
|
||||||
Text("Use your camera to ensure you take breaks")
|
isTestModeActive: $isTestModeActive,
|
||||||
.font(isCompact ? .subheadline : .title3)
|
cachedPreviewLayer: $cachedPreviewLayer,
|
||||||
.foregroundStyle(.secondary)
|
showAdvancedSettings: $showAdvancedSettings,
|
||||||
.multilineTextAlignment(.center)
|
showCalibrationWindow: $showCalibrationWindow,
|
||||||
|
isViewActive: $isViewActive,
|
||||||
VStack(spacing: isCompact ? 12 : 20) {
|
isProcessingToggle: isProcessingToggle,
|
||||||
HStack {
|
handleEnforceModeToggle: { enabled in
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
print("🎛️ Toggle changed to: \(enabled)")
|
||||||
Text("Enable Enforce Mode")
|
|
||||||
.font(isCompact ? .subheadline : .headline)
|
|
||||||
if !cameraHardwareAvailable {
|
|
||||||
Text("No camera hardware detected")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Text("Camera activates 3 seconds before lookaway reminders")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle(
|
|
||||||
"",
|
|
||||||
isOn: Binding(
|
|
||||||
get: {
|
|
||||||
settingsManager.isTimerEnabled(for: .lookAway) ||
|
|
||||||
settingsManager.isTimerEnabled(for: .blink) ||
|
|
||||||
settingsManager.isTimerEnabled(for: .posture)
|
|
||||||
},
|
|
||||||
set: { newValue in
|
|
||||||
print("🎛️ Toggle changed to: \(newValue)")
|
|
||||||
guard !isProcessingToggle else {
|
guard !isProcessingToggle else {
|
||||||
print("⚠️ Already processing toggle")
|
print("⚠️ Already processing toggle")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleEnforceModeToggle(enabled: newValue)
|
handleEnforceModeToggle(enabled: enabled)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
.padding(.top, 20)
|
||||||
.labelsHidden()
|
|
||||||
.disabled(isProcessingToggle || !cameraHardwareAvailable)
|
|
||||||
.controlSize(isCompact ? .small : .regular)
|
|
||||||
}
|
|
||||||
.padding(isCompact ? 10 : 16)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
cameraStatusView
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
if enforceModeService.isEnforceModeEnabled {
|
|
||||||
testModeButton
|
|
||||||
}
|
|
||||||
if isTestModeActive && enforceModeService.isCameraActive {
|
|
||||||
testModePreviewView
|
|
||||||
trackingConstantsView
|
|
||||||
} else if enforceModeService.isCameraActive && !isTestModeActive {
|
|
||||||
eyeTrackingStatusView
|
|
||||||
trackingConstantsView
|
|
||||||
}
|
|
||||||
privacyInfoView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
@@ -115,272 +66,6 @@ struct EnforceModeSetupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var testModeButton: some View {
|
|
||||||
Button(action: {
|
|
||||||
Task { @MainActor in
|
|
||||||
if isTestModeActive {
|
|
||||||
enforceModeService.stopTestMode()
|
|
||||||
isTestModeActive = false
|
|
||||||
cachedPreviewLayer = nil
|
|
||||||
} else {
|
|
||||||
await enforceModeService.startTestMode()
|
|
||||||
isTestModeActive = enforceModeService.isCameraActive
|
|
||||||
if isTestModeActive {
|
|
||||||
cachedPreviewLayer = eyeTrackingService.previewLayer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: isTestModeActive ? "stop.circle.fill" : "play.circle.fill")
|
|
||||||
.font(.title3)
|
|
||||||
Text(isTestModeActive ? "Stop Test" : "Test Tracking")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.large)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var calibrationSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "target")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
Text("Eye Tracking Calibration")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
|
|
||||||
if calibratorService.calibrationData.isComplete {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text(calibratorService.getCalibrationSummary())
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
if calibratorService.needsRecalibration() {
|
|
||||||
Label(
|
|
||||||
"Calibration expired - recalibration recommended",
|
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Label("Calibration active and valid", systemImage: "checkmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Not calibrated - using default thresholds")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
showCalibrationWindow = true
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "target")
|
|
||||||
Text(
|
|
||||||
calibratorService.calibrationData.isComplete
|
|
||||||
? "Recalibrate" : "Run Calibration")
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.regular)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(
|
|
||||||
GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: 12)
|
|
||||||
)
|
|
||||||
.sheet(isPresented: $showCalibrationWindow) {
|
|
||||||
EyeTrackingCalibrationView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var testModePreviewView: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
let lookingAway = !eyeTrackingService.userLookingAtScreen
|
|
||||||
let borderColor: NSColor = lookingAway ? .systemGreen : .systemRed
|
|
||||||
|
|
||||||
// Cache the preview layer to avoid recreating it
|
|
||||||
let previewLayer = eyeTrackingService.previewLayer ?? cachedPreviewLayer
|
|
||||||
|
|
||||||
if let layer = previewLayer {
|
|
||||||
ZStack {
|
|
||||||
CameraPreviewView(previewLayer: layer, borderColor: borderColor)
|
|
||||||
|
|
||||||
// Pupil detection overlay (drawn on video)
|
|
||||||
PupilOverlayView(eyeTrackingService: eyeTrackingService)
|
|
||||||
|
|
||||||
// Debug info overlay (top-right corner)
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
GazeOverlayView(eyeTrackingService: eyeTrackingService)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: isCompact ? 200 : 300)
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
.onAppear {
|
|
||||||
if cachedPreviewLayer == nil {
|
|
||||||
cachedPreviewLayer = eyeTrackingService.previewLayer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*VStack(alignment: .leading, spacing: 12) {*/
|
|
||||||
/*Text("Live Tracking Status")*/
|
|
||||||
/*.font(.headline)*/
|
|
||||||
|
|
||||||
/*HStack(spacing: 20) {*/
|
|
||||||
/*statusIndicator(*/
|
|
||||||
/*title: "Face Detected",*/
|
|
||||||
/*isActive: eyeTrackingService.faceDetected,*/
|
|
||||||
/*icon: "person.fill"*/
|
|
||||||
/*)*/
|
|
||||||
|
|
||||||
/*statusIndicator(*/
|
|
||||||
/*title: "Looking Away",*/
|
|
||||||
/*isActive: !eyeTrackingService.userLookingAtScreen,*/
|
|
||||||
/*icon: "arrow.turn.up.right"*/
|
|
||||||
/*)*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
/*Text(*/
|
|
||||||
/*lookingAway*/
|
|
||||||
/*? "✓ Break compliance detected" : "⚠️ Please look away from screen"*/
|
|
||||||
/*)*/
|
|
||||||
/*.font(.caption)*/
|
|
||||||
/*.foregroundStyle(lookingAway ? .green : .orange)*/
|
|
||||||
/*.frame(maxWidth: .infinity, alignment: .center)*/
|
|
||||||
/*.padding(.top, 4)*/
|
|
||||||
/*}*/
|
|
||||||
/*.padding()*/
|
|
||||||
/*.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cameraStatusView: some View {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Camera Access")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
if cameraService.isCameraAuthorized {
|
|
||||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
} else if let error = cameraService.cameraError {
|
|
||||||
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Label("Not authorized", systemImage: "xmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if !cameraService.isCameraAuthorized {
|
|
||||||
Button("Request Access") {
|
|
||||||
print("📷 Request Access button clicked")
|
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
|
||||||
try await cameraService.requestCameraAccess()
|
|
||||||
print("✓ Camera access granted via button")
|
|
||||||
} catch {
|
|
||||||
print("⚠️ Camera access failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var eyeTrackingStatusView: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Eye Tracking Status")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
statusIndicator(
|
|
||||||
title: "Face Detected",
|
|
||||||
isActive: eyeTrackingService.faceDetected,
|
|
||||||
icon: "person.fill"
|
|
||||||
)
|
|
||||||
|
|
||||||
statusIndicator(
|
|
||||||
title: "Looking Away",
|
|
||||||
isActive: !eyeTrackingService.userLookingAtScreen,
|
|
||||||
icon: "arrow.turn.up.right"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func statusIndicator(title: String, isActive: Bool, icon: String) -> some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundStyle(isActive ? .green : .secondary)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var privacyInfoView: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "lock.shield.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
Text("Privacy Information")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
privacyBullet("All processing happens on-device")
|
|
||||||
privacyBullet("No images are stored or transmitted")
|
|
||||||
privacyBullet("Camera only active during lookaway reminders (3 second window)")
|
|
||||||
privacyBullet("You can always force quit with cmd+q")
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(
|
|
||||||
GlassStyle.regular.tint(.blue.opacity(0.1)), in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func privacyBullet(_ text: String) -> some View {
|
|
||||||
HStack(alignment: .top, spacing: 8) {
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleEnforceModeToggle(enabled: Bool) {
|
private func handleEnforceModeToggle(enabled: Bool) {
|
||||||
print("🎛️ handleEnforceModeToggle called with enabled: \(enabled)")
|
print("🎛️ handleEnforceModeToggle called with enabled: \(enabled)")
|
||||||
isProcessingToggle = true
|
isProcessingToggle = true
|
||||||
@@ -411,232 +96,6 @@ struct EnforceModeSetupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var trackingConstantsView: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
HStack {
|
|
||||||
Text("Tracking Sensitivity")
|
|
||||||
.font(.headline)
|
|
||||||
Spacer()
|
|
||||||
Button(action: {
|
|
||||||
eyeTrackingService.enableDebugLogging.toggle()
|
|
||||||
}) {
|
|
||||||
Image(
|
|
||||||
systemName: eyeTrackingService.enableDebugLogging
|
|
||||||
? "ant.circle.fill" : "ant.circle"
|
|
||||||
)
|
|
||||||
.foregroundStyle(eyeTrackingService.enableDebugLogging ? .orange : .secondary)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.help("Toggle console debug logging")
|
|
||||||
|
|
||||||
Button(showAdvancedSettings ? "Hide Settings" : "Show Settings") {
|
|
||||||
withAnimation {
|
|
||||||
showAdvancedSettings.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug info always visible when tracking
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Live Values:")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
if let leftRatio = eyeTrackingService.debugLeftPupilRatio,
|
|
||||||
let rightRatio = eyeTrackingService.debugRightPupilRatio
|
|
||||||
{
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("Left Pupil: \(String(format: "%.3f", leftRatio))")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(
|
|
||||||
!EyeTrackingConstants.minPupilEnabled
|
|
||||||
&& !EyeTrackingConstants.maxPupilEnabled
|
|
||||||
? .secondary
|
|
||||||
: (leftRatio < EyeTrackingConstants.minPupilRatio
|
|
||||||
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
|
||||||
? Color.orange : Color.green
|
|
||||||
)
|
|
||||||
Text("Right Pupil: \(String(format: "%.3f", rightRatio))")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(
|
|
||||||
!EyeTrackingConstants.minPupilEnabled
|
|
||||||
&& !EyeTrackingConstants.maxPupilEnabled
|
|
||||||
? .secondary
|
|
||||||
: (rightRatio < EyeTrackingConstants.minPupilRatio
|
|
||||||
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
|
||||||
? Color.orange : Color.green
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
|
||||||
Text(
|
|
||||||
"Range: \(String(format: "%.2f", EyeTrackingConstants.minPupilRatio)) - \(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))"
|
|
||||||
)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
let bothEyesOut =
|
|
||||||
(leftRatio < EyeTrackingConstants.minPupilRatio
|
|
||||||
|| leftRatio > EyeTrackingConstants.maxPupilRatio)
|
|
||||||
&& (rightRatio < EyeTrackingConstants.minPupilRatio
|
|
||||||
|| rightRatio > EyeTrackingConstants.maxPupilRatio)
|
|
||||||
Text(bothEyesOut ? "Both Out ⚠️" : "In Range ✓")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(bothEyesOut ? .orange : .green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Pupil data unavailable")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let yaw = eyeTrackingService.debugYaw,
|
|
||||||
let pitch = eyeTrackingService.debugPitch
|
|
||||||
{
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("Yaw: \(String(format: "%.3f", yaw))")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(
|
|
||||||
!EyeTrackingConstants.yawEnabled
|
|
||||||
? .secondary
|
|
||||||
: abs(yaw) > EyeTrackingConstants.yawThreshold
|
|
||||||
? Color.orange : Color.green
|
|
||||||
)
|
|
||||||
Text("Pitch: \(String(format: "%.3f", pitch))")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(
|
|
||||||
!EyeTrackingConstants.pitchUpEnabled
|
|
||||||
&& !EyeTrackingConstants.pitchDownEnabled
|
|
||||||
? .secondary
|
|
||||||
: (pitch > EyeTrackingConstants.pitchUpThreshold
|
|
||||||
|| pitch < EyeTrackingConstants.pitchDownThreshold)
|
|
||||||
? Color.orange : Color.green
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
|
||||||
Text(
|
|
||||||
"Yaw Max: \(String(format: "%.2f", EyeTrackingConstants.yawThreshold))"
|
|
||||||
)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text(
|
|
||||||
"Pitch: \(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) to \(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold))"
|
|
||||||
)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 4)
|
|
||||||
|
|
||||||
if showAdvancedSettings {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
// Display the current constant values
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Current Threshold Values:")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Yaw Threshold:")
|
|
||||||
Spacer()
|
|
||||||
Text("\(String(format: "%.2f", EyeTrackingConstants.yawThreshold)) rad")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Pitch Up Threshold:")
|
|
||||||
Spacer()
|
|
||||||
Text(
|
|
||||||
"\(String(format: "%.2f", EyeTrackingConstants.pitchUpThreshold)) rad"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Pitch Down Threshold:")
|
|
||||||
Spacer()
|
|
||||||
Text(
|
|
||||||
"\(String(format: "%.2f", EyeTrackingConstants.pitchDownThreshold)) rad"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Min Pupil Ratio:")
|
|
||||||
Spacer()
|
|
||||||
Text("\(String(format: "%.2f", EyeTrackingConstants.minPupilRatio))")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Max Pupil Ratio:")
|
|
||||||
Spacer()
|
|
||||||
Text("\(String(format: "%.2f", EyeTrackingConstants.maxPupilRatio))")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Eye Closed Threshold:")
|
|
||||||
Spacer()
|
|
||||||
Text(
|
|
||||||
"\(String(format: "%.3f", EyeTrackingConstants.eyeClosedThreshold))"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var debugEyeTrackingView: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Debug Eye Tracking Data")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Face Detected: \(eyeTrackingService.faceDetected ? "Yes" : "No")")
|
|
||||||
.font(.caption)
|
|
||||||
|
|
||||||
Text("Looking at Screen: \(eyeTrackingService.userLookingAtScreen ? "Yes" : "No")")
|
|
||||||
.font(.caption)
|
|
||||||
|
|
||||||
Text("Eyes Closed: \(eyeTrackingService.isEyesClosed ? "Yes" : "No")")
|
|
||||||
.font(.caption)
|
|
||||||
|
|
||||||
if eyeTrackingService.faceDetected {
|
|
||||||
Text("Yaw: 0.0")
|
|
||||||
.font(.caption)
|
|
||||||
|
|
||||||
Text("Roll: 0.0")
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ struct LookAwaySetupView: View {
|
|||||||
|
|
||||||
private func previewLookAway() {
|
private func previewLookAway() {
|
||||||
guard let screen = NSScreen.main else { return }
|
guard let screen = NSScreen.main else { return }
|
||||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
|
||||||
let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes
|
let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes
|
||||||
PreviewWindowHelper.showPreview(on: screen) { dismiss in
|
PreviewWindowHelper.showPreview(on: screen) { dismiss in
|
||||||
LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)
|
LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)
|
||||||
|
|||||||
@@ -9,201 +9,23 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SmartModeSetupView: View {
|
struct SmartModeSetupView: View {
|
||||||
@Bindable var settingsManager: SettingsManager
|
@Bindable var settingsManager: SettingsManager
|
||||||
@State private var permissionManager = ScreenCapturePermissionManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
SetupHeader(icon: "brain.fill", title: "Smart Mode", color: .purple)
|
SetupHeader(icon: "brain.fill", title: "Smart Mode", color: .purple)
|
||||||
|
|
||||||
Text("Automatically manage timers based on your activity")
|
SmartModeSetupContent(
|
||||||
.font(.subheadline)
|
settingsManager: settingsManager,
|
||||||
.foregroundStyle(.secondary)
|
presentation: .window
|
||||||
.padding(.bottom, 30)
|
)
|
||||||
|
.padding(.top, 24)
|
||||||
|
|
||||||
Spacer()
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
VStack(spacing: 24) {
|
|
||||||
fullscreenSection
|
|
||||||
idleSection
|
|
||||||
#if DEBUG
|
|
||||||
usageTrackingSection
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.frame(maxWidth: 600)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
.background(.clear)
|
.background(.clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fullscreenSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
Text("Auto-pause on Fullscreen")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
"Timers will automatically pause when you enter fullscreen mode (videos, games, presentations)"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnFullscreen)
|
|
||||||
.labelsHidden()
|
|
||||||
.onChange(of: settingsManager.settings.smartMode.autoPauseOnFullscreen) {
|
|
||||||
_, newValue in
|
|
||||||
if newValue {
|
|
||||||
permissionManager.requestAuthorizationIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if settingsManager.settings.smartMode.autoPauseOnFullscreen,
|
|
||||||
permissionManager.authorizationStatus != .authorized
|
|
||||||
{
|
|
||||||
permissionWarningView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var permissionWarningView: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Label(
|
|
||||||
permissionManager.authorizationStatus == .denied
|
|
||||||
? "Screen Recording permission required"
|
|
||||||
: "Grant Screen Recording access",
|
|
||||||
systemImage: "exclamationmark.shield"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
|
|
||||||
Text("macOS requires Screen Recording permission to detect other apps in fullscreen.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button("Grant Access") {
|
|
||||||
permissionManager.requestAuthorizationIfNeeded()
|
|
||||||
permissionManager.openSystemSettings()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
|
|
||||||
Button("Open Settings") {
|
|
||||||
permissionManager.openSystemSettings()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var idleSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "moon.zzz.fill")
|
|
||||||
.foregroundStyle(.indigo)
|
|
||||||
Text("Auto-pause on Idle")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
Text("Timers will pause when you're inactive for more than the threshold below")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle("", isOn: $settingsManager.settings.smartMode.autoPauseOnIdle)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
|
|
||||||
if settingsManager.settings.smartMode.autoPauseOnIdle {
|
|
||||||
ThresholdSlider(
|
|
||||||
label: "Idle Threshold:",
|
|
||||||
value: $settingsManager.settings.smartMode.idleThresholdMinutes,
|
|
||||||
range: 1...30,
|
|
||||||
unit: "min"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var usageTrackingSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "chart.line.uptrend.xyaxis")
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
Text("Track Usage Statistics")
|
|
||||||
.font(.headline)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
"Monitor active and idle time, with automatic reset after the specified duration"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle("", isOn: $settingsManager.settings.smartMode.trackUsage)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
|
|
||||||
if settingsManager.settings.smartMode.trackUsage {
|
|
||||||
ThresholdSlider(
|
|
||||||
label: "Reset After:",
|
|
||||||
value: $settingsManager.settings.smartMode.usageResetAfterMinutes,
|
|
||||||
range: 15...240,
|
|
||||||
step: 15,
|
|
||||||
unit: "min"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThresholdSlider: View {
|
|
||||||
let label: String
|
|
||||||
@Binding var value: Int
|
|
||||||
let range: ClosedRange<Int>
|
|
||||||
var step: Int = 1
|
|
||||||
let unit: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Text(label)
|
|
||||||
.font(.subheadline)
|
|
||||||
Spacer()
|
|
||||||
Text("\(value) \(unit)")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Slider(
|
|
||||||
value: Binding(
|
|
||||||
get: { Double(value) },
|
|
||||||
set: { value = Int($0) }
|
|
||||||
),
|
|
||||||
in: Double(range.lowerBound)...Double(range.upperBound),
|
|
||||||
step: Double(step)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Gaze</title>
|
<title>Gaze</title>
|
||||||
|
<item>
|
||||||
|
<title>0.5.0</title>
|
||||||
|
<pubDate>Fri, 30 Jan 2026 12:58:57 -0500</pubDate>
|
||||||
|
<sparkle:version>10</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>0.5.0</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
|
||||||
|
<enclosure url="https://freno.me/api/downloads/Gaze-0.5.0.dmg" length="5253164" type="application/octet-stream" sparkle:edSignature="eXTeHXkMiAO4O1drqvdeYYn6oY9bpilm4toHNZ5BGvWVeNOtwzFC9YOWb+abPEYDRMmu5oodbPBFyPE65w6BDg=="/>
|
||||||
|
<sparkle:deltas>
|
||||||
|
<enclosure url="https://freno.me/api/downloads/Gaze10-9.delta" sparkle:deltaFrom="9" length="721382" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="860560" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,uk" sparkle:edSignature="kO4XClM9nikVhnhX4QypN5zFIikIhVdnVcOQDsvL8ciBD6opGM/IH7pqnCRKRwP6N8SRobgAQTHsee6oOZyWCA=="/>
|
||||||
|
</sparkle:deltas>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>0.4.1</title>
|
<title>0.4.1</title>
|
||||||
<pubDate>Tue, 13 Jan 2026 17:27:46 -0500</pubDate>
|
<pubDate>Tue, 13 Jan 2026 17:27:46 -0500</pubDate>
|
||||||
@@ -62,13 +73,5 @@
|
|||||||
<enclosure url="https://freno.me/api/downloads/Gaze2-1.delta" sparkle:deltaFrom="1" length="94254" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="858560" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,uk" sparkle:edSignature="qfxSfqD9iVJ7GVL19V8T4OuOTz0ZgqJNceBH6W+dwoKel1R+BTPkU9Ia8xR12v07GoXkyyqc+ba79OOL7jIpBw=="/>
|
<enclosure url="https://freno.me/api/downloads/Gaze2-1.delta" sparkle:deltaFrom="1" length="94254" type="application/octet-stream" sparkle:deltaFromSparkleExecutableSize="858560" sparkle:deltaFromSparkleLocales="de,he,ar,el,ja,fa,uk" sparkle:edSignature="qfxSfqD9iVJ7GVL19V8T4OuOTz0ZgqJNceBH6W+dwoKel1R+BTPkU9Ia8xR12v07GoXkyyqc+ba79OOL7jIpBw=="/>
|
||||||
</sparkle:deltas>
|
</sparkle:deltas>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<title>0.1.1</title>
|
|
||||||
<pubDate>Sun, 11 Jan 2026 18:07:02 -0500</pubDate>
|
|
||||||
<sparkle:version>1</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>0.2.0</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://freno.me/api/downloads/Gaze-0.2.0.dmg" length="4831161" type="application/octet-stream" sparkle:edSignature="zCEmiiO4Q7HV7uGbI/CQcfJElm1uqrYorznE6uCWaKm/Zg1bUrWaeTRf9+Uv9f9+0iptyiS2FNdglLQB8RKkCA=="/>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
Reference in New Issue
Block a user