Compare commits
4 Commits
1ea0899bee
...
6685e5731c
| Author | SHA1 | Date | |
|---|---|---|---|
|
6685e5731c
|
|||
|
fe49c5b997
|
|||
|
8fff5f14b5
|
|||
|
28b2494a08
|
38
.gitea/workflows/android.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Android APK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'src-tauri/**'
|
||||
- 'src/**'
|
||||
- 'static/**'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'Dockerfile.android'
|
||||
- '.gitea/workflows/android.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build APK in container
|
||||
run: |
|
||||
docker build -f Dockerfile.android -t bocken-android .
|
||||
docker create --name apk-extract bocken-android
|
||||
docker cp apk-extract:/tmp/Bocken.apk ./Bocken.apk
|
||||
docker rm apk-extract
|
||||
|
||||
- name: Deploy APK to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: bocken.org
|
||||
username: homepage
|
||||
key: ${{ secrets.homepage_ssh }}
|
||||
passphrase: ${{ secrets.homepage_pass }}
|
||||
port: 22
|
||||
source: "Bocken.apk"
|
||||
target: "/var/www/static/"
|
||||
9
.gitignore
vendored
@@ -10,6 +10,13 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
src-tauri/gen/
|
||||
src-tauri/target/
|
||||
src-tauri/*.keystore
|
||||
# Android: ignore build output and caches, track source files
|
||||
src-tauri/gen/android/.gradle/
|
||||
src-tauri/gen/android/app/build/
|
||||
src-tauri/gen/android/buildSrc/.gradle/
|
||||
src-tauri/gen/android/buildSrc/build/
|
||||
src-tauri/gen/android/gradle/
|
||||
src-tauri/gen/android/gradlew
|
||||
src-tauri/gen/android/gradlew.bat
|
||||
|
||||
52
Dockerfile.android
Normal file
@@ -0,0 +1,52 @@
|
||||
FROM rust:1.87-bookworm
|
||||
|
||||
# Java 21
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openjdk-21-jdk-headless unzip wget curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
|
||||
# Android SDK
|
||||
ENV ANDROID_HOME=/opt/android-sdk
|
||||
RUN mkdir -p "$ANDROID_HOME/cmdline-tools" && \
|
||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/tools.zip && \
|
||||
unzip -q /tmp/tools.zip -d "$ANDROID_HOME/cmdline-tools" && \
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" && \
|
||||
rm /tmp/tools.zip
|
||||
ENV PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
RUN yes | sdkmanager --licenses > /dev/null 2>&1 && \
|
||||
sdkmanager "platforms;android-36" "build-tools;35.0.0" "ndk;27.0.12077973"
|
||||
ENV NDK_HOME="$ANDROID_HOME/ndk/27.0.12077973"
|
||||
|
||||
# Rust Android targets
|
||||
RUN rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
||||
|
||||
# Node 22 + pnpm
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
npm install -g pnpm@latest && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install deps first (cache layer)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build APK
|
||||
RUN pnpm tauri android build --apk
|
||||
|
||||
# Sign APK
|
||||
RUN keytool -genkey -v -keystore /tmp/debug.keystore \
|
||||
-alias debug -keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-storepass android -keypass android \
|
||||
-dname "CN=Debug,O=Bocken,C=DE" && \
|
||||
"$ANDROID_HOME/build-tools/35.0.0/zipalign" -f -v 4 \
|
||||
src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk \
|
||||
/tmp/Bocken.apk > /dev/null && \
|
||||
"$ANDROID_HOME/build-tools/35.0.0/apksigner" sign \
|
||||
--ks /tmp/debug.keystore --ks-pass pass:android --key-pass pass:android \
|
||||
/tmp/Bocken.apk
|
||||
@@ -10,6 +10,11 @@ Bilingual recipe collection with search, category filtering, and seasonal recomm
|
||||
### Faith (`/glaube` · `/faith`)
|
||||
Catholic prayer collection in German, English, and Latin. Includes an interactive Rosary with scroll-synced SVG bead visualization, mystery images (sticky column on desktop, draggable PiP on mobile), decade progress tracking, and a daily streak counter. Adapts prayers for liturgical seasons like Eastertide.
|
||||
|
||||
### Fitness (`/fitness`)
|
||||
Workout tracker with template-based training plans, set logging with RPE, rest timers synced across devices via SSE, workout history with statistics, and body measurement tracking. Cardio exercises support native GPS tracking via the Android app with background location recording.
|
||||
|
||||
**Android app**: [Download APK](https://bocken.org/static/Bocken.apk) — Tauri v2 shell with native GPS foreground service for screen-off tracking, live notification with elapsed time, distance, and pace.
|
||||
|
||||
### Expense Sharing (`/cospend`)
|
||||
Shared expense tracker with balance dashboards, debt breakdowns, monthly bar charts with category filtering, and payment management.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ APK_DIR="src-tauri/gen/android/app/build/outputs/apk/universal/release"
|
||||
APK_UNSIGNED="$APK_DIR/app-universal-release-unsigned.apk"
|
||||
APK_SIGNED="$APK_DIR/app-universal-release-signed.apk"
|
||||
KEYSTORE="src-tauri/debug.keystore"
|
||||
PACKAGE="org.bocken.fitness"
|
||||
PACKAGE="org.bocken.app"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [build|deploy|run]"
|
||||
@@ -30,7 +30,19 @@ ensure_keystore() {
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_android_project() {
|
||||
local id_path
|
||||
id_path="src-tauri/gen/android/app/src/main/java/$(echo "$PACKAGE" | tr '.' '/')"
|
||||
if [ ! -d "$id_path" ]; then
|
||||
echo ":: Android project missing or identifier changed, regenerating..."
|
||||
rm -rf src-tauri/gen/android
|
||||
pnpm tauri android init
|
||||
fi
|
||||
}
|
||||
|
||||
build() {
|
||||
ensure_android_project
|
||||
|
||||
echo ":: Building Android APK..."
|
||||
pnpm tauri android build --apk
|
||||
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
@@ -143,7 +143,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bocken-fitness"
|
||||
name = "bocken"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "bocken-fitness"
|
||||
name = "bocken"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "bocken_fitness_lib"
|
||||
name = "bocken_lib"
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
12
src-tauri/gen/android/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
src-tauri/gen/android/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
6
src-tauri/gen/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/src/main/**/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
70
src-tauri/gen/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,70 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "org.bocken.app"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "org.bocken.app"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
48
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.bocken"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".LocationForegroundService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.bocken.app
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.webkit.JavascriptInterface
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class AndroidBridge(private val context: Context) {
|
||||
|
||||
@JavascriptInterface
|
||||
fun startLocationService() {
|
||||
if (context is Activity) {
|
||||
// Request notification permission on Android 13+ (required for foreground service notification)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
1003
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Request background location on Android 10+ (required for screen-off GPS)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context,
|
||||
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
|
||||
1002
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(context, LocationForegroundService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun stopLocationService() {
|
||||
val intent = Intent(context, LocationForegroundService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getPoints(): String {
|
||||
return LocationForegroundService.drainPoints()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun isTracking(): Boolean {
|
||||
return LocationForegroundService.tracking
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package org.bocken.app
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.Collections
|
||||
import kotlin.math.*
|
||||
|
||||
class LocationForegroundService : Service() {
|
||||
|
||||
private var locationManager: LocationManager? = null
|
||||
private var locationListener: LocationListener? = null
|
||||
private var notificationManager: NotificationManager? = null
|
||||
private var pendingIntent: PendingIntent? = null
|
||||
private var startTimeMs: Long = 0L
|
||||
private var lastLat: Double = Double.NaN
|
||||
private var lastLng: Double = Double.NaN
|
||||
private var lastTimestamp: Long = 0L
|
||||
private var currentPaceMinKm: Double = 0.0
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "gps_tracking"
|
||||
const val NOTIFICATION_ID = 1001
|
||||
const val MIN_TIME_MS = 3000L
|
||||
const val MIN_DISTANCE_M = 0f
|
||||
|
||||
private val pointBuffer = Collections.synchronizedList(mutableListOf<JSONObject>())
|
||||
var tracking = false
|
||||
private set
|
||||
var totalDistanceKm: Double = 0.0
|
||||
private set
|
||||
|
||||
fun drainPoints(): String {
|
||||
val drained: List<JSONObject>
|
||||
synchronized(pointBuffer) {
|
||||
drained = ArrayList(pointBuffer)
|
||||
pointBuffer.clear()
|
||||
}
|
||||
val arr = JSONArray()
|
||||
for (p in drained) arr.put(p)
|
||||
return arr.toString()
|
||||
}
|
||||
|
||||
private fun haversineKm(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
|
||||
val R = 6371.0
|
||||
val dLat = Math.toRadians(lat2 - lat1)
|
||||
val dLng = Math.toRadians(lng2 - lng1)
|
||||
val a = sin(dLat / 2).pow(2) +
|
||||
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dLng / 2).pow(2)
|
||||
return 2 * R * asin(sqrt(a))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
notificationManager = getSystemService(NotificationManager::class.java)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startTimeMs = System.currentTimeMillis()
|
||||
totalDistanceKm = 0.0
|
||||
lastLat = Double.NaN
|
||||
lastLng = Double.NaN
|
||||
lastTimestamp = 0L
|
||||
currentPaceMinKm = 0.0
|
||||
|
||||
val notifIntent = Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, notifIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = buildNotification("0:00", "0.00 km", "")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
startLocationUpdates()
|
||||
tracking = true
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun formatPace(paceMinKm: Double): String {
|
||||
if (paceMinKm <= 0 || paceMinKm > 60) return ""
|
||||
val mins = paceMinKm.toInt()
|
||||
val secs = ((paceMinKm - mins) * 60).toInt()
|
||||
return "%d:%02d /km".format(mins, secs)
|
||||
}
|
||||
|
||||
private fun buildNotification(elapsed: String, distance: String, pace: String): Notification {
|
||||
val parts = mutableListOf(elapsed, distance)
|
||||
if (pace.isNotEmpty()) parts.add(pace)
|
||||
val text = parts.joinToString(" · ")
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Bocken — Tracking GPS for active Workout")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Notification.Builder(this)
|
||||
.setContentTitle("Bocken — Tracking GPS for active Workout")
|
||||
.setContentText(text)
|
||||
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatElapsed(): String {
|
||||
val secs = (System.currentTimeMillis() - startTimeMs) / 1000
|
||||
val h = secs / 3600
|
||||
val m = (secs % 3600) / 60
|
||||
val s = secs % 60
|
||||
return if (h > 0) {
|
||||
"%d:%02d:%02d".format(h, m, s)
|
||||
} else {
|
||||
"%d:%02d".format(m, s)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val notification = buildNotification(
|
||||
formatElapsed(),
|
||||
"%.2f km".format(totalDistanceKm),
|
||||
formatPace(currentPaceMinKm)
|
||||
)
|
||||
notificationManager?.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
@Suppress("MissingPermission")
|
||||
private fun startLocationUpdates() {
|
||||
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
locationListener = LocationListener { location ->
|
||||
val lat = location.latitude
|
||||
val lng = location.longitude
|
||||
|
||||
// Accumulate distance and compute pace
|
||||
val now = location.time
|
||||
if (!lastLat.isNaN()) {
|
||||
val segmentKm = haversineKm(lastLat, lastLng, lat, lng)
|
||||
totalDistanceKm += segmentKm
|
||||
if (segmentKm > 0.001 && lastTimestamp > 0) {
|
||||
val dtMin = (now - lastTimestamp) / 60000.0
|
||||
currentPaceMinKm = dtMin / segmentKm
|
||||
}
|
||||
}
|
||||
lastLat = lat
|
||||
lastLng = lng
|
||||
lastTimestamp = now
|
||||
|
||||
val point = JSONObject().apply {
|
||||
put("lat", lat)
|
||||
put("lng", lng)
|
||||
if (location.hasAltitude()) put("altitude", location.altitude)
|
||||
if (location.hasSpeed()) put("speed", location.speed.toDouble())
|
||||
put("timestamp", location.time)
|
||||
}
|
||||
pointBuffer.add(point)
|
||||
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
locationManager?.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
MIN_TIME_MS,
|
||||
MIN_DISTANCE_M,
|
||||
locationListener!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
tracking = false
|
||||
locationListener?.let { locationManager?.removeUpdates(it) }
|
||||
locationListener = null
|
||||
locationManager = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"GPS Tracking",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Shows while GPS is recording your workout"
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.bocken.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
webView.addJavascriptInterface(AndroidBridge(this), "AndroidBridge")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.bocken" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
10
src-tauri/gen/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Bocken</string>
|
||||
<string name="main_activity_title">Bocken</string>
|
||||
</resources>
|
||||
6
src-tauri/gen/android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.bocken" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
22
src-tauri/gen/android/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
23
src-tauri/gen/android/buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """pnpm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src-tauri/gen/android/gradle.properties
Normal file
@@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
3
src-tauri/gen/android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
||||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
2316
src-tauri/gen/schemas/android-schema.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"bocken-remote":{"identifier":"bocken-remote","description":"","remote":{"urls":["https://bocken.org/*","http://192.168.1.4:5173/*"]},"local":true,"windows":["main"],"permissions":["geolocation:allow-check-permissions","geolocation:allow-request-permissions","geolocation:allow-get-current-position","geolocation:allow-watch-position","geolocation:allow-clear-watch"]}}
|
||||
2316
src-tauri/gen/schemas/mobile-schema.json
Normal file
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
bocken_fitness_lib::run();
|
||||
bocken_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "Bocken Fitness",
|
||||
"identifier": "org.bocken.fitness",
|
||||
"productName": "Bocken",
|
||||
"identifier": "org.bocken.app",
|
||||
"version": "0.1.0",
|
||||
"build": {
|
||||
"devUrl": "http://192.168.1.4:5173",
|
||||
@@ -10,8 +10,8 @@
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Bocken Fitness",
|
||||
"url": "/fitness",
|
||||
"title": "Bocken",
|
||||
"url": "/",
|
||||
"fullscreen": false,
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}));
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{lang === 'en' ? 'Exercises' : 'Übungen'} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{lang === 'en' ? 'Exercises' : 'Übungen'} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="exercises-page">
|
||||
<h1>{t('exercises_title', lang)}</h1>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{exercise?.localName ?? (lang === 'en' ? 'Exercise' : 'Übung')} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{exercise?.localName ?? (lang === 'en' ? 'Exercise' : 'Übung')} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="exercise-detail">
|
||||
<h1>{exercise?.localName ?? 'Exercise'}</h1>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{t('history_title', lang)} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{t('history_title', lang)} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="history-page">
|
||||
<h1>{t('history_title', lang)}</h1>
|
||||
|
||||
@@ -506,7 +506,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{session?.name ?? (lang === 'en' ? 'Workout' : 'Training')} - Fitness</title>
|
||||
<title>{session?.name ?? (lang === 'en' ? 'Workout' : 'Training')} - Bocken</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
</svelte:head>
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{lang === 'en' ? 'Measure' : 'Messen'} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{lang === 'en' ? 'Measure' : 'Messen'} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="measure-page">
|
||||
<h1>{t('measure_title', lang)}</h1>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{t('stats_title', lang)} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{t('stats_title', lang)} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="stats-page">
|
||||
<h1>{t('stats_title', lang)}</h1>
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{lang === 'en' ? 'Workout' : 'Training'} - Fitness</title></svelte:head>
|
||||
<svelte:head><title>{lang === 'en' ? 'Workout' : 'Training'} - Bocken</title></svelte:head>
|
||||
|
||||
<div class="template-view">
|
||||
{#if hasSchedule && nextTemplate}
|
||||
|
||||
@@ -519,7 +519,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{workout.name || (lang === 'en' ? 'Workout' : 'Training')} - Fitness</title>
|
||||
<title>{workout.name || (lang === 'en' ? 'Workout' : 'Training')} - Bocken</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
</svelte:head>
|
||||
|
||||
@@ -755,7 +755,7 @@
|
||||
<button class="add-exercise-btn" onclick={() => showPicker = true}>
|
||||
{t('add_exercise', lang)}
|
||||
</button>
|
||||
<button class="cancel-btn" onclick={async () => { workout.cancel(); await sync.onWorkoutEnd(); await goto(`/fitness/${sl.workout}`); }}>
|
||||
<button class="cancel-btn" onclick={async () => { if (gps.isTracking) await gps.stop(); gps.reset(); workout.cancel(); await sync.onWorkoutEnd(); await goto(`/fitness/${sl.workout}`); }}>
|
||||
{t('cancel_workout', lang)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Bocken Rezepte",
|
||||
"short_name": "Rezepte",
|
||||
"description": "Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche",
|
||||
"start_url": "/rezepte",
|
||||
"name": "Bocken",
|
||||
"short_name": "Bocken",
|
||||
"description": "bocken.org",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#5E81AC",
|
||||
|
||||