Compare commits

..

5 Commits

Author SHA1 Message Date
6342bfed5c 2.3.1 - Dependency Updates, Better Live Notifications, and Calendar Fixes 2025-12-01 11:46:31 -07:00
869ca0fc0d Merge pull request '2.3.0 - Unified logging and app intents' (#6) from logging into main
All checks were successful
Ascently - Docs Deploy / build-and-push (push) Successful in 6m59s
Ascently - Sync Deploy / build-and-push (push) Successful in 2m0s
Reviewed-on: #6
2025-11-21 04:01:43 +00:00
33562e9d16 Merge branch 'main' into logging
All checks were successful
Ascently - Docs Deploy / build-and-push (pull_request) Successful in 7m42s
2025-11-21 04:01:16 +00:00
071e47f95e Update docs/src/content/docs/privacy.md
All checks were successful
Ascently - Docs Deploy / build-and-push (push) Successful in 4m18s
2025-10-25 09:41:27 +00:00
c6c3e6084b Update docs/src/content/docs/privacy.md
All checks were successful
Ascently - Docs Deploy / build-and-push (push) Successful in 4m19s
2025-10-25 09:33:33 +00:00
8 changed files with 207 additions and 204 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.ascently" applicationId = "com.atridad.ascently"
minSdk = 31 minSdk = 31
targetSdk = 36 targetSdk = 36
versionCode = 4 versionCode = 47
versionName = "2.3.0" versionName = "2.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -27,6 +27,7 @@
<!-- Permissions for notifications and foreground service --> <!-- Permissions for notifications and foreground service -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

View File

@@ -6,6 +6,8 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.atridad.ascently.MainActivity import com.atridad.ascently.MainActivity
@@ -15,6 +17,7 @@ import com.atridad.ascently.data.repository.ClimbRepository
import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.utils.AppLogger
import com.atridad.ascently.widget.ClimbStatsWidgetProvider import com.atridad.ascently.widget.ClimbStatsWidgetProvider
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
@@ -209,6 +212,54 @@ class SessionTrackingService : Service() {
repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList() repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
} }
val notificationBuilder =
NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_mountains)
.setOngoing(true)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(createOpenAppIntent())
.addAction(
R.drawable.ic_mountains,
"Open Session",
createOpenAppIntent()
)
.addAction(
android.R.drawable.ic_menu_close_clear_cancel,
"End Session",
createStopPendingIntent(sessionId)
)
// Use Live Update
if (Build.VERSION.SDK_INT >= 36) {
val startTimeMillis =
session.startTime?.let { startTime ->
try {
val start = LocalDateTime.parse(startTime)
val zoneId = ZoneId.systemDefault()
start.atZone(zoneId).toInstant().toEpochMilli()
} catch (_: Exception) {
System.currentTimeMillis()
}
}
?: System.currentTimeMillis()
notificationBuilder
.setContentTitle("Climbing Session Active")
.setContentText(
"${gym?.name ?: "Gym"}${attempts.size} attempts"
)
.setWhen(startTimeMillis)
.setUsesChronometer(true)
.setShowWhen(true)
val extras = Bundle()
extras.putBoolean("android.extra.REQUEST_PROMOTED_ONGOING", true)
notificationBuilder.setExtras(extras)
} else {
// Fallback for older versions
val duration = val duration =
session.startTime?.let { startTime -> session.startTime?.let { startTime ->
try { try {
@@ -230,30 +281,14 @@ class SessionTrackingService : Service() {
} }
?: "Active" ?: "Active"
val notification = notificationBuilder
NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Climbing Session Active") .setContentTitle("Climbing Session Active")
.setContentText( .setContentText(
"${gym?.name ?: "Gym"}$duration${attempts.size} attempts" "${gym?.name ?: "Gym"}$duration${attempts.size} attempts"
) )
.setSmallIcon(R.drawable.ic_mountains) }
.setOngoing(true)
.setAutoCancel(false) val notification = notificationBuilder.build()
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(createOpenAppIntent())
.addAction(
R.drawable.ic_mountains,
"Open Session",
createOpenAppIntent()
)
.addAction(
android.R.drawable.ic_menu_close_clear_cancel,
"End Session",
createStopPendingIntent(sessionId)
)
.build()
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)

View File

@@ -5,8 +5,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -38,6 +36,7 @@ import java.time.YearMonth
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.Locale import java.util.Locale
import androidx.core.content.edit
enum class ViewMode { enum class ViewMode {
LIST, LIST,
@@ -60,7 +59,7 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
mutableStateOf(if (savedViewMode == "CALENDAR") ViewMode.CALENDAR else ViewMode.LIST) mutableStateOf(if (savedViewMode == "CALENDAR") ViewMode.CALENDAR else ViewMode.LIST)
} }
var selectedMonth by remember { mutableStateOf(YearMonth.now()) } var selectedMonth by remember { mutableStateOf(YearMonth.now()) }
var selectedDate by remember { mutableStateOf<LocalDate?>(null) } var selectedDate by remember { mutableStateOf<LocalDate?>(LocalDate.now()) }
val completedSessions = sessions.filter { it.status == SessionStatus.COMPLETED } val completedSessions = sessions.filter { it.status == SessionStatus.COMPLETED }
val activeSessionGym = activeSession?.let { session -> gyms.find { it.id == session.gymId } } val activeSessionGym = activeSession?.let { session -> gyms.find { it.id == session.gymId } }
@@ -89,7 +88,7 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
viewMode = viewMode =
if (viewMode == ViewMode.LIST) ViewMode.CALENDAR else ViewMode.LIST if (viewMode == ViewMode.LIST) ViewMode.CALENDAR else ViewMode.LIST
selectedDate = null selectedDate = null
sharedPreferences.edit().putString("view_mode", viewMode.name).apply() sharedPreferences.edit { putString("view_mode", viewMode.name) }
} }
) { ) {
Icon( Icon(
@@ -147,16 +146,11 @@ fun SessionsScreen(viewModel: ClimbViewModel, onNavigateToSessionDetail: (String
CalendarView( CalendarView(
sessions = completedSessions, sessions = completedSessions,
gyms = gyms, gyms = gyms,
activeSession = activeSession,
activeSessionGym = activeSessionGym,
selectedMonth = selectedMonth, selectedMonth = selectedMonth,
onMonthChange = { selectedMonth = it }, onMonthChange = { selectedMonth = it },
selectedDate = selectedDate, selectedDate = selectedDate,
onDateSelected = { selectedDate = it }, onDateSelected = { selectedDate = it },
onNavigateToSessionDetail = onNavigateToSessionDetail, onNavigateToSessionDetail = onNavigateToSessionDetail
onEndSession = {
activeSession?.let { viewModel.endSession(context, it.id) }
}
) )
} }
} }
@@ -315,14 +309,11 @@ fun EmptyStateMessage(
fun CalendarView( fun CalendarView(
sessions: List<ClimbSession>, sessions: List<ClimbSession>,
gyms: List<com.atridad.ascently.data.model.Gym>, gyms: List<com.atridad.ascently.data.model.Gym>,
activeSession: ClimbSession?,
activeSessionGym: com.atridad.ascently.data.model.Gym?,
selectedMonth: YearMonth, selectedMonth: YearMonth,
onMonthChange: (YearMonth) -> Unit, onMonthChange: (YearMonth) -> Unit,
selectedDate: LocalDate?, selectedDate: LocalDate?,
onDateSelected: (LocalDate?) -> Unit, onDateSelected: (LocalDate?) -> Unit,
onNavigateToSessionDetail: (String) -> Unit, onNavigateToSessionDetail: (String) -> Unit
onEndSession: () -> Unit
) { ) {
val sessionsByDate = val sessionsByDate =
remember(sessions) { remember(sessions) {
@@ -331,13 +322,23 @@ fun CalendarView(
java.time.Instant.parse(it.date) java.time.Instant.parse(it.date)
.atZone(java.time.ZoneId.systemDefault()) .atZone(java.time.ZoneId.systemDefault())
.toLocalDate() .toLocalDate()
} catch (e: Exception) { } catch (_: Exception) {
LocalDate.parse(it.date, DateTimeFormatter.ISO_LOCAL_DATE) LocalDate.parse(it.date, DateTimeFormatter.ISO_LOCAL_DATE)
} }
} }
} }
Column(modifier = Modifier.fillMaxSize()) { val firstDayOfMonth = selectedMonth.atDay(1)
val daysInMonth = selectedMonth.lengthOfMonth()
val firstDayOfWeek = firstDayOfMonth.dayOfWeek.value % 7
val totalCells =
((firstDayOfWeek + daysInMonth) / 7.0).let {
if (it == it.toInt().toDouble()) it.toInt() * 7 else (it.toInt() + 1) * 7
}
val numRows = totalCells / 7
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = colors =
@@ -346,7 +347,8 @@ fun CalendarView(
) )
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 12.dp), modifier =
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Row( Row(
@@ -410,19 +412,15 @@ fun CalendarView(
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
val firstDayOfMonth = selectedMonth.atDay(1)
val daysInMonth = selectedMonth.lengthOfMonth()
val firstDayOfWeek = firstDayOfMonth.dayOfWeek.value % 7
val totalCells =
((firstDayOfWeek + daysInMonth) / 7.0).let {
if (it == it.toInt().toDouble()) it.toInt() * 7 else (it.toInt() + 1) * 7
} }
LazyVerticalGrid(columns = GridCells.Fixed(7), modifier = Modifier.fillMaxWidth()) { items(numRows) { rowIndex ->
items(totalCells) { index -> Row(modifier = Modifier.fillMaxWidth()) {
for (colIndex in 0 until 7) {
val index = rowIndex * 7 + colIndex
val dayNumber = index - firstDayOfWeek + 1 val dayNumber = index - firstDayOfWeek + 1
Box(modifier = Modifier.weight(1f)) {
if (dayNumber in 1..daysInMonth) { if (dayNumber in 1..daysInMonth) {
val date = selectedMonth.atDay(dayNumber) val date = selectedMonth.atDay(dayNumber)
val sessionsOnDate = sessionsByDate[date] ?: emptyList() val sessionsOnDate = sessionsByDate[date] ?: emptyList()
@@ -445,10 +443,13 @@ fun CalendarView(
} }
} }
} }
}
}
if (selectedDate != null) { if (selectedDate != null) {
val sessionsOnSelectedDate = sessionsByDate[selectedDate] ?: emptyList() val sessionsOnSelectedDate = sessionsByDate[selectedDate] ?: emptyList()
item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
@@ -458,8 +459,8 @@ fun CalendarView(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 8.dp) modifier = Modifier.padding(vertical = 8.dp)
) )
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(sessionsOnSelectedDate) { session -> items(sessionsOnSelectedDate) { session ->
SessionCard( SessionCard(
session = session, session = session,
@@ -468,7 +469,8 @@ fun CalendarView(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
}
item { Spacer(modifier = Modifier.height(16.dp)) }
} }
} }
} }

View File

@@ -583,41 +583,6 @@ fun SettingsScreen(viewModel: ClimbViewModel) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Card(
shape = RoundedCornerShape(12.dp),
colors =
CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceVariant.copy(
alpha = 0.3f
)
)
) {
ListItem(
headlineContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
painter =
painterResource(
id = R.drawable.ic_mountains
),
contentDescription = "Ascently Logo",
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
Text("Ascently")
}
},
supportingContent = { Text("Track your climbing progress") },
leadingContent = {}
)
}
Spacer(modifier = Modifier.height(8.dp))
Card( Card(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = colors =

View File

@@ -1,6 +1,6 @@
[versions] [versions]
agp = "8.12.3" agp = "8.12.3"
kotlin = "2.2.20" kotlin = "2.2.21"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
@@ -9,17 +9,17 @@ androidxTestCore = "1.7.0"
androidxTestExt = "1.3.0" androidxTestExt = "1.3.0"
androidxTestRunner = "1.7.0" androidxTestRunner = "1.7.0"
androidxTestRules = "1.7.0" androidxTestRules = "1.7.0"
lifecycleRuntimeKtx = "2.9.4" lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.11.0" activityCompose = "1.12.0"
composeBom = "2025.10.00" composeBom = "2025.11.01"
room = "2.8.2" room = "2.8.4"
navigation = "2.9.5" navigation = "2.9.6"
viewmodel = "2.9.4" viewmodel = "2.10.0"
kotlinxSerialization = "1.9.0" kotlinxSerialization = "1.9.0"
kotlinxCoroutines = "1.10.2" kotlinxCoroutines = "1.10.2"
coil = "2.7.0" coil = "2.7.0"
ksp = "2.2.20-2.0.3" ksp = "2.2.20-2.0.3"
exifinterface = "1.3.6" exifinterface = "1.4.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

View File

@@ -5,7 +5,7 @@ description: Ascently's Privacy Policy
**Last updated: September 29, 2025** **Last updated: September 29, 2025**
This Privacy Policy describes our policies and procedures regarding the collection, use, and disclosure of your information when you use my software. This Privacy Policy describes my policies and procedures regarding the collection, use, and disclosure of your information when you use my software.
## No Data Collection ## No Data Collection
@@ -36,7 +36,7 @@ You may optionally integrate with Apple Health or Android Health Connect to impo
This software does not use cookies, tracking pixels, or any other analytics or tracking mechanisms. Your usage of the software is completely private. This software does not use cookies, tracking pixels, or any other analytics or tracking mechanisms. Your usage of the software is completely private.
## Contact Us ## Contact
If you have any questions about this Privacy Policy, you can contact me: If you have any questions about this Privacy Policy, you can contact me: