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 */
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -70,6 +71,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
27CF3CCB2F2D266600D67058 /* MacroVisionKit in Frameworks */,
|
||||
275915892F132A9200D0E60D /* Lottie in Frameworks */,
|
||||
27SPARKLE00000000003 /* Sparkle in Frameworks */,
|
||||
);
|
||||
@@ -134,6 +136,7 @@
|
||||
packageProductDependencies = (
|
||||
27AE10B12F10B1FC00E00DBC /* Lottie */,
|
||||
27SPARKLE00000000002 /* Sparkle */,
|
||||
27CF3CCA2F2D266600D67058 /* MacroVisionKit */,
|
||||
);
|
||||
productName = Gaze;
|
||||
productReference = 27A21B3C2F0F69DC0018C4F3 /* Gaze.app */;
|
||||
@@ -220,6 +223,7 @@
|
||||
packageReferences = (
|
||||
27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */,
|
||||
27SPARKLE00000000001 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
27CF3CC92F2D266600D67058 /* XCRemoteSwiftPackageReference "MacroVisionKit" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 27A21B3D2F0F69DC0018C4F3 /* Products */;
|
||||
@@ -424,7 +428,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -439,7 +443,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -462,7 +466,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Gaze/Gaze.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -477,7 +481,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.Gaze;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -496,11 +500,11 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -517,11 +521,11 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -537,10 +541,10 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -556,10 +560,10 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_TEAM = 6GK4F9L62V;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mikefreno.GazeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -621,6 +625,14 @@
|
||||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||
@@ -637,6 +649,11 @@
|
||||
package = 27AE10B02F10B1FC00E00DBC /* XCRemoteSwiftPackageReference "lottie-spm" */;
|
||||
productName = Lottie;
|
||||
};
|
||||
27CF3CCA2F2D266600D67058 /* MacroVisionKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 27CF3CC92F2D266600D67058 /* XCRemoteSwiftPackageReference "MacroVisionKit" */;
|
||||
productName = MacroVisionKit;
|
||||
};
|
||||
27SPARKLE00000000002 /* Sparkle */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
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" : [
|
||||
{
|
||||
"identity" : "lottie-spm",
|
||||
@@ -10,6 +10,15 @@
|
||||
"version" : "4.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "macrovisionkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TheBoredTeam/MacroVisionKit.git",
|
||||
"state" : {
|
||||
"revision" : "da481a6be8d8b1bf7fcb218507a72428bbcae7b0",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -7,11 +7,35 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PauseReason: Codable, Equatable, Hashable {
|
||||
enum PauseReason: nonisolated Codable, nonisolated Sendable, nonisolated Equatable, nonisolated Hashable {
|
||||
case manual
|
||||
case fullscreen
|
||||
case idle
|
||||
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
|
||||
|
||||
/// Unified identifier for both built-in and user-defined timers
|
||||
enum TimerIdentifier: Hashable, Codable {
|
||||
enum TimerIdentifier: Hashable, Codable, Sendable {
|
||||
case builtIn(TimerType)
|
||||
case user(id: String)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TimerState: Equatable, Hashable {
|
||||
struct TimerState: Equatable, Hashable, Sendable {
|
||||
let identifier: TimerIdentifier
|
||||
var remainingSeconds: Int
|
||||
var isPaused: Bool
|
||||
@@ -45,7 +45,7 @@ struct TimerState: Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
enum TimerStateBuilder {
|
||||
enum TimerStateBuilder: Sendable {
|
||||
static func make(
|
||||
identifier: TimerIdentifier,
|
||||
intervalSeconds: Int,
|
||||
|
||||
@@ -10,16 +10,20 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
protocol CameraSessionDelegate: AnyObject {
|
||||
nonisolated func cameraSession(
|
||||
@MainActor func cameraSession(
|
||||
_ manager: CameraSessionManager,
|
||||
didOutput pixelBuffer: CVPixelBuffer,
|
||||
imageSize: CGSize
|
||||
)
|
||||
}
|
||||
|
||||
private struct PixelBufferBox: @unchecked Sendable {
|
||||
let buffer: CVPixelBuffer
|
||||
}
|
||||
|
||||
final class CameraSessionManager: NSObject, ObservableObject {
|
||||
@Published private(set) var isRunning = false
|
||||
weak var delegate: CameraSessionDelegate?
|
||||
nonisolated(unsafe) weak var delegate: CameraSessionDelegate?
|
||||
|
||||
private var captureSession: AVCaptureSession?
|
||||
private var videoOutput: AVCaptureVideoDataOutput?
|
||||
@@ -116,6 +120,11 @@ extension CameraSessionManager: AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
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 {
|
||||
nonisolated func cameraSession(
|
||||
@MainActor func cameraSession(
|
||||
_ manager: CameraSessionManager,
|
||||
didOutput pixelBuffer: CVPixelBuffer,
|
||||
imageSize: CGSize
|
||||
@@ -174,28 +174,23 @@ extension EyeTrackingService: CameraSessionDelegate {
|
||||
if let leftRatio = result.leftPupilRatio,
|
||||
let rightRatio = result.rightPupilRatio,
|
||||
let faceWidth = result.faceWidthRatio {
|
||||
Task { @MainActor in
|
||||
guard CalibratorService.shared.isCalibrating else { return }
|
||||
CalibratorService.shared.submitSampleToBridge(
|
||||
leftRatio: leftRatio,
|
||||
rightRatio: rightRatio,
|
||||
leftVertical: result.leftVerticalRatio,
|
||||
rightVertical: result.rightVerticalRatio,
|
||||
faceWidthRatio: faceWidth
|
||||
)
|
||||
}
|
||||
guard CalibratorService.shared.isCalibrating else { return }
|
||||
CalibratorService.shared.submitSampleToBridge(
|
||||
leftRatio: leftRatio,
|
||||
rightRatio: rightRatio,
|
||||
leftVertical: result.leftVerticalRatio,
|
||||
rightVertical: result.rightVerticalRatio,
|
||||
faceWidthRatio: faceWidth
|
||||
)
|
||||
}
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.faceDetected = result.faceDetected
|
||||
self.isEyesClosed = result.isEyesClosed
|
||||
self.userLookingAtScreen = result.userLookingAtScreen
|
||||
self.debugAdapter.update(from: result)
|
||||
self.debugAdapter.updateEyeImages(from: PupilDetector.self)
|
||||
self.syncDebugState()
|
||||
self.updateGazeConfiguration()
|
||||
}
|
||||
self.faceDetected = result.faceDetected
|
||||
self.isEyesClosed = result.isEyesClosed
|
||||
self.userLookingAtScreen = result.userLookingAtScreen
|
||||
self.debugAdapter.update(from: result)
|
||||
self.debugAdapter.updateEyeImages(from: PupilDetector.self)
|
||||
self.syncDebugState()
|
||||
self.updateGazeConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
import simd
|
||||
|
||||
struct EyeTrackingProcessingResult: Sendable {
|
||||
@@ -54,19 +54,19 @@ final class GazeDetector: @unchecked Sendable {
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var configuration: Configuration
|
||||
private nonisolated(unsafe) var configuration: Configuration
|
||||
|
||||
init(configuration: Configuration) {
|
||||
nonisolated init(configuration: Configuration) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
func updateConfiguration(_ configuration: Configuration) {
|
||||
nonisolated func updateConfiguration(_ configuration: Configuration) {
|
||||
lock.lock()
|
||||
self.configuration = configuration
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
nonisolated func process(
|
||||
func process(
|
||||
analysis: VisionPipeline.FaceAnalysis,
|
||||
pixelBuffer: CVPixelBuffer
|
||||
) -> EyeTrackingProcessingResult {
|
||||
@@ -75,7 +75,7 @@ final class GazeDetector: @unchecked Sendable {
|
||||
config = configuration
|
||||
lock.unlock()
|
||||
|
||||
guard analysis.faceDetected, let face = analysis.face else {
|
||||
guard analysis.faceDetected, let face = analysis.face?.value else {
|
||||
return EyeTrackingProcessingResult(
|
||||
faceDetected: false,
|
||||
isEyesClosed: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ import Accelerate
|
||||
import CoreImage
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
|
||||
struct PupilPosition: Equatable, Sendable {
|
||||
let x: CGFloat
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
@preconcurrency import Vision
|
||||
|
||||
final class VisionPipeline: @unchecked Sendable {
|
||||
struct FaceAnalysis: Sendable {
|
||||
let faceDetected: Bool
|
||||
let face: VNFaceObservation?
|
||||
let face: NonSendableFaceObservation?
|
||||
let imageSize: CGSize
|
||||
let debugYaw: Double?
|
||||
let debugPitch: Double?
|
||||
}
|
||||
|
||||
struct NonSendableFaceObservation: @unchecked Sendable {
|
||||
nonisolated(unsafe) let value: VNFaceObservation
|
||||
}
|
||||
|
||||
nonisolated func analyze(
|
||||
pixelBuffer: CVPixelBuffer,
|
||||
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(
|
||||
faceDetected: false,
|
||||
face: nil,
|
||||
@@ -58,7 +62,7 @@ final class VisionPipeline: @unchecked Sendable {
|
||||
|
||||
return FaceAnalysis(
|
||||
faceDetected: true,
|
||||
face: face,
|
||||
face: NonSendableFaceObservation(value: face),
|
||||
imageSize: imageSize,
|
||||
debugYaw: face.yaw?.doubleValue,
|
||||
debugPitch: face.pitch?.doubleValue
|
||||
|
||||
@@ -7,139 +7,45 @@
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
import MacroVisionKit
|
||||
|
||||
final class FullscreenDetectionService: ObservableObject {
|
||||
@Published private(set) var isFullscreenActive = false
|
||||
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var frontmostAppObserver: AnyCancellable?
|
||||
private var fullscreenTask: Task<Void, Never>?
|
||||
private let permissionManager: ScreenCapturePermissionManaging
|
||||
private let environmentProvider: FullscreenEnvironmentProviding
|
||||
private let windowMatcher = FullscreenWindowMatcher()
|
||||
#if canImport(MacroVisionKit)
|
||||
private let monitor = FullScreenMonitor.shared
|
||||
#endif
|
||||
|
||||
init(
|
||||
permissionManager: ScreenCapturePermissionManaging,
|
||||
environmentProvider: FullscreenEnvironmentProviding
|
||||
permissionManager: ScreenCapturePermissionManaging
|
||||
) {
|
||||
self.permissionManager = permissionManager
|
||||
self.environmentProvider = environmentProvider
|
||||
setupObservers()
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
/// Convenience initializer using default services
|
||||
convenience init() {
|
||||
self.init(
|
||||
permissionManager: ScreenCapturePermissionManager.shared,
|
||||
environmentProvider: SystemFullscreenEnvironmentProvider()
|
||||
permissionManager: ScreenCapturePermissionManager.shared
|
||||
)
|
||||
}
|
||||
|
||||
// Factory method to safely create instances from non-main actor contexts
|
||||
static func create(
|
||||
permissionManager: ScreenCapturePermissionManaging? = nil,
|
||||
environmentProvider: FullscreenEnvironmentProviding? = nil
|
||||
permissionManager: ScreenCapturePermissionManaging? = nil
|
||||
) async -> FullscreenDetectionService {
|
||||
await MainActor.run {
|
||||
return FullscreenDetectionService(
|
||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
|
||||
environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
||||
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
let notificationCenter = NSWorkspace.shared.notificationCenter
|
||||
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()
|
||||
fullscreenTask?.cancel()
|
||||
}
|
||||
|
||||
private func canReadWindowInfo() -> Bool {
|
||||
@@ -151,25 +57,17 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
private func checkFullscreenState() {
|
||||
guard canReadWindowInfo() else { return }
|
||||
|
||||
guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else {
|
||||
setFullscreenState(false)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
private func startMonitoring() {
|
||||
fullscreenTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
let stream = await monitor.spaceChanges()
|
||||
for await spaces in stream {
|
||||
guard self.canReadWindowInfo() else { continue }
|
||||
self.setFullscreenState(!spaces.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
setFullscreenState(false)
|
||||
forceUpdate()
|
||||
}
|
||||
|
||||
fileprivate func setFullscreenState(_ isActive: Bool) {
|
||||
@@ -179,7 +77,12 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -188,16 +91,3 @@ final class FullscreenDetectionService: ObservableObject {
|
||||
}
|
||||
#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.
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
struct AdditionalModifiersView: View {
|
||||
@@ -12,6 +13,13 @@ struct AdditionalModifiersView: View {
|
||||
@State private var frontCardIndex: Int = 0
|
||||
@State private var dragOffset: CGFloat = 0
|
||||
@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
|
||||
|
||||
private var backCardOffset: CGFloat { isCompact ? 20 : AdaptiveLayout.Card.backOffset }
|
||||
@@ -50,15 +58,41 @@ struct AdditionalModifiersView: View {
|
||||
|
||||
ZStack {
|
||||
#if DEBUG
|
||||
cardView(for: 0, width: cardWidth, height: cardHeight)
|
||||
.zIndex(zIndex(for: 0))
|
||||
.scaleEffect(scale(for: 0))
|
||||
.offset(x: xOffset(for: 0), y: yOffset(for: 0))
|
||||
setupCard(
|
||||
presentation: .card,
|
||||
content:
|
||||
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
|
||||
cardView(for: 1, width: cardWidth, height: cardHeight)
|
||||
.zIndex(zIndex(for: 1))
|
||||
.scaleEffect(scale(for: 1))
|
||||
.offset(x: xOffset(for: 1), y: yOffset(for: 1))
|
||||
setupCard(
|
||||
presentation: .card,
|
||||
content: SmartModeSetupContent(
|
||||
settingsManager: settingsManager,
|
||||
presentation: .card
|
||||
),
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
index: 1
|
||||
)
|
||||
}
|
||||
.padding(isCompact ? 12 : 20)
|
||||
.gesture(dragGesture)
|
||||
@@ -198,226 +232,28 @@ struct AdditionalModifiersView: View {
|
||||
// MARK: - Card Views
|
||||
|
||||
@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 {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color(NSColor.windowBackgroundColor))
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
||||
|
||||
Group {
|
||||
if index == 0 {
|
||||
enforceModeContent
|
||||
} else {
|
||||
smartModeContent
|
||||
}
|
||||
ScrollView {
|
||||
content
|
||||
.padding(isCompact ? 12 : 20)
|
||||
}
|
||||
.padding(isCompact ? 12 : 20)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
}
|
||||
|
||||
@ObservedObject var cameraService = CameraAccessService.shared
|
||||
|
||||
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))
|
||||
.zIndex(zIndex(for: index))
|
||||
.scaleEffect(scale(for: index))
|
||||
.offset(x: xOffset(for: index), y: yOffset(for: index))
|
||||
}
|
||||
|
||||
// MARK: - Gestures & Navigation
|
||||
|
||||
@@ -76,7 +76,10 @@ final class MenuBarGuideOverlayPresenter {
|
||||
private func startCheckTimer() {
|
||||
checkTimer?.invalidate()
|
||||
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
|
||||
onboardingWindowObserver = onboardingWindow.observe(\.frame, options: [.new, .old]) {
|
||||
[weak self] _, _ in
|
||||
self?.checkWindowFrame()
|
||||
guard let self else { return }
|
||||
Task { @MainActor in
|
||||
self.checkWindowFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Add observer for when the onboarding window is closed
|
||||
@@ -145,7 +151,10 @@ final class MenuBarGuideOverlayPresenter {
|
||||
}
|
||||
|
||||
// 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)
|
||||
case .posture:
|
||||
PostureSetupView(settingsManager: settingsManager)
|
||||
#if ENFORCE_READY
|
||||
#if DEBUG
|
||||
case .enforceMode:
|
||||
EnforceModeSetupView(settingsManager: settingsManager)
|
||||
#endif
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct EnforceModeSetupView: View {
|
||||
@Bindable var settingsManager: SettingsManager
|
||||
@ObservedObject var cameraService = CameraAccessService.shared
|
||||
@ObservedObject var eyeTrackingService = EyeTrackingService.shared
|
||||
@ObservedObject var enforceModeService = EnforceModeService.shared
|
||||
@Environment(\.isCompactLayout) private var isCompact
|
||||
|
||||
@State private var isProcessingToggle = false
|
||||
@State private var isTestModeActive = false
|
||||
@@ -23,7 +20,6 @@ struct EnforceModeSetupView: View {
|
||||
@State private var isViewActive = false
|
||||
@State private var showAdvancedSettings = false
|
||||
@State private var showCalibrationWindow = false
|
||||
@ObservedObject var calibratorService = CalibratorService.shared
|
||||
|
||||
private var cameraHardwareAvailable: Bool {
|
||||
cameraService.hasCameraHardware
|
||||
@@ -33,72 +29,27 @@ struct EnforceModeSetupView: View {
|
||||
VStack(spacing: 0) {
|
||||
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: isCompact ? 16 : 30) {
|
||||
Text("Use your camera to ensure you take breaks")
|
||||
.font(isCompact ? .subheadline : .title3)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(spacing: isCompact ? 12 : 20) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
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 {
|
||||
print("⚠️ Already processing toggle")
|
||||
return
|
||||
}
|
||||
handleEnforceModeToggle(enabled: newValue)
|
||||
}
|
||||
)
|
||||
)
|
||||
.labelsHidden()
|
||||
.disabled(isProcessingToggle || !cameraHardwareAvailable)
|
||||
.controlSize(isCompact ? .small : .regular)
|
||||
EnforceModeSetupContent(
|
||||
settingsManager: settingsManager,
|
||||
presentation: .window,
|
||||
isTestModeActive: $isTestModeActive,
|
||||
cachedPreviewLayer: $cachedPreviewLayer,
|
||||
showAdvancedSettings: $showAdvancedSettings,
|
||||
showCalibrationWindow: $showCalibrationWindow,
|
||||
isViewActive: $isViewActive,
|
||||
isProcessingToggle: isProcessingToggle,
|
||||
handleEnforceModeToggle: { enabled in
|
||||
print("🎛️ Toggle changed to: \(enabled)")
|
||||
guard !isProcessingToggle else {
|
||||
print("⚠️ Already processing toggle")
|
||||
return
|
||||
}
|
||||
.padding(isCompact ? 10 : 16)
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||
|
||||
cameraStatusView
|
||||
|
||||
if enforceModeService.isEnforceModeEnabled {
|
||||
testModeButton
|
||||
}
|
||||
if isTestModeActive && enforceModeService.isCameraActive {
|
||||
testModePreviewView
|
||||
trackingConstantsView
|
||||
} else if enforceModeService.isCameraActive && !isTestModeActive {
|
||||
eyeTrackingStatusView
|
||||
trackingConstantsView
|
||||
}
|
||||
privacyInfoView
|
||||
handleEnforceModeToggle(enabled: enabled)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.top, 20)
|
||||
|
||||
Spacer()
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.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) {
|
||||
print("🎛️ handleEnforceModeToggle called with enabled: \(enabled)")
|
||||
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 {
|
||||
|
||||
@@ -54,7 +54,6 @@ struct LookAwaySetupView: View {
|
||||
|
||||
private func previewLookAway() {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
|
||||
let lookAwayIntervalMinutes = settingsManager.settings.lookAwayIntervalMinutes
|
||||
PreviewWindowHelper.showPreview(on: screen) { dismiss in
|
||||
LookAwayReminderView(countdownSeconds: lookAwayIntervalMinutes * 60, onDismiss: dismiss)
|
||||
|
||||
@@ -9,201 +9,23 @@ import SwiftUI
|
||||
|
||||
struct SmartModeSetupView: View {
|
||||
@Bindable var settingsManager: SettingsManager
|
||||
@State private var permissionManager = ScreenCapturePermissionManager.shared
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SetupHeader(icon: "brain.fill", title: "Smart Mode", color: .purple)
|
||||
|
||||
Text("Automatically manage timers based on your activity")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
SmartModeSetupContent(
|
||||
settingsManager: settingsManager,
|
||||
presentation: .window
|
||||
)
|
||||
.padding(.top, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 24) {
|
||||
fullscreenSection
|
||||
idleSection
|
||||
#if DEBUG
|
||||
usageTrackingSection
|
||||
#endif
|
||||
}
|
||||
.frame(maxWidth: 600)
|
||||
|
||||
Spacer()
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.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 {
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<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>
|
||||
<title>0.4.1</title>
|
||||
<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=="/>
|
||||
</sparkle:deltas>
|
||||
</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>
|
||||
</rss>
|
||||
Reference in New Issue
Block a user