This commit is contained in:
litruv
2026-02-21 17:50:50 +11:00
parent caf1869e3e
commit 22f5c5e7e4
92 changed files with 6344 additions and 26256 deletions

View File

@@ -1,9 +0,0 @@
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.aarch64-linux-android]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

View File

@@ -1,6 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools
.tauri-private.key

View File

@@ -1 +0,0 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExREU1NkVBMDY4MzcxMjAKUldRZ2NZTUc2bGJlRVNaQldnMXpJcHlocVpCWUNNaUdicjBGMVRsck1GQXhvTnJiOUNhVVRHSzQK

8276
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package]
name = "paarrot"
version = "4.10.2"
description = "Yet another matrix client"
authors = ["Ajay Bura"]
license = "AGPL-3.0-only"
repository = "https://github.com/cinnyapp/cinny-desktop"
default-run = "paarrot"
edition = "2021"
rust-version = "1.70"
[lib]
name = "paarrot_lib"
crate-type = ["staticlib", "cdylib", "lib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2", features = ["devtools", "tray-icon", "image-png"] }
tauri-plugin-opener = "2"
tauri-plugin-notification = "2"
log = "0.4"
tauri-plugin-http = "2"
[target."cfg(target_os = \"linux\")".dependencies]
arboard = { version = "3", features = ["wayland-data-control"] }
png = "0.17"
base64 = "0.22"
webkit2gtk = "2.0"
gtk = "0.18"
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
tauri-plugin-deep-link = "2"
# Matrix SDK for native background sync (no sqlite on mobile - use in-memory store)
matrix-sdk = { version = "0.7", default-features = false, features = ["rustls-tls", "e2e-encryption"] }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-localhost = "2"
tauri-plugin-window-state = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
tauri-plugin-process = "2"
open = "5"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[dependencies.ppv-lite86]
version = "=0.2.20"

View File

@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@@ -1,29 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for all windows",
"windows": ["*"],
"platforms": ["linux", "windows", "macOS"],
"permissions": [
"core:default",
"core:webview:allow-internal-toggle-devtools",
"opener:default",
"opener:allow-open-url",
"opener:allow-default-urls",
"notification:default",
"notification:allow-register-listener",
"autostart:default",
"updater:default",
"updater:allow-check",
"updater:allow-download",
"updater:allow-download-and-install",
"dialog:default",
"process:allow-restart",
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.telegram.org/**" }
]
}
]
}

View File

@@ -1,13 +0,0 @@
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "mobile",
"description": "Default capability for mobile",
"windows": ["*"],
"platforms": ["android", "iOS"],
"permissions": [
"core:default",
"notification:default",
"deep-link:default",
"opener:default"
]
}

View File

@@ -1,12 +0,0 @@
# 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

View File

@@ -1,19 +0,0 @@
*.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

View File

@@ -1,6 +0,0 @@
/src/main/java/wtf/ruv/paarrot/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties

View File

@@ -1,73 +0,0 @@
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 = "wtf.ruv.paarrot"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "wtf.ruv.paarrot"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
ndk {
abiFilters += listOf("arm64-v8a")
}
}
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")

View File

@@ -1,21 +0,0 @@
# 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

View File

@@ -1,73 +0,0 @@
<?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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- Audio/Video call permissions -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Hardware features for calls (optional) -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<!-- 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.paarrot"
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>
<!-- Background sync service -->
<service
android:name=".SyncService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<!-- Boot receiver to restart service -->
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<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>

View File

@@ -1,22 +0,0 @@
package wtf.ruv.paarrot
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
/**
* Receives BOOT_COMPLETED broadcast to restart the sync service after device reboot.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
val serviceIntent = Intent(context, SyncService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}

View File

@@ -1,73 +0,0 @@
package wtf.ruv.paarrot
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
companion object {
private const val TAG = "PaarrotMain"
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// Start the foreground sync service to keep the app alive for notifications
startSyncService()
// Request battery optimization exemption for reliable background operation
requestBatteryOptimizationExemption()
}
override fun onResume() {
super.onResume()
// Ensure service is running when app comes to foreground
startSyncService()
}
private fun startSyncService() {
try {
val serviceIntent = Intent(this, SyncService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
Log.d(TAG, "SyncService started")
} catch (e: Exception) {
Log.e(TAG, "Failed to start SyncService", e)
}
}
private fun requestBatteryOptimizationExemption() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
Log.d(TAG, "Requesting battery optimization exemption")
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
}
startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Failed to request battery optimization exemption", e)
// Try opening battery settings instead
try {
val settingsIntent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
startActivity(settingsIntent)
} catch (e2: Exception) {
Log.e(TAG, "Failed to open battery settings", e2)
}
}
} else {
Log.d(TAG, "Already exempt from battery optimization")
}
}
}
}

View File

@@ -1,278 +0,0 @@
package wtf.ruv.paarrot
import android.app.AlarmManager
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.net.Uri
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
/**
* Foreground service that keeps the app alive for background sync.
* This allows Matrix sync to continue even when the app is in the background.
*/
class SyncService : Service() {
companion object {
private const val TAG = "PaarrotSync"
private const val NOTIFICATION_ID = 1
private const val CHANNEL_ID = "paarrot_sync_channel"
private const val CHANNEL_NAME = "Background Sync"
private const val WAKE_LOCK_TAG = "Paarrot:SyncWakeLock"
private const val ALARM_REQUEST_CODE = 1001
private const val KEEP_ALIVE_INTERVAL_MS = 4 * 60 * 1000L // 4 minutes (before 5 min Doze threshold)
private const val WAKE_LOCK_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes max wake lock
}
private var wakeLock: PowerManager.WakeLock? = null
private val handler = Handler(Looper.getMainLooper())
private var isRunning = false
private val keepAliveRunnable = object : Runnable {
override fun run() {
if (!isRunning) return
Log.d(TAG, "Keep-alive tick - refreshing wake lock")
ensureWakeLock()
refreshNotification()
handler.postDelayed(this, KEEP_ALIVE_INTERVAL_MS)
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
Log.d(TAG, "SyncService created")
isRunning = true
createNotificationChannel()
acquireWakeLock()
scheduleKeepAlive()
requestBatteryOptimizationExemption()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "SyncService onStartCommand")
startForeground(NOTIFICATION_ID, createNotification())
ensureWakeLock()
// Return REDELIVER_INTENT so the system restarts us with the last intent
return START_REDELIVER_INTENT
}
override fun onDestroy() {
Log.d(TAG, "SyncService being destroyed - scheduling restart")
isRunning = false
handler.removeCallbacks(keepAliveRunnable)
// Schedule immediate restart
scheduleRestart()
releaseWakeLock()
super.onDestroy()
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.d(TAG, "Task removed - scheduling restart")
scheduleRestart()
super.onTaskRemoved(rootIntent)
}
override fun onLowMemory() {
Log.w(TAG, "Low memory warning")
super.onLowMemory()
}
override fun onTrimMemory(level: Int) {
Log.d(TAG, "Trim memory level: $level")
super.onTrimMemory(level)
}
private fun scheduleRestart() {
// Try to restart the service immediately
val restartIntent = Intent(applicationContext, SyncService::class.java)
restartIntent.setPackage(packageName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
startForegroundService(restartIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to restart service directly", e)
// Fall back to alarm
scheduleAlarmRestart()
}
} else {
startService(restartIntent)
}
}
private fun scheduleAlarmRestart() {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, SyncService::class.java)
val pendingIntent = PendingIntent.getService(
this,
ALARM_REQUEST_CODE + 1,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Schedule restart in 1 second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 1000,
pendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 1000,
pendingIntent
)
}
}
private fun requestBatteryOptimizationExemption() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
Log.d(TAG, "Requesting battery optimization exemption")
// We can't directly request, but the app should prompt the user
// This is just a note that we need the exemption
}
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Keeps Paarrot connected for message notifications"
setShowBadge(false)
enableLights(false)
enableVibration(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(): android.app.Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Paarrot")
.setContentText("Connected to Matrix")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setShowWhen(false)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
}
private fun refreshNotification() {
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, createNotification())
}
private fun scheduleKeepAlive() {
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
scheduleAlarm()
}
private fun scheduleAlarm() {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, SyncService::class.java)
val pendingIntent = PendingIntent.getService(
this,
ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Use setExactAndAllowWhileIdle for better Doze mode support
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
pendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
pendingIntent
)
}
}
private fun cancelAlarm() {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, SyncService::class.java)
val pendingIntent = PendingIntent.getService(
this,
ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
}
private fun acquireWakeLock() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
WAKE_LOCK_TAG
).apply {
// Acquire with timeout to prevent battery drain if something goes wrong
acquire(WAKE_LOCK_TIMEOUT_MS)
}
Log.d(TAG, "Wake lock acquired")
}
private fun ensureWakeLock() {
wakeLock?.let {
if (!it.isHeld) {
Log.d(TAG, "Wake lock was released, re-acquiring")
it.acquire(WAKE_LOCK_TIMEOUT_MS)
}
} ?: run {
Log.d(TAG, "Wake lock was null, creating new one")
acquireWakeLock()
}
// Re-schedule alarm each time we refresh
scheduleAlarm()
}
private fun releaseWakeLock() {
wakeLock?.let {
if (it.isHeld) {
it.release()
Log.d(TAG, "Wake lock released")
}
}
wakeLock = null
}
}

View File

@@ -1,30 +0,0 @@
<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>

View File

@@ -1,170 +0,0 @@
<?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>

View File

@@ -1,18 +0,0 @@
<?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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.paarrot" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -1,10 +0,0 @@
<?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>

View File

@@ -1,4 +0,0 @@
<resources>
<string name="app_name">Paarrot</string>
<string name="main_activity_title">Paarrot</string>
</resources>

View File

@@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.paarrot" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -1,5 +0,0 @@
<?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>

View File

@@ -1,22 +0,0 @@
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")
}

View File

@@ -1,23 +0,0 @@
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")
}

View File

@@ -1,68 +0,0 @@
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 = """npm""";
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("run", "--", "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()
}
}

View File

@@ -1,85 +0,0 @@
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
)
}
}
}
}
}

View File

@@ -1,24 +0,0 @@
# 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

View File

@@ -1,6 +0,0 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,3 +0,0 @@
include ':app'
apply from: 'tauri.settings.gradle'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
{"default":{"identifier":"default","description":"Default capability for all windows","local":true,"windows":["*"],"permissions":["core:default","core:webview:allow-internal-toggle-devtools","opener:default","opener:allow-open-url","opener:allow-default-urls","notification:default","notification:allow-register-listener","autostart:default","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","dialog:default","process:allow-restart",{"identifier":"http:default","allow":[{"url":"https://api.telegram.org/**"}]}],"platforms":["linux","windows","macOS"]},"mobile":{"identifier":"mobile","description":"Default capability for mobile","local":true,"windows":["*"],"permissions":["core:default","notification:default","deep-link:default","opener:default"],"platforms":["android","iOS"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

View File

@@ -1,133 +0,0 @@
//! Background sync module for Matrix client
//!
//! This module provides native Rust-based Matrix sync that runs independently
//! of the WebView, allowing notifications to work even when the app is backgrounded.
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
/// Credentials needed to connect to Matrix homeserver
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatrixCredentials {
pub homeserver_url: String,
pub user_id: String,
pub access_token: String,
pub device_id: String,
}
/// State of the background sync
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncState {
Stopped,
Starting,
Running,
Error(String),
}
/// Background sync manager that handles Matrix sync in native Rust
pub struct BackgroundSyncManager {
credentials: RwLock<Option<MatrixCredentials>>,
sync_state: RwLock<SyncState>,
stop_flag: Mutex<bool>,
}
impl BackgroundSyncManager {
/// Creates a new BackgroundSyncManager
pub fn new() -> Self {
Self {
credentials: RwLock::new(None),
sync_state: RwLock::new(SyncState::Stopped),
stop_flag: Mutex::new(false),
}
}
/// Sets the Matrix credentials for syncing
pub async fn set_credentials(&self, credentials: MatrixCredentials) {
let mut creds = self.credentials.write().await;
*creds = Some(credentials);
}
/// Clears the stored credentials
pub async fn clear_credentials(&self) {
let mut creds = self.credentials.write().await;
*creds = None;
}
/// Gets the current sync state
pub async fn get_state(&self) -> SyncState {
self.sync_state.read().await.clone()
}
/// Starts the background sync
pub async fn start_sync(&self) -> Result<(), String> {
// Check if we have credentials
let creds = self.credentials.read().await;
let credentials = creds.as_ref().ok_or("No credentials set")?;
// Update state
{
let mut state = self.sync_state.write().await;
*state = SyncState::Starting;
}
// Reset stop flag
{
let mut stop = self.stop_flag.lock().await;
*stop = false;
}
log::info!(
"Background sync starting for user: {}",
credentials.user_id
);
// Update state to running
{
let mut state = self.sync_state.write().await;
*state = SyncState::Running;
}
Ok(())
}
/// Stops the background sync
pub async fn stop_sync(&self) {
{
let mut stop = self.stop_flag.lock().await;
*stop = true;
}
let mut state = self.sync_state.write().await;
*state = SyncState::Stopped;
log::info!("Background sync stopped");
}
/// Checks if sync should stop
pub async fn should_stop(&self) -> bool {
*self.stop_flag.lock().await
}
/// Sets the sync state to an error
pub async fn set_error(&self, error: String) {
let mut state = self.sync_state.write().await;
*state = SyncState::Error(error);
}
}
impl Default for BackgroundSyncManager {
fn default() -> Self {
Self::new()
}
}
/// Global instance of the background sync manager
static SYNC_MANAGER: std::sync::OnceLock<Arc<BackgroundSyncManager>> = std::sync::OnceLock::new();
/// Gets or creates the global sync manager instance
pub fn get_sync_manager() -> Arc<BackgroundSyncManager> {
SYNC_MANAGER
.get_or_init(|| Arc::new(BackgroundSyncManager::new()))
.clone()
}

View File

@@ -1,475 +0,0 @@
//! Paarrot Desktop library for cross-platform builds including Android
#[cfg(mobile)]
mod mobile;
#[cfg(any(target_os = "android", target_os = "ios"))]
mod background_sync;
#[cfg(any(target_os = "android", target_os = "ios"))]
mod matrix_sync;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use tauri::{
Manager,
WebviewUrl,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
webview::WebviewWindowBuilder,
WindowEvent,
};
// Linux: Import for permission handling
#[cfg(target_os = "linux")]
use webkit2gtk::{PermissionRequestExt, WebViewExt};
#[cfg(target_os = "linux")]
use gtk::prelude::*;
/// Read image from clipboard on Linux using arboard with Wayland support
#[cfg(target_os = "linux")]
#[tauri::command]
fn read_clipboard_image() -> Result<Option<String>, String> {
use arboard::Clipboard;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
match clipboard.get_image() {
Ok(img) => {
// Convert RGBA image data to PNG
let width = img.width as u32;
let height = img.height as u32;
let mut png_data = Vec::new();
{
let mut encoder = png::Encoder::new(&mut png_data, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
writer.write_image_data(&img.bytes).map_err(|e| e.to_string())?;
}
let base64_data = BASE64.encode(&png_data);
Ok(Some(format!("data:image/png;base64,{}", base64_data)))
}
Err(_) => Ok(None), // No image in clipboard
}
}
/// Stub for non-Linux platforms - returns None
#[cfg(not(target_os = "linux"))]
#[tauri::command]
fn read_clipboard_image() -> Result<Option<String>, String> {
Ok(None)
}
/// Open a URL in the default browser (bypasses ACL issues with localhost plugin)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
fn open_external_url(url: String) -> Result<(), String> {
open::that(&url).map_err(|e| e.to_string())
}
/// Open a URL in the default browser on mobile platforms
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tauri::command]
fn open_external_url(url: String) -> Result<(), String> {
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| e.to_string())
}
/// YouTube stream info returned by yt-dlp
#[derive(serde::Serialize)]
struct YouTubeStreamInfo {
/// Direct video stream URL (may include video+audio or video only)
video_url: String,
/// Title of the video
title: String,
}
/// Extract direct YouTube stream URL using yt-dlp
/// Requires yt-dlp to be installed and available in PATH
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn get_youtube_stream(url: String) -> Result<YouTubeStreamInfo, String> {
use std::process::Command;
// First get the title
let title_output = Command::new("yt-dlp")
.args(["--get-title", &url])
.output()
.map_err(|e| format!("Failed to run yt-dlp (is it installed?): {}", e))?;
let title = if title_output.status.success() {
String::from_utf8_lossy(&title_output.stdout).trim().to_string()
} else {
"YouTube Video".to_string()
};
// Get the best format with video+audio combined (up to 1080p)
// -f "best[height<=1080]" gets combined format
// Fallback to "bestvideo[height<=1080]+bestaudio/best" for separate streams
let output = Command::new("yt-dlp")
.args([
"-g", // Get URL only
"-f", "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
&url
])
.output()
.map_err(|e| format!("Failed to run yt-dlp: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("yt-dlp error: {}", stderr));
}
let video_url = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.unwrap_or("")
.trim()
.to_string();
if video_url.is_empty() {
return Err("yt-dlp returned empty URL".to_string());
}
Ok(YouTubeStreamInfo { video_url, title })
}
/// Stub for mobile platforms - YouTube streaming not supported
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tauri::command]
async fn get_youtube_stream(_url: String) -> Result<YouTubeStreamInfo, String> {
Err("YouTube streaming not supported on mobile".to_string())
}
/// Start background Matrix sync with the given credentials (mobile only)
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tauri::command]
async fn start_background_sync(
app_handle: tauri::AppHandle,
homeserver_url: String,
user_id: String,
access_token: String,
device_id: String,
) -> Result<(), String> {
use crate::background_sync::{MatrixCredentials, get_sync_manager};
use crate::matrix_sync::{init_client, run_sync_loop};
let credentials = MatrixCredentials {
homeserver_url,
user_id,
access_token,
device_id,
};
// Set credentials
let manager = get_sync_manager();
manager.set_credentials(credentials.clone()).await;
// Initialize the Matrix client
init_client(&credentials).await?;
// Start sync in background task
let app = app_handle.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = run_sync_loop(app).await {
log::error!("Sync loop error: {}", e);
}
});
Ok(())
}
/// Stop background Matrix sync (mobile only)
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tauri::command]
async fn stop_background_sync() -> Result<(), String> {
crate::matrix_sync::stop_sync().await;
Ok(())
}
/// Get background sync state (mobile only)
#[cfg(any(target_os = "android", target_os = "ios"))]
#[tauri::command]
async fn get_background_sync_state() -> Result<String, String> {
use crate::background_sync::get_sync_manager;
let manager = get_sync_manager();
let state = manager.get_state().await;
Ok(format!("{:?}", state))
}
/// Stub commands for desktop (no-op since background sync is mobile-only)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn start_background_sync(
_app_handle: tauri::AppHandle,
_homeserver_url: String,
_user_id: String,
_access_token: String,
_device_id: String,
) -> Result<(), String> {
Ok(()) // Desktop doesn't need background sync
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn stop_background_sync() -> Result<(), String> {
Ok(())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn get_background_sync_state() -> Result<String, String> {
Ok("NotApplicable".to_string())
}
/// Window control commands for custom decorations
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_minimize(window: tauri::Window) -> Result<(), String> {
window.minimize().map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_maximize(window: tauri::Window) -> Result<(), String> {
window.maximize().map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_unmaximize(window: tauri::Window) -> Result<(), String> {
window.unmaximize().map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_close(window: tauri::Window) -> Result<(), String> {
window.close().map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_is_maximized(window: tauri::Window) -> Result<bool, String> {
window.is_maximized().map_err(|e| e.to_string())
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tauri::command]
async fn window_start_drag(window: tauri::Window) -> Result<(), String> {
window.start_dragging().map_err(|e| e.to_string())
}
/// Runs the Tauri application
pub fn run() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let port = 44548;
tauri::Builder::default()
.plugin(tauri_plugin_localhost::Builder::new(port).build())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// When a second instance tries to launch, focus the existing window
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}))
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--minimized"]),
))
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_http::init())
.setup(move |app| {
// In dev mode, use Vite dev server; in production, use localhost plugin
let url: tauri::Url = if cfg!(dev) {
"http://localhost:8080".parse().unwrap()
} else {
format!("http://localhost:{}", port).parse().unwrap()
};
// Create the main window manually with navigation handler for external links
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(url))
.title("Paarrot")
.inner_size(1280.0, 905.0)
.center()
.resizable(true)
.decorations(false)
.disable_drag_drop_handler()
.on_navigation(|url| {
let url_str = url.as_str();
// Allow navigation to localhost (our app) and special protocols
if url_str.starts_with("http://localhost")
|| url_str.starts_with("https://localhost")
|| url_str.starts_with("tauri://")
|| url_str.starts_with("blob:")
|| url_str.starts_with("data:")
{
return true;
}
// Block external URLs - open them in default browser
if url_str.starts_with("http://") || url_str.starts_with("https://") {
let _ = tauri_plugin_opener::open_url(url_str, None::<&str>);
return false;
}
true
})
.build()?;
// Explicitly ensure decorations are disabled (important for Linux)
window.set_decorations(false)?;
// Linux: Set up permission handler to auto-allow microphone/camera access
#[cfg(target_os = "linux")]
{
if let Some(webview_window) = app.get_webview_window("main") {
let _ = webview_window.with_webview(|webview| {
use webkit2gtk::UserMediaPermissionRequestExt;
let wv = webview.inner();
wv.connect_permission_request(|_webview, permission_request| {
// Check if this is a user media (microphone/camera) permission request
if let Some(user_media_request) = permission_request.downcast_ref::<webkit2gtk::UserMediaPermissionRequest>() {
// Log what's being requested
let is_audio = user_media_request.is_for_audio_device();
let is_video = user_media_request.is_for_video_device();
eprintln!("Paarrot: Media permission request - audio: {}, video: {}", is_audio, is_video);
// Allow the request
permission_request.allow();
return true;
}
// For other permission types, allow by default
permission_request.allow();
true
});
});
}
}
// Create system tray
let show_item = MenuItem::with_id(app, "show", "Show Paarrot", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
// Force decorations off one final time after all plugins have initialized
// This ensures window-state plugin doesn't override our settings
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_decorations(false);
}
Ok(())
})
.on_window_event(|window, event| {
// Minimize to tray on close instead of quitting
if let WindowEvent::CloseRequested { api, .. } = event {
window.hide().unwrap();
api.prevent_close();
}
})
.invoke_handler(tauri::generate_handler![
read_clipboard_image,
open_external_url,
get_youtube_stream,
start_background_sync,
stop_background_sync,
get_background_sync_state,
window_minimize,
window_maximize,
window_unmaximize,
window_close,
window_is_maximized,
window_start_drag
])
.run(tauri::generate_context!())
.expect("error while building tauri application");
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
use tauri::{WebviewUrl, webview::WebviewWindowBuilder};
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
// Create the main window for mobile with navigation handler for external links
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.on_navigation(|url| {
let url_str = url.as_str();
// Allow navigation to app resources and special protocols
if url_str.starts_with("tauri://")
|| url_str.starts_with("http://tauri.localhost")
|| url_str.starts_with("https://tauri.localhost")
|| url_str.starts_with("blob:")
|| url_str.starts_with("data:")
|| url_str.starts_with("about:")
{
return true;
}
// External URLs - open in default browser
if url_str.starts_with("http://") || url_str.starts_with("https://") {
let _ = tauri_plugin_opener::open_url(url_str, None::<&str>);
return false;
}
true
})
.build()?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
read_clipboard_image,
open_external_url,
get_youtube_stream,
start_background_sync,
stop_background_sync,
get_background_sync_state
])
.run(tauri::generate_context!())
.expect("error while building tauri application");
}
}

View File

@@ -1,8 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
fn main() {
paarrot_lib::run();
}

View File

@@ -1,174 +0,0 @@
//! Matrix sync implementation using matrix-sdk
//!
//! This module handles the actual Matrix sync loop and notification triggering.
use matrix_sdk::{
Client,
config::SyncSettings,
matrix_auth::MatrixSession,
ruma::{
events::room::message::{MessageType, SyncRoomMessageEvent},
OwnedUserId, OwnedDeviceId,
},
};
use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt;
use tokio::sync::RwLock;
use crate::background_sync::{MatrixCredentials, get_sync_manager};
/// Active Matrix client for background sync
static MATRIX_CLIENT: std::sync::OnceLock<RwLock<Option<Client>>> = std::sync::OnceLock::new();
fn get_client_lock() -> &'static RwLock<Option<Client>> {
MATRIX_CLIENT.get_or_init(|| RwLock::new(None))
}
/// Initializes the Matrix client with the given credentials
pub async fn init_client(credentials: &MatrixCredentials) -> Result<Client, String> {
// Pass the URL string directly - ClientBuilder::homeserver_url accepts impl AsRef<str>
let client = Client::builder()
.homeserver_url(&credentials.homeserver_url)
.build()
.await
.map_err(|e| format!("Failed to build client: {}", e))?;
// Restore the session
let user_id: OwnedUserId = credentials.user_id.parse()
.map_err(|e| format!("Invalid user ID: {}", e))?;
let device_id: OwnedDeviceId = credentials.device_id.clone().into();
let session = MatrixSession {
meta: matrix_sdk::SessionMeta {
user_id,
device_id,
},
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
access_token: credentials.access_token.clone(),
refresh_token: None,
},
};
client.matrix_auth().restore_session(session).await
.map_err(|e| format!("Failed to restore session: {}", e))?;
// Store the client
{
let mut lock = get_client_lock().write().await;
*lock = Some(client.clone());
}
Ok(client)
}
/// Runs the sync loop and triggers notifications for new messages
pub async fn run_sync_loop<R: tauri::Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
let manager = get_sync_manager();
let client = {
let lock = get_client_lock().read().await;
lock.clone().ok_or("Client not initialized")?
};
let own_user_id = client.user_id()
.ok_or("Not logged in")?
.to_owned();
log::info!("Starting sync loop for {}", own_user_id);
// Set up event handler for room messages
let app_handle_clone = app_handle.clone();
let own_user_clone = own_user_id.clone();
client.add_event_handler(move |event: SyncRoomMessageEvent, room: matrix_sdk::Room| {
let app = app_handle_clone.clone();
let own_user = own_user_clone.clone();
async move {
// Only process original messages (not edits/reactions)
if let SyncRoomMessageEvent::Original(original) = event {
// Don't notify for our own messages
if original.sender == own_user {
return;
}
// Get room name for notification
let room_name = room.display_name().await
.map(|n| n.to_string())
.unwrap_or_else(|_| "Unknown room".to_string());
// Get sender display name
let sender_name = room.get_member(&original.sender).await
.ok()
.flatten()
.and_then(|m| m.display_name().map(|s| s.to_string()))
.unwrap_or_else(|| original.sender.to_string());
// Get message body
let body = match &original.content.msgtype {
MessageType::Text(text) => text.body.clone(),
MessageType::Image(_) => "📷 Image".to_string(),
MessageType::Video(_) => "🎥 Video".to_string(),
MessageType::Audio(_) => "🎵 Audio".to_string(),
MessageType::File(_) => "📎 File".to_string(),
MessageType::Location(_) => "📍 Location".to_string(),
MessageType::Emote(emote) => format!("* {} {}", sender_name, emote.body),
_ => "New message".to_string(),
};
// Send notification
let title = format!("{} in {}", sender_name, room_name);
if let Err(e) = app.notification()
.builder()
.title(&title)
.body(&body)
.show()
{
log::error!("Failed to show notification: {}", e);
}
}
}
});
// Run the sync loop
let settings = SyncSettings::default();
loop {
// Check if we should stop
if manager.should_stop().await {
log::info!("Sync loop stopping due to stop flag");
break;
}
// Perform one sync iteration
match client.sync_once(settings.clone()).await {
Ok(response) => {
log::debug!("Sync completed, next_batch: {}", response.next_batch);
}
Err(e) => {
log::error!("Sync error: {}", e);
// Update state to error
manager.set_error(e.to_string()).await;
// Wait a bit before retrying
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
// Small delay between syncs to avoid hammering the server
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
Ok(())
}
/// Stops the sync and cleans up
pub async fn stop_sync() {
let manager = get_sync_manager();
manager.stop_sync().await;
// Clear the client
let mut lock = get_client_lock().write().await;
*lock = None;
}

View File

@@ -1,7 +0,0 @@
//! Mobile-specific entry points for Android and iOS
/// Mobile entry point
#[tauri::mobile_entry_point]
fn main() {
crate::run();
}

View File

@@ -1,82 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Paarrot",
"version": "4.10.26",
"identifier": "wtf.ruv.paarrot",
"build": {
"frontendDist": "../cinny/dist",
"devUrl": "http://localhost:8080",
"beforeDevCommand": "cd cinny && npm start",
"beforeBuildCommand": "cd cinny && npm run build"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "Paarrot",
"width": 1200,
"height": 800,
"minWidth": 480,
"minHeight": 480,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true,
"create": false
}
],
"security": {
"csp": "script-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' https: http: data: blob:; frame-src https: http:"
}
},
"plugins": {
"updater": {
"endpoints": [
"http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases/download/latest/update.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExREU1NkVBMDY4MzcxMjAKUldRZ2NZTUc2bGJlRVNaQldnMXpJcHlocVpCWUNNaUdicjBGMVRsck1GQXhvTnJiOUNhVVRHSzQK",
"dangerousInsecureTransportProtocol": true
}
},
"bundle": {
"active": true,
"targets": ["msi", "appimage", "deb", "rpm"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "SocialNetworking",
"shortDescription": "Squawk to yer mateys!",
"longDescription": "",
"linux": {
"deb": {
"depends": []
}
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"exceptionDomain": "",
"signingIdentity": null,
"providerShortName": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"wix": {
"bannerPath": "wix/banner.bmp",
"dialogImagePath": "wix/dialogImage.bmp"
},
"nsis": null
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 KiB