commit 4640db87fd9d1cded5012c5f279cf5d5f818ccc3 Author: warzazel Date: Sat Mar 14 21:29:54 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..02c4aa5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d1a6873 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.warzazel.voidpush" + compileSdk { + version = release(36) { + minorApiLevel = 1 + } + } + + defaultConfig { + applicationId = "com.warzazel.voidpush" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) + implementation("androidx.compose.material:material:1.6.0") + implementation("androidx.browser:browser:1.8.0") +// UnifiedPush - von Maven Central +// UnifiedPush + implementation("org.unifiedpush.android:connector:3.3.2") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/warzazel/voidpush/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/warzazel/voidpush/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9fdcf83 --- /dev/null +++ b/app/src/androidTest/java/com/warzazel/voidpush/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.warzazel.voidpush + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.warzazel.voidpush", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..770970c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..1d46219 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/warzazel/voidpush/MainActivity.kt b/app/src/main/java/com/warzazel/voidpush/MainActivity.kt new file mode 100644 index 0000000..c356184 --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/MainActivity.kt @@ -0,0 +1,416 @@ +package com.warzazel.voidpush + +import android.Manifest +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import com.warzazel.voidpush.ui.theme.VoidPushTheme +import org.json.JSONArray +import org.unifiedpush.android.connector.UnifiedPush +import java.text.SimpleDateFormat +import java.util.* +import androidx.compose.ui.graphics.Color + +class MainActivity : ComponentActivity() { + + private val requestPermission = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) registriereBeiSunup() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + registriereBeiSunup() + } + } else { + registriereBeiSunup() + } + + setContent { + VoidPushTheme { + Surface(modifier = Modifier.fillMaxSize()) { + VoidPushScreen(activity = this@MainActivity) + + } + } + } + } + + private fun registriereBeiSunup() { + UnifiedPush.tryUseCurrentOrDefaultDistributor(this) { success -> + if (success) { + UnifiedPush.register(this) + } + } + } +} + +data class HistorieEintrag( + val title: String, + val body: String, + val link: String, + val timestamp: Long, + val category: String = "Sonstige" +) + +fun ladeHistorie(context: Context): List { + val prefs = context.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + val json = prefs.getString("notification_history", "[]") + val array = JSONArray(json) + val liste = mutableListOf() + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + liste.add(HistorieEintrag( + title = obj.getString("title"), + body = obj.getString("body"), + link = obj.optString("link", ""), + timestamp = obj.getLong("timestamp"), + category = obj.optString("category", "Sonstige") + )) + } + return liste +} + +@Composable +fun VoidPushScreen( + endpointUrlOverride: String? = null, + activity: ComponentActivity? = null +) { + var selectedTab by remember { mutableIntStateOf(0) } + val tabs = listOf("Historie", "Status") + + Column(modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + ) { + TabRow( + selectedTabIndex = selectedTab, + containerColor = Color(0xFFFFC107), + contentColor = Color.Black + ) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTab == index, + onClick = { selectedTab = index }, + text = { Text(title) } + ) + } + } + when (selectedTab) { + 0 -> HistorieTab() + 1 -> StatusTab(endpointUrlOverride, activity) + } + } +} + +@Composable +fun StatusTab( + endpointUrlOverride: String? = null, + activity: ComponentActivity? = null +) { + val context = LocalContext.current + val prefs = context.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + + var endpointUrl by remember { + mutableStateOf(endpointUrlOverride ?: prefs.getString("endpoint_url", null)) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "VoidPush", style = MaterialTheme.typography.headlineLarge) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "2026 by \uD835\uDD9C\uD835\uDD86\uD835\uDD97\uD835\uDD9F\uD835\uDD86\uD835\uDD9F\uD835\uDD8A\uD835\uDD91⛧", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "www.voidofxulub.com", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Hubzilla → UnifiedPush → Sunup", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(48.dp)) + + if (endpointUrl != null) { + Text( + text = "✓ Registriert bei Sunup", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Endpoint-URL:", style = MaterialTheme.typography.labelMedium) + Spacer(modifier = Modifier.height(8.dp)) + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = endpointUrl!!, + modifier = Modifier.padding(12.dp), + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("Endpoint URL", endpointUrl)) + Toast.makeText(context, "URL kopiert!", Toast.LENGTH_SHORT).show() + }) { + Text("URL kopieren") + } + } else { + Text( + text = "⚠ Noch nicht registriert", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Stelle sicher dass Sunup installiert ist.", + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { + val target = activity ?: return@Button + UnifiedPush.tryUseCurrentOrDefaultDistributor(target) { success -> + if (success) { + UnifiedPush.register(context) + val newUrl = prefs.getString("endpoint_url", null) + endpointUrl = newUrl + } + } + }) { + Text("Erneut versuchen") + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HistorieTab() { + val context = LocalContext.current + var historie by remember { mutableStateOf(ladeHistorie(context)) } + val sdf = remember { SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()) } + var selectedCategory by remember { mutableIntStateOf(0) } + val kategorien = listOf("Alle", "Erwähnung", "Kommentar", "Like", "Sonstige") + + fun speichereHistorie(liste: List) { + val prefs = context.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + val array = org.json.JSONArray() + liste.forEach { e -> + array.put(org.json.JSONObject().apply { + put("title", e.title) + put("body", e.body) + put("link", e.link) + put("timestamp", e.timestamp) + put("category", e.category) + }) + } + prefs.edit().putString("notification_history", array.toString()).apply() + } + + val gefilterteHistorie = if (selectedCategory == 0) historie + else historie.filter { it.category == kategorien[selectedCategory] } + + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("${gefilterteHistorie.size} Einträge", style = MaterialTheme.typography.bodySmall) + TextButton(onClick = { + historie = emptyList() + speichereHistorie(emptyList()) + }) { + Text("Alle löschen") + } + } + + ScrollableTabRow( + selectedTabIndex = selectedCategory, + edgePadding = 16.dp, + containerColor = Color(0xFFFFC107), + contentColor = Color.Black, + indicator = {} + ) { + kategorien.forEachIndexed { index, name -> + Tab( + selected = selectedCategory == index, + onClick = { selectedCategory = index }, + modifier = Modifier.background( + color = if (selectedCategory == index) Color(0xFF5b5b5b) else Color(0xFFFFC107), + shape = RoundedCornerShape(3.dp) + ), + text = { Text(name, color = Color.Black) } + ) + } + } + + if (gefilterteHistorie.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = "Keine Einträge", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + LazyColumn { + items(gefilterteHistorie, key = { it.timestamp }) { eintrag -> + val dismissState = rememberDismissState( + confirmStateChange = { value -> + if (value == DismissValue.DismissedToStart || + value == DismissValue.DismissedToEnd) { + val neu = historie.filter { it.timestamp != eintrag.timestamp } + historie = neu + speichereHistorie(neu) + true + } else false + } + ) + SwipeToDismiss( + state = dismissState, + background = { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.errorContainer) + ) + } + ) { + HistorieItem(eintrag, sdf, onDelete = { + val neu = historie.filter { it.timestamp != eintrag.timestamp } + historie = neu + speichereHistorie(neu) + }) + } + HorizontalDivider() + } + } + } + } +} + +@Composable +fun HistorieItem(eintrag: HistorieEintrag, sdf: SimpleDateFormat, onDelete: () -> Unit) { + val context = LocalContext.current + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 6.dp) + .clickable { + if (eintrag.link.isNotEmpty()) { + val intent = + android.content.Intent(context, OpenLinkActivity::class.java).apply { + putExtra("link", eintrag.link) + flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } + onDelete() + }, + shape = RoundedCornerShape(6.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = eintrag.title, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f) + ) + Text( + text = sdf.format(Date(eintrag.timestamp)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = eintrag.body, + style = MaterialTheme.typography.bodyMedium, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + if (eintrag.link.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "Tippen zum Öffnen · Swipen zum Löschen →", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + } +} + +@Preview(showBackground = true, name = "Registriert") +@Composable +fun PreviewRegistriert() { + VoidPushTheme { + VoidPushScreen(endpointUrlOverride = "https://push.example.com/abc123") + } +} + +@Preview(showBackground = true, name = "Nicht registriert") +@Composable +fun PreviewNichtRegistriert() { + VoidPushTheme { + VoidPushScreen(endpointUrlOverride = null) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/warzazel/voidpush/OpenLinkActivity.kt b/app/src/main/java/com/warzazel/voidpush/OpenLinkActivity.kt new file mode 100644 index 0000000..9ca5abe --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/OpenLinkActivity.kt @@ -0,0 +1,23 @@ +package com.warzazel.voidpush + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.browser.customtabs.CustomTabsIntent +import android.net.Uri + +class OpenLinkActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val link = intent.getStringExtra("link") + + if (!link.isNullOrEmpty()) { + CustomTabsIntent.Builder() + .build() + .launchUrl(this, Uri.parse(link)) + } + + finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/warzazel/voidpush/PushService.kt b/app/src/main/java/com/warzazel/voidpush/PushService.kt new file mode 100644 index 0000000..c02fee5 --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/PushService.kt @@ -0,0 +1,226 @@ +package com.warzazel.voidpush + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import org.unifiedpush.android.connector.FailedReason +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage +import org.json.JSONArray +import org.json.JSONObject + +private const val TAG = "VoidPush" +private const val CHANNEL_ID = "hubzilla_push" +private const val MAX_HISTORY = 50 +private const val NOTIFICATION_ID = 1 // FIX 1: Feste ID → ersetzt statt stapeln + +class VoidPushService : org.unifiedpush.android.connector.PushService() { + + override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) { + Log.d(TAG, "Neue Endpoint-URL: ${endpoint.url}") + val prefs = applicationContext.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + prefs.edit().putString("endpoint_url", endpoint.url).apply() + } + + override fun onMessage(message: PushMessage, instance: String) { + val payload = message.content.toString(Charsets.UTF_8) + Log.d(TAG, "Nachricht empfangen: $payload") + val title = bereinige(extractJson(payload, "title") ?: "Hubzilla") + val body = bereinige(extractJson(payload, "body") ?: payload) + val link = extractJson(payload, "link") ?: "" + + zeigeNotification(title, body, link) + val category = erkennKategorie(title) + speichereInHistorie(title, body, link, category) + } + + override fun onRegistrationFailed(reason: FailedReason, instance: String) { + Log.e(TAG, "Registrierung fehlgeschlagen: $reason") + } + + override fun onUnregistered(instance: String) { + Log.d(TAG, "Von Sunup abgemeldet") + val prefs = applicationContext.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + prefs.edit().remove("endpoint_url").apply() + } + + private fun bereinige(text: String): String { + var result = text + // Unicode-Escapes dekodieren: \u00e4 → ä etc. +// Unicode-Escapes dekodieren inkl. Surrogate Pairs (\ud835\udd16 → 𝖜) + val sb = StringBuilder() + var i = 0 + while (i < result.length) { + if (i + 5 < result.length && + result[i] == '\\' && + result[i + 1] == 'u' && + result.substring(i + 2, i + 6).all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' } + ) { + val code1 = result.substring(i + 2, i + 6).toInt(16) + // Prüfen ob erstes Surrogate (D800–DBFF) gefolgt von zweitem (DC00–DFFF) + if (code1 in 0xD800..0xDBFF && + i + 11 <= result.length && + result[i + 6] == '\\' && + result[i + 7] == 'u' + ) { + val code2 = result.substring(i + 8, i + 12).toInt(16) + if (code2 in 0xDC00..0xDFFF) { + val codePoint = 0x10000 + (code1 - 0xD800) * 0x400 + (code2 - 0xDC00) + sb.appendCodePoint(codePoint) + i += 12 + } else { + sb.append(code1.toChar()) + i += 6 + } + } else { + sb.append(code1.toChar()) + i += 6 + } + } else { + sb.append(result[i]) + i++ + } + } + + result = sb.toString() + result = ersetzeEmojis(result) // ← neu + result = result.replace("(Hubzilla) ", "") + result = result.replace("(Hubzilla) ", "") + result = result.replace("(Hubzilla)", "") + result = result.replace("\\n", "\n") + result = result.replace("\\t", "\t") + result = result.replace("€", "€") + // BBCode [url=...]Linktext[/url] → nur Linktext behalten + // BBCode [url=...]Linktext[/url] → nur Linktext behalten + result = result.replace(Regex("\\[url=[^\\]]*\\]([^\\[]*?)\\[/url\\]"), "$1") + // BBCode [url=...] ohne /url → komplett entfernen + result = result.replace(Regex("\\[url=[^\\]]*\\]", RegexOption.DOT_MATCHES_ALL), "") + result = result.replace("&", "&") + result = result.replace("/", "/") + result = result.replace("<", "<") + result = result.replace(">", ">") + result = result.replace("'", "'") + // Übrige BBCode-Tags entfernen: [b], [i], [/b] etc. + result = result.replace(Regex("\\[[^\\]]*\\]"), "") + // Escapes wie \/ → / + result = result.replace("\\/", "/") + result = result.replace("\\r", "") + result = result.replace("\r", "") + return result.trim() + + } + private fun ersetzeEmojis(text: String): String { + val emojis = mapOf( + ":smile:" to "😊", ":laughing:" to "😄", ":grinning:" to "😀", + ":rofl:" to "🤣", ":joy:" to "😂", ":sweat_smile:" to "😅", + ":wink:" to "😉", ":blush:" to "😊", ":heart_eyes:" to "😍", + ":kissing_heart:" to "😘", ":thinking:" to "🤔", ":neutral_face:" to "😐", + ":expressionless:" to "😑", ":unamused:" to "😒", ":roll_eyes:" to "🙄", + ":grimacing:" to "😬", ":lying_face:" to "🤥", ":relieved:" to "😌", + ":pensive:" to "😔", ":sleepy:" to "😪", ":sleeping:" to "😴", + ":mask:" to "😷", ":sunglasses:" to "😎", ":clown_face:" to "🤡", + ":sob:" to "😭", ":rage:" to "😡", ":angry:" to "😠", + ":skull:" to "💀", ":ghost:" to "👻", ":alien:" to "👽", + ":heart:" to "❤️", ":broken_heart:" to "💔", ":fire:" to "🔥", + ":star:" to "⭐", ":sparkles:" to "✨", ":boom:" to "💥", + ":thumbsup:" to "👍", ":thumbsdown:" to "👎", ":clap:" to "👏", + ":wave:" to "👋", ":ok_hand:" to "👌", ":raised_hands:" to "🙌", + ":pray:" to "🙏", ":point_right:" to "👉", ":point_left:" to "👈", + ":eyes:" to "👀", ":brain:" to "🧠", ":rocket:" to "🚀", + ":pizza:" to "🍕", ":coffee:" to "☕", ":beer:" to "🍺", + ":cat:" to "🐱", ":dog:" to "🐶", ":penguin:" to "🐧", + ":earth_africa:" to "🌍", ":earth_americas:" to "🌎", ":earth_asia:" to "🌏", + ":rainbow:" to "🌈", ":sunny:" to "☀️", ":umbrella:" to "☔", + ":checkmark:" to "✅", ":x:" to "❌", ":warning:" to "⚠️", + ":100:" to "💯", ":tada:" to "🎉", ":musical_note:" to "🎵" + ) + var result = text + // Bekannte Shortcodes ersetzen + emojis.forEach { (code, emoji) -> result = result.replace(code, emoji) } + // Unbekannte Shortcodes entfernen + result = result.replace(Regex(":[a-zA-Z0-9_]+:"), "") + return result + } + private fun speichereInHistorie(title: String, body: String, link: String, category: String) { val prefs = applicationContext.getSharedPreferences("voidpush", Context.MODE_PRIVATE) + val json = prefs.getString("notification_history", "[]") + val array = JSONArray(json) + + // Duplikat-Schutz: gleicher title+body → nicht nochmal speichern + val neuerKey = "$title|$body" + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + val key = "${obj.optString("title")}|${obj.optString("body")}" + if (key == neuerKey) { + Log.d(TAG, "Duplikat ignoriert: $title") + return + } + } + + val entry = JSONObject().apply { + put("title", title) + put("body", body) + put("link", link) + put("timestamp", System.currentTimeMillis()) + put("category", category) + } + + val newArray = JSONArray() + newArray.put(entry) + for (i in 0 until minOf(array.length(), MAX_HISTORY - 1)) { + newArray.put(array.getJSONObject(i)) + } + + prefs.edit().putString("notification_history", newArray.toString()).apply() + } + + private fun zeigeNotification(title: String, body: String, link: String) { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel( + CHANNEL_ID, "Hubzilla Benachrichtigungen", NotificationManager.IMPORTANCE_DEFAULT + ) + manager.createNotificationChannel(channel) + + val pendingIntent = if (link.isNotEmpty()) { + val intent = Intent(applicationContext, OpenLinkActivity::class.java).apply { + putExtra("link", link) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + PendingIntent.getActivity( + applicationContext, + link.hashCode(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } else null + + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .apply { if (pendingIntent != null) setContentIntent(pendingIntent) } + .build() + + manager.notify(NOTIFICATION_ID, notification) // FIX 1: Feste ID + } + + private fun extractJson(json: String, key: String): String? { + val pattern = Regex("\"$key\"\\s*:\\s*\"([^\"]+)\"") + return pattern.find(json)?.groupValues?.get(1) + } + + private fun erkennKategorie(title: String): String { + return when { + title.contains("Erwähnung", ignoreCase = true) || + title.contains("Mention", ignoreCase = true) -> "Erwähnung" + title.contains("Kommentar", ignoreCase = true) || + title.contains("Comment", ignoreCase = true) -> "Kommentar" + title.contains("Like", ignoreCase = true) -> "Like" + else -> "Sonstige" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/warzazel/voidpush/ui/theme/Color.kt b/app/src/main/java/com/warzazel/voidpush/ui/theme/Color.kt new file mode 100644 index 0000000..35f3df3 --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.warzazel.voidpush.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/warzazel/voidpush/ui/theme/Theme.kt b/app/src/main/java/com/warzazel/voidpush/ui/theme/Theme.kt new file mode 100644 index 0000000..81693de --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.warzazel.voidpush.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun VoidPushTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/warzazel/voidpush/ui/theme/Type.kt b/app/src/main/java/com/warzazel/voidpush/ui/theme/Type.kt new file mode 100644 index 0000000..31fabd5 --- /dev/null +++ b/app/src/main/java/com/warzazel/voidpush/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.warzazel.voidpush.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..05582bb Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..318b880 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1708571 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..a58c575 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..ca89c46 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..3d9a491 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..00f619b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..5f423ec Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c1680bb Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..7d40898 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..88907b0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..825679c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..abb54ec Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..65af6af Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..cec3d92 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..3d583be --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #680000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..855affe --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + VoidPush + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..64cdc07 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +