Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a59591d6ca | ||
|
|
e586e5ecbf | ||
|
|
e004f25a31 |
@@ -0,0 +1,10 @@
|
||||
<?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.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.apt.fuelboard</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FuelBoardWatch</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>WKAppBundleIdentifier</key>
|
||||
<string>com.apt.fuelboard.watchapp</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.watchkit</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>FuelBoardWatch</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>com.apt.fuelboard</string>
|
||||
<key>WKWatchKitApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,7 +1,4 @@
|
||||
// !$*UTF8*$!
|
||||
// FuelBoard — hand-crafted project (objectVersion 77, synchronized folders).
|
||||
// Targets: FuelBoard (app) + FuelBoardWidgets (widget extension, embedded).
|
||||
// Shared/ is a synchronized root group included in BOTH targets.
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
@@ -10,10 +7,26 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
ADCE5B5A3033A32C008C131C /* FuelBoardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
ADCE5B623033A32D008C131C /* FuelBoardWatch Watch App Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
BB0000000000000000000001 /* FuelBoardWidgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = BB0000000000000000000002 /* FuelBoardWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
ADCE5B643033A32D008C131C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = ADCE5B4E3033A329008C131C;
|
||||
remoteInfo = "FuelBoardWatch Watch App Extension";
|
||||
};
|
||||
ADCE5B6F3033A32F008C131C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = ADCE5B673033A32D008C131C;
|
||||
remoteInfo = "FuelBoardWatch Watch App";
|
||||
};
|
||||
BB0000000000000000000003 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BB0000000000000000000004 /* Project object */;
|
||||
@@ -24,6 +37,28 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
ADCE5B653033A32D008C131C /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
ADCE5B623033A32D008C131C /* FuelBoardWatch Watch App Extension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B5B3033A32C008C131C /* Embed Watch Content */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
ADCE5B5A3033A32C008C131C /* FuelBoardWatch Watch App.app in Embed Watch Content */,
|
||||
);
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000006 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -38,11 +73,23 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FuelBoardWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FuelBoardWatch Watch App Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB0000000000000000000002 /* FuelBoardWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FuelBoardWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB0000000000000000000007 /* FuelBoard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FuelBoard.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "FuelBoardWatch Watch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "FuelBoardWatch Wrapper";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB0000000000000000000008 /* FuelBoard */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = FuelBoard;
|
||||
@@ -61,6 +108,20 @@
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
ADCE5B4C3033A329008C131C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B6D3033A32E008C131C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB000000000000000000000B /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -84,6 +145,8 @@
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB0000000000000000000009 /* FuelBoardWidgets */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */,
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||
BB000000000000000000000E /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -93,6 +156,8 @@
|
||||
children = (
|
||||
BB0000000000000000000007 /* FuelBoard.app */,
|
||||
BB0000000000000000000002 /* FuelBoardWidgets.appex */,
|
||||
ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */,
|
||||
ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -100,30 +165,50 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
BB000000000000000000000F /* FuelBoard */ = {
|
||||
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BB0000000000000000000010 /* Build configuration list for PBXNativeTarget "FuelBoard" */;
|
||||
buildConfigurationList = ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */;
|
||||
buildPhases = (
|
||||
BB0000000000000000000011 /* Sources */,
|
||||
BB000000000000000000000B /* Frameworks */,
|
||||
BB0000000000000000000012 /* Resources */,
|
||||
BB0000000000000000000006 /* Embed App Extensions */,
|
||||
ADCE5B4B3033A329008C131C /* Sources */,
|
||||
ADCE5B4C3033A329008C131C /* Frameworks */,
|
||||
ADCE5B4D3033A329008C131C /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BB0000000000000000000013 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
ADCE5B503033A329008C131C /* FuelBoardWatch Watch App */,
|
||||
);
|
||||
name = FuelBoard;
|
||||
name = "FuelBoardWatch Watch App Extension";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = FuelBoard;
|
||||
productReference = BB0000000000000000000007 /* FuelBoard.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
productName = "FuelBoardWatch Watch App Extension";
|
||||
productReference = ADCE5B633033A32D008C131C /* FuelBoardWatch Watch App Extension.appex */;
|
||||
productType = "com.apple.product-type.watchkit2-extension";
|
||||
};
|
||||
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = ADCE5B6A3033A32D008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */;
|
||||
buildPhases = (
|
||||
ADCE5B6E3033A32E008C131C /* Resources */,
|
||||
ADCE5B653033A32D008C131C /* Embed App Extensions */,
|
||||
ADCE5B6D3033A32E008C131C /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
ADCE5B683033A32D008C131C /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
ADCE5B663033A32D008C131C /* FuelBoardWatch Wrapper */,
|
||||
);
|
||||
name = "FuelBoardWatch Watch App";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "FuelBoardWatch Watch App";
|
||||
productReference = ADCE5B4F3033A329008C131C /* FuelBoardWatch Watch App.app */;
|
||||
productType = "com.apple.product-type.application.watchapp2";
|
||||
};
|
||||
BB0000000000000000000005 /* FuelBoardWidgets */ = {
|
||||
isa = PBXNativeTarget;
|
||||
@@ -148,6 +233,33 @@
|
||||
productReference = BB0000000000000000000002 /* FuelBoardWidgets.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
BB000000000000000000000F /* FuelBoard */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BB0000000000000000000010 /* Build configuration list for PBXNativeTarget "FuelBoard" */;
|
||||
buildPhases = (
|
||||
BB0000000000000000000011 /* Sources */,
|
||||
BB000000000000000000000B /* Frameworks */,
|
||||
BB0000000000000000000012 /* Resources */,
|
||||
ADCE5B5B3033A32C008C131C /* Embed Watch Content */,
|
||||
BB0000000000000000000006 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
ADCE5B703033A32F008C131C /* PBXTargetDependency */,
|
||||
BB0000000000000000000013 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
BB0000000000000000000008 /* FuelBoard */,
|
||||
BB000000000000000000000A /* Shared */,
|
||||
);
|
||||
name = FuelBoard;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = FuelBoard;
|
||||
productReference = BB0000000000000000000007 /* FuelBoard.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -155,15 +267,18 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1600;
|
||||
LastSwiftUpdateCheck = 2630;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
BB000000000000000000000F = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
ADCE5B4E3033A329008C131C = {
|
||||
CreatedOnToolsVersion = 26.3;
|
||||
};
|
||||
BB0000000000000000000005 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
BB000000000000000000000F = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */;
|
||||
@@ -182,11 +297,27 @@
|
||||
targets = (
|
||||
BB000000000000000000000F /* FuelBoard */,
|
||||
BB0000000000000000000005 /* FuelBoardWidgets */,
|
||||
ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */,
|
||||
ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
ADCE5B4D3033A329008C131C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
ADCE5B6E3033A32E008C131C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000012 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -204,6 +335,13 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
ADCE5B4B3033A329008C131C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BB0000000000000000000011 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -221,6 +359,16 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
ADCE5B683033A32D008C131C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = ADCE5B4E3033A329008C131C /* FuelBoardWatch Watch App Extension */;
|
||||
targetProxy = ADCE5B643033A32D008C131C /* PBXContainerItemProxy */;
|
||||
};
|
||||
ADCE5B703033A32F008C131C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = ADCE5B673033A32D008C131C /* FuelBoardWatch Watch App */;
|
||||
targetProxy = ADCE5B6F3033A32F008C131C /* PBXContainerItemProxy */;
|
||||
};
|
||||
BB0000000000000000000013 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BB0000000000000000000005 /* FuelBoardWidgets */;
|
||||
@@ -229,6 +377,262 @@
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
ADCE5B573033A32B008C131C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = 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_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__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
INFOPLIST_FILE = Config/WatchExtension-Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp.watchkitextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
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 = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
ADCE5B583033A32B008C131C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = 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_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__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/WatchApp.entitlements;
|
||||
INFOPLIST_FILE = Config/WatchExtension-Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp.watchkitextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
ADCE5B6B3033A32D008C131C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = 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_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__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/WatchWrapper-Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
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 = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
ADCE5B6C3033A32D008C131C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = 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_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__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
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;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/WatchWrapper-Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.apt.fuelboard.watchapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BB0000000000000000000018 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -294,7 +698,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/App-Info.plist;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -317,7 +721,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/App-Info.plist;
|
||||
INFOPLIST_FILE = "Config/App-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -340,7 +744,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -365,7 +769,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = F3BE6NE7U3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Config/Widget-Info.plist;
|
||||
INFOPLIST_FILE = "Config/Widget-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -384,11 +788,20 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
||||
ADCE5B593033A32B008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BB0000000000000000000018 /* Debug */,
|
||||
BB0000000000000000000019 /* Release */,
|
||||
ADCE5B573033A32B008C131C /* Debug */,
|
||||
ADCE5B583033A32B008C131C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
ADCE5B6A3033A32D008C131C /* Build configuration list for PBXNativeTarget "FuelBoardWatch Watch App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
ADCE5B6B3033A32D008C131C /* Debug */,
|
||||
ADCE5B6C3033A32D008C131C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
@@ -411,6 +824,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BB0000000000000000000017 /* Build configuration list for PBXProject "FuelBoard" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BB0000000000000000000018 /* Debug */,
|
||||
BB0000000000000000000019 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = BB0000000000000000000004 /* Project object */;
|
||||
|
||||
@@ -384,7 +384,10 @@ struct ContentView: View {
|
||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||
updateLiveActivity()
|
||||
refreshRoadDistancesIfNeeded()
|
||||
// No network fetch on foreground — pull-to-refresh is the override.
|
||||
if FuelStore.hasPendingWatchRefreshRequest {
|
||||
Task { await handlePendingWatchRefreshRequest() }
|
||||
}
|
||||
// No network fetch on foreground unless the watch explicitly requests one.
|
||||
} else {
|
||||
locationManager.stopForegroundTracking()
|
||||
}
|
||||
@@ -831,6 +834,11 @@ struct ContentView: View {
|
||||
fuel: alertsFuel, radiusKM: effectiveAlertsRadiusKM)
|
||||
}
|
||||
|
||||
private func handlePendingWatchRefreshRequest() async {
|
||||
guard FuelStore.hasPendingWatchRefreshRequest else { return }
|
||||
await refresh(force: true)
|
||||
}
|
||||
|
||||
/// Fetches fresh prices, but only when the cache is stale — unless
|
||||
/// `force` is true (pull-to-refresh is the manual override).
|
||||
private func refresh(force: Bool = false) async {
|
||||
@@ -849,6 +857,9 @@ struct ContentView: View {
|
||||
stations = fetched
|
||||
FuelStore.saveStations(fetched)
|
||||
FuelStore.saveLastRefresh()
|
||||
if force, FuelStore.hasPendingWatchRefreshRequest {
|
||||
FuelStore.markWatchRefreshHandled()
|
||||
}
|
||||
// Persist envelope metadata (source, station count, GOV.UK
|
||||
// dataset update time) for the Settings → About section — the
|
||||
// live chain records whichever leg served the fetch.
|
||||
@@ -871,6 +882,9 @@ struct ContentView: View {
|
||||
// Live data restored — any status banner no longer applies.
|
||||
dataStatus = .live
|
||||
} catch {
|
||||
if force, FuelStore.hasPendingWatchRefreshRequest {
|
||||
FuelStore.markWatchRefreshHandled()
|
||||
}
|
||||
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached data."
|
||||
if FuelStore.loadStations().isEmpty {
|
||||
// No cached prices — last resort is the bundled REAL dump
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
private enum WatchFuelType: String, CaseIterable, Identifiable, Codable {
|
||||
case e10
|
||||
case e5
|
||||
case diesel
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .e10: return "Unleaded"
|
||||
case .e5: return "Premium"
|
||||
case .diesel: return "Diesel"
|
||||
}
|
||||
}
|
||||
|
||||
var shortName: String {
|
||||
switch self {
|
||||
case .e10: return "E10"
|
||||
case .e5: return "E5"
|
||||
case .diesel: return "B7"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .e10: return .green
|
||||
case .e5: return .yellow
|
||||
case .diesel: return .blue
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
switch self {
|
||||
case .e10: return "fuelpump.fill"
|
||||
case .e5: return "fuelpump.circle.fill"
|
||||
case .diesel: return "drop.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var sortRank: Int {
|
||||
switch self {
|
||||
case .e10: return 0
|
||||
case .e5: return 1
|
||||
case .diesel: return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum WatchDistanceUnit: String, Codable {
|
||||
case miles
|
||||
case kilometers
|
||||
|
||||
func format(km: Double) -> String {
|
||||
switch self {
|
||||
case .miles:
|
||||
return String(format: "%.1f mi", km * 0.621371)
|
||||
case .kilometers:
|
||||
return String(format: "%.1f km", km)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum WatchPriceStyle: String, Codable {
|
||||
case stationSign
|
||||
case poundsPence
|
||||
}
|
||||
|
||||
private struct WatchCoordinate {
|
||||
let lat: Double
|
||||
let lng: Double
|
||||
}
|
||||
|
||||
private struct WatchStation: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let brand: String
|
||||
let address: String
|
||||
let postcode: String
|
||||
let lat: Double
|
||||
let lng: Double
|
||||
let prices: [WatchFuelType: Double]
|
||||
let priceUpdated: TimeInterval?
|
||||
|
||||
func distanceKM(to location: WatchCoordinate) -> Double {
|
||||
let r = 6371.0
|
||||
let dLat = (location.lat - lat) * .pi / 180
|
||||
let dLng = (location.lng - lng) * .pi / 180
|
||||
let a = sin(dLat / 2) * sin(dLat / 2)
|
||||
+ cos(lat * .pi / 180) * cos(location.lat * .pi / 180)
|
||||
* sin(dLng / 2) * sin(dLng / 2)
|
||||
return r * 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
}
|
||||
}
|
||||
|
||||
private struct WatchFavouriteEntry: Codable, Identifiable {
|
||||
let station: WatchStation
|
||||
let fuel: WatchFuelType
|
||||
|
||||
var id: String { "\(fuel.rawValue)|\(station.id)" }
|
||||
}
|
||||
|
||||
private struct WatchFavouriteRow: Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let priceText: String
|
||||
let distanceText: String?
|
||||
let isCheapest: Bool
|
||||
}
|
||||
|
||||
private enum WatchRefreshState {
|
||||
case idle(lastUpdated: String)
|
||||
case stale(lastUpdated: String)
|
||||
case checking
|
||||
case requested(lastUpdated: String)
|
||||
case needsPhone
|
||||
case noData
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .idle(let lastUpdated): return "Updated \(lastUpdated)"
|
||||
case .stale(let lastUpdated): return "Cache from \(lastUpdated)"
|
||||
case .checking: return "Checking…"
|
||||
case .requested(let lastUpdated): return "Requested · \(lastUpdated)"
|
||||
case .needsPhone: return "Refresh on iPhone"
|
||||
case .noData: return "No cached prices yet"
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Color {
|
||||
switch self {
|
||||
case .idle: return .secondary
|
||||
case .stale: return .yellow
|
||||
case .checking: return .yellow
|
||||
case .requested: return .green
|
||||
case .needsPhone: return .orange
|
||||
case .noData: return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .idle: return "clock"
|
||||
case .stale: return "exclamationmark.circle"
|
||||
case .checking: return "arrow.triangle.2.circlepath"
|
||||
case .requested: return "checkmark.circle"
|
||||
case .needsPhone: return "iphone"
|
||||
case .noData: return "tray"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum WatchFuelCache {
|
||||
static let appGroupSuite = "group.com.apt.fuelboard"
|
||||
static let favouritesKey = "fuelboard.favourites"
|
||||
static let stationsKey = "fuelboard.stations"
|
||||
static let fuelKey = "fuelboard.selectedFuel"
|
||||
static let distanceUnitKey = "fuelboard.distanceUnit"
|
||||
static let priceStyleKey = "fuelboard.priceDisplayStyle"
|
||||
static let locationKey = "fuelboard.lastLocation"
|
||||
static let lastRefreshKey = "fuelboard.lastRefresh"
|
||||
static let watchRefreshRequestKey = "fuelboard.watchRefreshRequest"
|
||||
static let watchRefreshHandledKey = "fuelboard.watchRefreshHandled"
|
||||
static let refreshInterval: TimeInterval = 12 * 60 * 60
|
||||
|
||||
static func defaults() -> UserDefaults? {
|
||||
UserDefaults(suiteName: appGroupSuite)
|
||||
}
|
||||
|
||||
static func loadSelectedFuel() -> WatchFuelType {
|
||||
guard let raw = defaults()?.string(forKey: fuelKey), let fuel = WatchFuelType(rawValue: raw) else {
|
||||
return .e10
|
||||
}
|
||||
return fuel
|
||||
}
|
||||
|
||||
static func loadDistanceUnit() -> WatchDistanceUnit {
|
||||
guard let raw = defaults()?.string(forKey: distanceUnitKey), let unit = WatchDistanceUnit(rawValue: raw) else {
|
||||
return .miles
|
||||
}
|
||||
return unit
|
||||
}
|
||||
|
||||
static func loadPriceStyle() -> WatchPriceStyle {
|
||||
guard let raw = defaults()?.string(forKey: priceStyleKey), let style = WatchPriceStyle(rawValue: raw) else {
|
||||
return .stationSign
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
static func loadLocation() -> WatchCoordinate? {
|
||||
guard let raw = defaults()?.string(forKey: locationKey) else { return nil }
|
||||
let parts = raw.split(separator: ",").compactMap { Double($0) }
|
||||
guard parts.count >= 2 else { return nil }
|
||||
return WatchCoordinate(lat: parts[0], lng: parts[1])
|
||||
}
|
||||
|
||||
static func loadLastRefresh() -> Date? {
|
||||
guard let raw = defaults()?.string(forKey: lastRefreshKey), let ts = TimeInterval(raw) else { return nil }
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
|
||||
static func isCacheFresh(lastRefresh: Date?) -> Bool {
|
||||
guard let lastRefresh else { return false }
|
||||
return Date().timeIntervalSince(lastRefresh) < refreshInterval
|
||||
}
|
||||
|
||||
static func requestRefresh(_ date: Date = Date()) {
|
||||
defaults()?.set(String(date.timeIntervalSince1970), forKey: watchRefreshRequestKey)
|
||||
}
|
||||
|
||||
static func loadRefreshRequest() -> Date? {
|
||||
guard let raw = defaults()?.string(forKey: watchRefreshRequestKey), let ts = TimeInterval(raw) else { return nil }
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
|
||||
static func loadRefreshHandled() -> Date? {
|
||||
guard let raw = defaults()?.string(forKey: watchRefreshHandledKey), let ts = TimeInterval(raw) else { return nil }
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
|
||||
static func wasRefreshHandled(since requestDate: Date) -> Bool {
|
||||
guard let handled = loadRefreshHandled() else { return false }
|
||||
return handled >= requestDate
|
||||
}
|
||||
|
||||
static func loadStations() -> [WatchStation] {
|
||||
guard let data = defaults()?.data(forKey: stationsKey),
|
||||
let stations = try? JSONDecoder().decode([WatchStation].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return stations
|
||||
}
|
||||
|
||||
static func loadFavourites() -> [WatchFavouriteEntry] {
|
||||
guard let data = defaults()?.data(forKey: favouritesKey),
|
||||
let favourites = try? JSONDecoder().decode([WatchFavouriteEntry].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return favourites
|
||||
}
|
||||
|
||||
static func refreshedFavourites(_ favourites: [WatchFavouriteEntry], from stations: [WatchStation]) -> [WatchFavouriteEntry] {
|
||||
favourites.map { favourite in
|
||||
guard let fresh = stations.first(where: { $0.id == favourite.station.id }) else { return favourite }
|
||||
return WatchFavouriteEntry(station: fresh, fuel: favourite.fuel)
|
||||
}
|
||||
}
|
||||
|
||||
static func cheapestFavourite(in favourites: [WatchFavouriteEntry], fuel: WatchFuelType) -> WatchFavouriteEntry? {
|
||||
var best: WatchFavouriteEntry?
|
||||
for favourite in favourites where favourite.fuel == fuel {
|
||||
guard let price = favourite.station.prices[fuel] else { continue }
|
||||
guard let current = best, let currentPrice = current.station.prices[fuel] else {
|
||||
best = favourite
|
||||
continue
|
||||
}
|
||||
if price < currentPrice {
|
||||
best = favourite
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
static func priceText(_ pence: Double, style: WatchPriceStyle) -> String {
|
||||
switch style {
|
||||
case .stationSign:
|
||||
return String(format: "%.1f", pence)
|
||||
case .poundsPence:
|
||||
let tenths = Int((pence * 10).rounded())
|
||||
let whole = tenths / 1000
|
||||
let major = (tenths % 1000) / 10
|
||||
let minor = tenths % 10
|
||||
let superscripts: [Character] = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"]
|
||||
return String(format: "£%d.%02d", whole, major) + String(superscripts[minor]) + "/L"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var fuel: WatchFuelType = WatchFuelCache.loadSelectedFuel()
|
||||
@State private var refreshState: WatchRefreshState = .noData
|
||||
@State private var favourites: [WatchFavouriteEntry] = []
|
||||
@State private var stations: [WatchStation] = []
|
||||
@State private var lastLocation: WatchCoordinate?
|
||||
@State private var distanceUnit: WatchDistanceUnit = WatchFuelCache.loadDistanceUnit()
|
||||
@State private var priceStyle: WatchPriceStyle = WatchFuelCache.loadPriceStyle()
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $fuel) {
|
||||
ForEach(WatchFuelType.allCases) { fuelCase in
|
||||
fuelPage(for: fuelCase)
|
||||
.tag(fuelCase)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.verticalPage)
|
||||
.onAppear(perform: reloadFromCache)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func fuelPage(for fuelCase: WatchFuelType) -> some View {
|
||||
let fuelFavourites = favouritesForFuel(fuelCase)
|
||||
let cheapest = WatchFuelCache.cheapestFavourite(in: fuelFavourites, fuel: fuelCase)
|
||||
let rows = rows(for: fuelCase)
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
header(for: fuelCase)
|
||||
|
||||
if let cheapest {
|
||||
cheapestCard(for: fuelCase, favourite: cheapest)
|
||||
} else {
|
||||
emptyCheapestCard(for: fuelCase)
|
||||
}
|
||||
|
||||
statusRow
|
||||
|
||||
Button {
|
||||
runCheckNow()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: refreshState.icon)
|
||||
Text("Check now")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(fuelCase.color)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Favourites")
|
||||
.font(.headline)
|
||||
|
||||
if rows.isEmpty {
|
||||
Text("No favourites for \(fuelCase.displayName.lowercased()) yet.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(rows) { row in
|
||||
favouriteRow(row, fuel: fuelCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
|
||||
private func favouritesForFuel(_ fuelCase: WatchFuelType) -> [WatchFavouriteEntry] {
|
||||
favourites.filter { $0.fuel == fuelCase }
|
||||
}
|
||||
|
||||
private func rows(for fuelCase: WatchFuelType) -> [WatchFavouriteRow] {
|
||||
let fuelFavourites = favouritesForFuel(fuelCase)
|
||||
let cheapest = WatchFuelCache.cheapestFavourite(in: fuelFavourites, fuel: fuelCase)
|
||||
|
||||
return fuelFavourites.map { favourite in
|
||||
let distanceText = lastLocation.map { distanceUnit.format(km: favourite.station.distanceKM(to: $0)) }
|
||||
let priceText = favourite.station.prices[fuelCase].map { WatchFuelCache.priceText($0, style: priceStyle) } ?? "—"
|
||||
return WatchFavouriteRow(
|
||||
id: favourite.id,
|
||||
name: favourite.station.name,
|
||||
priceText: priceText,
|
||||
distanceText: distanceText,
|
||||
isCheapest: favourite.id == cheapest?.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func header(for fuel: WatchFuelType) -> some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
Image(systemName: fuel.symbol)
|
||||
.foregroundStyle(fuel.color)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("FuelBoard")
|
||||
.font(.headline)
|
||||
Text(fuel.displayName)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Text(fuel.shortName)
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(fuel.color.opacity(0.18), in: Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
private func cheapestCard(for fuel: WatchFuelType, favourite: WatchFavouriteEntry) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Cheapest favourite")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(favourite.station.name)
|
||||
.font(.headline)
|
||||
.lineLimit(2)
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(favourite.station.prices[fuel].map { WatchFuelCache.priceText($0, style: priceStyle) } ?? "—")
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
if let location = lastLocation {
|
||||
Text(distanceUnit.format(km: favourite.station.distanceKM(to: location)))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(fuel.color.opacity(0.15), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
}
|
||||
|
||||
private func emptyCheapestCard(for fuel: WatchFuelType) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Cheapest favourite")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("No favourite saved")
|
||||
.font(.headline)
|
||||
Text("Add favourites on iPhone to see them here.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(fuel.color.opacity(0.15), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
}
|
||||
|
||||
private var statusRow: some View {
|
||||
Label(refreshState.label, systemImage: refreshState.icon)
|
||||
.font(.caption)
|
||||
.foregroundStyle(refreshState.tint)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private func favouriteRow(_ favourite: WatchFavouriteRow, fuel: WatchFuelType) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 4) {
|
||||
Text(favourite.name)
|
||||
.font(.caption)
|
||||
.fontWeight(favourite.isCheapest ? .semibold : .regular)
|
||||
.lineLimit(1)
|
||||
if favourite.isCheapest {
|
||||
Text("TOP")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 2)
|
||||
.background(fuel.color.opacity(0.18), in: Capsule())
|
||||
}
|
||||
}
|
||||
if let distanceText = favourite.distanceText {
|
||||
Text(distanceText)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(favourite.priceText)
|
||||
.font(.callout.monospacedDigit())
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
}
|
||||
|
||||
private func reloadFromCache() {
|
||||
stations = WatchFuelCache.loadStations()
|
||||
favourites = WatchFuelCache.refreshedFavourites(WatchFuelCache.loadFavourites(), from: stations)
|
||||
distanceUnit = WatchFuelCache.loadDistanceUnit()
|
||||
priceStyle = WatchFuelCache.loadPriceStyle()
|
||||
lastLocation = WatchFuelCache.loadLocation()
|
||||
refreshState = statusFromCache()
|
||||
|
||||
let availableFuels = Set(favourites.map(\.fuel))
|
||||
if !availableFuels.isEmpty, !availableFuels.contains(fuel) {
|
||||
fuel = availableFuels.sorted { $0.sortRank < $1.sortRank }.first ?? .e10
|
||||
}
|
||||
}
|
||||
|
||||
private func statusFromCache() -> WatchRefreshState {
|
||||
guard !stations.isEmpty else { return .noData }
|
||||
guard let lastRefresh = WatchFuelCache.loadLastRefresh() else { return .stale(lastUpdated: "earlier") }
|
||||
let formatted = relativeRefreshText(for: lastRefresh)
|
||||
return WatchFuelCache.isCacheFresh(lastRefresh: lastRefresh)
|
||||
? .idle(lastUpdated: formatted)
|
||||
: .stale(lastUpdated: formatted)
|
||||
}
|
||||
|
||||
private func runCheckNow() {
|
||||
refreshState = .checking
|
||||
let requestDate = Date()
|
||||
WatchFuelCache.requestRefresh(requestDate)
|
||||
Task {
|
||||
for _ in 0..<12 {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
if WatchFuelCache.wasRefreshHandled(since: requestDate) {
|
||||
await MainActor.run {
|
||||
reloadFromCache()
|
||||
let label = WatchFuelCache.loadLastRefresh().map(relativeRefreshText(for:)) ?? "just now"
|
||||
refreshState = .requested(lastUpdated: label)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
reloadFromCache()
|
||||
refreshState = .needsPhone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func relativeRefreshText(for date: Date) -> String {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .short
|
||||
return formatter.localizedString(for: date, relativeTo: Date())
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct FuelBoardWatchApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,8 @@ struct FuelStore {
|
||||
static let liveActivityRadiusKey = "fuelboard.liveActivityRadiusMiles" // Int miles (5/10/15)
|
||||
static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
|
||||
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
|
||||
static let watchRefreshRequestKey = "fuelboard.watchRefreshRequest" // TimeInterval (seconds since 1970)
|
||||
static let watchRefreshHandledKey = "fuelboard.watchRefreshHandled" // TimeInterval (seconds since 1970)
|
||||
static let relaySourceKey = "fuelboard.relaySource" // String — "api" | "csv"
|
||||
static let stationCountKey = "fuelboard.stationCount" // String — station count
|
||||
static let dataUpdatedKey = "fuelboard.dataUpdated" // String — govUK dataset update time
|
||||
@@ -877,6 +879,34 @@ struct FuelStore {
|
||||
saveString(String(date.timeIntervalSince1970), service: lastRefreshKey)
|
||||
}
|
||||
|
||||
static func requestWatchRefresh(_ date: Date = Date()) {
|
||||
saveString(String(date.timeIntervalSince1970), service: watchRefreshRequestKey)
|
||||
}
|
||||
|
||||
static func loadWatchRefreshRequest() -> Date? {
|
||||
if let raw = loadString(service: watchRefreshRequestKey), let ts = TimeInterval(raw) {
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func markWatchRefreshHandled(_ date: Date = Date()) {
|
||||
saveString(String(date.timeIntervalSince1970), service: watchRefreshHandledKey)
|
||||
}
|
||||
|
||||
static func loadWatchRefreshHandled() -> Date? {
|
||||
if let raw = loadString(service: watchRefreshHandledKey), let ts = TimeInterval(raw) {
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static var hasPendingWatchRefreshRequest: Bool {
|
||||
guard let requested = loadWatchRefreshRequest() else { return false }
|
||||
guard let handled = loadWatchRefreshHandled() else { return true }
|
||||
return requested > handled
|
||||
}
|
||||
|
||||
// MARK: Relay metadata — shown in Settings → About. Written after each
|
||||
// successful full fetch so the About section reflects the live source.
|
||||
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
PROJECT="$PWD/FuelBoard.xcodeproj"
|
||||
SCHEME="FuelBoard"
|
||||
DERIVED_DATA="/tmp/fuelboard-build-ipas"
|
||||
SERVE_DIR="/tmp/ipa-serve"
|
||||
FULL_IPA="$SERVE_DIR/FuelBoard-watch.ipa"
|
||||
SIDELOAD_IPA="$SERVE_DIR/FuelBoard-sideload.ipa"
|
||||
HOST="192.168.1.131"
|
||||
PORT="8765"
|
||||
|
||||
APP_PATH="$DERIVED_DATA/Build/Products/Release-iphoneos/FuelBoard.app"
|
||||
STAGE_FULL="/tmp/fuelboard-ipa-stage-full"
|
||||
STAGE_SIDELOAD="/tmp/fuelboard-ipa-stage-sideload"
|
||||
|
||||
echo "==> Refresh bundled dump"
|
||||
"$PWD/scripts/refresh_bundled_dump.sh"
|
||||
|
||||
echo "==> Clean build dirs"
|
||||
rm -rf "$DERIVED_DATA" "$STAGE_FULL" "$STAGE_SIDELOAD"
|
||||
mkdir -p "$SERVE_DIR"
|
||||
|
||||
echo "==> Build Release app (generic iOS destination)"
|
||||
xcodebuild \
|
||||
-project "$PROJECT" \
|
||||
-scheme "$SCHEME" \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-derivedDataPath "$DERIVED_DATA" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
CODE_SIGN_IDENTITY='' \
|
||||
build >/tmp/fuelboard-build-ipas-xcodebuild.log
|
||||
|
||||
if [ ! -d "$APP_PATH" ]; then
|
||||
echo "missing built app: $APP_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Package full watch-capable IPA"
|
||||
python3 - <<'PY'
|
||||
import os, shutil, zipfile
|
||||
app = '/tmp/fuelboard-build-ipas/Build/Products/Release-iphoneos/FuelBoard.app'
|
||||
stage = '/tmp/fuelboard-ipa-stage-full'
|
||||
payload = os.path.join(stage, 'Payload')
|
||||
out = '/tmp/ipa-serve/FuelBoard-watch.ipa'
|
||||
if os.path.exists(stage):
|
||||
shutil.rmtree(stage)
|
||||
os.makedirs(payload, exist_ok=True)
|
||||
shutil.copytree(app, os.path.join(payload, 'FuelBoard.app'))
|
||||
tmp = out + '.tmp'
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
with zipfile.ZipFile(tmp, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for root, dirs, files in os.walk(stage):
|
||||
dirs.sort(); files.sort()
|
||||
for name in dirs:
|
||||
path = os.path.join(root, name)
|
||||
zf.write(path, os.path.relpath(path, stage))
|
||||
for name in files:
|
||||
path = os.path.join(root, name)
|
||||
zf.write(path, os.path.relpath(path, stage))
|
||||
os.replace(tmp, out)
|
||||
print(out)
|
||||
PY
|
||||
|
||||
echo "==> Package sideload-safe IPA (strip Watch/)"
|
||||
python3 - <<'PY'
|
||||
import os, shutil, zipfile
|
||||
app = '/tmp/fuelboard-build-ipas/Build/Products/Release-iphoneos/FuelBoard.app'
|
||||
stage = '/tmp/fuelboard-ipa-stage-sideload'
|
||||
payload = os.path.join(stage, 'Payload')
|
||||
out = '/tmp/ipa-serve/FuelBoard-sideload.ipa'
|
||||
if os.path.exists(stage):
|
||||
shutil.rmtree(stage)
|
||||
os.makedirs(payload, exist_ok=True)
|
||||
dst = os.path.join(payload, 'FuelBoard.app')
|
||||
shutil.copytree(app, dst)
|
||||
watch_dir = os.path.join(dst, 'Watch')
|
||||
if os.path.exists(watch_dir):
|
||||
shutil.rmtree(watch_dir)
|
||||
tmp = out + '.tmp'
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
with zipfile.ZipFile(tmp, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for root, dirs, files in os.walk(stage):
|
||||
dirs.sort(); files.sort()
|
||||
for name in dirs:
|
||||
path = os.path.join(root, name)
|
||||
zf.write(path, os.path.relpath(path, stage))
|
||||
for name in files:
|
||||
path = os.path.join(root, name)
|
||||
zf.write(path, os.path.relpath(path, stage))
|
||||
os.replace(tmp, out)
|
||||
print(out)
|
||||
PY
|
||||
|
||||
echo "==> Verify artifacts"
|
||||
ls -lh "$FULL_IPA" "$SIDELOAD_IPA"
|
||||
python3 - <<'PY'
|
||||
import os, plistlib, json
|
||||
base = '/tmp/fuelboard-build-ipas/Build/Products/Release-iphoneos/FuelBoard.app'
|
||||
paths = {
|
||||
'host': os.path.join(base, 'Info.plist'),
|
||||
'watch': os.path.join(base, 'Watch', 'FuelBoardWatch Watch App.app', 'Info.plist'),
|
||||
'watch_ext': os.path.join(base, 'Watch', 'FuelBoardWatch Watch App.app', 'PlugIns', 'FuelBoardWatch Watch App Extension.appex', 'Info.plist'),
|
||||
}
|
||||
for name, path in paths.items():
|
||||
print(f'## {name} {os.path.exists(path)}')
|
||||
if os.path.exists(path):
|
||||
with open(path, 'rb') as f:
|
||||
pl = plistlib.load(f)
|
||||
print(json.dumps({k: pl.get(k) for k in ['CFBundleIdentifier','CFBundleShortVersionString','CFBundleVersion']}, indent=2))
|
||||
PY
|
||||
|
||||
echo "==> URLs"
|
||||
echo "Watch-capable IPA: http://$HOST:$PORT/FuelBoard-watch.ipa"
|
||||
echo "Sideload-safe IPA: http://$HOST:$PORT/FuelBoard-sideload.ipa"
|
||||
echo "Xcode log: /tmp/fuelboard-build-ipas-xcodebuild.log"
|
||||
Reference in New Issue
Block a user