diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 000000000..408d99d3e --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Installs the Android SDK + cmdline-tools needed to run `./gradlew lintDebug` +# and `./gradlew testDebugUnitTest` in the remote Claude Code session. +# +# Only runs in the remote environment. +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/android-sdk}" +CMDLINE_TOOLS_VERSION="11076708" # commandlinetools-linux-11076708_latest.zip +PLATFORM_API="37.0" +BUILD_TOOLS="37.0.0" + +mkdir -p "$ANDROID_SDK_ROOT" + +if [ ! -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then + echo "Installing Android cmdline-tools…" + TMP_ZIP="$(mktemp -d)/cmdline-tools.zip" + curl -fsSL -o "$TMP_ZIP" \ + "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" + rm -rf "$ANDROID_SDK_ROOT/cmdline-tools" + mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" + unzip -q "$TMP_ZIP" -d "$ANDROID_SDK_ROOT/cmdline-tools" + mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest" + rm -f "$TMP_ZIP" +fi + +export ANDROID_SDK_ROOT +export ANDROID_HOME="$ANDROID_SDK_ROOT" +export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" + +yes 2>/dev/null | sdkmanager --licenses >/dev/null || true + +sdkmanager --install \ + "platforms;android-${PLATFORM_API}" \ + "build-tools;${BUILD_TOOLS}" \ + "platform-tools" >/dev/null + +# Persist for the rest of the session. +{ + echo "export ANDROID_SDK_ROOT=\"$ANDROID_SDK_ROOT\"" + echo "export ANDROID_HOME=\"$ANDROID_SDK_ROOT\"" + echo "export PATH=\"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:\$PATH\"" +} >> "$CLAUDE_ENV_FILE" + +echo "Android SDK ready at $ANDROID_SDK_ROOT" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..e06b0338e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] + } +} diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 40de01f55..38349deeb 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -9,17 +9,6 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1633,7 +1743,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -1644,7 +1754,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -1655,7 +1765,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1666,7 +1776,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -1677,7 +1787,7 @@ errorLine2=" ~~~~~~"> @@ -1688,7 +1798,7 @@ errorLine2=" ~~~~~~"> @@ -1699,7 +1809,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -1710,7 +1820,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1721,7 +1831,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1732,7 +1842,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -1743,7 +1853,7 @@ errorLine2=" ~~~~~~"> @@ -1754,7 +1864,7 @@ errorLine2=" ~~~~~~"> @@ -1765,7 +1875,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -1776,7 +1886,7 @@ errorLine2=" ~~~~~"> @@ -1787,7 +1897,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1798,7 +1908,7 @@ errorLine2=" ~~~~~~"> @@ -1809,7 +1919,7 @@ errorLine2=" ~~~~~~"> @@ -1820,7 +1930,7 @@ errorLine2=" ~~~~~"> @@ -1831,7 +1941,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1842,7 +1952,7 @@ errorLine2=" ~~~~~~"> @@ -1853,7 +1963,7 @@ errorLine2=" ~~~~~~"> @@ -1864,7 +1974,7 @@ errorLine2=" ~~~~~"> @@ -1875,7 +1985,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1886,7 +1996,7 @@ errorLine2=" ~~~~~~"> @@ -1897,7 +2007,7 @@ errorLine2=" ~~~~~~"> @@ -1908,7 +2018,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1919,7 +2029,7 @@ errorLine2=" ~~~~~~~~~"> @@ -1930,7 +2040,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -1941,7 +2051,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -1952,7 +2062,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -1963,7 +2073,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -1974,7 +2084,7 @@ errorLine2=" ~~~~"> @@ -1985,7 +2095,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -1996,7 +2106,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -2007,7 +2117,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2018,7 +2128,7 @@ errorLine2=" ~~~~~~"> @@ -2029,7 +2139,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -2040,7 +2150,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -2051,7 +2161,7 @@ errorLine2=" ^"> @@ -2062,7 +2172,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -2073,7 +2183,7 @@ errorLine2=" ~~~~~~"> @@ -2084,7 +2194,7 @@ errorLine2=" ~~~~"> @@ -2095,7 +2205,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2106,7 +2216,7 @@ errorLine2=" ~~~~"> @@ -2117,7 +2227,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2128,7 +2238,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2139,7 +2249,7 @@ errorLine2=" ~~~~~~"> @@ -2150,7 +2260,7 @@ errorLine2=" ~~~~~~"> @@ -2161,7 +2271,7 @@ errorLine2=" ~~~~~~"> @@ -2172,7 +2282,7 @@ errorLine2=" ~~~~"> @@ -2183,7 +2293,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2194,7 +2304,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2205,7 +2315,7 @@ errorLine2=" ~~~~~~"> @@ -2216,7 +2326,7 @@ errorLine2=" ~~~~~~"> @@ -2227,7 +2337,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2238,7 +2348,7 @@ errorLine2=" ~~~~"> @@ -2249,7 +2359,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2260,7 +2370,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2271,7 +2381,7 @@ errorLine2=" ~~~~~~"> @@ -2282,7 +2392,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -2293,7 +2403,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2304,7 +2414,7 @@ errorLine2=" ~~~~"> @@ -2315,7 +2425,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2326,7 +2436,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2337,7 +2447,7 @@ errorLine2=" ~~~~~~"> @@ -2348,7 +2458,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -2359,7 +2469,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2370,7 +2480,7 @@ errorLine2=" ~~~~"> @@ -2381,7 +2491,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2392,7 +2502,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2403,7 +2513,7 @@ errorLine2=" ~~~~~~"> @@ -2414,7 +2524,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2425,7 +2535,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2436,7 +2546,7 @@ errorLine2=" ~~~~"> @@ -2447,7 +2557,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2458,7 +2568,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2469,7 +2579,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -2480,7 +2590,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2491,7 +2601,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -2502,7 +2612,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2513,7 +2623,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -2524,7 +2634,7 @@ errorLine2=" ~~~~~~"> @@ -2535,7 +2645,7 @@ errorLine2=" ~~~~~~"> @@ -2546,7 +2656,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -2557,7 +2667,7 @@ errorLine2=" ~~~~~"> @@ -2568,7 +2678,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2579,7 +2689,7 @@ errorLine2=" ~~~~~~"> @@ -2590,7 +2700,7 @@ errorLine2=" ~~~~~~"> @@ -2601,7 +2711,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2612,7 +2722,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2623,7 +2733,7 @@ errorLine2=" ~~~~~~"> @@ -2634,7 +2744,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2645,7 +2755,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -2656,7 +2766,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2667,7 +2777,7 @@ errorLine2=" ~~~~"> @@ -2678,7 +2788,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2689,7 +2799,7 @@ errorLine2=" ~~~~~~"> @@ -2700,7 +2810,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2711,7 +2821,7 @@ errorLine2=" ~~~~~~"> @@ -2722,7 +2832,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -2733,7 +2843,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -2744,7 +2854,7 @@ errorLine2=" ^"> @@ -2755,7 +2865,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -2766,7 +2876,7 @@ errorLine2=" ~~~~~~"> @@ -2777,7 +2887,7 @@ errorLine2=" ~~~~"> @@ -2788,7 +2898,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2799,7 +2909,7 @@ errorLine2=" ~~~~"> @@ -2810,7 +2920,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2821,7 +2931,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2832,7 +2942,7 @@ errorLine2=" ~~~~~~"> @@ -2843,7 +2953,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2854,7 +2964,7 @@ errorLine2=" ~~~~~~"> @@ -2865,7 +2975,7 @@ errorLine2=" ~~~~"> @@ -2876,7 +2986,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2887,7 +2997,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2898,7 +3008,7 @@ errorLine2=" ~~~~~~"> @@ -2909,7 +3019,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2920,7 +3030,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2931,7 +3041,7 @@ errorLine2=" ~~~~"> @@ -2942,7 +3052,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2953,7 +3063,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2964,7 +3074,7 @@ errorLine2=" ~~~~~~"> @@ -2975,7 +3085,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -2986,7 +3096,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2997,7 +3107,7 @@ errorLine2=" ~~~~"> @@ -3008,7 +3118,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3019,7 +3129,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3030,7 +3140,7 @@ errorLine2=" ~~~~~~"> @@ -3041,7 +3151,7 @@ errorLine2=" ~~~~~~"> @@ -3052,7 +3162,7 @@ errorLine2=" ~~~~~~"> @@ -3063,7 +3173,7 @@ errorLine2=" ~~~~"> @@ -3074,7 +3184,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3085,7 +3195,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3096,7 +3206,7 @@ errorLine2=" ~~~~~~"> @@ -3107,7 +3217,7 @@ errorLine2=" ~~~~~~~"> @@ -3118,7 +3228,7 @@ errorLine2=" ~~~~~~"> @@ -3129,7 +3239,7 @@ errorLine2=" ~~~~"> @@ -3140,7 +3250,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3151,7 +3261,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3162,7 +3272,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -3173,7 +3283,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -3184,7 +3294,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3195,7 +3305,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3206,7 +3316,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -3217,7 +3327,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -3228,7 +3338,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3239,7 +3349,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -3250,7 +3360,7 @@ errorLine2=" ~~~~"> @@ -3261,7 +3371,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3272,7 +3382,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -3283,7 +3393,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3294,7 +3404,7 @@ errorLine2=" ~~~~~~"> @@ -3305,7 +3415,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -3316,7 +3426,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -3327,7 +3437,7 @@ errorLine2=" ^"> @@ -3338,7 +3448,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -3349,7 +3459,7 @@ errorLine2=" ~~~~~~"> @@ -3360,7 +3470,7 @@ errorLine2=" ~~~~"> @@ -3371,7 +3481,7 @@ errorLine2=" ~~~~~~"> @@ -3382,7 +3492,7 @@ errorLine2=" ~~~~"> @@ -3393,7 +3503,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3404,7 +3514,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3415,7 +3525,7 @@ errorLine2=" ~~~~~~"> @@ -3426,7 +3536,7 @@ errorLine2=" ~~~~~~"> @@ -3437,7 +3547,7 @@ errorLine2=" ~~~~~~"> @@ -3448,7 +3558,7 @@ errorLine2=" ~~~~"> @@ -3459,7 +3569,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3470,7 +3580,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3481,7 +3591,7 @@ errorLine2=" ~~~~~~"> @@ -3492,7 +3602,7 @@ errorLine2=" ~~~~~~~"> @@ -3503,7 +3613,7 @@ errorLine2=" ~~~~~~"> @@ -3514,7 +3624,7 @@ errorLine2=" ~~~~"> @@ -3525,7 +3635,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3536,7 +3646,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3547,7 +3657,7 @@ errorLine2=" ~~~~~~"> @@ -3558,7 +3668,7 @@ errorLine2=" ~~~~~~~"> @@ -3569,7 +3679,7 @@ errorLine2=" ~~~~~~"> @@ -3580,7 +3690,7 @@ errorLine2=" ~~~~"> @@ -3591,7 +3701,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3602,7 +3712,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3613,7 +3723,7 @@ errorLine2=" ~~~~~~"> @@ -3624,7 +3734,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3635,7 +3745,7 @@ errorLine2=" ~~~~~~"> @@ -3646,7 +3756,7 @@ errorLine2=" ~~~~"> @@ -3657,7 +3767,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3668,7 +3778,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3679,7 +3789,7 @@ errorLine2=" ~~~~~~"> @@ -3690,7 +3800,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -3701,7 +3811,7 @@ errorLine2=" ~~~~~~"> @@ -3712,7 +3822,7 @@ errorLine2=" ~~~~"> @@ -3723,7 +3833,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3734,7 +3844,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3745,7 +3855,7 @@ errorLine2=" ~~~~~~"> @@ -3756,7 +3866,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -3767,7 +3877,7 @@ errorLine2=" ~~~~~~"> @@ -3778,7 +3888,7 @@ errorLine2=" ~~~~"> @@ -3789,7 +3899,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3800,7 +3910,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3811,7 +3921,7 @@ errorLine2=" ~~~~~~"> @@ -3822,7 +3932,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -3833,7 +3943,7 @@ errorLine2=" ~~~~~~"> @@ -3844,7 +3954,7 @@ errorLine2=" ~~~~"> @@ -3855,7 +3965,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3866,7 +3976,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3877,7 +3987,7 @@ errorLine2=" ~~~~~~"> @@ -3888,7 +3998,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -3899,7 +4009,7 @@ errorLine2=" ~~~~~~"> @@ -3910,7 +4020,7 @@ errorLine2=" ~~~~"> @@ -3921,7 +4031,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3932,7 +4042,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3943,7 +4053,7 @@ errorLine2=" ~~~~~~"> @@ -3954,7 +4064,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3965,7 +4075,7 @@ errorLine2=" ~~~~~~"> @@ -3976,7 +4086,7 @@ errorLine2=" ~~~~"> @@ -3987,7 +4097,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3998,7 +4108,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4009,7 +4119,7 @@ errorLine2=" ~~~~~~"> @@ -4020,7 +4130,7 @@ errorLine2=" ~~~~~~~~"> @@ -4031,7 +4141,7 @@ errorLine2=" ~~~~~~"> @@ -4042,7 +4152,7 @@ errorLine2=" ~~~~"> @@ -4053,7 +4163,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4064,7 +4174,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4075,7 +4185,7 @@ errorLine2=" ~~~~~~"> @@ -4086,7 +4196,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -4097,7 +4207,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4108,7 +4218,7 @@ errorLine2=" ~~~~"> @@ -4119,7 +4229,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4130,7 +4240,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4141,7 +4251,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -4152,7 +4262,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -4163,7 +4273,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4174,7 +4284,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4185,7 +4295,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4196,7 +4306,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -4207,7 +4317,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4218,7 +4328,7 @@ errorLine2=" ~~~~~~~~~~~~~~~"> @@ -4229,7 +4339,7 @@ errorLine2=" ~~~~"> @@ -4240,7 +4350,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -4251,7 +4361,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4262,7 +4372,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4273,7 +4383,7 @@ errorLine2=" ~~~~~~"> @@ -4284,7 +4394,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -4295,7 +4405,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -4306,7 +4416,7 @@ errorLine2=" ^"> @@ -4317,7 +4427,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -4328,7 +4438,7 @@ errorLine2=" ~~~~"> @@ -4339,7 +4449,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -4350,7 +4460,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -4361,7 +4471,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4372,7 +4482,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4383,7 +4493,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -5886,7 +5996,7 @@ @@ -5923,7 +6033,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -5934,7 +6044,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -5945,7 +6055,7 @@ errorLine2=" ~~~~"> @@ -5956,7 +6066,7 @@ errorLine2=" ^"> @@ -5967,7 +6077,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -5978,7 +6088,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -5989,7 +6099,7 @@ errorLine2=" ~~~~"> @@ -6000,7 +6110,7 @@ errorLine2=" ^"> @@ -6011,7 +6121,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6022,7 +6132,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6033,7 +6143,7 @@ errorLine2=" ~~~~"> @@ -6044,7 +6154,7 @@ errorLine2=" ^"> @@ -6055,7 +6165,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6066,7 +6176,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6077,7 +6187,7 @@ errorLine2=" ~~~~"> @@ -6088,7 +6198,7 @@ errorLine2=" ^"> @@ -6099,7 +6209,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6110,7 +6220,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6121,7 +6231,7 @@ errorLine2=" ~~~~"> @@ -6132,7 +6242,7 @@ errorLine2=" ^"> @@ -6143,18 +6253,18 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6165,7 +6275,7 @@ errorLine2=" ~~~~"> @@ -6176,7 +6286,7 @@ errorLine2=" ^"> @@ -6187,7 +6297,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6198,7 +6308,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6209,7 +6319,7 @@ errorLine2=" ~~~~"> @@ -6220,7 +6330,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6231,7 +6341,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6242,7 +6352,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6253,7 +6363,7 @@ errorLine2=" ~~~~~~~"> @@ -6264,7 +6374,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6275,7 +6385,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6286,7 +6396,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6297,7 +6407,7 @@ errorLine2=" ~~~~~~~~"> @@ -6308,7 +6418,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6319,7 +6429,7 @@ errorLine2=" ~~~~~~"> @@ -6330,7 +6440,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6341,7 +6451,7 @@ errorLine2=" ~~~"> @@ -6352,7 +6462,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6363,7 +6473,7 @@ errorLine2=" ~~~~~~"> @@ -6374,7 +6484,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6385,7 +6495,7 @@ errorLine2=" ~~~~~~~"> @@ -6396,7 +6506,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6407,7 +6517,7 @@ errorLine2=" ~~~~~~"> @@ -6418,7 +6528,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6429,7 +6539,7 @@ errorLine2=" ~~~~~~~~"> @@ -6440,7 +6550,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6451,7 +6561,7 @@ errorLine2=" ~~~~~~"> @@ -6462,7 +6572,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6473,7 +6583,7 @@ errorLine2=" ~~~"> @@ -6484,7 +6594,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6495,7 +6605,7 @@ errorLine2=" ~~~~~~"> @@ -6506,7 +6616,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6517,7 +6627,7 @@ errorLine2=" ~~~~~~~"> @@ -6528,7 +6638,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6539,7 +6649,7 @@ errorLine2=" ~~~~~~"> @@ -6550,7 +6660,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6561,7 +6671,7 @@ errorLine2=" ~~~~~~~~"> @@ -6572,7 +6682,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6583,7 +6693,7 @@ errorLine2=" ~~~~~~"> @@ -6594,7 +6704,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6605,7 +6715,7 @@ errorLine2=" ~~~"> @@ -6616,7 +6726,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6627,7 +6737,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -6638,7 +6748,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6649,7 +6759,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6660,7 +6770,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6671,7 +6781,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6682,7 +6792,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6693,7 +6803,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6704,7 +6814,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6715,7 +6825,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6726,7 +6836,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -6737,7 +6847,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6748,7 +6858,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6759,7 +6869,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6770,7 +6880,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6781,7 +6891,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6792,7 +6902,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6803,7 +6913,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6814,7 +6924,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6825,7 +6935,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -6836,7 +6946,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6847,7 +6957,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6858,7 +6968,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6869,7 +6979,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6880,7 +6990,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6891,7 +7001,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6902,7 +7012,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6913,7 +7023,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6924,7 +7034,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -6935,7 +7045,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6946,7 +7056,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6957,7 +7067,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6968,7 +7078,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6979,7 +7089,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -6990,7 +7100,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7001,7 +7111,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7012,7 +7122,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7023,7 +7133,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -7034,7 +7144,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7045,7 +7155,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7056,7 +7166,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7067,7 +7177,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7078,7 +7188,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7089,7 +7199,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7100,7 +7210,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7111,7 +7221,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7122,7 +7232,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -7133,7 +7243,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7144,7 +7254,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7155,7 +7265,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7166,7 +7276,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7177,7 +7287,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7188,7 +7298,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7199,7 +7309,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7210,7 +7320,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7221,7 +7331,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -7232,7 +7342,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7243,7 +7353,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7254,7 +7364,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7265,7 +7375,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7276,7 +7386,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7287,7 +7397,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7298,7 +7408,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7309,7 +7419,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7320,7 +7430,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -7331,7 +7441,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7342,7 +7452,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7353,7 +7463,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7364,7 +7474,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7375,7 +7485,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7386,7 +7496,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7397,10 +7507,32 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + + + + + + + + @@ -7419,7 +7551,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7430,7 +7562,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7441,7 +7573,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7452,7 +7584,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7463,7 +7595,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7474,7 +7606,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7485,7 +7617,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -7496,7 +7628,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7507,7 +7639,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7518,7 +7650,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7529,7 +7661,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7540,7 +7672,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7551,7 +7683,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7562,7 +7694,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7573,7 +7705,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7584,7 +7716,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -7595,7 +7727,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7606,7 +7738,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7617,7 +7749,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7628,7 +7760,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7639,7 +7771,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7650,7 +7782,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7661,7 +7793,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7672,7 +7804,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -7683,7 +7815,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -7694,7 +7826,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -7705,7 +7837,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -7716,7 +7848,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -7727,7 +7859,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -7738,7 +7870,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -7749,7 +7881,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -7760,7 +7892,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -7771,7 +7903,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -7782,7 +7914,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -7793,7 +7925,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -7804,13 +7936,13 @@ errorLine2=" ~~~~~~~~~~~~~~"> + message=""profile" is not translated in "in" (Indonesian)" + errorLine1=" <string name="profile">Profile</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""settings" is not translated in "in" (Indonesian)" + errorLine1=" <string name="settings">Settings</string>" + errorLine2=" ~~~~~~~~~~~~~~~"> + message=""about" is not translated in "in" (Indonesian)" + errorLine1=" <string name="about">About</string>" + errorLine2=" ~~~~~~~~~~~~"> + message=""add_exercise" is not translated in "in" (Indonesian) or "ar" (Arabic)" + errorLine1=" <string name="add_exercise">Add exercise</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + message=""more_options" is not translated in "in" (Indonesian)" + errorLine1=" <string name="more_options">More options</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + message=""navigate_back" is not translated in "in" (Indonesian) or "ar" (Arabic)" + errorLine1=" <string name="navigate_back">Back to the previous screen</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + message=""library" is not translated in "in" (Indonesian)" + errorLine1=" <string name="library">Library</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""open" is not translated in "in" (Indonesian) or "zh" (Chinese)" + errorLine1=" <string name="open">Open</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""copy" is not translated in "in" (Indonesian)" + errorLine1=" <string name="copy">Copy</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""support_project" is not translated in "in" (Indonesian) or "ar" (Arabic)" + errorLine1=" <string name="support_project">Support the project</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + message=""info" is not translated in "in" (Indonesian)" + errorLine1=" <string name="info">Info</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""privacy_policy" is not translated in "in" (Indonesian)" + errorLine1=" <string name="privacy_policy">Privacy policy</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""privacy_policy_desc" is not translated in "in" (Indonesian)" + errorLine1=" <string name="privacy_policy_desc">LibreFit cannot access the Internet. This means user data never leaves the device.</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""website" is not translated in "in" (Indonesian)" + errorLine1=" <string name="website">Website</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""source_code" is not translated in "in" (Indonesian)" + errorLine1=" <string name="source_code">Source code</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + message=""license" is not translated in "in" (Indonesian)" + errorLine1=" <string name="license">License</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""license_desc" is not translated in "in" (Indonesian)" + errorLine1=" <string name="license_desc">LibreFit is licensed under the GNU General Public License v3.0 (GPL-3).</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + message=""contributors" is not translated in "in" (Indonesian)" + errorLine1=" <string name="contributors">Contributors</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + message=""founder" is not translated in "in" (Indonesian)" + errorLine1=" <string name="founder">Founder</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""translators" is not translated in "in" (Indonesian)" + errorLine1=" <string name="translators">Translators</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + message=""contributed_to" is not translated in "in" (Indonesian) or "zh" (Chinese)" + errorLine1=" <string name="contributed_to">Contributed to: </string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""view_online_version" is not translated in "in" (Indonesian) or "zh" (Chinese)" + errorLine1=" <string name="view_online_version">View online version</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""version" is not translated in "in" (Indonesian)" + errorLine1=" <string name="version">Version</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""dependencies" is not translated in "in" (Indonesian)" + errorLine1=" <string name="dependencies">Dependencies</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + message=""lets_build_it_together" is not translated in "in" (Indonesian) or "zh" (Chinese)" + errorLine1=" <string name="lets_build_it_together">Let\'s build it together</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""tutorial_desc" is not translated in "in" (Indonesian) or "zh" (Chinese)" + errorLine1=" <string name="tutorial_desc">Learn how to create routines and track your workouts.</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + message=""donators" is not translated in "de" (German), "in" (Indonesian), "zh" (Chinese), "nl" (Dutch)" + errorLine1=" <string name="donators">Donators</string>" + errorLine2=" ~~~~~~~~~~~~~~~"> + message=""url_source_code_codeberg" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="url_source_code_codeberg">https://codeberg.org/LibreFitOrg/LibreFit</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""unlink_routine_question" is not translated in "in" (Indonesian), "zh" (Chinese), "ar" (Arabic)" + errorLine1=" <string name="unlink_routine_question">Unlink routine?</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""unlink_routine_text" is not translated in "hi" (Hindi), "in" (Indonesian), "zh" (Chinese), "ar" (Arabic)" + errorLine1=" <string name="unlink_routine_text">Linking routines allows to measure progress of workouts over time. If you unlink this workout from a routine, the latter will not be deleted and any workout linked to that routine will remain unchanged. The unlink will affect only the current workout.</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""overview" is not translated in "in" (Indonesian), "zh" (Chinese), "ar" (Arabic)" + errorLine1=" <string name="overview">Overview</string>" + errorLine2=" ~~~~~~~~~~~~~~~"> + message=""statistics" is not translated in "in" (Indonesian), "zh" (Chinese), "ar" (Arabic)" + errorLine1=" <string name="statistics">Statistics</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~"> + message=""volume" is not translated in "in" (Indonesian), "zh" (Chinese), "ar" (Arabic)" + errorLine1=" <string name="volume">Volume</string>" + errorLine2=" ~~~~~~~~~~~~~"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + message=""url_dialog" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="url_dialog">URL</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~"> + message=""unlink_dialog" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="unlink_dialog">Unlink</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + message=""discard_changes_question" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="discard_changes_question">Discard changes?</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""discard_changes_text" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="discard_changes_text">Are you sure you want to quit? Your changes won\'t be saved</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""delete" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="delete">Delete</string>" + errorLine2=" ~~~~~~~~~~~~~"> + message=""seconds" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="seconds">Seconds</string>" + errorLine2=" ~~~~~~~~~~~~~~"> + message=""done" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="done">Done</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""menu" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="menu">Menu</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""add" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="add">Add</string>" + errorLine2=" ~~~~~~~~~~"> + message=""save" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="save">Save</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""others" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="others">Others</string>" + errorLine2=" ~~~~~~~~~~~~~"> + message=""help" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="help">Help</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""paste" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="paste">Paste</string>" + errorLine2=" ~~~~~~~~~~~~"> + message=""name" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="name">Name</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""edit" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="edit">Edit</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""do_not_show_again" is not translated in "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan)" + errorLine1=" <string name="do_not_show_again">Do not show again</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""undo" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="undo">Undo</string>" + errorLine2=" ~~~~~~~~~~~"> + message=""clear" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "in" (Indonesian), "es" (Spanish), "zh" (Chinese), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="clear">Clear</string>" + errorLine2=" ~~~~~~~~~~~~"> + message=""hiit_mode" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_mode">HIIT mode</string>" + errorLine2=" ~~~~~~~~~~~~~~~~"> + message=""hiit_mode_desc" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_mode_desc">Auto-advance: countdown → rest → next set</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_target_duration" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_target_duration">Target duration (seconds)</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_phase_ready" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_phase_ready">READY</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_phase_set" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_phase_set">SET</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_phase_rest" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_phase_rest">REST</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_phase_done" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_phase_done">DONE</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_start_set" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_start_set">Start set</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_skip_rest" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_skip_rest">Skip rest</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_all_sets_complete" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_all_sets_complete">All sets complete! Move to next exercise.</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""hiit_set_progress" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="hiit_set_progress">Set %1$d / %2$d</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> + message=""exercise_details" is not translated in "de" (German), "hi" (Hindi), "pt" (Portuguese), "gl" (Galician), "in" (Indonesian), "it" (Italian), "fr" (French), "es" (Spanish), "zh" (Chinese), "cs" (Czech), "ar" (Arabic), "uk" (Ukrainian), "ca" (Catalan), "nl" (Dutch)" + errorLine1=" <string name="exercise_details">Exercise details</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - @@ -12662,7 +13432,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12673,7 +13443,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12684,7 +13454,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -12695,7 +13465,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12706,7 +13476,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12717,7 +13487,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -12728,7 +13498,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -12739,7 +13509,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -12750,7 +13520,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -12761,7 +13531,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12772,7 +13542,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12783,7 +13553,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -12794,7 +13564,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -12805,7 +13575,7 @@ errorLine2=" ~~~~~~~~~~~~~"> diff --git a/app/schemas/org.librefit.db.AppDatabase/4.json b/app/schemas/org.librefit.db.AppDatabase/4.json new file mode 100644 index 000000000..5b68a79ec --- /dev/null +++ b/app/schemas/org.librefit.db.AppDatabase/4.json @@ -0,0 +1,394 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "8c52442f7620d14e4deb09d53fe8ee4d", + "entities": [ + { + "tableName": "workouts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `routineId` INTEGER NOT NULL, `notes` TEXT NOT NULL, `title` TEXT NOT NULL, `state` TEXT NOT NULL, `timeElapsed` INTEGER NOT NULL, `created` TEXT NOT NULL, `completed` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "routineId", + "columnName": "routineId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeElapsed", + "columnName": "timeElapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "exercises", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `idExerciseDC` TEXT NOT NULL, `notes` TEXT NOT NULL, `setMode` TEXT NOT NULL, `restTime` INTEGER NOT NULL, `position` INTEGER NOT NULL, `workoutId` INTEGER NOT NULL, `targetDuration` INTEGER NOT NULL, `autoAdvanceSets` INTEGER NOT NULL, FOREIGN KEY(`workoutId`) REFERENCES `workouts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`idExerciseDC`) REFERENCES `dataset`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idExerciseDC", + "columnName": "idExerciseDC", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "setMode", + "columnName": "setMode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "restTime", + "columnName": "restTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workoutId", + "columnName": "workoutId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetDuration", + "columnName": "targetDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoAdvanceSets", + "columnName": "autoAdvanceSets", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_exercises_workoutId", + "unique": false, + "columnNames": [ + "workoutId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_exercises_workoutId` ON `${TABLE_NAME}` (`workoutId`)" + }, + { + "name": "index_exercises_workoutId_position", + "unique": false, + "columnNames": [ + "workoutId", + "position" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_exercises_workoutId_position` ON `${TABLE_NAME}` (`workoutId`, `position`)" + }, + { + "name": "index_exercises_idExerciseDC", + "unique": false, + "columnNames": [ + "idExerciseDC" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_exercises_idExerciseDC` ON `${TABLE_NAME}` (`idExerciseDC`)" + } + ], + "foreignKeys": [ + { + "table": "workouts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "workoutId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "dataset", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "idExerciseDC" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `load` REAL NOT NULL, `reps` INTEGER NOT NULL, `elapsedTime` INTEGER NOT NULL, `completed` INTEGER NOT NULL, `exerciseId` INTEGER NOT NULL, FOREIGN KEY(`exerciseId`) REFERENCES `exercises`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "load", + "columnName": "load", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "reps", + "columnName": "reps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "elapsedTime", + "columnName": "elapsedTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exerciseId", + "columnName": "exerciseId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sets_exerciseId", + "unique": false, + "columnNames": [ + "exerciseId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sets_exerciseId` ON `${TABLE_NAME}` (`exerciseId`)" + } + ], + "foreignKeys": [ + { + "table": "exercises", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "exerciseId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "measurements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bodyWeight` REAL NOT NULL, `bodyFatPercentage` INTEGER NOT NULL, `muscleMassPercentage` INTEGER NOT NULL, `date` TEXT NOT NULL, `notes` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bodyWeight", + "columnName": "bodyWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "bodyFatPercentage", + "columnName": "bodyFatPercentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "muscleMassPercentage", + "columnName": "muscleMassPercentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "dataset", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `force` TEXT, `level` TEXT NOT NULL, `mechanic` TEXT, `equipment` TEXT, `primaryMuscles` TEXT NOT NULL, `secondaryMuscles` TEXT NOT NULL, `instructions` TEXT NOT NULL, `category` TEXT NOT NULL, `images` TEXT NOT NULL, `isCustomExercise` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "force", + "columnName": "force", + "affinity": "TEXT" + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mechanic", + "columnName": "mechanic", + "affinity": "TEXT" + }, + { + "fieldPath": "equipment", + "columnName": "equipment", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryMuscles", + "columnName": "primaryMuscles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondaryMuscles", + "columnName": "secondaryMuscles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instructions", + "columnName": "instructions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images", + "columnName": "images", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isCustomExercise", + "columnName": "isCustomExercise", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8c52442f7620d14e4deb09d53fe8ee4d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/librefit/db/AppDatabase.kt b/app/src/main/java/org/librefit/db/AppDatabase.kt index d6797a2b5..66d5df3e7 100644 --- a/app/src/main/java/org/librefit/db/AppDatabase.kt +++ b/app/src/main/java/org/librefit/db/AppDatabase.kt @@ -27,7 +27,7 @@ import org.librefit.db.entity.Workout @Database( entities = [Workout::class, Exercise::class, Set::class, Measurement::class, ExerciseDC::class], - version = 3, + version = 4, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2) @@ -38,6 +38,23 @@ abstract class AppDatabase : RoomDatabase() { companion object { const val NAME = "librefit_database" + val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + ALTER TABLE exercises + ADD COLUMN targetDuration INTEGER NOT NULL DEFAULT 0 + """.trimIndent() + ) + db.execSQL( + """ + ALTER TABLE exercises + ADD COLUMN autoAdvanceSets INTEGER NOT NULL DEFAULT 0 + """.trimIndent() + ) + } + } + val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( diff --git a/app/src/main/java/org/librefit/db/entity/Exercise.kt b/app/src/main/java/org/librefit/db/entity/Exercise.kt index e5edcd68b..a915a0896 100644 --- a/app/src/main/java/org/librefit/db/entity/Exercise.kt +++ b/app/src/main/java/org/librefit/db/entity/Exercise.kt @@ -67,5 +67,9 @@ data class Exercise( val setMode: SetMode = SetMode.LOAD, val restTime: Int = 0, val position: Int = 0, - val workoutId: Long = 0// Foreign key reference to Workout + val workoutId: Long = 0,// Foreign key reference to Workout + /** Target duration in seconds for DURATION sets used in countdown mode (HIIT). 0 = use stopwatch instead. */ + val targetDuration: Int = 0, + /** When true, sets auto-advance: countdown → rest → next set countdown, without user input. */ + val autoAdvanceSets: Boolean = false ) diff --git a/app/src/main/java/org/librefit/di/DatabaseModule.kt b/app/src/main/java/org/librefit/di/DatabaseModule.kt index bdcbec36b..5fba56f00 100644 --- a/app/src/main/java/org/librefit/di/DatabaseModule.kt +++ b/app/src/main/java/org/librefit/di/DatabaseModule.kt @@ -35,7 +35,7 @@ object DatabaseModule { AppDatabase::class.java, AppDatabase.NAME ) - .addMigrations(AppDatabase.MIGRATION_2_3) + .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4) .build() } diff --git a/app/src/main/java/org/librefit/enums/WorkoutServiceActions.kt b/app/src/main/java/org/librefit/enums/WorkoutServiceActions.kt index ffda015cf..28fc13a66 100644 --- a/app/src/main/java/org/librefit/enums/WorkoutServiceActions.kt +++ b/app/src/main/java/org/librefit/enums/WorkoutServiceActions.kt @@ -15,5 +15,9 @@ enum class WorkoutServiceActions(val string: String) { MODIFY_REST_TIMER("PAUSE_REST_TIMER"), WORKOUT_FOCUS("WORKOUT_FOCUS"), STOP_SERVICE("STOP_SERVICE"), - SET_ELAPSED_TIME("SET_ELAPSED_TIME") + SET_ELAPSED_TIME("SET_ELAPSED_TIME"), + /** Start a HIIT countdown timer (counts down from targetDuration to 0). */ + START_COUNTDOWN("START_COUNTDOWN"), + /** Cancel an active countdown without advancing to rest. */ + CANCEL_COUNTDOWN("CANCEL_COUNTDOWN") } \ No newline at end of file diff --git a/app/src/main/java/org/librefit/services/WorkoutService.kt b/app/src/main/java/org/librefit/services/WorkoutService.kt index 4ced37ecf..b1105251f 100644 --- a/app/src/main/java/org/librefit/services/WorkoutService.kt +++ b/app/src/main/java/org/librefit/services/WorkoutService.kt @@ -86,14 +86,39 @@ class WorkoutService : Service() { private val _restTime = MutableStateFlow(0) val restTime: StateFlow = _restTime + /** Countdown timer for HIIT sets (counts down to 0). */ + private val _countdownTime = MutableStateFlow(0) + val countdownTime: StateFlow = _countdownTime + + /** Whether a HIIT countdown is actively running. */ + private val _isCountdownActive = MutableStateFlow(false) + val isCountdownActive: StateFlow = _isCountdownActive + + /** Total duration for the current countdown (used for progress calculation). */ + private val _countdownTotal = MutableStateFlow(0) + val countdownTotal: StateFlow = _countdownTotal + + /** Emits true once when a countdown finishes — consumed by the ViewModel to auto-start rest. */ + private val _countdownFinished = MutableStateFlow(false) + val countdownFinished: StateFlow = _countdownFinished + const val EXTRA_INITIAL_REST_TIME = "EXTRA_INITIAL_REST_TIME" const val EXTRA_ADD_TEN_SECONDS = "EXTRA_ADD_TEN_SECONDS" const val EXTRA_IS_FOCUSED = "EXTRA_IS_FOCUSED" const val EXTRA_SET_ELAPSED_TIME = "EXTRA_SET_ELAPSED_TIME" + const val EXTRA_COUNTDOWN_DURATION = "EXTRA_COUNTDOWN_DURATION" + /** Rest duration to auto-start after countdown finishes (0 = skip rest). */ + const val EXTRA_COUNTDOWN_REST_DURATION = "EXTRA_COUNTDOWN_REST_DURATION" + + /** Acknowledge the [countdownFinished] signal (call from ViewModel after reading it). */ + fun clearCountdownFinished() { + _countdownFinished.update { false } + } } private var initialRestTime = 0 private var isFocused = true + private var countdownRestDuration = 0 @Inject lateinit var notificationHelper: NotificationHelper @@ -145,6 +170,24 @@ class WorkoutService : Service() { it + (intent?.getIntExtra(EXTRA_SET_ELAPSED_TIME, 0) ?: 0) } } + + WorkoutServiceActions.START_COUNTDOWN -> { + val duration = intent?.getIntExtra(EXTRA_COUNTDOWN_DURATION, 0) ?: 0 + countdownRestDuration = + intent?.getIntExtra(EXTRA_COUNTDOWN_REST_DURATION, 0) ?: 0 + countdownJob?.cancel() + _countdownTotal.update { duration } + _countdownTime.update { duration } + _isCountdownActive.update { true } + _countdownFinished.update { false } + startCountdown() + } + + WorkoutServiceActions.CANCEL_COUNTDOWN -> { + countdownJob?.cancel() + _isCountdownActive.update { false } + _countdownTime.update { 0 } + } } return START_STICKY @@ -153,9 +196,15 @@ class WorkoutService : Service() { fun stopService() { stopwatchJob?.cancel() restTimerJob?.cancel() + countdownJob?.cancel() _timeElapsed.update { 0 } _restTime.update { 0 } + _countdownTime.update { 0 } + _isCountdownActive.update { false } + _countdownFinished.update { false } + _countdownTotal.update { 0 } initialRestTime = 0 + countdownRestDuration = 0 stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } @@ -200,6 +249,34 @@ class WorkoutService : Service() { } + private var countdownJob: Job? = null + + /** + * HIIT countdown: counts from [countdownTime] down to 0, then: + * 1. Signals [countdownFinished] for the ViewModel + * 2. Auto-starts the rest timer if [countdownRestDuration] > 0 + */ + private fun startCountdown() { + countdownJob = serviceScope.launch { + while (countdownTime.value > 0) { + delay(1000) + _countdownTime.update { (it - 1).coerceAtLeast(0) } + } + _isCountdownActive.update { false } + _countdownFinished.update { true } + + // Auto-start rest if configured + if (countdownRestDuration > 0) { + restTimerJob?.cancel() + initialRestTime = countdownRestDuration + _restTime.update { countdownRestDuration } + startRestTimer() + } + } + } + + + private var restTimerJob: Job? = null private fun startRestTimer() { diff --git a/app/src/main/java/org/librefit/services/WorkoutServiceManager.kt b/app/src/main/java/org/librefit/services/WorkoutServiceManager.kt index cc49e5792..4bf83f37d 100644 --- a/app/src/main/java/org/librefit/services/WorkoutServiceManager.kt +++ b/app/src/main/java/org/librefit/services/WorkoutServiceManager.kt @@ -13,6 +13,8 @@ import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext import org.librefit.enums.WorkoutServiceActions import org.librefit.services.WorkoutService.Companion.EXTRA_ADD_TEN_SECONDS +import org.librefit.services.WorkoutService.Companion.EXTRA_COUNTDOWN_DURATION +import org.librefit.services.WorkoutService.Companion.EXTRA_COUNTDOWN_REST_DURATION import org.librefit.services.WorkoutService.Companion.EXTRA_INITIAL_REST_TIME import org.librefit.services.WorkoutService.Companion.EXTRA_IS_FOCUSED import org.librefit.services.WorkoutService.Companion.EXTRA_SET_ELAPSED_TIME @@ -73,6 +75,26 @@ class WorkoutServiceManager @Inject constructor( context.startForegroundService(serviceIntent) } + /** + * Start a HIIT countdown timer that counts down from [durationSeconds] to 0, + * then auto-starts rest timer for [restDurationSeconds] if > 0. + */ + fun startCountdown(durationSeconds: Int, restDurationSeconds: Int) { + val serviceIntent = workoutServiceIntent.apply { + action = WorkoutServiceActions.START_COUNTDOWN.string + putExtra(EXTRA_COUNTDOWN_DURATION, durationSeconds) + putExtra(EXTRA_COUNTDOWN_REST_DURATION, restDurationSeconds) + } + context.startForegroundService(serviceIntent) + } + + fun cancelCountdown() { + val serviceIntent = workoutServiceIntent.apply { + action = WorkoutServiceActions.CANCEL_COUNTDOWN.string + } + context.startForegroundService(serviceIntent) + } + fun stopService() { val serviceIntent = workoutServiceIntent.apply { action = WorkoutServiceActions.STOP_SERVICE.string diff --git a/app/src/main/java/org/librefit/ui/components/HiitCountdownCard.kt b/app/src/main/java/org/librefit/ui/components/HiitCountdownCard.kt new file mode 100644 index 000000000..2d9fa7423 --- /dev/null +++ b/app/src/main/java/org/librefit/ui/components/HiitCountdownCard.kt @@ -0,0 +1,358 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026. The LibreFit Contributors + * + * LibreFit is subject to additional terms covering author attribution and trademark usage; + * see the ADDITIONAL_TERMS.md and TRADEMARK_POLICY.md files in the project root. + */ + +package org.librefit.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.librefit.R +import org.librefit.enums.userPreferences.ThemeMode +import org.librefit.ui.screens.workout.HiitPhase +import org.librefit.ui.theme.LibreFitTheme +import org.librefit.util.Formatter + +/** + * Card showing a circular countdown timer for an HIIT-style exercise. + * + * The card has four visual states driven by [phase]: + * - [HiitPhase.Idle]: the user can press play to start. + * - [HiitPhase.SetCountdown]: counts down the set duration (primary color). + * - [HiitPhase.RestBetweenSets]: counts down the rest period (tertiary color). + * - [HiitPhase.ExerciseDone]: all sets complete (secondary color). + * + * The composable is purely visual — all transitions are owned by the caller. This makes it + * trivial to preview and to unit-test the state machine separately. + * + * @param exerciseName Name displayed in the header. + * @param currentSetIndex 0-based index of the set the countdown applies to. + * @param totalSets Total number of sets for the exercise. + * @param countdownSeconds Remaining seconds in the active set countdown. + * @param countdownTotal Total seconds for the active set countdown (used for progress). + * @param restSeconds Remaining seconds in the rest period. + * @param restTotal Total seconds for the rest period (used for progress). + * @param phase Current [HiitPhase]. + */ +@Composable +fun HiitCountdownCard( + exerciseName: String, + currentSetIndex: Int, + totalSets: Int, + countdownSeconds: Int, + countdownTotal: Int, + restSeconds: Int, + restTotal: Int, + phase: HiitPhase, + onPlayPressed: () -> Unit, + onCancelPressed: () -> Unit, + onSkipRest: () -> Unit, + onInfoPressed: () -> Unit, + modifier: Modifier = Modifier +) { + val displaySeconds = when (phase) { + is HiitPhase.SetCountdown -> countdownSeconds + is HiitPhase.RestBetweenSets -> restSeconds + else -> 0 + } + val displayTotal = when (phase) { + is HiitPhase.SetCountdown -> countdownTotal.coerceAtLeast(1) + is HiitPhase.RestBetweenSets -> restTotal.coerceAtLeast(1) + else -> 1 + } + val progress by animateFloatAsState( + targetValue = displaySeconds.toFloat() / displayTotal, + animationSpec = tween(durationMillis = 300), + label = "hiit_countdown_progress" + ) + + val arcColor by animateColorAsState( + targetValue = when (phase) { + is HiitPhase.RestBetweenSets -> MaterialTheme.colorScheme.tertiary + is HiitPhase.ExerciseDone -> MaterialTheme.colorScheme.secondary + else -> MaterialTheme.colorScheme.primary + }, + animationSpec = tween(300), + label = "hiit_arc_color" + ) + + val phaseLabel = stringResource( + when (phase) { + is HiitPhase.SetCountdown -> R.string.hiit_phase_set + is HiitPhase.RestBetweenSets -> R.string.hiit_phase_rest + is HiitPhase.ExerciseDone -> R.string.hiit_phase_done + HiitPhase.Idle -> R.string.hiit_phase_ready + } + ) + + ElevatedCard( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = exerciseName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource( + R.string.hiit_set_progress, currentSetIndex + 1, totalSets + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = onInfoPressed) { + Icon( + painter = painterResource(R.drawable.ic_info), + contentDescription = stringResource(R.string.exercise_details) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = phaseLabel, + style = MaterialTheme.typography.labelLarge, + color = arcColor, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val trackColor = MaterialTheme.colorScheme.surfaceVariant + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(200.dp) + ) { + Canvas(modifier = Modifier.size(200.dp)) { + val strokeWidth = 12.dp.toPx() + val diameter = size.minDimension - strokeWidth + val topLeft = Offset(strokeWidth / 2, strokeWidth / 2) + + drawArc( + color = trackColor, + startAngle = -90f, + sweepAngle = 360f, + useCenter = false, + topLeft = topLeft, + size = Size(diameter, diameter), + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + drawArc( + color = arcColor, + startAngle = -90f, + sweepAngle = 360f * progress, + useCenter = false, + topLeft = topLeft, + size = Size(diameter, diameter), + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + } + + // Show MM:SS during countdowns; show "Done" label when complete. + Text( + text = if (phase is HiitPhase.ExerciseDone) { + stringResource(R.string.hiit_phase_done) + } else { + Formatter.formatTime(displaySeconds).substring(3) + }, + fontSize = 48.sp, + fontWeight = FontWeight.Bold, + color = if (phase is HiitPhase.ExerciseDone) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.onSurface + }, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + when (phase) { + HiitPhase.Idle -> { + FilledTonalButton(onClick = onPlayPressed) { + Icon( + painter = painterResource(R.drawable.ic_play_arrow), + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.hiit_start_set)) + } + } + + is HiitPhase.SetCountdown -> { + FilledTonalButton(onClick = onCancelPressed) { + Icon( + painter = painterResource(R.drawable.ic_pause), + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.cancel_dialog)) + } + } + + is HiitPhase.RestBetweenSets -> { + FilledTonalButton(onClick = onSkipRest) { + Icon( + painter = painterResource(R.drawable.ic_arrow_forward), + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.hiit_skip_rest)) + } + } + + is HiitPhase.ExerciseDone -> { + Text( + text = stringResource(R.string.hiit_all_sets_complete), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun HiitCountdownCardSetCountdownPreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitCountdownCard( + exerciseName = "Mountain Climbers", + currentSetIndex = 1, + totalSets = 4, + countdownSeconds = 18, + countdownTotal = 30, + restSeconds = 0, + restTotal = 15, + phase = HiitPhase.SetCountdown(exerciseId = 1L, setIndex = 1), + onPlayPressed = {}, + onCancelPressed = {}, + onSkipRest = {}, + onInfoPressed = {} + ) + } +} + +@Preview +@Composable +private fun HiitCountdownCardRestPreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitCountdownCard( + exerciseName = "Burpees", + currentSetIndex = 2, + totalSets = 4, + countdownSeconds = 0, + countdownTotal = 30, + restSeconds = 8, + restTotal = 15, + phase = HiitPhase.RestBetweenSets(exerciseId = 1L, nextSetIndex = 2), + onPlayPressed = {}, + onCancelPressed = {}, + onSkipRest = {}, + onInfoPressed = {} + ) + } +} + +@Preview +@Composable +private fun HiitCountdownCardIdlePreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitCountdownCard( + exerciseName = "Jumping Jacks", + currentSetIndex = 0, + totalSets = 3, + countdownSeconds = 0, + countdownTotal = 30, + restSeconds = 0, + restTotal = 15, + phase = HiitPhase.Idle, + onPlayPressed = {}, + onCancelPressed = {}, + onSkipRest = {}, + onInfoPressed = {} + ) + } +} + +@Preview +@Composable +private fun HiitCountdownCardDonePreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitCountdownCard( + exerciseName = "Plank", + currentSetIndex = 3, + totalSets = 4, + countdownSeconds = 0, + countdownTotal = 30, + restSeconds = 0, + restTotal = 15, + phase = HiitPhase.ExerciseDone, + onPlayPressed = {}, + onCancelPressed = {}, + onSkipRest = {}, + onInfoPressed = {} + ) + } +} diff --git a/app/src/main/java/org/librefit/ui/components/HiitSettingsCard.kt b/app/src/main/java/org/librefit/ui/components/HiitSettingsCard.kt new file mode 100644 index 000000000..d7bc87871 --- /dev/null +++ b/app/src/main/java/org/librefit/ui/components/HiitSettingsCard.kt @@ -0,0 +1,126 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026. The LibreFit Contributors + * + * LibreFit is subject to additional terms covering author attribution and trademark usage; + * see the ADDITIONAL_TERMS.md and TRADEMARK_POLICY.md files in the project root. + */ + +package org.librefit.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.librefit.R +import org.librefit.enums.userPreferences.ThemeMode +import org.librefit.ui.theme.LibreFitTheme + +/** + * Toggle + duration input that lets the user enable HIIT auto-advance for a single + * DURATION exercise. Visually subordinate to the exercise card it sits next to. + * + * When [autoAdvanceSets] is on, the parent screen renders [HiitCountdownCard] for this + * exercise. When off, the exercise behaves like a normal duration exercise. + */ +@Composable +fun HiitSettingsCard( + autoAdvanceSets: Boolean, + targetDurationSeconds: Int, + onAutoAdvanceChange: (Boolean) -> Unit, + onTargetDurationChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + OutlinedCard( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + shape = MaterialTheme.shapes.large + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.hiit_mode), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(R.string.hiit_mode_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = autoAdvanceSets, + onCheckedChange = onAutoAdvanceChange + ) + } + AnimatedVisibility(visible = autoAdvanceSets) { + Column { + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = if (targetDurationSeconds == 0) "" else targetDurationSeconds.toString(), + onValueChange = { input -> + // Only accept digits; treat empty as 0. Cap at 9999 to avoid silly values. + val sanitized = input.filter { it.isDigit() }.take(4) + onTargetDurationChange(sanitized.toIntOrNull() ?: 0) + }, + label = { Text(stringResource(R.string.hiit_target_duration)) }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } +} + +@Preview +@Composable +private fun HiitSettingsCardOnPreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitSettingsCard( + autoAdvanceSets = true, + targetDurationSeconds = 30, + onAutoAdvanceChange = {}, + onTargetDurationChange = {} + ) + } +} + +@Preview +@Composable +private fun HiitSettingsCardOffPreview() { + LibreFitTheme(dynamicColor = false, themeMode = ThemeMode.DARK) { + HiitSettingsCard( + autoAdvanceSets = false, + targetDurationSeconds = 0, + onAutoAdvanceChange = {}, + onTargetDurationChange = {} + ) + } +} diff --git a/app/src/main/java/org/librefit/ui/models/UiExercise.kt b/app/src/main/java/org/librefit/ui/models/UiExercise.kt index 7ace3079f..dab39970a 100644 --- a/app/src/main/java/org/librefit/ui/models/UiExercise.kt +++ b/app/src/main/java/org/librefit/ui/models/UiExercise.kt @@ -28,5 +28,7 @@ data class UiExercise( val setMode: SetMode = SetMode.LOAD, val restTime: Int = 0, val position: Int = 0, - val workoutId: Long = 0 + val workoutId: Long = 0, + val targetDuration: Int = 0, + val autoAdvanceSets: Boolean = false ) diff --git a/app/src/main/java/org/librefit/ui/models/mappers/UiExerciseMapper.kt b/app/src/main/java/org/librefit/ui/models/mappers/UiExerciseMapper.kt index ede9429f9..5783e7efb 100644 --- a/app/src/main/java/org/librefit/ui/models/mappers/UiExerciseMapper.kt +++ b/app/src/main/java/org/librefit/ui/models/mappers/UiExerciseMapper.kt @@ -19,7 +19,9 @@ fun Exercise.toUi(): UiExercise { setMode = this.setMode, restTime = this.restTime, position = this.position, - workoutId = this.workoutId + workoutId = this.workoutId, + targetDuration = this.targetDuration, + autoAdvanceSets = this.autoAdvanceSets ) } @@ -31,6 +33,8 @@ fun UiExercise.toEntity(): Exercise { setMode = this.setMode, restTime = this.restTime, position = this.position, - workoutId = this.workoutId + workoutId = this.workoutId, + targetDuration = this.targetDuration, + autoAdvanceSets = this.autoAdvanceSets ) } diff --git a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt index 350a97146..c26649e7a 100644 --- a/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/infoExercise/InfoExerciseScreen.kt @@ -474,7 +474,7 @@ private fun DetailsPage( @Composable private fun InstructionsPage( maxHeight: Dp, - instructions: List, + instructions: List ) { LazyColumn( modifier = Modifier.height(maxHeight), @@ -485,7 +485,6 @@ private fun InstructionsPage( Text( text = buildString { instructions.forEachIndexed { index, instruction -> - // For all items except the first, add the separator BEFORE the item. if (index > 0) { append("\n\n") } diff --git a/app/src/main/java/org/librefit/ui/screens/workout/HiitPhase.kt b/app/src/main/java/org/librefit/ui/screens/workout/HiitPhase.kt new file mode 100644 index 000000000..da9c18624 --- /dev/null +++ b/app/src/main/java/org/librefit/ui/screens/workout/HiitPhase.kt @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026. The LibreFit Contributors + * + * LibreFit is subject to additional terms covering author attribution and trademark usage; + * see the ADDITIONAL_TERMS.md and TRADEMARK_POLICY.md files in the project root. + */ + +package org.librefit.ui.screens.workout + +import androidx.compose.runtime.Immutable + +/** + * State of the HIIT countdown for a single exercise. + * + * The phase is owned by [WorkoutScreenViewModel] and consumed by + * [org.librefit.ui.components.HiitCountdownCard]. The state machine is: + * + * ``` + * Idle ──onPlay──▶ SetCountdown ──finish──▶ RestBetweenSets ──finish──▶ SetCountdown ──▶ … ──▶ ExerciseDone + * │ │ + * └──cancel──▶ Idle ◀────────┘ (skip rest re-enters SetCountdown for the next set) + * ``` + */ +@Immutable +sealed class HiitPhase { + /** No HIIT countdown running. The user can press play to begin. */ + data object Idle : HiitPhase() + + /** Countdown is active for the given exercise's [setIndex]. */ + data class SetCountdown(val exerciseId: Long, val setIndex: Int) : HiitPhase() + + /** Rest countdown that precedes set [nextSetIndex]. */ + data class RestBetweenSets(val exerciseId: Long, val nextSetIndex: Int) : HiitPhase() + + /** All sets for the current exercise are complete. */ + data object ExerciseDone : HiitPhase() +} + +/** + * Pure state-transition helper used when a set countdown finishes. Returns the next phase + * given the current set index, the total number of sets, and whether auto-advance is on. + * + * Extracted to make the transition logic trivially unit-testable without spinning up + * a ViewModel. + */ +fun HiitPhase.SetCountdown.nextPhaseAfterCountdown( + totalSets: Int, + autoAdvance: Boolean +): HiitPhase { + val nextIndex = setIndex + 1 + return if (autoAdvance && nextIndex < totalSets) { + HiitPhase.RestBetweenSets(exerciseId = exerciseId, nextSetIndex = nextIndex) + } else { + HiitPhase.ExerciseDone + } +} diff --git a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt index 5fc1a3cca..36357c731 100644 --- a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt +++ b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreen.kt @@ -83,6 +83,8 @@ import org.librefit.enums.exercise.Equipment import org.librefit.enums.userPreferences.ThemeMode import org.librefit.nav.Route import org.librefit.ui.components.ExerciseCard +import org.librefit.ui.components.HiitCountdownCard +import org.librefit.ui.components.HiitSettingsCard import org.librefit.ui.components.LibreFitLazyColumn import org.librefit.ui.components.LibreFitScaffold import org.librefit.ui.components.animations.DumbbellLottie @@ -138,6 +140,11 @@ fun SharedTransitionScope.WorkoutScreen( val dismissScrollWheelInputAutomatically by viewModel.dismissScrollWheelInputAutomatically.collectAsStateWithLifecycle() + // HIIT countdown state + val hiitPhase by viewModel.hiitPhase.collectAsStateWithLifecycle() + val countdownTime by viewModel.countdownTime.collectAsStateWithLifecycle() + val countdownTotal by viewModel.countdownTotal.collectAsStateWithLifecycle() + //It keeps the screen turned on if (keepWorkoutScreenOn) { @@ -221,6 +228,10 @@ fun SharedTransitionScope.WorkoutScreen( isHeaderSticky = isHeaderSticky, useScrollWheelForInput = useScrollWheelForInput, dismissScrollWheelInputAutomatically = dismissScrollWheelInputAutomatically, + hiitPhase = hiitPhase, + countdownSeconds = countdownTime, + countdownTotal = countdownTotal, + restSeconds = restTime, toggleStopwatch = viewModel::toggleStopwatch, updateIdSetWithRunningStopwatch = viewModel::updateIdSetWithRunningStopwatch, onSelectedExerciseIdChange = { id, idExerciseDC -> @@ -245,7 +256,11 @@ fun SharedTransitionScope.WorkoutScreen( }, moveExercise = viewModel::moveExercise, showInfo = { infoMode.value = it }, - applyPreviousSetPerformance = viewModel::applyPreviousSetPerformance + applyPreviousSetPerformance = viewModel::applyPreviousSetPerformance, + onStartHiitCountdown = viewModel::startHiitCountdown, + onCancelHiitCountdown = viewModel::cancelHiitCountdown, + updateExerciseTargetDuration = viewModel::updateExerciseTargetDuration, + updateExerciseAutoAdvanceSets = viewModel::updateExerciseAutoAdvanceSets ) } } @@ -283,6 +298,10 @@ private fun SharedTransitionScope.WorkoutScreenContent( isHeaderSticky: Boolean, useScrollWheelForInput: Boolean, dismissScrollWheelInputAutomatically: Boolean, + hiitPhase: HiitPhase, + countdownSeconds: Int, + countdownTotal: Int, + restSeconds: Int, toggleStopwatch: () -> Unit, updateIdSetWithRunningStopwatch: (Long?) -> Unit, addSetToExercise: (Long) -> Unit, @@ -298,7 +317,11 @@ private fun SharedTransitionScope.WorkoutScreenContent( moveExercise: (Int, Int) -> Unit, onSelectedExerciseIdChange: (Long, String) -> Unit, showInfo: (InfoMode) -> Unit, - applyPreviousSetPerformance: (Long) -> Unit + applyPreviousSetPerformance: (Long) -> Unit, + onStartHiitCountdown: (Long, Int) -> Unit, + onCancelHiitCountdown: () -> Unit, + updateExerciseTargetDuration: (Int, Long) -> Unit, + updateExerciseAutoAdvanceSets: (Boolean, Long) -> Unit ) { val lazyListState = rememberLazyListState() val hapticFeedback = LocalHapticFeedback.current @@ -399,6 +422,56 @@ private fun SharedTransitionScope.WorkoutScreenContent( items = exercisesWithSets, key = { _, exercise -> exercise.exercise.id } ) { i, exerciseWithSets -> + val exercise = exerciseWithSets.exercise + if (exercise.setMode == SetMode.DURATION) { + HiitSettingsCard( + autoAdvanceSets = exercise.autoAdvanceSets, + targetDurationSeconds = exercise.targetDuration, + onAutoAdvanceChange = { updateExerciseAutoAdvanceSets(it, exercise.id) }, + onTargetDurationChange = { updateExerciseTargetDuration(it, exercise.id) } + ) + + val isHiitForThisExercise = exercise.autoAdvanceSets + && exercise.targetDuration > 0 + && when (val p = hiitPhase) { + is HiitPhase.SetCountdown -> p.exerciseId == exercise.id + is HiitPhase.RestBetweenSets -> p.exerciseId == exercise.id + HiitPhase.Idle, HiitPhase.ExerciseDone -> true + } + + if (isHiitForThisExercise) { + val currentSetIndex = when (val p = hiitPhase) { + is HiitPhase.SetCountdown -> p.setIndex + is HiitPhase.RestBetweenSets -> p.nextSetIndex - 1 + HiitPhase.Idle, HiitPhase.ExerciseDone -> 0 + }.coerceAtLeast(0) + HiitCountdownCard( + exerciseName = exerciseWithSets.exerciseDC.name, + currentSetIndex = currentSetIndex, + totalSets = exerciseWithSets.sets.size, + countdownSeconds = countdownSeconds, + countdownTotal = countdownTotal, + restSeconds = restSeconds, + restTotal = exercise.restTime, + phase = hiitPhase, + onPlayPressed = { onStartHiitCountdown(exercise.id, currentSetIndex) }, + onCancelPressed = onCancelHiitCountdown, + onSkipRest = { + val p = hiitPhase + if (p is HiitPhase.RestBetweenSets) { + onStartHiitCountdown(p.exerciseId, p.nextSetIndex) + } + }, + onInfoPressed = { + onSelectedExerciseIdChange( + exercise.id, + exerciseWithSets.exerciseDC.id + ) + } + ) + } + } + ReorderableItem(reorderableLazyListState, key = exerciseWithSets.exercise.id) { isDragging -> ExerciseCard( modifier = Modifier.animateItem(), @@ -622,6 +695,14 @@ private fun WorkoutScreenPreview() { WorkoutScreenContent( animatedVisibilityScope = this@AnimatedVisibility, exercisesWithSets = e, + hiitPhase = HiitPhase.Idle, + countdownSeconds = 0, + countdownTotal = 0, + restSeconds = 0, + onStartHiitCountdown = { _, _ -> }, + onCancelHiitCountdown = {}, + updateExerciseTargetDuration = { _, _ -> }, + updateExerciseAutoAdvanceSets = { _, _ -> }, previousPerformances = listOf( listOf( PreviousPerformanceSet(time = 612) diff --git a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt index eaf939883..ba4b36ff6 100644 --- a/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt +++ b/app/src/main/java/org/librefit/ui/screens/workout/WorkoutScreenViewModel.kt @@ -406,6 +406,29 @@ class WorkoutScreenViewModel @Inject constructor( syncToRepository() } + fun updateExerciseTargetDuration(targetDuration: Int, id: Long) { + _exercises.update { currentExercises -> + currentExercises.map { eWs -> + if (eWs.exercise.id == id) { + eWs.copy(exercise = eWs.exercise.copy(targetDuration = targetDuration)) + } else eWs + } + } + syncToRepository() + } + + fun updateExerciseAutoAdvanceSets(autoAdvance: Boolean, id: Long) { + _exercises.update { currentExercises -> + currentExercises.map { eWs -> + if (eWs.exercise.id == id) { + eWs.copy(exercise = eWs.exercise.copy(autoAdvanceSets = autoAdvance)) + } else eWs + } + } + if (!autoAdvance) cancelHiitCountdown() + syncToRepository() + } + fun updateExerciseSetMode(setMode: SetMode, id: Long) { _exercises.update { currentExercises -> currentExercises.map { eWs -> @@ -455,6 +478,14 @@ class WorkoutScreenViewModel @Inject constructor( val isStopwatchPaused = WorkoutService.isStopwatchPaused val restTime = WorkoutService.restTime + // ── HIIT countdown state from service ── + val countdownTime = WorkoutService.countdownTime + val isCountdownActive = WorkoutService.isCountdownActive + val countdownTotal = WorkoutService.countdownTotal + + private val _hiitPhase = MutableStateFlow(HiitPhase.Idle) + val hiitPhase = _hiitPhase.asStateFlow() + private var initialRestTime = 1 private var isFocused = true @@ -463,6 +494,8 @@ class WorkoutScreenViewModel @Inject constructor( init { workoutServiceManager.startStopwatch() observeChanges() + observeHiitCountdownFinished() + observeHiitRestFinished() } @@ -480,6 +513,94 @@ class WorkoutScreenViewModel @Inject constructor( } } + // ── HIIT auto-advance logic ── + + /** + * Start a HIIT countdown for a specific exercise and set. + * The service will count down [UiExercise.targetDuration] seconds, then auto-start rest. + */ + fun startHiitCountdown(exerciseId: Long, setIndex: Int) { + val eWs = exercises.value.find { it.exercise.id == exerciseId } ?: return + val exercise = eWs.exercise + if (exercise.setMode != SetMode.DURATION || exercise.targetDuration <= 0) return + + _hiitPhase.update { HiitPhase.SetCountdown(exerciseId, setIndex) } + workoutServiceManager.startCountdown( + durationSeconds = exercise.targetDuration, + restDurationSeconds = exercise.restTime + ) + } + + /** Cancel an active HIIT countdown and return to idle. */ + fun cancelHiitCountdown() { + workoutServiceManager.cancelCountdown() + _hiitPhase.update { HiitPhase.Idle } + } + + /** + * Observes [WorkoutService.countdownFinished]. + * When a countdown completes, marks the set done and transitions to rest phase. + */ + private fun observeHiitCountdownFinished() { + viewModelScope.launch(mainDispatcher) { + WorkoutService.countdownFinished.collect { finished -> + if (!finished) return@collect + val phase = _hiitPhase.value + if (phase is HiitPhase.SetCountdown) { + val eWs = exercises.value.find { it.exercise.id == phase.exerciseId } + if (eWs != null) { + eWs.sets.getOrNull(phase.setIndex)?.let { set -> + // Mark the set complete and persist its elapsed time. We skip + // [updateSetCompleted] because the service already auto-starts the + // rest timer after the countdown finishes. + updateSetTime(eWs.exercise.targetDuration, set.id) + _exercises.update { currentExercises -> + currentExercises.map { e -> + if (e.sets.any { it.id == set.id }) { + e.copy( + sets = e.sets.map { s -> + if (s.id == set.id) s.copy(completed = true) else s + }.toImmutableList() + ) + } else e + } + } + syncToRepository() + } + _hiitPhase.update { + phase.nextPhaseAfterCountdown( + totalSets = eWs.sets.size, + autoAdvance = eWs.exercise.autoAdvanceSets + ) + } + } + } + WorkoutService.clearCountdownFinished() + } + } + } + + /** + * Observes rest timer. When rest finishes during a HIIT [HiitPhase.RestBetweenSets], + * auto-starts the next set's countdown. + */ + private fun observeHiitRestFinished() { + viewModelScope.launch(mainDispatcher) { + var previousRestTime = 0 + WorkoutService.restTime.collect { currentRestTime -> + // Detect transition from >0 to 0 + if (previousRestTime > 0 && currentRestTime == 0) { + val phase = _hiitPhase.value + if (phase is HiitPhase.RestBetweenSets) { + // Auto-start next set countdown + startHiitCountdown(phase.exerciseId, phase.nextSetIndex) + } + } + previousRestTime = currentRestTime + } + } + } + fun toggleStopwatch() { if (isStopwatchPaused.value) { workoutServiceManager.startStopwatch() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ad23d7fa..eaee532cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -490,5 +490,17 @@ Do not show again Undo Clear + HIIT mode + Auto-advance: countdown → rest → next set + Target duration (seconds) + READY + SET + REST + DONE + Start set + Skip rest + All sets complete! Move to next exercise. + Set %1$d / %2$d + Exercise details diff --git a/app/src/test/java/org/librefit/ui/screens/workout/HiitPhaseTest.kt b/app/src/test/java/org/librefit/ui/screens/workout/HiitPhaseTest.kt new file mode 100644 index 000000000..e001d420b --- /dev/null +++ b/app/src/test/java/org/librefit/ui/screens/workout/HiitPhaseTest.kt @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright (c) 2025-2026. The LibreFit Contributors + * + * LibreFit is subject to additional terms covering author attribution and trademark usage; + * see the ADDITIONAL_TERMS.md and TRADEMARK_POLICY.md files in the project root. + */ + +package org.librefit.ui.screens.workout + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class HiitPhaseTest { + + private val exerciseId = 42L + + @Test + fun `nextPhaseAfterCountdown transitions to rest when there are more sets and auto-advance is on`() { + val current = HiitPhase.SetCountdown(exerciseId = exerciseId, setIndex = 0) + + val next = current.nextPhaseAfterCountdown(totalSets = 3, autoAdvance = true) + + assertThat(next).isEqualTo( + HiitPhase.RestBetweenSets(exerciseId = exerciseId, nextSetIndex = 1) + ) + } + + @Test + fun `nextPhaseAfterCountdown finishes exercise when the last set has just completed`() { + val current = HiitPhase.SetCountdown(exerciseId = exerciseId, setIndex = 2) + + val next = current.nextPhaseAfterCountdown(totalSets = 3, autoAdvance = true) + + assertThat(next).isEqualTo(HiitPhase.ExerciseDone) + } + + @Test + fun `nextPhaseAfterCountdown finishes exercise when auto-advance is off`() { + val current = HiitPhase.SetCountdown(exerciseId = exerciseId, setIndex = 0) + + val next = current.nextPhaseAfterCountdown(totalSets = 3, autoAdvance = false) + + assertThat(next).isEqualTo(HiitPhase.ExerciseDone) + } + + @Test + fun `nextPhaseAfterCountdown preserves the exercise id across transitions`() { + val current = HiitPhase.SetCountdown(exerciseId = 9999L, setIndex = 0) + + val next = current.nextPhaseAfterCountdown(totalSets = 2, autoAdvance = true) + + assertThat(next).isInstanceOf(HiitPhase.RestBetweenSets::class.java) + assertThat((next as HiitPhase.RestBetweenSets).exerciseId).isEqualTo(9999L) + } +}