Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.PHONY: release debug pull tasks find repo one install clean

JAVA_HOME := /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home

release:
./gradlew --no-daemon assembleRelease

debug:
./gradlew --no-daemon assembleDebug

repo:
open https://github.com/Wavesonics/FastTrack
tasks:
./gradlew --no-daemon -q :tasks

find:
find app/build/outputs -iname '*.apk'
open -R app/build/outputs/apk/release/*.apk

one:
adb -s 321c9dca install app/build/outputs/apk/release/*.apk

install:
adb install app/build/outputs/apk/release/*.apk

install-debug:
adb install app/build/outputs/apk/debug/*.apk

clean:
./gradlew --no-daemon clean

check-jetifier:
./gradlew checkJetifier

test:
./gradlew app:testDebugUnitTest
10 changes: 6 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ plugins {
alias(libs.plugins.kotlin.compose)
}

// make APK filenames follow <archivesName>-<buildType>.apk,
// i.e. FastTrack-release.apk and FastTrack-dev.apk
base {
archivesName.set("FastTrack")
}

android {
namespace = "com.darkrockstudios.apps.fasttrack"
compileSdk = libs.versions.compileSdk.get().toInt()
Expand Down Expand Up @@ -144,16 +150,12 @@ dependencies {
implementation(libs.koin.androidx.compose)
implementation(libs.koin.androidx.workmanager)

implementation(libs.customdatetimepicker)

implementation(libs.satchel.core)
implementation(libs.satchel.storer.encrypted.file)
implementation(libs.satchel.serializer.base64.android)

implementation(libs.compose.markdown)

implementation(libs.materialabout)

implementation(libs.core)
implementation(libs.ext.latex)
implementation(libs.ext.strikethrough)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "82e9fabc07c671aa9a050a69e505a918",
"entities": [
{
"tableName": "FastEntry",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `start` INTEGER NOT NULL, `length` INTEGER NOT NULL, `notes` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "start",
"columnName": "start",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "length",
"columnName": "length",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notes",
"columnName": "notes",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
}
}
],
"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, '82e9fabc07c671aa9a050a69e505a918')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class FakeSettingsDatasource : SettingsDatasource {
private var showFastingNotification: Boolean = true
private var useMetricSystem: Boolean? = null
private var themeMode: ThemeMode = ThemeMode.SYSTEM
private var dateStyle: DateStyle = DateStyle.OPTIMIZED_COMPACT
private var logViewMode: LogViewMode = LogViewMode.LIST

override fun getFastingAlerts(): Boolean = fastingAlerts
Expand Down Expand Up @@ -46,8 +47,32 @@ class FakeSettingsDatasource : SettingsDatasource {
themeMode = mode
}

override fun getDateStyle(): DateStyle = dateStyle
override fun setDateStyle(style: DateStyle) {
dateStyle = style
}

override fun getLogViewMode(): LogViewMode = logViewMode
override fun setLogViewMode(mode: LogViewMode) {
logViewMode = mode
}

// In-memory phase visibility, matching the production defaults so a screen
// driven by this fake behaves like the real app unless a test overrides it.
private var showFatBurn: Boolean = true
private var showKetosis: Boolean = true
private var showAutophagy: Boolean = true
private var phaseAutoMode: Boolean = false

override fun getShowFatBurn(): Boolean = showFatBurn
override fun setShowFatBurn(enabled: Boolean) { showFatBurn = enabled }
override fun getShowKetosis(): Boolean = showKetosis
override fun setShowKetosis(enabled: Boolean) { showKetosis = enabled }
override fun getShowAutophagy(): Boolean = showAutophagy
override fun setShowAutophagy(enabled: Boolean) { showAutophagy = enabled }
override fun getPhaseAutoMode(): Boolean = phaseAutoMode
override fun setPhaseAutoMode(enabled: Boolean) { phaseAutoMode = enabled }

override fun phaseVisibilityFlow(): Flow<PhaseVisibility> =
flowOf(PhaseVisibility(showFatBurn, showKetosis, showAutophagy, phaseAutoMode))
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class FastTrackApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()

Napier.base(DebugAntilog())
if (BuildConfig.DEBUG) {
Napier.base(DebugAntilog())
}

Satchel.init(
storer = FileSatchelStorer(File(filesDir, Data.STORAGE_PATH)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import androidx.compose.ui.graphics.toArgb
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import com.darkrockstudios.apps.fasttrack.data.Stages
import com.darkrockstudios.apps.fasttrack.data.descriptionFor
import com.darkrockstudios.apps.fasttrack.screens.main.MainActivity
import com.darkrockstudios.apps.fasttrack.utils.formatDuration
import com.darkrockstudios.apps.fasttrack.utils.getColorFor
import io.github.aakira.napier.Napier
import kotlin.time.Duration
Expand Down Expand Up @@ -53,10 +55,8 @@ object FastingNotificationManager {
}
val stage = Stages.stage[stageIndex]

// Format elapsed time
val timeText = elapsedTime.toComponents { hours, _, _, _ ->
"%d".format(hours)
}
// The notification refreshes hourly, so keep the duration hour-granular
val timeText = formatDuration(context, elapsedTime, withMinutes = false)

// Get energy mode
val energyMode = if (currentPhase.fatBurning) {
Expand All @@ -66,14 +66,14 @@ object FastingNotificationManager {
}

// Build notification content
val title = context.getString(R.string.notification_fasting_title, timeText)
val title = context.getString(R.string.notification_fasting_title_duration, timeText)
val contentText = context.getString(stage.title)
val expandedText = buildString {
append(context.getString(stage.title))
append("\n")
append(context.getString(R.string.notification_fasting_energy_mode, energyMode))
append("\n")
append(context.getString(stage.description))
append(context.getString(descriptionFor(stage, elapsedHours)))
}

val intent = Intent(context, MainActivity::class.java).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ object Data
const val KEY_FASTING_NOTIFICATION = "fasting_notification"
const val KEY_METRIC_SYSTEM = "metric_system"
const val KEY_THEME_MODE = "theme_mode"
const val KEY_DATE_STYLE = "date_style"
const val KEY_LOG_VIEW_MODE = "log_view_mode"
const val KEY_SHOW_FAT_BURN = "show_fat_burn"
const val KEY_SHOW_KETOSIS = "show_ketosis"
const val KEY_SHOW_AUTOPHAGY = "show_autophagy"
const val KEY_PHASE_AUTO_MODE = "phase_auto_mode"

private const val CM_INCH_RATIO = 2.54
fun inchToCm(inches: Int): Double = inches.toDouble() * CM_INCH_RATIO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.darkrockstudios.apps.fasttrack.data

import androidx.annotation.StringRes
import com.darkrockstudios.apps.fasttrack.R

/**
* One stage of the fasting journey as shown on the dial.
* The canonical stage text lives in fasting-stages.md at the repo root;
*/
data class JourneyStage(
val startHours: Int,
/** null means the stage is open-ended (the final stage) */
val endHours: Int?,
val emoji: String,
@StringRes val title: Int,
@StringRes val body: Int,
)

object FastingJourney {
val stages = listOf(
JourneyStage(0, 2, "🌅", R.string.journey_stage_0_title, R.string.journey_stage_0_body),
JourneyStage(2, 5, "🌊", R.string.journey_stage_1_title, R.string.journey_stage_1_body),
JourneyStage(5, 8, "⚖️", R.string.journey_stage_2_title, R.string.journey_stage_2_body),
JourneyStage(8, 12, "🧪", R.string.journey_stage_3_title, R.string.journey_stage_3_body),
JourneyStage(12, 18, "🔥", R.string.journey_stage_4_title, R.string.journey_stage_4_body),
JourneyStage(18, 24, "💎", R.string.journey_stage_5_title, R.string.journey_stage_5_body),
JourneyStage(24, 48, "♻️", R.string.journey_stage_6_title, R.string.journey_stage_6_body),
JourneyStage(48, 54, "💪", R.string.journey_stage_7_title, R.string.journey_stage_7_body),
JourneyStage(54, 72, "🎯", R.string.journey_stage_8_title, R.string.journey_stage_8_body),
JourneyStage(72, null, "🌱", R.string.journey_stage_9_title, R.string.journey_stage_9_body),
)

fun indexFor(elapsedHours: Double): Int =
stages.indexOfLast { elapsedHours >= it.startHours }.coerceAtLeast(0)

fun stageFor(elapsedHours: Double): JourneyStage = stages[indexFor(elapsedHours)]
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ object Stages {
)
}

/** Hours spent in ketosis for a fast of the given [length] (0 if it never reached ketosis). */
fun ketosisHours(length: Duration): Double {
val start = Stages.PHASE_KETOSIS.hours.toDouble()
return (length.toDouble(DurationUnit.HOURS) - start).coerceAtLeast(0.0)
}

/** Hours spent in autophagy for a fast of the given [length] (0 if it never reached autophagy). */
fun autophagyHours(length: Duration): Double {
val start = Stages.PHASE_AUTOPHAGY.hours.toDouble()
return (length.toDouble(DurationUnit.HOURS) - start).coerceAtLeast(0.0)
}

/**
* The description to show for [stage] at [elapsedHours]. Optimal autophagy's copy
* talks about the window "between now and 72 hours", which no longer applies once a
* fast is past 72h, so beyond that we switch to a version without the deadline.
*/
@StringRes
fun descriptionFor(stage: Stage, elapsedHours: Long): Int =
if (stage.description == R.string.stage_optimal_autophagy_description && elapsedHours >= 72L) {
R.string.stage_optimal_autophagy_description_past72
} else {
stage.description
}

fun phaseForStage(stage: Stage): Phase {
return when {
stage.hours >= 48 -> Stages.PHASE_OPTIMAL_AUTOPHAGY
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.darkrockstudios.apps.fasttrack.data.activefast

import android.content.Context
import android.preference.PreferenceManager
import androidx.core.content.edit
import com.darkrockstudios.apps.fasttrack.data.Data
import kotlin.time.Instant

class ActiveFastPreferencesDataSource(appContext: Context) : ActiveFastDataSource {

private val storage = PreferenceManager.getDefaultSharedPreferences(appContext)
// The historical default-preferences file, addressed directly now that
// android.preference.PreferenceManager is deprecated (same file, same data).
private val storage = appContext.getSharedPreferences(
"${appContext.packageName}_preferences", Context.MODE_PRIVATE
)

override fun getFastStart(): Instant? {
val mills = storage.getLong(Data.KEY_FAST_START, -1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ package com.darkrockstudios.apps.fasttrack.data.database

import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [FastEntry::class], version = 1)
abstract class AppDatabase: RoomDatabase()
{
@Database(entities = [FastEntry::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun fastDao(): FastEntryDao

companion object {
/** v2 adds the optional `notes` column to each fast entry. */
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE FastEntry ADD COLUMN notes TEXT NOT NULL DEFAULT ''")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,26 @@ package com.darkrockstudios.apps.fasttrack.data.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.darkrockstudios.apps.fasttrack.data.Stages
import com.darkrockstudios.apps.fasttrack.data.autophagyHours
import com.darkrockstudios.apps.fasttrack.data.ketosisHours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit

/**
* start - a linux UTC epoch timestamp in milliseconds
* start - a linux UTC epoch timestamp in milliseconds
* length - duration in milliseconds of the fast
* notes - optional user note captured when a fast ends (may be empty)
*/
@Entity
data class FastEntry(
@PrimaryKey(autoGenerate = true) val uid: Int = 0,
@ColumnInfo val start: Long,
@ColumnInfo val length: Long
@ColumnInfo val length: Long,
@ColumnInfo(defaultValue = "''") val notes: String = ""
) {
fun lengthHours() = length.milliseconds.toDouble(DurationUnit.HOURS)

fun calculateKetosis(): Double {
val ketosisStart = Stages.PHASE_KETOSIS.hours.toDouble()
val lenHours = lengthHours()
return if (lenHours > ketosisStart) {
return lenHours - ketosisStart
} else {
0.0
}
}
fun calculateKetosis(): Double = ketosisHours(length.milliseconds)

fun calculateAutophagy(): Double {
val autophagyStart = Stages.PHASE_AUTOPHAGY.hours.toDouble()
val lenHours = lengthHours()
return if (lenHours > autophagyStart) {
return lenHours - autophagyStart
} else {
0.0
}
}
fun calculateAutophagy(): Double = autophagyHours(length.milliseconds)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ interface FastEntryDao
@Query("DELETE FROM fastentry WHERE start = :startTime")
fun deleteByStartTime(startTime: Long): Int

@Query("DELETE FROM fastentry WHERE start >= :fromInclusive AND start < :toExclusive")
fun deleteByStartRange(fromInclusive: Long, toExclusive: Long): Int

@Query("DELETE FROM fastentry WHERE uid = :uid")
fun deleteByUid(uid: Int): Int

@Query("DELETE FROM fastentry")
fun deleteAll(): Int
}
Loading