remove DeepWorkTimer Project
This commit is contained in:
parent
a63ed44e49
commit
b5be034b19
@ -1,9 +0,0 @@
|
|||||||
# Project: DeepWork Timer (iOS)
|
|
||||||
- Frameworks: SwiftUI, SwiftData, Swift Charts, ActivityKit
|
|
||||||
- Architecture: MVVM (Model-View-ViewModel)
|
|
||||||
|
|
||||||
# Guidelines
|
|
||||||
- Only use native Apple frameworks. Do not use external APIs, Cocoapods, or third-party libraries.
|
|
||||||
- Never write the entire project at once. Follow the user's specific file/milestone targets.
|
|
||||||
- Explain iOS-specific concepts (e.g., @State, @Environment, MainActor) briefly when introducing them.
|
|
||||||
- Always implement safe SwiftData migrations and error handling.
|
|
||||||
@ -1,526 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
72106C082FDC71B400CDE600 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72106C072FDC71B400CDE600 /* WidgetKit.framework */; };
|
|
||||||
72106C0A2FDC71B400CDE600 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72106C092FDC71B400CDE600 /* SwiftUI.framework */; };
|
|
||||||
72106C172FDC71B600CDE600 /* TimeWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
/* End PBXBuildFile section */
|
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
|
||||||
72106C152FDC71B600CDE600 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 72106BE22FDC6B3000CDE600 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 72106C042FDC71B400CDE600;
|
|
||||||
remoteInfo = TimeWidgetExtension;
|
|
||||||
};
|
|
||||||
/* End PBXContainerItemProxy section */
|
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
|
||||||
72106C1C2FDC71B600CDE600 /* Embed Foundation Extensions */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
dstPath = "";
|
|
||||||
dstSubfolderSpec = 13;
|
|
||||||
files = (
|
|
||||||
72106C172FDC71B600CDE600 /* TimeWidgetExtension.appex in Embed Foundation Extensions */,
|
|
||||||
);
|
|
||||||
name = "Embed Foundation Extensions";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DeepWorkTimer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimeWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
72106C072FDC71B400CDE600 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
|
||||||
72106C092FDC71B400CDE600 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
||||||
72106C182FDC71B600CDE600 /* Exceptions for "TimeWidget" folder in "TimeWidgetExtension" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
Info.plist,
|
|
||||||
);
|
|
||||||
target = 72106C042FDC71B400CDE600 /* TimeWidgetExtension */;
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = DeepWorkTimer;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
72106C0B2FDC71B400CDE600 /* TimeWidget */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
72106C182FDC71B600CDE600 /* Exceptions for "TimeWidget" folder in "TimeWidgetExtension" target */,
|
|
||||||
);
|
|
||||||
path = TimeWidget;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
72106BE72FDC6B3000CDE600 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
72106C022FDC71B400CDE600 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
72106C0A2FDC71B400CDE600 /* SwiftUI.framework in Frameworks */,
|
|
||||||
72106C082FDC71B400CDE600 /* WidgetKit.framework in Frameworks */,
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
72106BE12FDC6B3000CDE600 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */,
|
|
||||||
72106C0B2FDC71B400CDE600 /* TimeWidget */,
|
|
||||||
72106C062FDC71B400CDE600 /* Frameworks */,
|
|
||||||
72106BEB2FDC6B3000CDE600 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
72106BEB2FDC6B3000CDE600 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */,
|
|
||||||
72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
72106C062FDC71B400CDE600 /* Frameworks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
72106C072FDC71B400CDE600 /* WidgetKit.framework */,
|
|
||||||
72106C092FDC71B400CDE600 /* SwiftUI.framework */,
|
|
||||||
);
|
|
||||||
name = Frameworks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
72106BE92FDC6B3000CDE600 /* DeepWorkTimer */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 72106BF52FDC6B3100CDE600 /* Build configuration list for PBXNativeTarget "DeepWorkTimer" */;
|
|
||||||
buildPhases = (
|
|
||||||
72106BE62FDC6B3000CDE600 /* Sources */,
|
|
||||||
72106BE72FDC6B3000CDE600 /* Frameworks */,
|
|
||||||
72106BE82FDC6B3000CDE600 /* Resources */,
|
|
||||||
72106C1C2FDC71B600CDE600 /* Embed Foundation Extensions */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
72106C162FDC71B600CDE600 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */,
|
|
||||||
);
|
|
||||||
name = DeepWorkTimer;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = DeepWorkTimer;
|
|
||||||
productReference = 72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
72106C042FDC71B400CDE600 /* TimeWidgetExtension */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 72106C192FDC71B600CDE600 /* Build configuration list for PBXNativeTarget "TimeWidgetExtension" */;
|
|
||||||
buildPhases = (
|
|
||||||
72106C012FDC71B400CDE600 /* Sources */,
|
|
||||||
72106C022FDC71B400CDE600 /* Frameworks */,
|
|
||||||
72106C032FDC71B400CDE600 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
72106C0B2FDC71B400CDE600 /* TimeWidget */,
|
|
||||||
);
|
|
||||||
name = TimeWidgetExtension;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = TimeWidgetExtension;
|
|
||||||
productReference = 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */;
|
|
||||||
productType = "com.apple.product-type.app-extension";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
72106BE22FDC6B3000CDE600 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 2650;
|
|
||||||
LastUpgradeCheck = 2650;
|
|
||||||
TargetAttributes = {
|
|
||||||
72106BE92FDC6B3000CDE600 = {
|
|
||||||
CreatedOnToolsVersion = 26.5;
|
|
||||||
};
|
|
||||||
72106C042FDC71B400CDE600 = {
|
|
||||||
CreatedOnToolsVersion = 26.5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = 72106BE52FDC6B3000CDE600 /* Build configuration list for PBXProject "DeepWorkTimer" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = 72106BE12FDC6B3000CDE600;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 72106BEB2FDC6B3000CDE600 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
72106BE92FDC6B3000CDE600 /* DeepWorkTimer */,
|
|
||||||
72106C042FDC71B400CDE600 /* TimeWidgetExtension */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
72106BE82FDC6B3000CDE600 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
72106C032FDC71B400CDE600 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
72106BE62FDC6B3000CDE600 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
72106C012FDC71B400CDE600 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
|
||||||
72106C162FDC71B600CDE600 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 72106C042FDC71B400CDE600 /* TimeWidgetExtension */;
|
|
||||||
targetProxy = 72106C152FDC71B600CDE600 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
72106BF32FDC6B3100CDE600 /* 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;
|
|
||||||
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;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
72106BF42FDC6B3100CDE600 /* 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";
|
|
||||||
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;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
72106BF62FDC6B3100CDE600 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = "DeepWorkTimer/DeepWorkTimer.entitlements";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
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;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
72106BF72FDC6B3100CDE600 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = "DeepWorkTimer/DeepWorkTimer.entitlements";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
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;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
72106C1A2FDC71B600CDE600 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_FILE = TimeWidget/Info.plist;
|
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = TimeWidget;
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer.TimeWidget;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
72106C1B2FDC71B600CDE600 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_FILE = TimeWidget/Info.plist;
|
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = TimeWidget;
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer.TimeWidget;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
72106BE52FDC6B3000CDE600 /* Build configuration list for PBXProject "DeepWorkTimer" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
72106BF32FDC6B3100CDE600 /* Debug */,
|
|
||||||
72106BF42FDC6B3100CDE600 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
72106BF52FDC6B3100CDE600 /* Build configuration list for PBXNativeTarget "DeepWorkTimer" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
72106BF62FDC6B3100CDE600 /* Debug */,
|
|
||||||
72106BF72FDC6B3100CDE600 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
72106C192FDC71B600CDE600 /* Build configuration list for PBXNativeTarget "TimeWidgetExtension" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
72106C1A2FDC71B600CDE600 /* Debug */,
|
|
||||||
72106C1B2FDC71B600CDE600 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = 72106BE22FDC6B3000CDE600 /* Project object */;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
Binary file not shown.
@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>DeepWorkTimer.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
<key>TimeWidgetExtension.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// DeepWorkTimer
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
// @State owns the ViewModel; @Observable tracks only the properties this view reads.
|
|
||||||
@State private var viewModel = TimerViewModel()
|
|
||||||
@Query(sort: \FocusSession.startTime, order: .reverse) private var sessions: [FocusSession]
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
// scenePhase tells us when the app moves to/from the background.
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
TabView {
|
|
||||||
timerTab
|
|
||||||
.tabItem {
|
|
||||||
Label("Timer", systemImage: "timer")
|
|
||||||
}
|
|
||||||
|
|
||||||
DashboardView()
|
|
||||||
.tabItem {
|
|
||||||
Label("Dashboard", systemImage: "chart.bar.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: scenePhase) { _, newPhase in
|
|
||||||
switch newPhase {
|
|
||||||
case .background:
|
|
||||||
viewModel.appWentBackground()
|
|
||||||
case .active:
|
|
||||||
viewModel.appReturnedForeground()
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var timerTab: some View {
|
|
||||||
NavigationStack {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 40) {
|
|
||||||
timerRing
|
|
||||||
controlButtons
|
|
||||||
if !sessions.isEmpty {
|
|
||||||
sessionList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 24)
|
|
||||||
}
|
|
||||||
.navigationTitle("Deep Work Timer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Timer Ring
|
|
||||||
|
|
||||||
private var timerRing: some View {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.stroke(Color(.systemGray5), lineWidth: 18)
|
|
||||||
Circle()
|
|
||||||
.trim(from: 0, to: viewModel.progress)
|
|
||||||
.stroke(
|
|
||||||
Color.accentColor,
|
|
||||||
style: StrokeStyle(lineWidth: 18, lineCap: .round)
|
|
||||||
)
|
|
||||||
.rotationEffect(.degrees(-90))
|
|
||||||
.animation(.linear(duration: 1), value: viewModel.progress)
|
|
||||||
VStack(spacing: 6) {
|
|
||||||
Text(formattedTime)
|
|
||||||
.font(.system(size: 52, weight: .thin, design: .monospaced))
|
|
||||||
Text(statusLabel)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.textCase(.uppercase)
|
|
||||||
.tracking(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 270, height: 270)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var formattedTime: String {
|
|
||||||
String(format: "%02d:%02d", viewModel.timeRemaining / 60, viewModel.timeRemaining % 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statusLabel: String {
|
|
||||||
if viewModel.isRunning { return "Focus" }
|
|
||||||
if viewModel.sessionStartTime != nil { return "Paused" }
|
|
||||||
return "Ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Control Buttons
|
|
||||||
|
|
||||||
private var controlButtons: some View {
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
Button {
|
|
||||||
viewModel.isRunning ? viewModel.pause() : viewModel.start()
|
|
||||||
} label: {
|
|
||||||
Label(
|
|
||||||
viewModel.isRunning ? "Pause" : "Start",
|
|
||||||
systemImage: viewModel.isRunning ? "pause.fill" : "play.fill"
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.large)
|
|
||||||
|
|
||||||
Button(role: .destructive) {
|
|
||||||
viewModel.stop(context: modelContext)
|
|
||||||
} label: {
|
|
||||||
Label("Stop", systemImage: "stop.fill")
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.large)
|
|
||||||
.disabled(viewModel.sessionStartTime == nil)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Session List
|
|
||||||
|
|
||||||
private var sessionList: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Text("Completed Sessions")
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
|
|
||||||
ForEach(sessions) { session in
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(session.category)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
Text(session.startTime.formatted(date: .abbreviated, time: .shortened))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Text("\(session.durationInMinutes) min")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color(.secondarySystemBackground))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
.modelContainer(for: FocusSession.self, inMemory: true)
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.developer.live-activity</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
//
|
|
||||||
// DeepWorkTimerApp.swift
|
|
||||||
// DeepWorkTimer
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct DeepWorkTimerApp: App {
|
|
||||||
// modelContainer registers FocusSession with SwiftData and injects
|
|
||||||
// the store into the SwiftUI environment for all child views.
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
.modelContainer(for: FocusSession.self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
//
|
|
||||||
// FocusSession.swift
|
|
||||||
// DeepWorkTimer
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
// @Model turns this plain Swift class into a SwiftData-managed, persistable entity.
|
|
||||||
@Model
|
|
||||||
final class FocusSession {
|
|
||||||
var id: UUID
|
|
||||||
var startTime: Date
|
|
||||||
var durationInMinutes: Int
|
|
||||||
var category: String
|
|
||||||
|
|
||||||
init(id: UUID = UUID(), startTime: Date = .now, durationInMinutes: Int, category: String) {
|
|
||||||
self.id = id
|
|
||||||
self.startTime = startTime
|
|
||||||
self.durationInMinutes = durationInMinutes
|
|
||||||
self.category = category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// Shared ActivityAttributes definition.
|
|
||||||
// NOTE: This file must belong to BOTH the DeepWorkTimer and TimeWidgetExtension targets.
|
|
||||||
// Because this project uses Xcode 16 file system synchronized groups, a physical copy
|
|
||||||
// lives in DeepWorkTimer/Models/ (for the app) and in TimeWidget/ (for the extension).
|
|
||||||
// Keep both files identical.
|
|
||||||
|
|
||||||
import ActivityKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct TimerAttributes: ActivityAttributes {
|
|
||||||
// ContentState holds data the system can update while the activity is live.
|
|
||||||
struct ContentState: Codable, Hashable {
|
|
||||||
// The exact moment the timer expires — drives Text(timerInterval:) real-time countdown.
|
|
||||||
var endDate: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fixed for the lifetime of this activity.
|
|
||||||
var sessionName: String
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
//
|
|
||||||
// TimerViewModel.swift
|
|
||||||
// DeepWorkTimer
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
import SwiftData
|
|
||||||
import ActivityKit
|
|
||||||
|
|
||||||
// @Observable replaces ObservableObject + @Published.
|
|
||||||
// SwiftUI views automatically track only the specific properties they read.
|
|
||||||
@Observable
|
|
||||||
final class TimerViewModel {
|
|
||||||
private(set) var timeRemaining: Int
|
|
||||||
private(set) var isRunning = false
|
|
||||||
private(set) var sessionStartTime: Date?
|
|
||||||
|
|
||||||
let totalDuration: Int
|
|
||||||
|
|
||||||
private var timerTask: Task<Void, Never>?
|
|
||||||
// Stores the moment the app moved to the background so elapsed time
|
|
||||||
// can be calculated accurately when the app returns.
|
|
||||||
private var backgroundEntryDate: Date?
|
|
||||||
private var currentActivity: Activity<TimerAttributes>?
|
|
||||||
|
|
||||||
var progress: Double {
|
|
||||||
totalDuration > 0 ? Double(timeRemaining) / Double(totalDuration) : 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
init(duration: Int = 25 * 60) {
|
|
||||||
totalDuration = duration
|
|
||||||
timeRemaining = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
guard !isRunning else { return }
|
|
||||||
if sessionStartTime == nil {
|
|
||||||
sessionStartTime = .now
|
|
||||||
}
|
|
||||||
isRunning = true
|
|
||||||
scheduleTick()
|
|
||||||
startLiveActivity()
|
|
||||||
}
|
|
||||||
|
|
||||||
func pause() {
|
|
||||||
isRunning = false
|
|
||||||
cancelTick()
|
|
||||||
endLiveActivity()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop(context: ModelContext) {
|
|
||||||
let start = sessionStartTime ?? .now
|
|
||||||
let elapsed = totalDuration - timeRemaining
|
|
||||||
let minutes = max(1, elapsed / 60)
|
|
||||||
context.insert(FocusSession(startTime: start, durationInMinutes: minutes, category: "Deep Work"))
|
|
||||||
endLiveActivity()
|
|
||||||
reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call when scenePhase → .background
|
|
||||||
func appWentBackground() {
|
|
||||||
guard isRunning else { return }
|
|
||||||
backgroundEntryDate = .now
|
|
||||||
cancelTick()
|
|
||||||
// isRunning stays true so appReturnedForeground knows to resume.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call when scenePhase → .active
|
|
||||||
func appReturnedForeground() {
|
|
||||||
guard isRunning, let entry = backgroundEntryDate else { return }
|
|
||||||
let elapsed = Int(Date.now.timeIntervalSince(entry))
|
|
||||||
timeRemaining = max(0, timeRemaining - elapsed)
|
|
||||||
backgroundEntryDate = nil
|
|
||||||
if timeRemaining > 0 {
|
|
||||||
scheduleTick()
|
|
||||||
} else {
|
|
||||||
isRunning = false
|
|
||||||
endLiveActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startLiveActivity() {
|
|
||||||
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
||||||
let endDate = Date.now.addingTimeInterval(TimeInterval(timeRemaining))
|
|
||||||
let state = TimerAttributes.ContentState(endDate: endDate)
|
|
||||||
let content = ActivityContent(state: state, staleDate: endDate)
|
|
||||||
let attributes = TimerAttributes(sessionName: "Deep Work")
|
|
||||||
do {
|
|
||||||
currentActivity = try Activity.request(attributes: attributes, content: content)
|
|
||||||
} catch {
|
|
||||||
// Live Activities may fail if the device doesn't support them or the limit is reached.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func endLiveActivity() {
|
|
||||||
guard let activity = currentActivity else { return }
|
|
||||||
currentActivity = nil
|
|
||||||
Task {
|
|
||||||
let finalState = TimerAttributes.ContentState(endDate: .now)
|
|
||||||
let content = ActivityContent(state: finalState, staleDate: nil)
|
|
||||||
await activity.end(content, dismissalPolicy: .immediate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleTick() {
|
|
||||||
timerTask = Task {
|
|
||||||
while !Task.isCancelled && timeRemaining > 0 {
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
guard !Task.isCancelled else { break }
|
|
||||||
timeRemaining = max(0, timeRemaining - 1)
|
|
||||||
}
|
|
||||||
if timeRemaining == 0 {
|
|
||||||
isRunning = false
|
|
||||||
endLiveActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cancelTick() {
|
|
||||||
timerTask?.cancel()
|
|
||||||
timerTask = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reset() {
|
|
||||||
cancelTick()
|
|
||||||
isRunning = false
|
|
||||||
sessionStartTime = nil
|
|
||||||
timeRemaining = totalDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
//
|
|
||||||
// DashboardView.swift
|
|
||||||
// DeepWorkTimer
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
import Charts
|
|
||||||
|
|
||||||
// Lightweight struct to hold one day's aggregated focus data for the chart.
|
|
||||||
struct DayFocusData: Identifiable {
|
|
||||||
var id: Date { date }
|
|
||||||
let date: Date
|
|
||||||
let minutes: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DashboardView: View {
|
|
||||||
// @Query fetches every persisted FocusSession from SwiftData automatically.
|
|
||||||
@Query(sort: \FocusSession.startTime, order: .forward) private var sessions: [FocusSession]
|
|
||||||
|
|
||||||
// MARK: - Computed stats
|
|
||||||
|
|
||||||
private var last7Days: [DayFocusData] {
|
|
||||||
let calendar = Calendar.current
|
|
||||||
let today = calendar.startOfDay(for: .now)
|
|
||||||
|
|
||||||
var totals: [Date: Int] = [:]
|
|
||||||
for session in sessions {
|
|
||||||
let day = calendar.startOfDay(for: session.startTime)
|
|
||||||
totals[day, default: 0] += session.durationInMinutes
|
|
||||||
}
|
|
||||||
|
|
||||||
return (0..<7).reversed().compactMap { offset -> DayFocusData? in
|
|
||||||
guard let day = calendar.date(byAdding: .day, value: -offset, to: today) else { return nil }
|
|
||||||
return DayFocusData(date: day, minutes: totals[day, default: 0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var totalFocusMinutes: Int {
|
|
||||||
sessions.reduce(0) { $0 + $1.durationInMinutes }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var averageDailyMinutes: Int {
|
|
||||||
let activeDays = last7Days.filter { $0.minutes > 0 }
|
|
||||||
guard !activeDays.isEmpty else { return 0 }
|
|
||||||
return activeDays.reduce(0) { $0 + $1.minutes } / activeDays.count
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Body
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
|
||||||
statsCards
|
|
||||||
chartSection
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
}
|
|
||||||
.navigationTitle("Dashboard")
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Stats Cards
|
|
||||||
|
|
||||||
private var statsCards: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
statCard(
|
|
||||||
title: "Total Focus Time",
|
|
||||||
value: formatted(totalFocusMinutes),
|
|
||||||
systemImage: "clock.fill",
|
|
||||||
tint: .blue
|
|
||||||
)
|
|
||||||
statCard(
|
|
||||||
title: "Avg Daily Focus",
|
|
||||||
value: formatted(averageDailyMinutes),
|
|
||||||
systemImage: "chart.line.uptrend.xyaxis",
|
|
||||||
tint: .purple
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func statCard(title: String, value: String, systemImage: String, tint: Color) -> some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: systemImage)
|
|
||||||
.foregroundStyle(tint)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Text(value)
|
|
||||||
.font(.system(size: 32, weight: .bold, design: .rounded))
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
.minimumScaleFactor(0.7)
|
|
||||||
.lineLimit(1)
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.background(Color(.secondarySystemBackground))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Chart Section
|
|
||||||
|
|
||||||
private var chartSection: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
|
||||||
Text("Last 7 Days")
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
|
|
||||||
Chart(last7Days) { day in
|
|
||||||
BarMark(
|
|
||||||
x: .value("Day", day.date, unit: .day),
|
|
||||||
y: .value("Minutes", day.minutes)
|
|
||||||
)
|
|
||||||
.foregroundStyle(
|
|
||||||
LinearGradient(
|
|
||||||
colors: [Color.blue.opacity(0.5), Color.purple],
|
|
||||||
startPoint: .bottom,
|
|
||||||
endPoint: .top
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
|
||||||
.chartXAxis {
|
|
||||||
AxisMarks(values: .stride(by: .day)) { _ in
|
|
||||||
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.chartYAxis {
|
|
||||||
AxisMarks(position: .leading) { value in
|
|
||||||
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4]))
|
|
||||||
.foregroundStyle(Color(.systemGray4))
|
|
||||||
AxisValueLabel {
|
|
||||||
if let minutes = value.as(Int.self) {
|
|
||||||
Text("\(minutes)m")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 220)
|
|
||||||
.padding(16)
|
|
||||||
.background(Color(.secondarySystemBackground))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
private func formatted(_ totalMinutes: Int) -> String {
|
|
||||||
let hours = totalMinutes / 60
|
|
||||||
let mins = totalMinutes % 60
|
|
||||||
if hours > 0 { return "\(hours)h \(mins)m" }
|
|
||||||
return "\(mins)m"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
DashboardView()
|
|
||||||
.modelContainer(for: FocusSession.self, inMemory: true)
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSExtension</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExtensionPointIdentifier</key>
|
|
||||||
<string>com.apple.widgetkit-extension</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
//
|
|
||||||
// TimeWidget.swift
|
|
||||||
// TimeWidget
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct Provider: TimelineProvider {
|
|
||||||
func placeholder(in context: Context) -> SimpleEntry {
|
|
||||||
SimpleEntry(date: Date(), emoji: "😀")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
|
||||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
|
||||||
completion(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
||||||
var entries: [SimpleEntry] = []
|
|
||||||
|
|
||||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
|
||||||
let currentDate = Date()
|
|
||||||
for hourOffset in 0 ..< 5 {
|
|
||||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
|
||||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
|
||||||
completion(timeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func relevances() async -> WidgetRelevances<Void> {
|
|
||||||
// // Generate a list containing the contexts this widget is relevant in.
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SimpleEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
let emoji: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimeWidgetEntryView : View {
|
|
||||||
var entry: Provider.Entry
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("Time:")
|
|
||||||
Text(entry.date, style: .time)
|
|
||||||
|
|
||||||
Text("Emoji:")
|
|
||||||
Text(entry.emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TimeWidget: Widget {
|
|
||||||
let kind: String = "TimeWidget"
|
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
|
||||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
TimeWidgetEntryView(entry: entry)
|
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
|
||||||
} else {
|
|
||||||
TimeWidgetEntryView(entry: entry)
|
|
||||||
.padding()
|
|
||||||
.background()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.configurationDisplayName("My Widget")
|
|
||||||
.description("This is an example widget.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview(as: .systemSmall) {
|
|
||||||
TimeWidget()
|
|
||||||
} timeline: {
|
|
||||||
SimpleEntry(date: .now, emoji: "😀")
|
|
||||||
SimpleEntry(date: .now, emoji: "🤩")
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// TimeWidgetBundle.swift
|
|
||||||
// TimeWidget
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct TimeWidgetBundle: WidgetBundle {
|
|
||||||
var body: some Widget {
|
|
||||||
TimeWidget()
|
|
||||||
TimeWidgetLiveActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
//
|
|
||||||
// TimeWidgetLiveActivity.swift
|
|
||||||
// TimeWidget
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/13/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import ActivityKit
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct TimeWidgetLiveActivity: Widget {
|
|
||||||
var body: some WidgetConfiguration {
|
|
||||||
ActivityConfiguration(for: TimerAttributes.self) { context in
|
|
||||||
// Lock Screen / Notification Banner
|
|
||||||
// Text(timerInterval:) is a system primitive that counts down in real time
|
|
||||||
// without waking the app — no Timer or polling needed.
|
|
||||||
HStack(spacing: 14) {
|
|
||||||
Image(systemName: "timer")
|
|
||||||
.font(.title2.weight(.semibold))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(context.attributes.sessionName)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.white.opacity(0.75))
|
|
||||||
|
|
||||||
let safeEnd = max(Date.now, context.state.endDate)
|
|
||||||
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
||||||
.font(.title.monospacedDigit().bold())
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
.activityBackgroundTint(Color(red: 0.07, green: 0.07, blue: 0.18))
|
|
||||||
.activitySystemActionForegroundColor(.white)
|
|
||||||
|
|
||||||
} dynamicIsland: { context in
|
|
||||||
DynamicIsland {
|
|
||||||
// Expanded — shown when the user long-presses the Dynamic Island.
|
|
||||||
DynamicIslandExpandedRegion(.leading) {
|
|
||||||
Label(context.attributes.sessionName, systemImage: "timer")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
DynamicIslandExpandedRegion(.trailing) {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
DynamicIslandExpandedRegion(.center) {
|
|
||||||
let safeEnd = max(Date.now, context.state.endDate)
|
|
||||||
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
||||||
.font(.title2.monospacedDigit().bold())
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
}
|
|
||||||
DynamicIslandExpandedRegion(.bottom) {
|
|
||||||
Text("Stay focused")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
}
|
|
||||||
} compactLeading: {
|
|
||||||
Image(systemName: "timer")
|
|
||||||
.foregroundStyle(.indigo)
|
|
||||||
} compactTrailing: {
|
|
||||||
let safeEnd = max(Date.now, context.state.endDate)
|
|
||||||
Text(timerInterval: Date.now...safeEnd, countsDown: true)
|
|
||||||
.font(.caption.monospacedDigit().bold())
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
.frame(width: 52)
|
|
||||||
} minimal: {
|
|
||||||
Image(systemName: "timer")
|
|
||||||
.foregroundStyle(.indigo)
|
|
||||||
}
|
|
||||||
.keylineTint(.indigo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Previews
|
|
||||||
|
|
||||||
extension TimerAttributes {
|
|
||||||
fileprivate static var preview: TimerAttributes {
|
|
||||||
TimerAttributes(sessionName: "Deep Work")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimerAttributes.ContentState {
|
|
||||||
fileprivate static var active: TimerAttributes.ContentState {
|
|
||||||
TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(22 * 60))
|
|
||||||
}
|
|
||||||
fileprivate static var nearEnd: TimerAttributes.ContentState {
|
|
||||||
TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(2 * 60))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("Notification", as: .content, using: TimerAttributes.preview) {
|
|
||||||
TimeWidgetLiveActivity()
|
|
||||||
} contentStates: {
|
|
||||||
TimerAttributes.ContentState.active
|
|
||||||
TimerAttributes.ContentState.nearEnd
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// Shared ActivityAttributes definition.
|
|
||||||
// NOTE: This file must belong to BOTH the DeepWorkTimer and TimeWidgetExtension targets.
|
|
||||||
// Because this project uses Xcode 16 file system synchronized groups, a physical copy
|
|
||||||
// lives in DeepWorkTimer/Models/ (for the app) and in TimeWidget/ (for the extension).
|
|
||||||
// Keep both files identical.
|
|
||||||
|
|
||||||
import ActivityKit
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct TimerAttributes: ActivityAttributes {
|
|
||||||
// ContentState holds data the system can update while the activity is live.
|
|
||||||
struct ContentState: Codable, Hashable {
|
|
||||||
// The exact moment the timer expires — drives Text(timerInterval:) real-time countdown.
|
|
||||||
var endDate: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fixed for the lifetime of this activity.
|
|
||||||
var sessionName: String
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user