ios widget showing what is available at chucks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: init

+1932
.DS_Store

This is a binary file and will not be displayed.

+534
wasup-chucks.xcodeproj/project.pbxproj
··· 1 + // !$*UTF8*$! 2 + { 3 + archiveVersion = 1; 4 + classes = { 5 + }; 6 + objectVersion = 77; 7 + objects = { 8 + 9 + /* Begin PBXBuildFile section */ 10 + 0BBF20382F2D46AF00AF0585 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BBF20372F2D46AF00AF0585 /* WidgetKit.framework */; }; 11 + 0BBF203A2F2D46AF00AF0585 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BBF20392F2D46AF00AF0585 /* SwiftUI.framework */; }; 12 + 0BBF204B2F2D46B200AF0585 /* widgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0BBF20352F2D46AF00AF0585 /* widgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 13 + /* End PBXBuildFile section */ 14 + 15 + /* Begin PBXContainerItemProxy section */ 16 + 0BBF20492F2D46B200AF0585 /* PBXContainerItemProxy */ = { 17 + isa = PBXContainerItemProxy; 18 + containerPortal = 0BBF20152F2D443D00AF0585 /* Project object */; 19 + proxyType = 1; 20 + remoteGlobalIDString = 0BBF20342F2D46AF00AF0585; 21 + remoteInfo = widgetExtension; 22 + }; 23 + /* End PBXContainerItemProxy section */ 24 + 25 + /* Begin PBXCopyFilesBuildPhase section */ 26 + 0BBF20502F2D46B200AF0585 /* Embed Foundation Extensions */ = { 27 + isa = PBXCopyFilesBuildPhase; 28 + buildActionMask = 2147483647; 29 + dstPath = ""; 30 + dstSubfolderSpec = 13; 31 + files = ( 32 + 0BBF204B2F2D46B200AF0585 /* widgetExtension.appex in Embed Foundation Extensions */, 33 + ); 34 + name = "Embed Foundation Extensions"; 35 + runOnlyForDeploymentPostprocessing = 0; 36 + }; 37 + /* End PBXCopyFilesBuildPhase section */ 38 + 39 + /* Begin PBXFileReference section */ 40 + 0BBF201D2F2D443D00AF0585 /* wasup-chucks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "wasup-chucks.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 41 + 0BBF20352F2D46AF00AF0585 /* widgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = widgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 42 + 0BBF20372F2D46AF00AF0585 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 43 + 0BBF20392F2D46AF00AF0585 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 44 + /* End PBXFileReference section */ 45 + 46 + /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 47 + 0BBF204C2F2D46B200AF0585 /* Exceptions for "widget" folder in "widgetExtension" target */ = { 48 + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 49 + membershipExceptions = ( 50 + Info.plist, 51 + ); 52 + target = 0BBF20342F2D46AF00AF0585 /* widgetExtension */; 53 + }; 54 + /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 55 + 56 + /* Begin PBXFileSystemSynchronizedRootGroup section */ 57 + 0BBF201F2F2D443D00AF0585 /* wasup-chucks */ = { 58 + isa = PBXFileSystemSynchronizedRootGroup; 59 + path = "wasup-chucks"; 60 + sourceTree = "<group>"; 61 + }; 62 + 0BBF203B2F2D46AF00AF0585 /* widget */ = { 63 + isa = PBXFileSystemSynchronizedRootGroup; 64 + exceptions = ( 65 + 0BBF204C2F2D46B200AF0585 /* Exceptions for "widget" folder in "widgetExtension" target */, 66 + ); 67 + path = widget; 68 + sourceTree = "<group>"; 69 + }; 70 + /* End PBXFileSystemSynchronizedRootGroup section */ 71 + 72 + /* Begin PBXFrameworksBuildPhase section */ 73 + 0BBF201A2F2D443D00AF0585 /* Frameworks */ = { 74 + isa = PBXFrameworksBuildPhase; 75 + buildActionMask = 2147483647; 76 + files = ( 77 + ); 78 + runOnlyForDeploymentPostprocessing = 0; 79 + }; 80 + 0BBF20322F2D46AF00AF0585 /* Frameworks */ = { 81 + isa = PBXFrameworksBuildPhase; 82 + buildActionMask = 2147483647; 83 + files = ( 84 + 0BBF203A2F2D46AF00AF0585 /* SwiftUI.framework in Frameworks */, 85 + 0BBF20382F2D46AF00AF0585 /* WidgetKit.framework in Frameworks */, 86 + ); 87 + runOnlyForDeploymentPostprocessing = 0; 88 + }; 89 + /* End PBXFrameworksBuildPhase section */ 90 + 91 + /* Begin PBXGroup section */ 92 + 0BBF20142F2D443D00AF0585 = { 93 + isa = PBXGroup; 94 + children = ( 95 + 0BBF201F2F2D443D00AF0585 /* wasup-chucks */, 96 + 0BBF203B2F2D46AF00AF0585 /* widget */, 97 + 0BBF20362F2D46AF00AF0585 /* Frameworks */, 98 + 0BBF201E2F2D443D00AF0585 /* Products */, 99 + ); 100 + sourceTree = "<group>"; 101 + }; 102 + 0BBF201E2F2D443D00AF0585 /* Products */ = { 103 + isa = PBXGroup; 104 + children = ( 105 + 0BBF201D2F2D443D00AF0585 /* wasup-chucks.app */, 106 + 0BBF20352F2D46AF00AF0585 /* widgetExtension.appex */, 107 + ); 108 + name = Products; 109 + sourceTree = "<group>"; 110 + }; 111 + 0BBF20362F2D46AF00AF0585 /* Frameworks */ = { 112 + isa = PBXGroup; 113 + children = ( 114 + 0BBF20372F2D46AF00AF0585 /* WidgetKit.framework */, 115 + 0BBF20392F2D46AF00AF0585 /* SwiftUI.framework */, 116 + ); 117 + name = Frameworks; 118 + sourceTree = "<group>"; 119 + }; 120 + /* End PBXGroup section */ 121 + 122 + /* Begin PBXNativeTarget section */ 123 + 0BBF201C2F2D443D00AF0585 /* wasup-chucks */ = { 124 + isa = PBXNativeTarget; 125 + buildConfigurationList = 0BBF20282F2D443F00AF0585 /* Build configuration list for PBXNativeTarget "wasup-chucks" */; 126 + buildPhases = ( 127 + 0BBF20192F2D443D00AF0585 /* Sources */, 128 + 0BBF201A2F2D443D00AF0585 /* Frameworks */, 129 + 0BBF201B2F2D443D00AF0585 /* Resources */, 130 + 0BBF20502F2D46B200AF0585 /* Embed Foundation Extensions */, 131 + ); 132 + buildRules = ( 133 + ); 134 + dependencies = ( 135 + 0BBF204A2F2D46B200AF0585 /* PBXTargetDependency */, 136 + ); 137 + fileSystemSynchronizedGroups = ( 138 + 0BBF201F2F2D443D00AF0585 /* wasup-chucks */, 139 + ); 140 + name = "wasup-chucks"; 141 + packageProductDependencies = ( 142 + ); 143 + productName = "wasup-chucks"; 144 + productReference = 0BBF201D2F2D443D00AF0585 /* wasup-chucks.app */; 145 + productType = "com.apple.product-type.application"; 146 + }; 147 + 0BBF20342F2D46AF00AF0585 /* widgetExtension */ = { 148 + isa = PBXNativeTarget; 149 + buildConfigurationList = 0BBF204D2F2D46B200AF0585 /* Build configuration list for PBXNativeTarget "widgetExtension" */; 150 + buildPhases = ( 151 + 0BBF20312F2D46AF00AF0585 /* Sources */, 152 + 0BBF20322F2D46AF00AF0585 /* Frameworks */, 153 + 0BBF20332F2D46AF00AF0585 /* Resources */, 154 + ); 155 + buildRules = ( 156 + ); 157 + dependencies = ( 158 + ); 159 + fileSystemSynchronizedGroups = ( 160 + 0BBF203B2F2D46AF00AF0585 /* widget */, 161 + ); 162 + name = widgetExtension; 163 + packageProductDependencies = ( 164 + ); 165 + productName = widgetExtension; 166 + productReference = 0BBF20352F2D46AF00AF0585 /* widgetExtension.appex */; 167 + productType = "com.apple.product-type.app-extension"; 168 + }; 169 + /* End PBXNativeTarget section */ 170 + 171 + /* Begin PBXProject section */ 172 + 0BBF20152F2D443D00AF0585 /* Project object */ = { 173 + isa = PBXProject; 174 + attributes = { 175 + BuildIndependentTargetsInParallel = 1; 176 + LastSwiftUpdateCheck = 2620; 177 + LastUpgradeCheck = 2620; 178 + TargetAttributes = { 179 + 0BBF201C2F2D443D00AF0585 = { 180 + CreatedOnToolsVersion = 26.2; 181 + }; 182 + 0BBF20342F2D46AF00AF0585 = { 183 + CreatedOnToolsVersion = 26.2; 184 + }; 185 + }; 186 + }; 187 + buildConfigurationList = 0BBF20182F2D443D00AF0585 /* Build configuration list for PBXProject "wasup-chucks" */; 188 + developmentRegion = en; 189 + hasScannedForEncodings = 0; 190 + knownRegions = ( 191 + en, 192 + Base, 193 + ); 194 + mainGroup = 0BBF20142F2D443D00AF0585; 195 + minimizedProjectReferenceProxies = 1; 196 + preferredProjectObjectVersion = 77; 197 + productRefGroup = 0BBF201E2F2D443D00AF0585 /* Products */; 198 + projectDirPath = ""; 199 + projectRoot = ""; 200 + targets = ( 201 + 0BBF201C2F2D443D00AF0585 /* wasup-chucks */, 202 + 0BBF20342F2D46AF00AF0585 /* widgetExtension */, 203 + ); 204 + }; 205 + /* End PBXProject section */ 206 + 207 + /* Begin PBXResourcesBuildPhase section */ 208 + 0BBF201B2F2D443D00AF0585 /* Resources */ = { 209 + isa = PBXResourcesBuildPhase; 210 + buildActionMask = 2147483647; 211 + files = ( 212 + ); 213 + runOnlyForDeploymentPostprocessing = 0; 214 + }; 215 + 0BBF20332F2D46AF00AF0585 /* Resources */ = { 216 + isa = PBXResourcesBuildPhase; 217 + buildActionMask = 2147483647; 218 + files = ( 219 + ); 220 + runOnlyForDeploymentPostprocessing = 0; 221 + }; 222 + /* End PBXResourcesBuildPhase section */ 223 + 224 + /* Begin PBXSourcesBuildPhase section */ 225 + 0BBF20192F2D443D00AF0585 /* Sources */ = { 226 + isa = PBXSourcesBuildPhase; 227 + buildActionMask = 2147483647; 228 + files = ( 229 + ); 230 + runOnlyForDeploymentPostprocessing = 0; 231 + }; 232 + 0BBF20312F2D46AF00AF0585 /* Sources */ = { 233 + isa = PBXSourcesBuildPhase; 234 + buildActionMask = 2147483647; 235 + files = ( 236 + ); 237 + runOnlyForDeploymentPostprocessing = 0; 238 + }; 239 + /* End PBXSourcesBuildPhase section */ 240 + 241 + /* Begin PBXTargetDependency section */ 242 + 0BBF204A2F2D46B200AF0585 /* PBXTargetDependency */ = { 243 + isa = PBXTargetDependency; 244 + target = 0BBF20342F2D46AF00AF0585 /* widgetExtension */; 245 + targetProxy = 0BBF20492F2D46B200AF0585 /* PBXContainerItemProxy */; 246 + }; 247 + /* End PBXTargetDependency section */ 248 + 249 + /* Begin XCBuildConfiguration section */ 250 + 0BBF20262F2D443F00AF0585 /* Debug */ = { 251 + isa = XCBuildConfiguration; 252 + buildSettings = { 253 + ALWAYS_SEARCH_USER_PATHS = NO; 254 + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 255 + CLANG_ANALYZER_NONNULL = YES; 256 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 257 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 258 + CLANG_ENABLE_MODULES = YES; 259 + CLANG_ENABLE_OBJC_ARC = YES; 260 + CLANG_ENABLE_OBJC_WEAK = YES; 261 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 262 + CLANG_WARN_BOOL_CONVERSION = YES; 263 + CLANG_WARN_COMMA = YES; 264 + CLANG_WARN_CONSTANT_CONVERSION = YES; 265 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 266 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 + CLANG_WARN_EMPTY_BODY = YES; 269 + CLANG_WARN_ENUM_CONVERSION = YES; 270 + CLANG_WARN_INFINITE_RECURSION = YES; 271 + CLANG_WARN_INT_CONVERSION = YES; 272 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 273 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 274 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 275 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 277 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 278 + CLANG_WARN_STRICT_PROTOTYPES = YES; 279 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 280 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 281 + CLANG_WARN_UNREACHABLE_CODE = YES; 282 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 + COPY_PHASE_STRIP = NO; 284 + DEBUG_INFORMATION_FORMAT = dwarf; 285 + DEVELOPMENT_TEAM = M67B42LX8D; 286 + ENABLE_STRICT_OBJC_MSGSEND = YES; 287 + ENABLE_TESTABILITY = YES; 288 + ENABLE_USER_SCRIPT_SANDBOXING = YES; 289 + GCC_C_LANGUAGE_STANDARD = gnu17; 290 + GCC_DYNAMIC_NO_PIC = NO; 291 + GCC_NO_COMMON_BLOCKS = YES; 292 + GCC_OPTIMIZATION_LEVEL = 0; 293 + GCC_PREPROCESSOR_DEFINITIONS = ( 294 + "DEBUG=1", 295 + "$(inherited)", 296 + ); 297 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 298 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 299 + GCC_WARN_UNDECLARED_SELECTOR = YES; 300 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 301 + GCC_WARN_UNUSED_FUNCTION = YES; 302 + GCC_WARN_UNUSED_VARIABLE = YES; 303 + IPHONEOS_DEPLOYMENT_TARGET = 26.2; 304 + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 305 + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 306 + MTL_FAST_MATH = YES; 307 + ONLY_ACTIVE_ARCH = YES; 308 + SDKROOT = iphoneos; 309 + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 310 + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 311 + }; 312 + name = Debug; 313 + }; 314 + 0BBF20272F2D443F00AF0585 /* Release */ = { 315 + isa = XCBuildConfiguration; 316 + buildSettings = { 317 + ALWAYS_SEARCH_USER_PATHS = NO; 318 + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 319 + CLANG_ANALYZER_NONNULL = YES; 320 + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 321 + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 322 + CLANG_ENABLE_MODULES = YES; 323 + CLANG_ENABLE_OBJC_ARC = YES; 324 + CLANG_ENABLE_OBJC_WEAK = YES; 325 + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 326 + CLANG_WARN_BOOL_CONVERSION = YES; 327 + CLANG_WARN_COMMA = YES; 328 + CLANG_WARN_CONSTANT_CONVERSION = YES; 329 + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 330 + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 331 + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 332 + CLANG_WARN_EMPTY_BODY = YES; 333 + CLANG_WARN_ENUM_CONVERSION = YES; 334 + CLANG_WARN_INFINITE_RECURSION = YES; 335 + CLANG_WARN_INT_CONVERSION = YES; 336 + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 337 + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 338 + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 339 + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 341 + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 + CLANG_WARN_STRICT_PROTOTYPES = YES; 343 + CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 + CLANG_WARN_UNREACHABLE_CODE = YES; 346 + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 + COPY_PHASE_STRIP = NO; 348 + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 349 + DEVELOPMENT_TEAM = M67B42LX8D; 350 + ENABLE_NS_ASSERTIONS = NO; 351 + ENABLE_STRICT_OBJC_MSGSEND = YES; 352 + ENABLE_USER_SCRIPT_SANDBOXING = YES; 353 + GCC_C_LANGUAGE_STANDARD = gnu17; 354 + GCC_NO_COMMON_BLOCKS = YES; 355 + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 356 + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 357 + GCC_WARN_UNDECLARED_SELECTOR = YES; 358 + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 359 + GCC_WARN_UNUSED_FUNCTION = YES; 360 + GCC_WARN_UNUSED_VARIABLE = YES; 361 + IPHONEOS_DEPLOYMENT_TARGET = 26.2; 362 + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 363 + MTL_ENABLE_DEBUG_INFO = NO; 364 + MTL_FAST_MATH = YES; 365 + SDKROOT = iphoneos; 366 + SWIFT_COMPILATION_MODE = wholemodule; 367 + VALIDATE_PRODUCT = YES; 368 + }; 369 + name = Release; 370 + }; 371 + 0BBF20292F2D443F00AF0585 /* Debug */ = { 372 + isa = XCBuildConfiguration; 373 + buildSettings = { 374 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 376 + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 377 + CODE_SIGN_STYLE = Automatic; 378 + CURRENT_PROJECT_VERSION = 1; 379 + DEVELOPMENT_TEAM = M67B42LX8D; 380 + ENABLE_PREVIEWS = YES; 381 + GENERATE_INFOPLIST_FILE = YES; 382 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks"; 383 + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; 384 + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 385 + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 386 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 387 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 388 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 389 + LD_RUNPATH_SEARCH_PATHS = ( 390 + "$(inherited)", 391 + "@executable_path/Frameworks", 392 + ); 393 + MARKETING_VERSION = 1.0; 394 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.wasup-chucks"; 395 + PRODUCT_NAME = "$(TARGET_NAME)"; 396 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 397 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 398 + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 399 + SWIFT_EMIT_LOC_STRINGS = YES; 400 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 401 + SWIFT_VERSION = 5.0; 402 + TARGETED_DEVICE_FAMILY = "1,2"; 403 + }; 404 + name = Debug; 405 + }; 406 + 0BBF202A2F2D443F00AF0585 /* Release */ = { 407 + isa = XCBuildConfiguration; 408 + buildSettings = { 409 + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 410 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 411 + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 412 + CODE_SIGN_STYLE = Automatic; 413 + CURRENT_PROJECT_VERSION = 1; 414 + DEVELOPMENT_TEAM = M67B42LX8D; 415 + ENABLE_PREVIEWS = YES; 416 + GENERATE_INFOPLIST_FILE = YES; 417 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks"; 418 + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; 419 + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 420 + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 421 + INFOPLIST_KEY_UILaunchScreen_Generation = YES; 422 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 423 + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 424 + LD_RUNPATH_SEARCH_PATHS = ( 425 + "$(inherited)", 426 + "@executable_path/Frameworks", 427 + ); 428 + MARKETING_VERSION = 1.0; 429 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.wasup-chucks"; 430 + PRODUCT_NAME = "$(TARGET_NAME)"; 431 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 432 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 433 + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; 434 + SWIFT_EMIT_LOC_STRINGS = YES; 435 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 436 + SWIFT_VERSION = 5.0; 437 + TARGETED_DEVICE_FAMILY = "1,2"; 438 + }; 439 + name = Release; 440 + }; 441 + 0BBF204E2F2D46B200AF0585 /* Debug */ = { 442 + isa = XCBuildConfiguration; 443 + buildSettings = { 444 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 445 + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 446 + CODE_SIGN_STYLE = Automatic; 447 + CURRENT_PROJECT_VERSION = 1; 448 + DEVELOPMENT_TEAM = M67B42LX8D; 449 + GENERATE_INFOPLIST_FILE = YES; 450 + INFOPLIST_FILE = widget/Info.plist; 451 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks Widget"; 452 + INFOPLIST_KEY_NSHumanReadableCopyright = ""; 453 + LD_RUNPATH_SEARCH_PATHS = ( 454 + "$(inherited)", 455 + "@executable_path/Frameworks", 456 + "@executable_path/../../Frameworks", 457 + ); 458 + MARKETING_VERSION = 1.0; 459 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.wasup-chucks.widget"; 460 + PRODUCT_NAME = "$(TARGET_NAME)"; 461 + SKIP_INSTALL = YES; 462 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 463 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 464 + SWIFT_EMIT_LOC_STRINGS = YES; 465 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 466 + SWIFT_VERSION = 5.0; 467 + TARGETED_DEVICE_FAMILY = "1,2"; 468 + }; 469 + name = Debug; 470 + }; 471 + 0BBF204F2F2D46B200AF0585 /* Release */ = { 472 + isa = XCBuildConfiguration; 473 + buildSettings = { 474 + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 475 + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 476 + CODE_SIGN_STYLE = Automatic; 477 + CURRENT_PROJECT_VERSION = 1; 478 + DEVELOPMENT_TEAM = M67B42LX8D; 479 + GENERATE_INFOPLIST_FILE = YES; 480 + INFOPLIST_FILE = widget/Info.plist; 481 + INFOPLIST_KEY_CFBundleDisplayName = "Wasup Chucks Widget"; 482 + INFOPLIST_KEY_NSHumanReadableCopyright = ""; 483 + LD_RUNPATH_SEARCH_PATHS = ( 484 + "$(inherited)", 485 + "@executable_path/Frameworks", 486 + "@executable_path/../../Frameworks", 487 + ); 488 + MARKETING_VERSION = 1.0; 489 + PRODUCT_BUNDLE_IDENTIFIER = "sh.dunkirk.sh.wasup-chucks.widget"; 490 + PRODUCT_NAME = "$(TARGET_NAME)"; 491 + SKIP_INSTALL = YES; 492 + STRING_CATALOG_GENERATE_SYMBOLS = YES; 493 + SWIFT_APPROACHABLE_CONCURRENCY = YES; 494 + SWIFT_EMIT_LOC_STRINGS = YES; 495 + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; 496 + SWIFT_VERSION = 5.0; 497 + TARGETED_DEVICE_FAMILY = "1,2"; 498 + }; 499 + name = Release; 500 + }; 501 + /* End XCBuildConfiguration section */ 502 + 503 + /* Begin XCConfigurationList section */ 504 + 0BBF20182F2D443D00AF0585 /* Build configuration list for PBXProject "wasup-chucks" */ = { 505 + isa = XCConfigurationList; 506 + buildConfigurations = ( 507 + 0BBF20262F2D443F00AF0585 /* Debug */, 508 + 0BBF20272F2D443F00AF0585 /* Release */, 509 + ); 510 + defaultConfigurationIsVisible = 0; 511 + defaultConfigurationName = Release; 512 + }; 513 + 0BBF20282F2D443F00AF0585 /* Build configuration list for PBXNativeTarget "wasup-chucks" */ = { 514 + isa = XCConfigurationList; 515 + buildConfigurations = ( 516 + 0BBF20292F2D443F00AF0585 /* Debug */, 517 + 0BBF202A2F2D443F00AF0585 /* Release */, 518 + ); 519 + defaultConfigurationIsVisible = 0; 520 + defaultConfigurationName = Release; 521 + }; 522 + 0BBF204D2F2D46B200AF0585 /* Build configuration list for PBXNativeTarget "widgetExtension" */ = { 523 + isa = XCConfigurationList; 524 + buildConfigurations = ( 525 + 0BBF204E2F2D46B200AF0585 /* Debug */, 526 + 0BBF204F2F2D46B200AF0585 /* Release */, 527 + ); 528 + defaultConfigurationIsVisible = 0; 529 + defaultConfigurationName = Release; 530 + }; 531 + /* End XCConfigurationList section */ 532 + }; 533 + rootObject = 0BBF20152F2D443D00AF0585 /* Project object */; 534 + }
+7
wasup-chucks.xcodeproj/project.xcworkspace/contents.xcworkspacedata
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <Workspace 3 + version = "1.0"> 4 + <FileRef 5 + location = "self:"> 6 + </FileRef> 7 + </Workspace>
wasup-chucks.xcodeproj/project.xcworkspace/xcuserdata/kierank.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+19
wasup-chucks.xcodeproj/xcuserdata/kierank.xcuserdatad/xcschemes/xcschememanagement.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>SchemeUserState</key> 6 + <dict> 7 + <key>wasup-chucks.xcscheme_^#shared#^_</key> 8 + <dict> 9 + <key>orderHint</key> 10 + <integer>0</integer> 11 + </dict> 12 + <key>widgetExtension.xcscheme_^#shared#^_</key> 13 + <dict> 14 + <key>orderHint</key> 15 + <integer>1</integer> 16 + </dict> 17 + </dict> 18 + </dict> 19 + </plist>
+10
wasup-chucks/AppIcon.icon/Assets/wasup-chucks.svg
··· 1 + <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <g clip-path="url(#clip0_42_2)"> 3 + <path d="M582.772 840.4C398.772 840.4 241.172 690.8 241.172 498.8C241.172 338 320.372 228.4 429.972 228.4C477.972 228.4 517.172 250 528.372 290.8C543.572 279.6 557.172 269.2 567.572 262C597.972 239.6 613.172 229.2 625.972 229.2C635.572 229.2 645.972 233.2 666.772 242C690.772 252.4 715.572 262.8 723.572 262.8C734.772 262.8 746.772 250.8 763.572 234L786.772 255.6L713.172 334C698.772 349.2 686.772 358 675.572 358C663.572 358 644.372 351.6 618.772 338.8C593.172 326 573.972 318 561.972 318C553.972 318 545.172 321.2 534.772 328.4C516.372 340.4 510.772 347.6 510.772 359.6C510.772 372.4 531.572 413.2 572.372 480.4C613.172 548.4 633.972 589.2 633.972 603.6C633.972 616.4 625.172 627.6 609.972 643.6C594.772 659.6 569.172 685.2 532.372 722C557.172 728.4 582.772 731.6 609.972 731.6C665.972 731.6 737.172 710.8 766.772 689.2L785.172 715.6L641.172 834.8C623.572 838.8 604.372 840.4 582.772 840.4ZM334.772 464.4C334.772 579.6 401.172 673.2 502.772 712.4C518.772 693.2 527.572 674 527.572 657.2C527.572 641.2 513.972 611.6 486.772 567.6L413.972 449.2C404.372 433.2 398.772 419.6 398.772 409.2C398.772 398.8 407.572 386 425.972 370C441.972 355.6 473.172 331.6 504.372 308.4C495.572 280.4 465.972 262.8 437.972 262.8C375.572 262.8 334.772 345.2 334.772 464.4Z" fill="white"/> 4 + </g> 5 + <defs> 6 + <clipPath id="clip0_42_2"> 7 + <rect width="1024" height="1024" fill="white"/> 8 + </clipPath> 9 + </defs> 10 + </svg>
+32
wasup-chucks/AppIcon.icon/icon.json
··· 1 + { 2 + "fill" : { 3 + "automatic-gradient" : "display-p3:0.12610,0.22424,0.37988,1.00000" 4 + }, 5 + "groups" : [ 6 + { 7 + "layers" : [ 8 + { 9 + "fill" : { 10 + "solid" : "display-p3:0.90967,0.67139,0.26123,1.00000" 11 + }, 12 + "image-name" : "wasup-chucks.svg", 13 + "name" : "wasup-chucks" 14 + } 15 + ], 16 + "shadow" : { 17 + "kind" : "neutral", 18 + "opacity" : 0.5 19 + }, 20 + "translucency" : { 21 + "enabled" : true, 22 + "value" : 0.5 23 + } 24 + } 25 + ], 26 + "supported-platforms" : { 27 + "circles" : [ 28 + "watchOS" 29 + ], 30 + "squares" : "shared" 31 + } 32 + }
+20
wasup-chucks/Assets.xcassets/AccentColor.colorset/Contents.json
··· 1 + { 2 + "colors" : [ 3 + { 4 + "color" : { 5 + "color-space" : "display-p3", 6 + "components" : { 7 + "alpha" : "1.000", 8 + "blue" : "0.261", 9 + "green" : "0.671", 10 + "red" : "0.910" 11 + } 12 + }, 13 + "idiom" : "universal" 14 + } 15 + ], 16 + "info" : { 17 + "author" : "xcode", 18 + "version" : 1 19 + } 20 + }
+6
wasup-chucks/Assets.xcassets/Contents.json
··· 1 + { 2 + "info" : { 3 + "author" : "xcode", 4 + "version" : 1 5 + } 6 + }
+289
wasup-chucks/ChucksModels.swift
··· 1 + // 2 + // ChucksModels.swift 3 + // wasup-chucks 4 + // 5 + // Created by Kieran Klukas on 1/30/26. 6 + // 7 + 8 + import Foundation 9 + 10 + // MARK: - API Models 11 + 12 + struct Allergen: Codable, Hashable { 13 + let url: String 14 + let alt: String 15 + } 16 + 17 + struct MenuItem: Codable, Hashable, Identifiable { 18 + let name: String 19 + let allergens: [Allergen] 20 + 21 + var id: String { name } 22 + } 23 + 24 + struct VenueMenu: Codable, Hashable, Identifiable { 25 + let venue: String 26 + let meal: String? 27 + let slot: String 28 + let items: [MenuItem] 29 + 30 + var id: String { "\(venue)-\(slot)" } 31 + } 32 + 33 + typealias MenuResponse = [String: [VenueMenu]] 34 + 35 + // MARK: - Meal Phase 36 + 37 + enum MealPhase: String, CaseIterable, Sendable { 38 + case breakfast = "Breakfast" 39 + case brunch = "Brunch" 40 + case lunch = "Lunch" 41 + case dinner = "Dinner" 42 + case closed = "Closed" 43 + 44 + var icon: String { 45 + switch self { 46 + case .breakfast: return "cup.and.saucer.fill" 47 + case .brunch: return "fork.knife" 48 + case .lunch: return "sun.max.fill" 49 + case .dinner: return "moon.stars.fill" 50 + case .closed: return "moon.zzz.fill" 51 + } 52 + } 53 + 54 + var shortName: String { 55 + switch self { 56 + case .breakfast: return "Breakfast" 57 + case .brunch: return "Brunch" 58 + case .lunch: return "Lunch" 59 + case .dinner: return "Dinner" 60 + case .closed: return "Closed" 61 + } 62 + } 63 + 64 + var apiSlot: String { 65 + switch self { 66 + case .breakfast, .brunch: return "breakfast" 67 + case .lunch: return "lunch" 68 + case .dinner: return "dinner" 69 + case .closed: return "" 70 + } 71 + } 72 + } 73 + 74 + // MARK: - Meal Schedule 75 + 76 + struct MealSchedule: Identifiable { 77 + var id: String { phase.rawValue } 78 + let phase: MealPhase 79 + let startHour: Int 80 + let startMinute: Int 81 + let endHour: Int 82 + let endMinute: Int 83 + 84 + var startMinutes: Int { startHour * 60 + startMinute } 85 + var endMinutes: Int { endHour * 60 + endMinute } 86 + 87 + // Mon-Fri: Hot Breakfast 7-8:15, Continental 8:15-9:30, Lunch 10:30-2:30, Dinner 4:30-7:30 88 + // Treating Hot + Continental as one "Breakfast" period for simplicity 89 + static let weekdaySchedule: [MealSchedule] = [ 90 + MealSchedule(phase: .breakfast, startHour: 7, startMinute: 0, endHour: 9, endMinute: 30), 91 + MealSchedule(phase: .lunch, startHour: 10, startMinute: 30, endHour: 14, endMinute: 30), 92 + MealSchedule(phase: .dinner, startHour: 16, startMinute: 30, endHour: 19, endMinute: 30) 93 + ] 94 + 95 + // Saturday: Continental 8-9, Brunch 11-1, Dinner 4:30-6:30 96 + static let saturdaySchedule: [MealSchedule] = [ 97 + MealSchedule(phase: .breakfast, startHour: 8, startMinute: 0, endHour: 9, endMinute: 0), 98 + MealSchedule(phase: .brunch, startHour: 11, startMinute: 0, endHour: 13, endMinute: 0), 99 + MealSchedule(phase: .dinner, startHour: 16, startMinute: 30, endHour: 18, endMinute: 30) 100 + ] 101 + 102 + // Sunday: Hot Breakfast 8-9, Lunch 11:30-2, Dinner 5-7:30 103 + static let sundaySchedule: [MealSchedule] = [ 104 + MealSchedule(phase: .breakfast, startHour: 8, startMinute: 0, endHour: 9, endMinute: 0), 105 + MealSchedule(phase: .lunch, startHour: 11, startMinute: 30, endHour: 14, endMinute: 0), 106 + MealSchedule(phase: .dinner, startHour: 17, startMinute: 0, endHour: 19, endMinute: 30) 107 + ] 108 + 109 + static func schedule(for weekday: Int) -> [MealSchedule] { 110 + switch weekday { 111 + case 1: return sundaySchedule 112 + case 7: return saturdaySchedule 113 + default: return weekdaySchedule 114 + } 115 + } 116 + } 117 + 118 + // MARK: - Chuck's Status 119 + 120 + struct ChucksStatus { 121 + let currentPhase: MealPhase 122 + let timeRemaining: TimeInterval? 123 + let nextPhase: MealPhase? 124 + let nextPhaseStart: Date? 125 + let isOpen: Bool 126 + let currentMealEnd: Date? 127 + 128 + static func calculate(for date: Date = Date()) -> ChucksStatus { 129 + let calendar = Calendar.current 130 + let weekday = calendar.component(.weekday, from: date) 131 + let schedule = MealSchedule.schedule(for: weekday) 132 + 133 + let hour = calendar.component(.hour, from: date) 134 + let minute = calendar.component(.minute, from: date) 135 + let currentMinutes = hour * 60 + minute 136 + 137 + for (index, meal) in schedule.enumerated() { 138 + if currentMinutes >= meal.startMinutes && currentMinutes < meal.endMinutes { 139 + let endDate = calendar.date(bySettingHour: meal.endHour, minute: meal.endMinute, second: 0, of: date)! 140 + let remaining = endDate.timeIntervalSince(date) 141 + 142 + let nextPhase: MealPhase? 143 + let nextStart: Date? 144 + if index + 1 < schedule.count { 145 + let next = schedule[index + 1] 146 + nextPhase = next.phase 147 + nextStart = calendar.date(bySettingHour: next.startHour, minute: next.startMinute, second: 0, of: date) 148 + } else { 149 + nextPhase = .closed 150 + nextStart = nil 151 + } 152 + 153 + return ChucksStatus( 154 + currentPhase: meal.phase, 155 + timeRemaining: remaining, 156 + nextPhase: nextPhase, 157 + nextPhaseStart: nextStart, 158 + isOpen: true, 159 + currentMealEnd: endDate 160 + ) 161 + } 162 + 163 + if currentMinutes < meal.startMinutes { 164 + let startDate = calendar.date(bySettingHour: meal.startHour, minute: meal.startMinute, second: 0, of: date)! 165 + let timeUntil = startDate.timeIntervalSince(date) 166 + 167 + return ChucksStatus( 168 + currentPhase: .closed, 169 + timeRemaining: timeUntil, 170 + nextPhase: meal.phase, 171 + nextPhaseStart: startDate, 172 + isOpen: false, 173 + currentMealEnd: nil 174 + ) 175 + } 176 + } 177 + 178 + let tomorrow = calendar.date(byAdding: .day, value: 1, to: date)! 179 + let tomorrowWeekday = calendar.component(.weekday, from: tomorrow) 180 + let tomorrowSchedule = MealSchedule.schedule(for: tomorrowWeekday) 181 + 182 + if let firstMeal = tomorrowSchedule.first { 183 + var nextStart = calendar.date(bySettingHour: firstMeal.startHour, minute: firstMeal.startMinute, second: 0, of: tomorrow)! 184 + if nextStart <= date { 185 + nextStart = calendar.date(byAdding: .day, value: 1, to: nextStart)! 186 + } 187 + let timeUntil = nextStart.timeIntervalSince(date) 188 + 189 + return ChucksStatus( 190 + currentPhase: .closed, 191 + timeRemaining: timeUntil, 192 + nextPhase: firstMeal.phase, 193 + nextPhaseStart: nextStart, 194 + isOpen: false, 195 + currentMealEnd: nil 196 + ) 197 + } 198 + 199 + return ChucksStatus( 200 + currentPhase: .closed, 201 + timeRemaining: nil, 202 + nextPhase: nil, 203 + nextPhaseStart: nil, 204 + isOpen: false, 205 + currentMealEnd: nil 206 + ) 207 + } 208 + } 209 + 210 + // MARK: - TimeInterval Extension 211 + 212 + extension TimeInterval { 213 + var countdownText: String { 214 + let totalSeconds = Int(self) 215 + let hours = totalSeconds / 3600 216 + let minutes = (totalSeconds % 3600) / 60 217 + let seconds = totalSeconds % 60 218 + 219 + if hours > 0 { 220 + return "\(hours)h" 221 + } else if minutes > 0 { 222 + return "\(minutes)m" 223 + } else { 224 + return "\(seconds)s" 225 + } 226 + } 227 + } 228 + 229 + // MARK: - API Service 230 + 231 + actor ChucksService { 232 + static let shared = ChucksService() 233 + 234 + private let baseURL = "https://diningdata.cedarville.edu/api/menus" 235 + private var cachedMenu: MenuResponse? 236 + private var cacheDate: Date? 237 + private let cacheExpiration: TimeInterval = 3600 238 + 239 + func fetchMenu(days: Int = 5) async throws -> MenuResponse { 240 + if let cached = cachedMenu, 241 + let date = cacheDate, 242 + Date().timeIntervalSince(date) < cacheExpiration { 243 + return cached 244 + } 245 + 246 + guard let url = URL(string: "\(baseURL)?days=\(days)") else { 247 + throw ChucksError.invalidURL 248 + } 249 + 250 + var request = URLRequest(url: url) 251 + request.setValue("*/*", forHTTPHeaderField: "Accept") 252 + request.setValue("https://www.cedarville.edu", forHTTPHeaderField: "Origin") 253 + request.setValue("https://www.cedarville.edu/offices/the-commons", forHTTPHeaderField: "Referer") 254 + 255 + let (data, response) = try await URLSession.shared.data(for: request) 256 + 257 + guard let httpResponse = response as? HTTPURLResponse, 258 + httpResponse.statusCode == 200 else { 259 + throw ChucksError.networkError 260 + } 261 + 262 + let menu = try JSONDecoder().decode(MenuResponse.self, from: data) 263 + cachedMenu = menu 264 + cacheDate = Date() 265 + 266 + return menu 267 + } 268 + 269 + func getSpecials(for date: Date, phase: MealPhase) async throws -> [MenuItem] { 270 + let menu = try await fetchMenu() 271 + let dateFormatter = DateFormatter() 272 + dateFormatter.dateFormat = "yyyy-MM-dd" 273 + let dateKey = dateFormatter.string(from: date) 274 + 275 + guard let dayMenu = menu[dateKey] else { 276 + return [] 277 + } 278 + 279 + let slot = await phase.apiSlot 280 + let homeCooking = dayMenu.filter { $0.venue == "Home Cooking" && $0.slot == slot } 281 + return homeCooking.flatMap { $0.items } 282 + } 283 + } 284 + 285 + enum ChucksError: Error { 286 + case invalidURL 287 + case networkError 288 + case decodingError 289 + }
+374
wasup-chucks/ContentView.swift
··· 1 + // 2 + // ContentView.swift 3 + // wasup-chucks 4 + // 5 + // Created by Kieran Klukas on 1/30/26. 6 + // 7 + 8 + import SwiftUI 9 + internal import Combine 10 + 11 + // MARK: - Main View 12 + 13 + struct ContentView: View { 14 + @State private var status = ChucksStatus.calculate() 15 + @State private var todayMenu: [VenueMenu] = [] 16 + @State private var isLoading = true 17 + @State private var selectedMeal: MealSchedule? = nil 18 + 19 + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 20 + 21 + var currentSlot: String { 22 + if status.isOpen { 23 + return status.currentPhase.apiSlot 24 + } else if let next = status.nextPhase { 25 + return next.apiSlot 26 + } 27 + return "lunch" 28 + } 29 + 30 + var body: some View { 31 + NavigationStack { 32 + ScrollView { 33 + VStack(spacing: 20) { 34 + StatusCard(status: status) 35 + 36 + ScheduleCard(status: status, todayMenu: todayMenu, selectedMeal: $selectedMeal) 37 + 38 + if isLoading { 39 + ProgressView() 40 + .frame(maxWidth: .infinity, minHeight: 200) 41 + } else { 42 + CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen) 43 + } 44 + } 45 + .padding(.horizontal) 46 + } 47 + .navigationTitle("Wasup Chucks") 48 + .onReceive(timer) { _ in 49 + status = ChucksStatus.calculate() 50 + } 51 + .task { 52 + await loadMenu() 53 + } 54 + .refreshable { 55 + await loadMenu() 56 + } 57 + .sheet(item: $selectedMeal) { meal in 58 + MealDetailSheet(meal: meal, menu: todayMenu) 59 + } 60 + } 61 + } 62 + 63 + func loadMenu() async { 64 + isLoading = true 65 + do { 66 + let menu = try await ChucksService.shared.fetchMenu() 67 + let dateFormatter = DateFormatter() 68 + dateFormatter.dateFormat = "yyyy-MM-dd" 69 + let dateKey = dateFormatter.string(from: Date()) 70 + todayMenu = menu[dateKey] ?? [] 71 + } catch { 72 + print("Failed to load menu: \(error)") 73 + } 74 + isLoading = false 75 + } 76 + } 77 + 78 + // MARK: - Status Card 79 + 80 + struct StatusCard: View { 81 + let status: ChucksStatus 82 + 83 + var body: some View { 84 + VStack(spacing: 8) { 85 + HStack { 86 + Image(systemName: status.isOpen ? status.currentPhase.icon : (status.nextPhase?.icon ?? "moon.zzz.fill")) 87 + .font(.title2) 88 + .foregroundStyle(status.isOpen ? .green : .orange) 89 + 90 + VStack(alignment: .leading, spacing: 2) { 91 + Text(status.isOpen ? "Open" : "Closed") 92 + .font(.subheadline) 93 + .fontWeight(.semibold) 94 + .foregroundStyle(status.isOpen ? .green : .orange) 95 + Text(status.isOpen ? status.currentPhase.rawValue : (status.nextPhase?.rawValue ?? "")) 96 + .font(.caption) 97 + .foregroundStyle(.secondary) 98 + } 99 + 100 + Spacer() 101 + } 102 + 103 + if let remaining = status.timeRemaining { 104 + Text(remaining.countdownText) 105 + .font(.system(size: 72, weight: .bold, design: .rounded)) 106 + .monospacedDigit() 107 + 108 + Text(status.isOpen ? "until \(status.currentPhase.shortName) ends" : "until \(status.nextPhase?.shortName ?? "open")") 109 + .font(.subheadline) 110 + .foregroundStyle(.secondary) 111 + } 112 + } 113 + .padding() 114 + .frame(maxWidth: .infinity) 115 + .background(.regularMaterial) 116 + .clipShape(RoundedRectangle(cornerRadius: 16)) 117 + } 118 + } 119 + 120 + // MARK: - Schedule Card 121 + 122 + struct ScheduleCard: View { 123 + let status: ChucksStatus 124 + let todayMenu: [VenueMenu] 125 + @Binding var selectedMeal: MealSchedule? 126 + 127 + var schedule: [MealSchedule] { 128 + MealSchedule.schedule(for: Calendar.current.component(.weekday, from: Date())) 129 + } 130 + 131 + var body: some View { 132 + VStack(alignment: .leading, spacing: 12) { 133 + Text("Today's Schedule") 134 + .font(.headline) 135 + 136 + HStack(spacing: 8) { 137 + ForEach(schedule, id: \.phase) { meal in 138 + ScheduleButton( 139 + meal: meal, 140 + isCurrent: status.isOpen && status.currentPhase == meal.phase 141 + ) { 142 + selectedMeal = meal 143 + } 144 + } 145 + } 146 + } 147 + .padding() 148 + .frame(maxWidth: .infinity, alignment: .leading) 149 + .background(.regularMaterial) 150 + .clipShape(RoundedRectangle(cornerRadius: 16)) 151 + } 152 + } 153 + 154 + struct ScheduleButton: View { 155 + let meal: MealSchedule 156 + let isCurrent: Bool 157 + let action: () -> Void 158 + 159 + var body: some View { 160 + Button(action: action) { 161 + VStack(spacing: 4) { 162 + Image(systemName: meal.phase.icon) 163 + .font(.title3) 164 + 165 + Text(meal.phase.shortName) 166 + .font(.caption2) 167 + .fontWeight(.medium) 168 + 169 + Text("\(formatTime(meal.startHour, meal.startMinute))-\(formatTime(meal.endHour, meal.endMinute))") 170 + .font(.caption2) 171 + .foregroundStyle(.secondary) 172 + } 173 + .frame(maxWidth: .infinity) 174 + .padding(.vertical, 10) 175 + .overlay( 176 + RoundedRectangle(cornerRadius: 10) 177 + .stroke(isCurrent ? Color.green : Color.clear, lineWidth: 2) 178 + ) 179 + } 180 + .buttonStyle(.plain) 181 + .foregroundStyle(isCurrent ? .green : .primary) 182 + } 183 + 184 + func formatTime(_ hour: Int, _ minute: Int) -> String { 185 + let period = hour >= 12 ? "PM" : "AM" 186 + let displayHour = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour) 187 + if minute == 0 { 188 + return "\(displayHour)\(period)" 189 + } 190 + return "\(displayHour):\(String(format: "%02d", minute))\(period)" 191 + } 192 + } 193 + 194 + // MARK: - Meal Detail Sheet 195 + 196 + struct MealDetailSheet: View { 197 + let meal: MealSchedule 198 + let menu: [VenueMenu] 199 + @Environment(\.dismiss) private var dismiss 200 + 201 + var venues: [VenueMenu] { 202 + let matching = menu.filter { $0.slot == meal.phase.apiSlot || $0.slot == "anytime" } 203 + // Merge venues that appear with both meal-specific and anytime slots 204 + var merged: [String: VenueMenu] = [:] 205 + for venue in matching { 206 + if let existing = merged[venue.venue] { 207 + // Combine items, preferring meal-specific slot 208 + let combinedItems = existing.items + venue.items.filter { item in 209 + !existing.items.contains { $0.name == item.name } 210 + } 211 + let preferredSlot = existing.slot != "anytime" ? existing.slot : venue.slot 212 + merged[venue.venue] = VenueMenu(venue: venue.venue, meal: venue.meal ?? existing.meal, slot: preferredSlot, items: combinedItems) 213 + } else { 214 + merged[venue.venue] = venue 215 + } 216 + } 217 + return Array(merged.values).sorted { $0.venue < $1.venue } 218 + } 219 + 220 + var body: some View { 221 + NavigationStack { 222 + ScrollView { 223 + VStack(alignment: .leading, spacing: 16) { 224 + ForEach(venues) { venue in 225 + StationCard(venue: venue, highlightAsSpecial: venue.venue == "Home Cooking") 226 + } 227 + } 228 + .padding() 229 + } 230 + .navigationTitle(meal.phase.rawValue) 231 + .navigationBarTitleDisplayMode(.inline) 232 + .toolbar { 233 + ToolbarItem(placement: .topBarTrailing) { 234 + Button("Done") { 235 + dismiss() 236 + } 237 + } 238 + } 239 + } 240 + } 241 + } 242 + 243 + // MARK: - Current Meal View 244 + 245 + struct CurrentMealView: View { 246 + let menu: [VenueMenu] 247 + let slot: String 248 + let isOpen: Bool 249 + 250 + var specialsVenue: VenueMenu? { 251 + menu.first { $0.venue == "Home Cooking" && $0.slot == slot } 252 + } 253 + 254 + var body: some View { 255 + VStack(alignment: .leading, spacing: 16) { 256 + if let specials = specialsVenue, !specials.items.isEmpty { 257 + VStack(alignment: .leading, spacing: 10) { 258 + Text("Home Cooking") 259 + .font(.headline) 260 + 261 + ForEach(specials.items) { item in 262 + HStack(spacing: 8) { 263 + Text("•") 264 + .foregroundStyle(.secondary) 265 + Text(item.name) 266 + .font(.body) 267 + Spacer() 268 + AllergenRow(allergens: item.allergens) 269 + } 270 + } 271 + } 272 + .padding() 273 + .background(.regularMaterial) 274 + .clipShape(RoundedRectangle(cornerRadius: 12)) 275 + } 276 + } 277 + } 278 + } 279 + 280 + // MARK: - Station Card 281 + 282 + struct StationCard: View { 283 + let venue: VenueMenu 284 + let highlightAsSpecial: Bool 285 + 286 + var body: some View { 287 + VStack(alignment: .leading, spacing: 10) { 288 + HStack { 289 + if highlightAsSpecial { 290 + Image(systemName: "star.fill") 291 + .foregroundStyle(.orange) 292 + .font(.caption) 293 + } 294 + Text(venue.venue) 295 + .font(.headline) 296 + Spacer() 297 + } 298 + 299 + ForEach(venue.items) { item in 300 + HStack(spacing: 8) { 301 + Text("•") 302 + .foregroundStyle(.secondary) 303 + Text(item.name) 304 + .font(.subheadline) 305 + Spacer() 306 + AllergenRow(allergens: item.allergens) 307 + } 308 + } 309 + } 310 + .padding() 311 + .frame(maxWidth: .infinity, alignment: .leading) 312 + .background(.regularMaterial) 313 + .clipShape(RoundedRectangle(cornerRadius: 12)) 314 + } 315 + } 316 + 317 + // MARK: - Allergen Row 318 + 319 + struct AllergenRow: View { 320 + let allergens: [Allergen] 321 + 322 + var body: some View { 323 + if !allergens.isEmpty { 324 + HStack(spacing: 2) { 325 + ForEach(allergens, id: \.alt) { allergen in 326 + AllergenBadge(allergen: allergen) 327 + } 328 + } 329 + } 330 + } 331 + } 332 + 333 + struct AllergenBadge: View { 334 + let allergen: Allergen 335 + 336 + var symbol: String { 337 + switch allergen.alt { 338 + case "gluten": return "G" 339 + case "dairy": return "D" 340 + case "egg": return "E" 341 + case "soy": return "S" 342 + case "fish": return "F" 343 + case "hasPeanut": return "P" 344 + case "tree nut": return "N" 345 + case "hasShellfish": return "SF" 346 + case "vegetarian": return "V" 347 + case "gluten-free": return "GF" 348 + default: return "?" 349 + } 350 + } 351 + 352 + var color: Color { 353 + switch allergen.alt { 354 + case "vegetarian", "gluten-free": return .green 355 + default: return .orange 356 + } 357 + } 358 + 359 + var body: some View { 360 + Text(symbol) 361 + .font(.system(size: 8, weight: .bold)) 362 + .foregroundStyle(color) 363 + .padding(.horizontal, 4) 364 + .padding(.vertical, 2) 365 + .background(color.opacity(0.15)) 366 + .clipShape(RoundedRectangle(cornerRadius: 4)) 367 + } 368 + } 369 + 370 + // MARK: - Preview 371 + 372 + #Preview { 373 + ContentView() 374 + }
+17
wasup-chucks/wasup_chucksApp.swift
··· 1 + // 2 + // wasup_chucksApp.swift 3 + // wasup-chucks 4 + // 5 + // Created by Kieran Klukas on 1/30/26. 6 + // 7 + 8 + import SwiftUI 9 + 10 + @main 11 + struct wasup_chucksApp: App { 12 + var body: some Scene { 13 + WindowGroup { 14 + ContentView() 15 + } 16 + } 17 + }
+11
widget/Info.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>NSExtension</key> 6 + <dict> 7 + <key>NSExtensionPointIdentifier</key> 8 + <string>com.apple.widgetkit-extension</string> 9 + </dict> 10 + </dict> 11 + </plist>
+597
widget/widget.swift
··· 1 + // 2 + // widget.swift 3 + // widget 4 + // 5 + // Created by Kieran Klukas on 1/30/26. 6 + // 7 + 8 + import WidgetKit 9 + import SwiftUI 10 + 11 + // MARK: - Models 12 + 13 + struct Allergen: Codable, Hashable { 14 + let url: String 15 + let alt: String 16 + } 17 + 18 + struct MenuItem: Codable, Hashable, Identifiable { 19 + let name: String 20 + let allergens: [Allergen] 21 + 22 + var id: String { name } 23 + } 24 + 25 + struct VenueMenu: Codable, Hashable { 26 + let venue: String 27 + let meal: String? 28 + let slot: String 29 + let items: [MenuItem] 30 + } 31 + 32 + typealias MenuResponse = [String: [VenueMenu]] 33 + 34 + enum MealPhase: String, CaseIterable { 35 + case breakfast = "Breakfast" 36 + case brunch = "Brunch" 37 + case lunch = "Lunch" 38 + case dinner = "Dinner" 39 + case closed = "Closed" 40 + 41 + var icon: String { 42 + switch self { 43 + case .breakfast: return "cup.and.saucer.fill" 44 + case .brunch: return "fork.knife" 45 + case .lunch: return "sun.max.fill" 46 + case .dinner: return "moon.stars.fill" 47 + case .closed: return "moon.zzz.fill" 48 + } 49 + } 50 + 51 + var shortName: String { 52 + switch self { 53 + case .breakfast: return "Breakfast" 54 + case .brunch: return "Brunch" 55 + case .lunch: return "Lunch" 56 + case .dinner: return "Dinner" 57 + case .closed: return "Closed" 58 + } 59 + } 60 + 61 + var apiSlot: String { 62 + switch self { 63 + case .breakfast, .brunch: return "breakfast" 64 + case .lunch: return "lunch" 65 + case .dinner: return "dinner" 66 + case .closed: return "" 67 + } 68 + } 69 + } 70 + 71 + struct MealSchedule { 72 + let phase: MealPhase 73 + let startHour: Int 74 + let startMinute: Int 75 + let endHour: Int 76 + let endMinute: Int 77 + 78 + var startMinutes: Int { startHour * 60 + startMinute } 79 + var endMinutes: Int { endHour * 60 + endMinute } 80 + 81 + // Mon-Fri: Hot Breakfast 7-8:15, Continental 8:15-9:30, Lunch 10:30-2:30, Dinner 4:30-7:30 82 + // Treating Hot + Continental as one "Breakfast" period for simplicity 83 + static let weekdaySchedule: [MealSchedule] = [ 84 + MealSchedule(phase: .breakfast, startHour: 7, startMinute: 0, endHour: 9, endMinute: 30), 85 + MealSchedule(phase: .lunch, startHour: 10, startMinute: 30, endHour: 14, endMinute: 30), 86 + MealSchedule(phase: .dinner, startHour: 16, startMinute: 30, endHour: 19, endMinute: 30) 87 + ] 88 + 89 + // Saturday: Continental 8-9, Brunch 11-1, Dinner 4:30-6:30 90 + static let saturdaySchedule: [MealSchedule] = [ 91 + MealSchedule(phase: .breakfast, startHour: 8, startMinute: 0, endHour: 9, endMinute: 0), 92 + MealSchedule(phase: .brunch, startHour: 11, startMinute: 0, endHour: 13, endMinute: 0), 93 + MealSchedule(phase: .dinner, startHour: 16, startMinute: 30, endHour: 18, endMinute: 30) 94 + ] 95 + 96 + // Sunday: Hot Breakfast 8-9, Lunch 11:30-2, Dinner 5-7:30 97 + static let sundaySchedule: [MealSchedule] = [ 98 + MealSchedule(phase: .breakfast, startHour: 8, startMinute: 0, endHour: 9, endMinute: 0), 99 + MealSchedule(phase: .lunch, startHour: 11, startMinute: 30, endHour: 14, endMinute: 0), 100 + MealSchedule(phase: .dinner, startHour: 17, startMinute: 0, endHour: 19, endMinute: 30) 101 + ] 102 + 103 + static func schedule(for weekday: Int) -> [MealSchedule] { 104 + switch weekday { 105 + case 1: return sundaySchedule 106 + case 7: return saturdaySchedule 107 + default: return weekdaySchedule 108 + } 109 + } 110 + } 111 + 112 + struct ChucksStatus { 113 + let currentPhase: MealPhase 114 + let timeRemaining: TimeInterval? 115 + let nextPhase: MealPhase? 116 + let nextPhaseStart: Date? 117 + let isOpen: Bool 118 + let currentMealEnd: Date? 119 + 120 + static func calculate(for date: Date = Date()) -> ChucksStatus { 121 + let calendar = Calendar.current 122 + let weekday = calendar.component(.weekday, from: date) 123 + let schedule = MealSchedule.schedule(for: weekday) 124 + 125 + let hour = calendar.component(.hour, from: date) 126 + let minute = calendar.component(.minute, from: date) 127 + let currentMinutes = hour * 60 + minute 128 + 129 + // Check if currently in a meal period 130 + for (index, meal) in schedule.enumerated() { 131 + if currentMinutes >= meal.startMinutes && currentMinutes < meal.endMinutes { 132 + let endDate = calendar.date(bySettingHour: meal.endHour, minute: meal.endMinute, second: 0, of: date)! 133 + let remaining = endDate.timeIntervalSince(date) 134 + 135 + let nextPhase: MealPhase? 136 + let nextStart: Date? 137 + if index + 1 < schedule.count { 138 + let next = schedule[index + 1] 139 + nextPhase = next.phase 140 + nextStart = calendar.date(bySettingHour: next.startHour, minute: next.startMinute, second: 0, of: date) 141 + } else { 142 + nextPhase = .closed 143 + nextStart = nil 144 + } 145 + 146 + return ChucksStatus( 147 + currentPhase: meal.phase, 148 + timeRemaining: remaining, 149 + nextPhase: nextPhase, 150 + nextPhaseStart: nextStart, 151 + isOpen: true, 152 + currentMealEnd: endDate 153 + ) 154 + } 155 + 156 + // Check if before this meal period 157 + if currentMinutes < meal.startMinutes { 158 + let startDate = calendar.date(bySettingHour: meal.startHour, minute: meal.startMinute, second: 0, of: date)! 159 + let timeUntil = startDate.timeIntervalSince(date) 160 + 161 + return ChucksStatus( 162 + currentPhase: .closed, 163 + timeRemaining: timeUntil, 164 + nextPhase: meal.phase, 165 + nextPhaseStart: startDate, 166 + isOpen: false, 167 + currentMealEnd: nil 168 + ) 169 + } 170 + } 171 + 172 + // After all meals today, find tomorrow's first meal 173 + let tomorrow = calendar.date(byAdding: .day, value: 1, to: date)! 174 + let tomorrowWeekday = calendar.component(.weekday, from: tomorrow) 175 + let tomorrowSchedule = MealSchedule.schedule(for: tomorrowWeekday) 176 + 177 + if let firstMeal = tomorrowSchedule.first { 178 + var nextStart = calendar.date(bySettingHour: firstMeal.startHour, minute: firstMeal.startMinute, second: 0, of: tomorrow)! 179 + if nextStart <= date { 180 + nextStart = calendar.date(byAdding: .day, value: 1, to: nextStart)! 181 + } 182 + let timeUntil = nextStart.timeIntervalSince(date) 183 + 184 + return ChucksStatus( 185 + currentPhase: .closed, 186 + timeRemaining: timeUntil, 187 + nextPhase: firstMeal.phase, 188 + nextPhaseStart: nextStart, 189 + isOpen: false, 190 + currentMealEnd: nil 191 + ) 192 + } 193 + 194 + return ChucksStatus( 195 + currentPhase: .closed, 196 + timeRemaining: nil, 197 + nextPhase: nil, 198 + nextPhaseStart: nil, 199 + isOpen: false, 200 + currentMealEnd: nil 201 + ) 202 + } 203 + } 204 + 205 + extension TimeInterval { 206 + var countdownText: String { 207 + let totalSeconds = Int(self) 208 + let hours = totalSeconds / 3600 209 + let minutes = (totalSeconds % 3600) / 60 210 + let seconds = totalSeconds % 60 211 + 212 + if hours > 0 { 213 + return "\(hours)h" 214 + } else if minutes > 0 { 215 + return "\(minutes)m" 216 + } else { 217 + return "\(seconds)s" 218 + } 219 + } 220 + } 221 + 222 + // MARK: - API Service 223 + 224 + actor ChucksService { 225 + static let shared = ChucksService() 226 + 227 + private let baseURL = "https://diningdata.cedarville.edu/api/menus" 228 + private var cachedMenu: MenuResponse? 229 + private var cacheDate: Date? 230 + private let cacheExpiration: TimeInterval = 3600 231 + 232 + func fetchMenu(days: Int = 5) async throws -> MenuResponse { 233 + if let cached = cachedMenu, 234 + let date = cacheDate, 235 + Date().timeIntervalSince(date) < cacheExpiration { 236 + return cached 237 + } 238 + 239 + guard let url = URL(string: "\(baseURL)?days=\(days)") else { 240 + throw ChucksError.invalidURL 241 + } 242 + 243 + var request = URLRequest(url: url) 244 + request.setValue("*/*", forHTTPHeaderField: "Accept") 245 + request.setValue("https://www.cedarville.edu", forHTTPHeaderField: "Origin") 246 + request.setValue("https://www.cedarville.edu/offices/the-commons", forHTTPHeaderField: "Referer") 247 + 248 + let (data, response) = try await URLSession.shared.data(for: request) 249 + 250 + guard let httpResponse = response as? HTTPURLResponse, 251 + httpResponse.statusCode == 200 else { 252 + throw ChucksError.networkError 253 + } 254 + 255 + let menu = try JSONDecoder().decode(MenuResponse.self, from: data) 256 + cachedMenu = menu 257 + cacheDate = Date() 258 + 259 + return menu 260 + } 261 + 262 + func getSpecials(for date: Date, phase: MealPhase) async throws -> [MenuItem] { 263 + let menu = try await fetchMenu() 264 + let dateFormatter = DateFormatter() 265 + dateFormatter.dateFormat = "yyyy-MM-dd" 266 + let dateKey = dateFormatter.string(from: date) 267 + 268 + guard let dayMenu = menu[dateKey] else { 269 + return [] 270 + } 271 + 272 + let homeCooking = dayMenu.filter { $0.venue == "Home Cooking" && $0.slot == phase.apiSlot } 273 + return homeCooking.flatMap { $0.items } 274 + } 275 + } 276 + 277 + enum ChucksError: Error { 278 + case invalidURL 279 + case networkError 280 + case decodingError 281 + } 282 + 283 + // MARK: - Widget Entry & Provider 284 + 285 + struct ChucksEntry: TimelineEntry { 286 + let date: Date 287 + let status: ChucksStatus 288 + let specials: [MenuItem] 289 + } 290 + 291 + struct ChucksProvider: TimelineProvider { 292 + func placeholder(in context: Context) -> ChucksEntry { 293 + ChucksEntry( 294 + date: Date(), 295 + status: ChucksStatus.calculate(), 296 + specials: [] 297 + ) 298 + } 299 + 300 + func getSnapshot(in context: Context, completion: @escaping (ChucksEntry) -> Void) { 301 + let entry = ChucksEntry( 302 + date: Date(), 303 + status: ChucksStatus.calculate(), 304 + specials: [] 305 + ) 306 + completion(entry) 307 + } 308 + 309 + func getTimeline(in context: Context, completion: @escaping (Timeline<ChucksEntry>) -> Void) { 310 + Task { 311 + let status = ChucksStatus.calculate() 312 + var specials: [MenuItem] = [] 313 + 314 + let phase = status.isOpen ? status.currentPhase : (status.nextPhase ?? .lunch) 315 + if phase != .closed { 316 + do { 317 + specials = try await ChucksService.shared.getSpecials(for: Date(), phase: phase) 318 + } catch { 319 + print("Failed to fetch specials: \(error)") 320 + } 321 + } 322 + 323 + let entry = ChucksEntry( 324 + date: Date(), 325 + status: status, 326 + specials: specials 327 + ) 328 + 329 + let nextUpdate: Date 330 + if let remaining = status.timeRemaining { 331 + nextUpdate = min( 332 + Date().addingTimeInterval(remaining + 60), 333 + Date().addingTimeInterval(15 * 60) 334 + ) 335 + } else { 336 + nextUpdate = Date().addingTimeInterval(15 * 60) 337 + } 338 + 339 + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) 340 + completion(timeline) 341 + } 342 + } 343 + } 344 + 345 + // MARK: - Small Widget View 346 + struct SmallWidgetView: View { 347 + let entry: ChucksEntry 348 + 349 + var body: some View { 350 + ZStack { 351 + // Status indicator in top-left corner 352 + VStack { 353 + HStack { 354 + Image(systemName: entry.status.isOpen ? entry.status.currentPhase.icon : (entry.status.nextPhase?.icon ?? "moon.zzz.fill")) 355 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 356 + Text(entry.status.isOpen ? "Open" : "Closed") 357 + .font(.caption) 358 + .fontWeight(.semibold) 359 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 360 + Spacer() 361 + } 362 + Spacer() 363 + } 364 + 365 + // Centered countdown 366 + VStack(spacing: 4) { 367 + if let remaining = entry.status.timeRemaining { 368 + Text(remaining.countdownText) 369 + .font(.system(size: 48, weight: .bold, design: .rounded)) 370 + .monospacedDigit() 371 + .minimumScaleFactor(0.5) 372 + } 373 + 374 + if entry.status.isOpen { 375 + Text("until \(entry.status.currentPhase.shortName) ends") 376 + .font(.caption) 377 + .foregroundStyle(.secondary) 378 + } else if let next = entry.status.nextPhase, next != .closed { 379 + Text("until \(next.shortName)") 380 + .font(.caption) 381 + .foregroundStyle(.secondary) 382 + } else { 383 + Text("See you tomorrow!") 384 + .font(.caption) 385 + .foregroundStyle(.secondary) 386 + } 387 + } 388 + } 389 + .frame(maxWidth: .infinity, maxHeight: .infinity) 390 + } 391 + } 392 + 393 + // MARK: - Medium Widget View 394 + struct MediumWidgetView: View { 395 + let entry: ChucksEntry 396 + 397 + var body: some View { 398 + HStack(spacing: 12) { 399 + // Left side - big countdown 400 + VStack(spacing: 4) { 401 + HStack { 402 + Image(systemName: entry.status.isOpen ? entry.status.currentPhase.icon : (entry.status.nextPhase?.icon ?? "moon.zzz.fill")) 403 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 404 + Text(entry.status.isOpen ? "Open" : "Closed") 405 + .font(.caption) 406 + .fontWeight(.semibold) 407 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 408 + } 409 + 410 + if let remaining = entry.status.timeRemaining { 411 + Text(remaining.countdownText) 412 + .font(.system(size: 44, weight: .bold, design: .rounded)) 413 + .monospacedDigit() 414 + .minimumScaleFactor(0.5) 415 + } 416 + 417 + if entry.status.isOpen { 418 + Text("until \(entry.status.currentPhase.shortName) ends") 419 + .font(.caption2) 420 + .foregroundStyle(.secondary) 421 + .multilineTextAlignment(.center) 422 + } else if let next = entry.status.nextPhase, next != .closed { 423 + Text("until \(next.shortName)") 424 + .font(.caption2) 425 + .foregroundStyle(.secondary) 426 + } 427 + 428 + } 429 + .frame(maxWidth: .infinity) 430 + 431 + Divider() 432 + 433 + // Right side - specials list 434 + VStack(alignment: .leading, spacing: 2) { 435 + Text("Home Cooking") 436 + .font(.caption) 437 + .fontWeight(.semibold) 438 + .foregroundStyle(.secondary) 439 + .padding(.bottom, 2) 440 + 441 + if entry.specials.isEmpty { 442 + Spacer() 443 + Text("No specials available") 444 + .font(.caption) 445 + .foregroundStyle(.tertiary) 446 + Spacer() 447 + } else { 448 + ForEach(entry.specials) { item in 449 + Text("• \(item.name)") 450 + .font(.caption2) 451 + } 452 + Spacer(minLength: 0) 453 + } 454 + } 455 + .frame(maxWidth: .infinity, alignment: .leading) 456 + } 457 + } 458 + } 459 + 460 + // MARK: - Large Widget View 461 + struct LargeWidgetView: View { 462 + let entry: ChucksEntry 463 + 464 + var body: some View { 465 + VStack(spacing: 16) { 466 + // Top section - status and countdown 467 + HStack(alignment: .center) { 468 + // Left - status 469 + HStack { 470 + Image(systemName: entry.status.isOpen ? entry.status.currentPhase.icon : (entry.status.nextPhase?.icon ?? "moon.zzz.fill")) 471 + .font(.title2) 472 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 473 + 474 + VStack(alignment: .leading, spacing: 2) { 475 + Text(entry.status.isOpen ? "Open" : "Closed") 476 + .font(.caption) 477 + .fontWeight(.semibold) 478 + .foregroundStyle(entry.status.isOpen ? .green : .orange) 479 + 480 + Text(entry.status.isOpen ? entry.status.currentPhase.shortName : (entry.status.nextPhase?.shortName ?? "")) 481 + .font(.headline) 482 + .fontWeight(.bold) 483 + } 484 + } 485 + 486 + Spacer() 487 + 488 + // Right - big countdown 489 + if let remaining = entry.status.timeRemaining { 490 + VStack(alignment: .trailing, spacing: 2) { 491 + Text(remaining.countdownText) 492 + .font(.system(size: 48, weight: .bold, design: .rounded)) 493 + .monospacedDigit() 494 + 495 + Text(entry.status.isOpen ? "until \(entry.status.currentPhase.shortName) ends" : "until open") 496 + .font(.caption) 497 + .foregroundStyle(.secondary) 498 + } 499 + } 500 + } 501 + 502 + Divider() 503 + 504 + // Bottom section - specials 505 + VStack(alignment: .leading, spacing: 8) { 506 + Text("Home Cooking") 507 + .font(.subheadline) 508 + .fontWeight(.semibold) 509 + .foregroundStyle(.secondary) 510 + 511 + if entry.specials.isEmpty { 512 + Spacer() 513 + HStack { 514 + Spacer() 515 + Text("No specials available") 516 + .font(.subheadline) 517 + .foregroundStyle(.tertiary) 518 + Spacer() 519 + } 520 + Spacer() 521 + } else { 522 + VStack(alignment: .leading, spacing: 4) { 523 + ForEach(entry.specials) { item in 524 + Text("• \(item.name)") 525 + .font(.callout) 526 + } 527 + } 528 + } 529 + } 530 + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 531 + } 532 + } 533 + } 534 + 535 + // MARK: - Widget Configuration 536 + struct ChucksWidget: Widget { 537 + let kind: String = "ChucksWidget" 538 + 539 + var body: some WidgetConfiguration { 540 + StaticConfiguration(kind: kind, provider: ChucksProvider()) { entry in 541 + WidgetView(entry: entry) 542 + .containerBackground(.fill.tertiary, for: .widget) 543 + } 544 + .configurationDisplayName("Chuck's Status") 545 + .description("See current meal times and specials at Chuck's.") 546 + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) 547 + } 548 + } 549 + 550 + struct WidgetView: View { 551 + @Environment(\.widgetFamily) var family 552 + let entry: ChucksEntry 553 + 554 + var body: some View { 555 + switch family { 556 + case .systemSmall: 557 + SmallWidgetView(entry: entry) 558 + case .systemMedium: 559 + MediumWidgetView(entry: entry) 560 + case .systemLarge: 561 + LargeWidgetView(entry: entry) 562 + default: 563 + SmallWidgetView(entry: entry) 564 + } 565 + } 566 + } 567 + 568 + // MARK: - Previews 569 + #Preview("Small", as: .systemSmall) { 570 + ChucksWidget() 571 + } timeline: { 572 + ChucksEntry(date: Date(), status: ChucksStatus.calculate(), specials: []) 573 + } 574 + 575 + #Preview("Medium", as: .systemMedium) { 576 + ChucksWidget() 577 + } timeline: { 578 + ChucksEntry(date: Date(), status: ChucksStatus.calculate(), specials: [ 579 + MenuItem(name: "Scrambled Eggs", allergens: []), 580 + MenuItem(name: "Sausage Patties", allergens: []), 581 + MenuItem(name: "Tater Tots", allergens: []), 582 + MenuItem(name: "Biscuits", allergens: []) 583 + ]) 584 + } 585 + 586 + #Preview("Large", as: .systemLarge) { 587 + ChucksWidget() 588 + } timeline: { 589 + ChucksEntry(date: Date(), status: ChucksStatus.calculate(), specials: [ 590 + MenuItem(name: "Scrambled Eggs", allergens: []), 591 + MenuItem(name: "Sausage Patties", allergens: []), 592 + MenuItem(name: "Tater Tots", allergens: []), 593 + MenuItem(name: "Biscuits", allergens: []), 594 + MenuItem(name: "Country Gravy", allergens: []), 595 + MenuItem(name: "Hash Browns", allergens: []) 596 + ]) 597 + }
+16
widget/widgetBundle.swift
··· 1 + // 2 + // widgetBundle.swift 3 + // widget 4 + // 5 + // Created by Kieran Klukas on 1/30/26. 6 + // 7 + 8 + import WidgetKit 9 + import SwiftUI 10 + 11 + @main 12 + struct widgetBundle: WidgetBundle { 13 + var body: some Widget { 14 + ChucksWidget() 15 + } 16 + }