Compare commits
5 Commits
SYNC_2.2.0
...
ANDROID_2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
c2f95f2793
|
|||
|
b7a3c98b2c
|
|||
|
fed9bab2ea
|
|||
|
862622b07b
|
|||
|
eba503eb5e
|
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.atridad.ascently"
|
applicationId = "com.atridad.ascently"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 45
|
versionCode = 46
|
||||||
versionName = "2.2.0"
|
versionName = "2.2.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import com.atridad.ascently.MainActivity
|
|||||||
import com.atridad.ascently.R
|
import com.atridad.ascently.R
|
||||||
import com.atridad.ascently.data.database.AscentlyDatabase
|
import com.atridad.ascently.data.database.AscentlyDatabase
|
||||||
import com.atridad.ascently.data.repository.ClimbRepository
|
import com.atridad.ascently.data.repository.ClimbRepository
|
||||||
import kotlinx.coroutines.*
|
import com.atridad.ascently.widget.ClimbStatsWidgetProvider
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class SessionTrackingService : Service() {
|
class SessionTrackingService : Service() {
|
||||||
@@ -71,12 +72,19 @@ class SessionTrackingService : Service() {
|
|||||||
val sessionId = intent.getStringExtra(EXTRA_SESSION_ID)
|
val sessionId = intent.getStringExtra(EXTRA_SESSION_ID)
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
val targetSession = when {
|
val targetSession =
|
||||||
|
when {
|
||||||
sessionId != null -> repository.getSessionById(sessionId)
|
sessionId != null -> repository.getSessionById(sessionId)
|
||||||
else -> repository.getActiveSession()
|
else -> repository.getActiveSession()
|
||||||
}
|
}
|
||||||
if (targetSession != null && targetSession.status == com.atridad.ascently.data.model.SessionStatus.ACTIVE) {
|
if (targetSession != null &&
|
||||||
val completed = with(com.atridad.ascently.data.model.ClimbSession) { targetSession.complete() }
|
targetSession.status ==
|
||||||
|
com.atridad.ascently.data.model.SessionStatus.ACTIVE
|
||||||
|
) {
|
||||||
|
val completed =
|
||||||
|
with(com.atridad.ascently.data.model.ClimbSession) {
|
||||||
|
targetSession.complete()
|
||||||
|
}
|
||||||
repository.updateSession(completed)
|
repository.updateSession(completed)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -97,11 +105,14 @@ class SessionTrackingService : Service() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
createAndShowNotification(sessionId)
|
createAndShowNotification(sessionId)
|
||||||
|
// Update widget when session tracking starts
|
||||||
|
ClimbStatsWidgetProvider.updateAllWidgets(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationJob = serviceScope.launch {
|
notificationJob =
|
||||||
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
if (!isNotificationActive()) {
|
if (!isNotificationActive()) {
|
||||||
delay(1000L)
|
delay(1000L)
|
||||||
@@ -117,7 +128,8 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
monitoringJob = serviceScope.launch {
|
monitoringJob =
|
||||||
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
delay(10000L)
|
delay(10000L)
|
||||||
@@ -127,7 +139,11 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val session = repository.getSessionById(sessionId)
|
val session = repository.getSessionById(sessionId)
|
||||||
if (session == null || session.status != com.atridad.ascently.data.model.SessionStatus.ACTIVE) {
|
if (session == null ||
|
||||||
|
session.status !=
|
||||||
|
com.atridad.ascently.data.model.SessionStatus
|
||||||
|
.ACTIVE
|
||||||
|
) {
|
||||||
stopSessionTracking()
|
stopSessionTracking()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -143,6 +159,8 @@ class SessionTrackingService : Service() {
|
|||||||
monitoringJob?.cancel()
|
monitoringJob?.cancel()
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
// Update widget when session tracking stops
|
||||||
|
ClimbStatsWidgetProvider.updateAllWidgets(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNotificationActive(): Boolean {
|
private fun isNotificationActive(): Boolean {
|
||||||
@@ -157,6 +175,8 @@ class SessionTrackingService : Service() {
|
|||||||
private suspend fun updateNotification(sessionId: String) {
|
private suspend fun updateNotification(sessionId: String) {
|
||||||
try {
|
try {
|
||||||
createAndShowNotification(sessionId)
|
createAndShowNotification(sessionId)
|
||||||
|
// Update widget when notification updates
|
||||||
|
ClimbStatsWidgetProvider.updateAllWidgets(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
@@ -172,23 +192,22 @@ class SessionTrackingService : Service() {
|
|||||||
|
|
||||||
private fun createAndShowNotification(sessionId: String) {
|
private fun createAndShowNotification(sessionId: String) {
|
||||||
try {
|
try {
|
||||||
val session = runBlocking {
|
val session = runBlocking { repository.getSessionById(sessionId) }
|
||||||
repository.getSessionById(sessionId)
|
if (session == null ||
|
||||||
}
|
session.status != com.atridad.ascently.data.model.SessionStatus.ACTIVE
|
||||||
if (session == null || session.status != com.atridad.ascently.data.model.SessionStatus.ACTIVE) {
|
) {
|
||||||
stopSessionTracking()
|
stopSessionTracking()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val gym = runBlocking {
|
val gym = runBlocking { repository.getGymById(session.gymId) }
|
||||||
repository.getGymById(session.gymId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val attempts = runBlocking {
|
val attempts = runBlocking {
|
||||||
repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
|
repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val duration = session.startTime?.let { startTime ->
|
val duration =
|
||||||
|
session.startTime?.let { startTime ->
|
||||||
try {
|
try {
|
||||||
val start = LocalDateTime.parse(startTime)
|
val start = LocalDateTime.parse(startTime)
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
@@ -205,11 +224,15 @@ class SessionTrackingService : Service() {
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
"Active"
|
||||||
}
|
}
|
||||||
} ?: "Active"
|
}
|
||||||
|
?: "Active"
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
val notification =
|
||||||
|
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle("Climbing Session Active")
|
.setContentTitle("Climbing Session Active")
|
||||||
.setContentText("${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts")
|
.setContentText(
|
||||||
|
"${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts"
|
||||||
|
)
|
||||||
.setSmallIcon(R.drawable.ic_mountains)
|
.setSmallIcon(R.drawable.ic_mountains)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
@@ -232,7 +255,6 @@ class SessionTrackingService : Service() {
|
|||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
throw e
|
throw e
|
||||||
@@ -240,7 +262,8 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createOpenAppIntent(): PendingIntent {
|
private fun createOpenAppIntent(): PendingIntent {
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent =
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
action = "OPEN_SESSION"
|
action = "OPEN_SESSION"
|
||||||
}
|
}
|
||||||
@@ -263,11 +286,13 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
"Session Tracking",
|
"Session Tracking",
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
).apply {
|
)
|
||||||
|
.apply {
|
||||||
description = "Shows active climbing session information"
|
description = "Shows active climbing session information"
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.atridad.ascently.MainActivity
|
|||||||
import com.atridad.ascently.R
|
import com.atridad.ascently.R
|
||||||
import com.atridad.ascently.data.database.AscentlyDatabase
|
import com.atridad.ascently.data.database.AscentlyDatabase
|
||||||
import com.atridad.ascently.data.repository.ClimbRepository
|
import com.atridad.ascently.data.repository.ClimbRepository
|
||||||
|
import java.time.LocalDate
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -48,53 +49,47 @@ class ClimbStatsWidgetProvider : AppWidgetProvider() {
|
|||||||
val database = AscentlyDatabase.getDatabase(context)
|
val database = AscentlyDatabase.getDatabase(context)
|
||||||
val repository = ClimbRepository(database, context)
|
val repository = ClimbRepository(database, context)
|
||||||
|
|
||||||
// Fetch stats data
|
// Get last 7 days date range (rolling period)
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val sevenDaysAgo = today.minusDays(6) // Today + 6 days ago = 7 days total
|
||||||
|
|
||||||
|
// Fetch all sessions and attempts
|
||||||
val sessions = repository.getAllSessions().first()
|
val sessions = repository.getAllSessions().first()
|
||||||
val problems = repository.getAllProblems().first()
|
|
||||||
val attempts = repository.getAllAttempts().first()
|
val attempts = repository.getAllAttempts().first()
|
||||||
val gyms = repository.getAllGyms().first()
|
|
||||||
|
|
||||||
// Calculate stats
|
// Filter for last 7 days across all gyms
|
||||||
val completedSessions = sessions.filter { it.endTime != null }
|
val weekSessions =
|
||||||
|
sessions.filter { session ->
|
||||||
// Count problems that have been completed (have at least one successful attempt)
|
try {
|
||||||
val completedProblems =
|
val sessionDate = LocalDate.parse(session.date.substring(0, 10))
|
||||||
problems
|
!sessionDate.isBefore(sevenDaysAgo) && !sessionDate.isAfter(today)
|
||||||
.filter { problem ->
|
} catch (_: Exception) {
|
||||||
attempts.any { attempt ->
|
false
|
||||||
attempt.problemId == problem.id &&
|
|
||||||
(attempt.result ==
|
|
||||||
com.atridad.ascently.data.model
|
|
||||||
.AttemptResult.SUCCESS ||
|
|
||||||
attempt.result ==
|
|
||||||
com.atridad.ascently.data.model
|
|
||||||
.AttemptResult.FLASH)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.size
|
|
||||||
|
|
||||||
val favoriteGym =
|
val weekSessionIds = weekSessions.map { it.id }.toSet()
|
||||||
sessions.groupBy { it.gymId }.maxByOrNull { it.value.size }?.let {
|
|
||||||
(gymId, _) ->
|
// Count total attempts this week
|
||||||
gyms.find { it.id == gymId }?.name
|
val totalAttempts =
|
||||||
}
|
attempts.count { attempt -> weekSessionIds.contains(attempt.sessionId) }
|
||||||
?: "No sessions yet"
|
|
||||||
|
// Count sessions this week
|
||||||
|
val totalSessions = weekSessions.size
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
val views = RemoteViews(context.packageName, R.layout.widget_climb_stats)
|
val views = RemoteViews(context.packageName, R.layout.widget_climb_stats)
|
||||||
|
|
||||||
views.setTextViewText(
|
// Set weekly stats
|
||||||
R.id.widget_total_sessions,
|
views.setTextViewText(R.id.widget_attempts_value, totalAttempts.toString())
|
||||||
completedSessions.size.toString()
|
views.setTextViewText(R.id.widget_sessions_value, totalSessions.toString())
|
||||||
)
|
|
||||||
views.setTextViewText(
|
|
||||||
R.id.widget_problems_completed,
|
|
||||||
completedProblems.toString()
|
|
||||||
)
|
|
||||||
views.setTextViewText(R.id.widget_total_problems, problems.size.toString())
|
|
||||||
views.setTextViewText(R.id.widget_favorite_gym, favoriteGym)
|
|
||||||
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
val intent =
|
||||||
|
Intent(context, MainActivity::class.java).apply {
|
||||||
|
flags =
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
val pendingIntent =
|
val pendingIntent =
|
||||||
PendingIntent.getActivity(
|
PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
@@ -110,10 +105,8 @@ class ClimbStatsWidgetProvider : AppWidgetProvider() {
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
val views = RemoteViews(context.packageName, R.layout.widget_climb_stats)
|
val views = RemoteViews(context.packageName, R.layout.widget_climb_stats)
|
||||||
views.setTextViewText(R.id.widget_total_sessions, "0")
|
views.setTextViewText(R.id.widget_attempts_value, "0")
|
||||||
views.setTextViewText(R.id.widget_problems_completed, "0")
|
views.setTextViewText(R.id.widget_sessions_value, "0")
|
||||||
views.setTextViewText(R.id.widget_total_problems, "0")
|
|
||||||
views.setTextViewText(R.id.widget_favorite_gym, "No data")
|
|
||||||
|
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
val pendingIntent =
|
val pendingIntent =
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM10,17L5,12L6.41,10.59L10,14.17L17.59,6.58L19,8L10,17Z"/>
|
||||||
|
</vector>
|
||||||
9
android/app/src/main/res/drawable/ic_circle_filled.xml
Normal file
9
android/app/src/main/res/drawable/ic_circle_filled.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M9,11.24V7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5C7,9.06 7.79,10.43 9,11.24zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11H13v-6C13,6.67 12.33,6 11.5,6S10,6.67 10,7.5v10.74l-3.43,-0.72c-0.08,-0.01 -0.15,-0.03 -0.24,-0.03c-0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8l4.94,4.94C9.96,23.83 10.34,24 10.75,24h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2C19.75,16.63 19.37,16.09 18.84,15.87z"/>
|
||||||
|
</vector>
|
||||||
@@ -5,190 +5,84 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/widget_background"
|
android:background="@drawable/widget_background"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="12dp">
|
android:padding="12dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header with icon and "Weekly" text -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:layout_marginBottom="12dp">
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="24dp"
|
android:layout_width="28dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="28dp"
|
||||||
android:src="@drawable/ic_mountains"
|
android:src="@drawable/ic_mountains"
|
||||||
android:tint="@color/widget_primary"
|
android:tint="@color/widget_primary"
|
||||||
android:layout_marginEnd="8dp" />
|
android:layout_marginEnd="8dp"
|
||||||
|
android:contentDescription="@string/ascently_icon" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:text="@string/weekly"
|
||||||
android:text="Ascently"
|
android:textSize="18sp"
|
||||||
android:textSize="16sp"
|
android:textColor="@color/widget_text_primary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Attempts Row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@drawable/ic_circle_filled"
|
||||||
|
android:tint="@color/widget_primary"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:contentDescription="Attempts icon" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widget_attempts_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="0"
|
||||||
|
android:textSize="40sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/widget_text_primary" />
|
android:textColor="@color/widget_text_primary" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Climbing Stats"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/widget_text_secondary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
<!-- Sessions Row -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<!-- Top Row -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_marginBottom="8dp">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<!-- Sessions Card -->
|
<ImageView
|
||||||
<LinearLayout
|
android:layout_width="32dp"
|
||||||
android:layout_width="0dp"
|
android:layout_height="32dp"
|
||||||
android:layout_height="match_parent"
|
android:src="@drawable/ic_play_arrow_24"
|
||||||
android:layout_weight="1"
|
android:tint="@color/widget_primary"
|
||||||
android:orientation="vertical"
|
android:layout_marginEnd="12dp"
|
||||||
android:gravity="center"
|
android:contentDescription="@string/sessions_icon" />
|
||||||
android:background="@drawable/widget_stat_card_background"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:padding="12dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widget_total_sessions"
|
android:id="@+id/widget_sessions_value"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="0"
|
android:text="@string/_0"
|
||||||
android:textSize="22sp"
|
android:textSize="40sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@color/widget_primary" />
|
android:textColor="@color/widget_text_primary" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Sessions"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/widget_text_secondary"
|
|
||||||
android:layout_marginTop="2dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Problems Card -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@drawable/widget_stat_card_background"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:padding="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/widget_problems_completed"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="0"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/widget_primary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Completed"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/widget_text_secondary"
|
|
||||||
android:layout_marginTop="2dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Bottom Row -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<!-- Success Rate Card -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@drawable/widget_stat_card_background"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:padding="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/widget_total_problems"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="0"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/widget_secondary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Problems"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/widget_text_secondary"
|
|
||||||
android:layout_marginTop="2dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- Favorite Gym Card -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@drawable/widget_stat_card_background"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
android:padding="12dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/widget_favorite_gym"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="No gyms"
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@color/widget_accent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:ellipsize="end" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Favorite"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/widget_text_secondary"
|
|
||||||
android:layout_marginTop="2dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -12,5 +12,9 @@
|
|||||||
<string name="shortcut_end_session_disabled">No active session to end</string>
|
<string name="shortcut_end_session_disabled">No active session to end</string>
|
||||||
|
|
||||||
<!-- Widget -->
|
<!-- Widget -->
|
||||||
<string name="widget_description">View your climbing stats at a glance</string>
|
<string name="widget_description">View your weekly climbing stats</string>
|
||||||
|
<string name="ascently_icon">Ascently icon</string>
|
||||||
|
<string name="weekly">Weekly</string>
|
||||||
|
<string name="sessions_icon">Sessions icon</string>
|
||||||
|
<string name="_0">0</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
android:description="@string/widget_description"
|
android:description="@string/widget_description"
|
||||||
android:initialKeyguardLayout="@layout/widget_climb_stats"
|
android:initialKeyguardLayout="@layout/widget_climb_stats"
|
||||||
android:initialLayout="@layout/widget_climb_stats"
|
android:initialLayout="@layout/widget_climb_stats"
|
||||||
android:minWidth="250dp"
|
android:minWidth="110dp"
|
||||||
android:minHeight="180dp"
|
android:minHeight="110dp"
|
||||||
|
android:maxResizeWidth="110dp"
|
||||||
|
android:maxResizeHeight="110dp"
|
||||||
android:previewImage="@drawable/ic_mountains"
|
android:previewImage="@drawable/ic_mountains"
|
||||||
android:previewLayout="@layout/widget_climb_stats"
|
android:previewLayout="@layout/widget_climb_stats"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="none"
|
||||||
android:targetCellWidth="4"
|
android:targetCellWidth="2"
|
||||||
android:targetCellHeight="2"
|
android:targetCellHeight="2"
|
||||||
android:updatePeriodMillis="1800000"
|
android:updatePeriodMillis="1800000"
|
||||||
android:widgetCategory="home_screen"
|
android:widgetCategory="home_screen" />
|
||||||
android:widgetFeatures="reconfigurable"
|
|
||||||
android:maxResizeWidth="320dp"
|
|
||||||
android:maxResizeHeight="240dp" />
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.0",
|
"@astrojs/node": "^9.5.0",
|
||||||
"@astrojs/starlight": "^0.36.1",
|
"@astrojs/starlight": "^0.36.1",
|
||||||
"astro": "^5.14.5",
|
"astro": "^5.14.6",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"sharp": "^0.34.4"
|
"sharp": "^0.34.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/qrcode": "^1.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
488
docs/pnpm-lock.yaml
generated
488
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
155
docs/src/components/DownloadButtons.astro
Normal file
155
docs/src/components/DownloadButtons.astro
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
import { Tabs, TabItem } from "@astrojs/starlight/components";
|
||||||
|
import { Card, CardGrid } from "@astrojs/starlight/components";
|
||||||
|
import { LinkButton } from "@astrojs/starlight/components";
|
||||||
|
import { Badge } from "@astrojs/starlight/components";
|
||||||
|
import QRCode from "./QRCode.astro";
|
||||||
|
import { downloadLinks, requirements } from "../config";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showQR?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { showQR = false } = Astro.props;
|
||||||
|
|
||||||
|
const hasLink = (link: string | undefined) => link && link.trim() !== "";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tabs syncKey="platform">
|
||||||
|
<TabItem label="Android" icon="star">
|
||||||
|
<CardGrid>
|
||||||
|
{
|
||||||
|
hasLink(downloadLinks.android.playStore) && (
|
||||||
|
<Card title="Google Play Store" icon="star">
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<LinkButton
|
||||||
|
href={downloadLinks.android.playStore}
|
||||||
|
variant="primary"
|
||||||
|
icon="external"
|
||||||
|
>
|
||||||
|
Get on Play Store
|
||||||
|
</LinkButton>
|
||||||
|
</p>
|
||||||
|
{showQR && (
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<QRCode
|
||||||
|
data={downloadLinks.android.playStore}
|
||||||
|
size={200}
|
||||||
|
alt="QR code for Play Store"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasLink(downloadLinks.android.obtainium) && (
|
||||||
|
<Card title="Obtainium" icon="rocket">
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<LinkButton
|
||||||
|
href={downloadLinks.android.obtainium}
|
||||||
|
variant="primary"
|
||||||
|
icon="external"
|
||||||
|
>
|
||||||
|
Get on Obtainium
|
||||||
|
</LinkButton>
|
||||||
|
</p>
|
||||||
|
{showQR && (
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<QRCode
|
||||||
|
data={downloadLinks.android.obtainium}
|
||||||
|
size={200}
|
||||||
|
alt="QR code for Obtainium"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasLink(downloadLinks.android.releases) && (
|
||||||
|
<Card title="Direct Download" icon="download">
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<LinkButton
|
||||||
|
href={downloadLinks.android.releases}
|
||||||
|
variant="secondary"
|
||||||
|
icon="external"
|
||||||
|
>
|
||||||
|
Download APK
|
||||||
|
</LinkButton>
|
||||||
|
</p>
|
||||||
|
{showQR && (
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<QRCode
|
||||||
|
data={downloadLinks.android.releases}
|
||||||
|
size={200}
|
||||||
|
alt="QR code for APK download"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
<p><strong>Requirements:</strong> {requirements.android}</p>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem label="iOS" icon="apple">
|
||||||
|
<CardGrid>
|
||||||
|
{
|
||||||
|
hasLink(downloadLinks.ios.appStore) && (
|
||||||
|
<Card title="App Store" icon="rocket">
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<LinkButton
|
||||||
|
href={downloadLinks.ios.appStore}
|
||||||
|
variant="primary"
|
||||||
|
icon="external"
|
||||||
|
>
|
||||||
|
Download on App Store
|
||||||
|
</LinkButton>
|
||||||
|
</p>
|
||||||
|
{showQR && (
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<QRCode
|
||||||
|
data={downloadLinks.ios.appStore}
|
||||||
|
size={200}
|
||||||
|
alt="QR code for App Store"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasLink(downloadLinks.ios.testFlight) && (
|
||||||
|
<Card title="TestFlight Beta" icon="warning">
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<LinkButton
|
||||||
|
href={downloadLinks.ios.testFlight}
|
||||||
|
variant="secondary"
|
||||||
|
icon="external"
|
||||||
|
>
|
||||||
|
Join TestFlight
|
||||||
|
</LinkButton>
|
||||||
|
</p>
|
||||||
|
{showQR && (
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<QRCode
|
||||||
|
data={downloadLinks.ios.testFlight}
|
||||||
|
size={200}
|
||||||
|
alt="QR code for TestFlight"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
<p><strong>Requirements:</strong> {requirements.ios}</p>
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
111
docs/src/components/QRCode.astro
Normal file
111
docs/src/components/QRCode.astro
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
import * as QR from "qrcode";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: string;
|
||||||
|
size?: number;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, size = 200, alt = "QR Code" } = Astro.props;
|
||||||
|
|
||||||
|
// Generate QR code for dark mode
|
||||||
|
let darkModeQR = "";
|
||||||
|
try {
|
||||||
|
darkModeQR = await QR.toDataURL(data, {
|
||||||
|
width: size,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: "#FFBF00",
|
||||||
|
light: "#17181C",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to generate dark mode QR code:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate QR code for light mode
|
||||||
|
let lightModeQR = "";
|
||||||
|
try {
|
||||||
|
lightModeQR = await QR.toDataURL(data, {
|
||||||
|
width: size,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: "#F24B3C",
|
||||||
|
light: "#FFFFFF",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to generate light mode QR code:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueId = `qr-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(darkModeQR || lightModeQR) && (
|
||||||
|
<img
|
||||||
|
id={uniqueId}
|
||||||
|
alt={alt}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
data-light-src={lightModeQR}
|
||||||
|
data-dark-src={darkModeQR}
|
||||||
|
style="margin: auto;"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ uniqueId, lightModeQR, darkModeQR }}>
|
||||||
|
(function () {
|
||||||
|
const img = document.getElementById(uniqueId);
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
const theme = document.documentElement.getAttribute("data-theme");
|
||||||
|
if (theme === "dark" && darkModeQR) {
|
||||||
|
img.setAttribute("src", darkModeQR);
|
||||||
|
} else if (lightModeQR) {
|
||||||
|
img.setAttribute("src", lightModeQR);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateQRCodes() {
|
||||||
|
const theme = document.documentElement.getAttribute("data-theme");
|
||||||
|
const qrImages = document.querySelectorAll(
|
||||||
|
"img[data-light-src][data-dark-src]",
|
||||||
|
);
|
||||||
|
|
||||||
|
qrImages.forEach((img) => {
|
||||||
|
const lightSrc = img.getAttribute("data-light-src");
|
||||||
|
const darkSrc = img.getAttribute("data-dark-src");
|
||||||
|
|
||||||
|
if (theme === "dark" && darkSrc) {
|
||||||
|
img.setAttribute("src", darkSrc);
|
||||||
|
} else if (lightSrc) {
|
||||||
|
img.setAttribute("src", lightSrc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial theme on page load
|
||||||
|
updateQRCodes();
|
||||||
|
|
||||||
|
// Watch for theme changes
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (
|
||||||
|
mutation.type === "attributes" &&
|
||||||
|
mutation.attributeName === "data-theme"
|
||||||
|
) {
|
||||||
|
updateQRCodes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["data-theme"],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
17
docs/src/config.ts
Normal file
17
docs/src/config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const requirements = {
|
||||||
|
android: "Android 12+",
|
||||||
|
ios: "iOS 17+",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const downloadLinks = {
|
||||||
|
android: {
|
||||||
|
releases: "https://git.atri.dad/atridad/Ascently/releases",
|
||||||
|
obtainium:
|
||||||
|
"https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://git.atri.dad/atridad/Ascently/releases",
|
||||||
|
playStore: "",
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
appStore: "https://apps.apple.com/ca/app/ascently/id6753959144",
|
||||||
|
testFlight: "https://testflight.apple.com/join/E2DYRGH8",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
title: Download
|
|
||||||
description: Get Ascently on your Android or iOS device
|
|
||||||
---
|
|
||||||
|
|
||||||
## Android
|
|
||||||
|
|
||||||
### Option 1: Direct APK Download
|
|
||||||
Download the latest APK from the [Releases page](https://git.atri.dad/atridad/Ascently/releases).
|
|
||||||
|
|
||||||
### Option 2: Obtainium
|
|
||||||
Use Obtainium for automatic updates:
|
|
||||||
|
|
||||||
[<img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Obtainium" height="41">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.atridad.ascently%22%2C%22url%22%3A%22https%3A%2F%2Fgit.atri.dad%2Fatridad%2FAscently%2Freleases%22%2C%22author%22%3A%22git.atri.dad%22%2C%22name%22%3A%22Ascently%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Ascently%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
|
||||||
|
|
||||||
## iOS
|
|
||||||
|
|
||||||
### App Store
|
|
||||||
Download from the app store [here](https://apps.apple.com/ca/app/ascently/id6753959144)
|
|
||||||
|
|
||||||
### TestFlight Beta
|
|
||||||
Join the TestFlight beta [here](https://testflight.apple.com/join/E2DYRGH8)
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- **Android 12+** or **iOS 17+**
|
|
||||||
10
docs/src/content/docs/download.mdx
Normal file
10
docs/src/content/docs/download.mdx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Download
|
||||||
|
description: Get Ascently on your Android or iOS device
|
||||||
|
---
|
||||||
|
|
||||||
|
import DownloadButtons from '../../components/DownloadButtons.astro';
|
||||||
|
|
||||||
|
Get Ascently on your device and start tracking your climbs today!
|
||||||
|
|
||||||
|
<DownloadButtons showQR={true} />
|
||||||
Binary file not shown.
Reference in New Issue
Block a user