diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 0e6b1e5..c928a22 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.ascently"
minSdk = 31
targetSdk = 36
- versionCode = 46
- versionName = "2.2.1"
+ versionCode = 4
+ versionName = "2.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -38,7 +38,10 @@ android {
java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }
- buildFeatures { compose = true }
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
}
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
diff --git a/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt b/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt
index d2a1964..ebcec39 100644
--- a/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt
+++ b/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt
@@ -3,11 +3,10 @@ package com.atridad.ascently.utils
import android.util.Log
import com.atridad.ascently.BuildConfig
-/**
- * Centralized logging utility to ensure all mobile logging happens only in debug builds.
- */
object AppLogger {
+ private const val DEFAULT_TAG = "Ascently"
+
enum class Level(val androidLevel: Int) {
DEBUG(Log.DEBUG),
INFO(Log.INFO),
@@ -46,6 +45,4 @@ object AppLogger {
Log.println(level.androidLevel, tag, message)
}
}
-
- private const val DEFAULT_TAG = "Ascently"
}
diff --git a/docs/package.json b/docs/package.json
index 85bc4bf..a948604 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -27,7 +27,7 @@
"dependencies": {
"@astrojs/node": "^9.5.1",
"@astrojs/starlight": "^0.36.2",
- "astro": "^5.15.9",
+ "astro": "^5.16.0",
"qrcode": "^1.5.4",
"sharp": "^0.34.5"
},
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
index eb2b905..9fd4330 100644
--- a/docs/pnpm-lock.yaml
+++ b/docs/pnpm-lock.yaml
@@ -10,13 +10,13 @@ importers:
dependencies:
'@astrojs/node':
specifier: ^9.5.1
- version: 9.5.1(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
+ version: 9.5.1(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
'@astrojs/starlight':
specifier: ^0.36.2
- version: 0.36.2(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
+ version: 0.36.2(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
astro:
- specifier: ^5.15.9
- version: 5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
+ specifier: ^5.16.0
+ version: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
qrcode:
specifier: ^1.5.4
version: 1.5.4
@@ -39,8 +39,8 @@ packages:
'@astrojs/markdown-remark@6.3.9':
resolution: {integrity: sha512-hX2cLC/KW74Io1zIbn92kI482j9J7LleBLGCVU9EP3BeH5MVrnFawOnqD0t/q6D1Z+ZNeQG2gNKMslCcO36wng==}
- '@astrojs/mdx@4.3.11':
- resolution: {integrity: sha512-ca18jxAiYDbPE1eAsNoiGnZoMYZGtfQpCmAJMXCB1WpyzTOHH7+KP1+gnKK8SFEA6XjHvjwI5Xzu8695c0Gabw==}
+ '@astrojs/mdx@4.3.12':
+ resolution: {integrity: sha512-pL3CVPtuQrPnDhWjy7zqbOibNyPaxP4VpQS8T8spwKqKzauJ4yoKyNkVTD8jrP7EAJHmBhZ7PTmUGZqOpKKp8g==}
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
peerDependencies:
astro: ^5.0.0
@@ -694,8 +694,8 @@ packages:
peerDependencies:
astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0
- astro@5.15.9:
- resolution: {integrity: sha512-XLDXxu0282cC/oYHswWZm3johGlRvk9rLRS7pWVWSne+HsZe9JgrpHI+vewAJSSNHBGd1aCyaQOElT5RNGe7IQ==}
+ astro@5.16.0:
+ resolution: {integrity: sha512-GaDRs2Mngpw3dr2vc085GnORh98NiXxwIjg/EoQQQl/icZt3Z7s0BRsYHDZ8swkZbOA6wZsqWJdrNirl+iKcDg==}
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
hasBin: true
@@ -791,6 +791,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
common-ancestor-path@1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
@@ -804,18 +808,33 @@ packages:
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
css-selector-parser@3.2.0:
resolution: {integrity: sha512-L1bdkNKUP5WYxiW5dW6vA2hd3sL8BdRNLy2FCX0rLVise4eNw9nBdeBuJHxlELieSE2H1f6bYQFfwVUwWCV9rQ==}
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -877,6 +896,19 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dset@3.1.4:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
@@ -894,6 +926,10 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -1077,8 +1113,8 @@ packages:
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
- http-errors@2.0.0:
- resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
i18next@23.16.8:
@@ -1219,6 +1255,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
@@ -1334,9 +1373,9 @@ packages:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
- mime-types@3.0.1:
- resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
- engines: {node: '>= 0.6'}
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
@@ -1433,6 +1472,9 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
+ piccolore@0.1.3:
+ resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1636,10 +1678,6 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
- statuses@2.0.1:
- resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
- engines: {node: '>= 0.8'}
-
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@@ -1672,6 +1710,11 @@ packages:
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+ svgo@4.0.0:
+ resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
+ engines: {node: '>=16'}
+ hasBin: true
+
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@@ -1769,8 +1812,8 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
- unstorage@1.17.2:
- resolution: {integrity: sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==}
+ unstorage@1.17.3:
+ resolution: {integrity: sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q==}
peerDependencies:
'@azure/app-configuration': ^1.8.0
'@azure/cosmos': ^4.2.0
@@ -1992,16 +2035,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@astrojs/mdx@4.3.11(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
+ '@astrojs/mdx@4.3.12(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
dependencies:
'@astrojs/markdown-remark': 6.3.9
'@mdx-js/mdx': 3.1.1
acorn: 8.15.0
- astro: 5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
+ astro: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
es-module-lexer: 1.7.0
estree-util-visit: 2.0.0
hast-util-to-html: 9.0.5
- picocolors: 1.1.1
+ piccolore: 0.1.3
rehype-raw: 7.0.0
remark-gfm: 4.0.1
remark-smartypants: 3.0.2
@@ -2011,10 +2054,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@astrojs/node@9.5.1(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
+ '@astrojs/node@9.5.1(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
dependencies:
'@astrojs/internal-helpers': 0.7.5
- astro: 5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
+ astro: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
send: 1.2.0
server-destroy: 1.0.1
transitivePeerDependencies:
@@ -2030,17 +2073,17 @@ snapshots:
stream-replace-string: 2.0.0
zod: 3.25.76
- '@astrojs/starlight@0.36.2(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
+ '@astrojs/starlight@0.36.2(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))':
dependencies:
'@astrojs/markdown-remark': 6.3.9
- '@astrojs/mdx': 4.3.11(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
+ '@astrojs/mdx': 4.3.12(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
'@astrojs/sitemap': 3.6.0
'@pagefind/default-ui': 1.4.0
'@types/hast': 3.0.4
'@types/js-yaml': 4.0.9
'@types/mdast': 4.0.4
- astro: 5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
- astro-expressive-code: 0.41.3(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
+ astro: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
+ astro-expressive-code: 0.41.3(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3))
bcp-47: 2.1.0
hast-util-from-html: 2.0.3
hast-util-select: 6.0.4
@@ -2552,12 +2595,12 @@ snapshots:
astring@1.9.0: {}
- astro-expressive-code@0.41.3(astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)):
+ astro-expressive-code@0.41.3(astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)):
dependencies:
- astro: 5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
+ astro: 5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3)
rehype-expressive-code: 0.41.3
- astro@5.15.9(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3):
+ astro@5.16.0(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.3):
dependencies:
'@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5
@@ -2598,20 +2641,21 @@ snapshots:
p-limit: 6.2.0
p-queue: 8.1.1
package-manager-detector: 1.5.0
- picocolors: 1.1.1
+ piccolore: 0.1.3
picomatch: 4.0.3
prompts: 2.4.2
rehype: 13.0.2
semver: 7.7.3
shiki: 3.15.0
smol-toml: 1.5.2
+ svgo: 4.0.0
tinyexec: 1.0.2
tinyglobby: 0.2.15
tsconfck: 3.1.6(typescript@5.9.3)
ultrahtml: 1.6.0
unifont: 0.6.0
unist-util-visit: 5.0.0
- unstorage: 1.17.2
+ unstorage: 1.17.3
vfile: 6.0.3
vite: 6.4.1(@types/node@24.10.1)
vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1))
@@ -2735,6 +2779,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
+ commander@11.1.0: {}
+
common-ancestor-path@1.0.1: {}
cookie-es@1.2.2: {}
@@ -2745,15 +2791,34 @@ snapshots:
dependencies:
uncrypto: 0.1.3
+ css-select@5.2.2:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
css-selector-parser@3.2.0: {}
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.1
+
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
+ css-what@6.2.2: {}
+
cssesc@3.0.0: {}
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -2794,6 +2859,24 @@ snapshots:
dlv@1.1.3: {}
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dset@3.1.4: {}
ee-first@1.1.1: {}
@@ -2804,6 +2887,8 @@ snapshots:
encodeurl@2.0.0: {}
+ entities@4.5.0: {}
+
entities@6.0.1: {}
es-module-lexer@1.7.0: {}
@@ -3153,12 +3238,12 @@ snapshots:
http-cache-semantics@4.2.0: {}
- http-errors@2.0.0:
+ http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
- statuses: 2.0.1
+ statuses: 2.0.2
toidentifier: 1.0.1
i18next@23.16.8:
@@ -3411,6 +3496,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ mdn-data@2.0.28: {}
+
mdn-data@2.12.2: {}
micromark-core-commonmark@2.0.3:
@@ -3689,7 +3776,7 @@ snapshots:
mime-db@1.54.0: {}
- mime-types@3.0.1:
+ mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
@@ -3794,6 +3881,8 @@ snapshots:
path-exists@4.0.0: {}
+ piccolore@0.1.3: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -4049,8 +4138,8 @@ snapshots:
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
- http-errors: 2.0.0
- mime-types: 3.0.1
+ http-errors: 2.0.1
+ mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
@@ -4123,8 +4212,6 @@ snapshots:
space-separated-tokens@2.0.2: {}
- statuses@2.0.1: {}
-
statuses@2.0.2: {}
stream-replace-string@2.0.0: {}
@@ -4162,6 +4249,16 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
+ svgo@4.0.0:
+ dependencies:
+ commander: 11.1.0
+ css-select: 5.2.2
+ css-tree: 3.1.0
+ css-what: 6.2.2
+ csso: 5.0.5
+ picocolors: 1.1.1
+ sax: 1.4.3
+
tiny-inflate@1.0.3: {}
tinyexec@1.0.2: {}
@@ -4267,7 +4364,7 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
- unstorage@1.17.2:
+ unstorage@1.17.3:
dependencies:
anymatch: 3.1.3
chokidar: 4.0.3
diff --git a/ios/Ascently.xcodeproj/project.pbxproj b/ios/Ascently.xcodeproj/project.pbxproj
index 1eaa810..658b5f3 100644
--- a/ios/Ascently.xcodeproj/project.pbxproj
+++ b/ios/Ascently.xcodeproj/project.pbxproj
@@ -465,7 +465,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 31;
+ CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -487,7 +487,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
- MARKETING_VERSION = 2.2.1;
+ MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -513,7 +513,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 31;
+ CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -535,7 +535,7 @@
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.6;
- MARKETING_VERSION = 2.2.1;
+ MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -602,7 +602,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 31;
+ CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -613,7 +613,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.2.1;
+ MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -632,7 +632,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 31;
+ CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -643,7 +643,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.2.1;
+ MARKETING_VERSION = 2.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate
index 927ebb7..af9392d 100644
Binary files a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/ios/Ascently/AppIntents/AscentlyShortcuts.swift b/ios/Ascently/AppIntents/AscentlyShortcuts.swift
new file mode 100644
index 0000000..7dd86a6
--- /dev/null
+++ b/ios/Ascently/AppIntents/AscentlyShortcuts.swift
@@ -0,0 +1,33 @@
+import AppIntents
+
+/// Provides a curated list of the most useful Ascently shortcuts for Siri and the Shortcuts app.
+/// Surfaces intents that users can trigger hands-free to manage their climbing sessions.
+struct AscentlyShortcuts: AppShortcutsProvider {
+
+ static var shortcutTileColor: ShortcutTileColor {
+ .teal
+ }
+
+ static var appShortcuts: [AppShortcut] {
+ return [
+ AppShortcut(
+ intent: StartLastGymSessionIntent(),
+ phrases: [
+ "Start my climb in \(.applicationName)",
+ "Begin my last gym session in \(.applicationName)",
+ ],
+ shortTitle: "Start Climb",
+ systemImageName: "figure.climbing"
+ ),
+ AppShortcut(
+ intent: EndActiveSessionIntent(),
+ phrases: [
+ "Finish my climb in \(.applicationName)",
+ "End my session in \(.applicationName)",
+ ],
+ shortTitle: "End Climb",
+ systemImageName: "flag.checkered"
+ ),
+ ]
+ }
+}
diff --git a/ios/Ascently/AppIntents/EndActiveSessionIntent.swift b/ios/Ascently/AppIntents/EndActiveSessionIntent.swift
new file mode 100644
index 0000000..f71530d
--- /dev/null
+++ b/ios/Ascently/AppIntents/EndActiveSessionIntent.swift
@@ -0,0 +1,40 @@
+import AppIntents
+import Foundation
+
+/// Ends the currently active climbing session so logging stays in sync across devices.
+/// Exposed to Shortcuts so users can wrap up a session without opening the app.
+struct EndActiveSessionIntent: AppIntent {
+
+ static var title: LocalizedStringResource {
+ "End Active Session"
+ }
+
+ static var description: IntentDescription {
+ IntentDescription(
+ "Stop the active climbing session and save its progress in Ascently."
+ )
+ }
+
+ static var openAppWhenRun: Bool {
+ false
+ }
+
+ func perform() async throws -> some IntentResult & ProvidesDialog {
+ do {
+ let summary = try await SessionIntentController().endActiveSession()
+ let dialog = IntentDialog("Session at \(summary.gymName) ended. Nice work!")
+ return .result(dialog: dialog)
+ } catch SessionIntentError.noActiveSession {
+ // No active session is fine - just return a friendly message
+ let dialog = IntentDialog("No active session to end.")
+ return .result(dialog: dialog)
+ } catch {
+ // Re-throw other errors
+ throw error
+ }
+ }
+
+ static var parameterSummary: some ParameterSummary {
+ Summary("End my current climbing session")
+ }
+}
diff --git a/ios/Ascently/AppIntents/SessionIntentSupport.swift b/ios/Ascently/AppIntents/SessionIntentSupport.swift
new file mode 100644
index 0000000..4f21dca
--- /dev/null
+++ b/ios/Ascently/AppIntents/SessionIntentSupport.swift
@@ -0,0 +1,95 @@
+import Foundation
+
+/// User-visible errors that can arise while handling session-related intents.
+enum SessionIntentError: LocalizedError {
+ case noRecentGym
+ case noActiveSession
+ case failedToStartSession
+ case failedToEndSession
+
+ var errorDescription: String? {
+ switch self {
+ case .noRecentGym:
+ return "There's no recent gym to start a session with."
+ case .noActiveSession:
+ return "There isn't an active session to end right now."
+ case .failedToStartSession:
+ return "Ascently couldn't start a new session."
+ case .failedToEndSession:
+ return "Ascently couldn't finish the active session."
+ }
+ }
+}
+
+struct SessionIntentSummary: Sendable {
+ let sessionId: UUID
+ let gymName: String
+ let status: SessionStatus
+}
+
+/// Central controller that exposes the minimal climbing session operations used by App Intents and shortcuts.
+@MainActor
+final class SessionIntentController {
+
+ private let dataManager: ClimbingDataManager
+
+ init(dataManager: ClimbingDataManager = .shared) {
+ self.dataManager = dataManager
+ }
+
+ /// Starts a new session using the most recently visited gym.
+ func startSessionWithLastUsedGym() async throws -> SessionIntentSummary {
+ // Give a moment for data to be ready if app just launched
+ if dataManager.gyms.isEmpty {
+ try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
+ }
+
+ guard let lastGym = dataManager.getLastUsedGym() else {
+ logFailure(.noRecentGym, context: "No recorded sessions available")
+ throw SessionIntentError.noRecentGym
+ }
+
+ guard let startedSession = await dataManager.startSessionAsync(gymId: lastGym.id) else {
+ logFailure(.failedToStartSession, context: "Data manager failed to create new session")
+ throw SessionIntentError.failedToStartSession
+ }
+
+ return SessionIntentSummary(
+ sessionId: startedSession.id,
+ gymName: lastGym.name,
+ status: startedSession.status
+ )
+ }
+
+ /// Ends the currently active climbing session, if one exists.
+ func endActiveSession() async throws -> SessionIntentSummary {
+ guard let activeSession = dataManager.activeSession else {
+ logFailure(.noActiveSession, context: "No active session stored in data manager")
+ throw SessionIntentError.noActiveSession
+ }
+
+ guard let completedSession = await dataManager.endSessionAsync(activeSession.id) else {
+ logFailure(
+ .failedToEndSession, context: "Data manager failed to complete active session")
+ throw SessionIntentError.failedToEndSession
+ }
+
+ guard let gym = dataManager.gym(withId: completedSession.gymId) else {
+ logFailure(
+ .failedToEndSession,
+ context: "Gym missing for completed session \(completedSession.id)")
+ throw SessionIntentError.failedToEndSession
+ }
+
+ return SessionIntentSummary(
+ sessionId: completedSession.id,
+ gymName: gym.name,
+ status: completedSession.status
+ )
+ }
+
+ private func logFailure(_ error: SessionIntentError, context: String) {
+ // Logging from intent context - errors are visible to user via dialog
+ print("SessionIntentError: \(error). Context: \(context)")
+ }
+}
diff --git a/ios/Ascently/AppIntents/StartLastGymSessionIntent.swift b/ios/Ascently/AppIntents/StartLastGymSessionIntent.swift
new file mode 100644
index 0000000..4af91d8
--- /dev/null
+++ b/ios/Ascently/AppIntents/StartLastGymSessionIntent.swift
@@ -0,0 +1,43 @@
+import AppIntents
+import Foundation
+
+/// Starts a climbing session at the most recently visited gym.
+/// Exposed to Shortcuts so users can begin logging without opening the app.
+struct StartLastGymSessionIntent: AppIntent {
+
+ static var title: LocalizedStringResource {
+ "Start Last Gym Session"
+ }
+
+ static var description: IntentDescription {
+ IntentDescription(
+ "Begin a new climbing session using the most recent gym you visited in Ascently."
+ )
+ }
+
+ static var openAppWhenRun: Bool {
+ true
+ }
+
+ func perform() async throws -> some IntentResult & ProvidesDialog {
+ // Delay to ensure app has time to fully initialize if just launched
+ try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
+
+ let summary = try await SessionIntentController().startSessionWithLastUsedGym()
+
+ // Give Live Activity extra time to start
+ try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
+
+ return .result(
+ dialog: Self.successDialog(for: summary.gymName)
+ )
+ }
+
+ private static func successDialog(for gymName: String) -> IntentDialog {
+ IntentDialog("Session started at \(gymName). Have an awesome climb!")
+ }
+
+ static var parameterSummary: some ParameterSummary {
+ Summary("Start a session at my last gym")
+ }
+}
diff --git a/ios/Ascently/AscentlyApp.swift b/ios/Ascently/AscentlyApp.swift
index 1d10d71..f476c2d 100644
--- a/ios/Ascently/AscentlyApp.swift
+++ b/ios/Ascently/AscentlyApp.swift
@@ -1,7 +1,19 @@
import SwiftUI
+class AppDelegate: NSObject, UIApplicationDelegate {
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ return true
+ }
+}
+
@main
struct AscentlyApp: App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ @Environment(\.scenePhase) private var scenePhase
+
var body: some Scene {
WindowGroup {
ContentView()
diff --git a/ios/Ascently/ContentView.swift b/ios/Ascently/ContentView.swift
index 54aef34..52dd10a 100644
--- a/ios/Ascently/ContentView.swift
+++ b/ios/Ascently/ContentView.swift
@@ -1,7 +1,7 @@
import SwiftUI
struct ContentView: View {
- @StateObject private var dataManager = ClimbingDataManager()
+ @StateObject private var dataManager = ClimbingDataManager.shared
@State private var selectedTab = 0
@Environment(\.scenePhase) private var scenePhase
@State private var notificationObservers: [NSObjectProtocol] = []
diff --git a/ios/Ascently/Info.plist b/ios/Ascently/Info.plist
index b71fedf..9a5b5a9 100644
--- a/ios/Ascently/Info.plist
+++ b/ios/Ascently/Info.plist
@@ -6,6 +6,7 @@
NSSupportsLiveActivities
+
NSPhotoLibraryUsageDescription
This app needs access to your photo library to add photos to climbing problems.
NSCameraUsageDescription
diff --git a/ios/Ascently/ViewModels/ClimbingDataManager.swift b/ios/Ascently/ViewModels/ClimbingDataManager.swift
index 1779540..246d123 100644
--- a/ios/Ascently/ViewModels/ClimbingDataManager.swift
+++ b/ios/Ascently/ViewModels/ClimbingDataManager.swift
@@ -15,6 +15,8 @@ import UniformTypeIdentifiers
@MainActor
class ClimbingDataManager: ObservableObject {
+ static let shared = ClimbingDataManager()
+
@Published var gyms: [Gym] = []
@Published var problems: [Problem] = []
@Published var sessions: [ClimbSession] = []
@@ -78,7 +80,7 @@ class ClimbingDataManager: ObservableObject {
let name: String
}
- init() {
+ fileprivate init() {
_ = ImageManager.shared
migrateFromOpenClimbIfNeeded()
loadAllData()
@@ -415,9 +417,16 @@ class ClimbingDataManager: ObservableObject {
}
func startSession(gymId: UUID, notes: String? = nil) {
- // End any currently active session
+ Task { @MainActor in
+ await startSessionAsync(gymId: gymId, notes: notes)
+ }
+ }
+
+ @discardableResult
+ func startSessionAsync(gymId: UUID, notes: String? = nil) async -> ClimbSession? {
+ // End any currently active session before starting a new one
if let currentActive = activeSession {
- endSession(currentActive.id)
+ await endSessionAsync(currentActive.id)
}
let newSession = ClimbSession(gymId: gymId, notes: notes)
@@ -430,64 +439,70 @@ class ClimbingDataManager: ObservableObject {
// MARK: - Start Live Activity for new session
if let gym = gym(withId: gymId) {
- Task {
- await LiveActivityManager.shared.startLiveActivity(
- for: newSession, gymName: gym.name)
- }
+ await LiveActivityManager.shared.startLiveActivity(
+ for: newSession,
+ gymName: gym.name)
}
if healthKitService.isEnabled {
- Task {
- do {
- try await healthKitService.startWorkout(
- startDate: newSession.startTime ?? Date(),
- sessionId: newSession.id)
- } catch {
- AppLogger.error(
- "Failed to start HealthKit workout: \(error.localizedDescription)",
- tag: LogTag.climbingData)
- }
+ do {
+ try await healthKitService.startWorkout(
+ startDate: newSession.startTime ?? Date(),
+ sessionId: newSession.id)
+ } catch {
+ AppLogger.error(
+ "Failed to start HealthKit workout: \(error.localizedDescription)",
+ tag: LogTag.climbingData)
}
}
+
+ return newSession
}
func endSession(_ sessionId: UUID) {
- if let session = sessions.first(where: { $0.id == sessionId && $0.status == .active }),
+ Task { @MainActor in
+ await endSessionAsync(sessionId)
+ }
+ }
+
+ @discardableResult
+ func endSessionAsync(_ sessionId: UUID) async -> ClimbSession? {
+ guard
+ let session = sessions.first(where: { $0.id == sessionId && $0.status == .active }),
let index = sessions.firstIndex(where: { $0.id == sessionId })
- {
+ else {
+ return nil
+ }
- let completedSession = session.completed()
- sessions[index] = completedSession
+ let completedSession = session.completed()
+ sessions[index] = completedSession
- if activeSession?.id == sessionId {
- activeSession = nil
- }
+ if activeSession?.id == sessionId {
+ activeSession = nil
+ }
- saveActiveSession()
- saveSessions()
- DataStateManager.shared.updateDataState()
+ saveActiveSession()
+ saveSessions()
+ DataStateManager.shared.updateDataState()
- // Trigger auto-sync if enabled
- syncService.triggerAutoSync(dataManager: self)
+ // Trigger auto-sync if enabled
+ syncService.triggerAutoSync(dataManager: self)
- // MARK: - End Live Activity after session ends
- Task {
- await LiveActivityManager.shared.endLiveActivity()
- }
+ // MARK: - End Live Activity after session ends
+ await LiveActivityManager.shared.endLiveActivity()
- if healthKitService.isEnabled {
- Task {
- do {
- try await healthKitService.endWorkout(
- endDate: completedSession.endTime ?? Date())
- } catch {
- AppLogger.error(
- "Failed to end HealthKit workout: \(error.localizedDescription)",
- tag: LogTag.climbingData)
- }
- }
+ if healthKitService.isEnabled {
+ do {
+ try await healthKitService.endWorkout(
+ endDate: completedSession.endTime ?? Date())
+ } catch {
+ AppLogger.error(
+ "Failed to end HealthKit workout: \(error.localizedDescription)",
+ tag: LogTag.climbingData)
}
}
+
+ return completedSession
}
func updateSession(_ session: ClimbSession) {
diff --git a/ios/SessionStatusLive/SessionStatusLiveBundle.swift b/ios/SessionStatusLive/SessionStatusLiveBundle.swift
index 34e1bef..b0b7e8a 100644
--- a/ios/SessionStatusLive/SessionStatusLiveBundle.swift
+++ b/ios/SessionStatusLive/SessionStatusLiveBundle.swift
@@ -8,7 +8,6 @@ import WidgetKit
struct SessionStatusLiveBundle: WidgetBundle {
var body: some Widget {
SessionStatusLive()
- SessionStatusLiveControl()
SessionStatusLiveLiveActivity()
}
}
diff --git a/ios/SessionStatusLive/SessionStatusLiveControl.swift b/ios/SessionStatusLive/SessionStatusLiveControl.swift
deleted file mode 100644
index 3471c8f..0000000
--- a/ios/SessionStatusLive/SessionStatusLiveControl.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-//
-// SessionStatusLiveControl.swift
-
-import AppIntents
-import SwiftUI
-import WidgetKit
-
-struct SessionStatusLiveControl: ControlWidget {
- static let kind: String = "com.atridad.Ascently.SessionStatusLive"
-
- var body: some ControlWidgetConfiguration {
- AppIntentControlConfiguration(
- kind: Self.kind,
- provider: Provider()
- ) { value in
- ControlWidgetToggle(
- "Start Timer",
- isOn: value.isRunning,
- action: StartTimerIntent(value.name)
- ) { isRunning in
- Label(isRunning ? "On" : "Off", systemImage: "timer")
- }
- }
- .displayName("Timer")
- .description("A an example control that runs a timer.")
- }
-}
-
-extension SessionStatusLiveControl {
- struct Value {
- var isRunning: Bool
- var name: String
- }
-
- struct Provider: AppIntentControlValueProvider {
- func previewValue(configuration: TimerConfiguration) -> Value {
- SessionStatusLiveControl.Value(isRunning: false, name: configuration.timerName)
- }
-
- func currentValue(configuration: TimerConfiguration) async throws -> Value {
- let isRunning = true // Check if the timer is running
- return SessionStatusLiveControl.Value(
- isRunning: isRunning, name: configuration.timerName)
- }
- }
-}
-
-struct TimerConfiguration: ControlConfigurationIntent {
- static let title: LocalizedStringResource = "Timer Name Configuration"
-
- @Parameter(title: "Timer Name", default: "Timer")
- var timerName: String
-}
-
-struct StartTimerIntent: SetValueIntent {
- static let title: LocalizedStringResource = "Start a timer"
-
- @Parameter(title: "Timer Name")
- var name: String
-
- @Parameter(title: "Timer is running")
- var value: Bool
-
- init() {}
-
- init(_ name: String) {
- self.name = name
- }
-
- func perform() async throws -> some IntentResult {
- // Start the timer…
- return .result()
- }
-}
diff --git a/sync/main.go b/sync/main.go
index 40632b7..40d335b 100644
--- a/sync/main.go
+++ b/sync/main.go
@@ -13,7 +13,7 @@ import (
"time"
)
-const VERSION = "2.2.0"
+const VERSION = "2.3.0"
func min(a, b int) int {
if a < b {