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 {