für playstore änderungen
All checks were successful
Deploy SingleChat / deploy (push) Successful in 23s

This commit is contained in:
Torsten Schulz (local)
2026-06-16 11:50:31 +02:00
parent 155fce15e1
commit 1f342f555e
22 changed files with 554 additions and 287 deletions

View File

@@ -34,8 +34,8 @@ android {
applicationId = "de.ypchat.android"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
versionCode = 2
versionName = "1.1.0"
}
lint {

Binary file not shown.

View File

@@ -1,12 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".YpChatApp"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:supportsRtl="true"
android:theme="@style/Theme.YpChat">
@@ -20,4 +21,3 @@
</activity>
</application>
</manifest>

View File

@@ -36,6 +36,7 @@ class ChatViewModel(private val repository: ChatRepository) : ViewModel() {
fun openConversation(userName: String) = repository.openConversation(userName)
fun closeConversation() = repository.closeConversation()
fun sendMessage(text: String) = repository.sendMessage(text)
fun setImageUploadMessage(message: String) = repository.setImageUploadState(false, message)
fun sendImage(context: Context, uri: Uri) {
val target = state.value.currentConversation ?: return
viewModelScope.launch {

View File

@@ -1,5 +1,10 @@
package de.ypchat.android.ui
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@@ -76,6 +81,8 @@ import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.io.File
import java.io.FileOutputStream
enum class AppTab(val labelRes: Int) {
Online(R.string.tab_online),
@@ -87,7 +94,7 @@ enum class AppTab(val labelRes: Int) {
}
private enum class MoreSection {
Overview, Feedback, Partners, Faq, Rules, Safety, Imprint
Overview, Feedback, Partners, Faq, Rules, Safety, Privacy, Imprint
}
private val BgApp = Color(0xFFF4F6F5)
@@ -105,6 +112,7 @@ private val Primary600 = Color(0xFF2F6F46)
private val Primary500 = Color(0xFF3D8654)
private val Primary100 = Color(0xFFE7F1EA)
private val Danger = Color(0xFFA24040)
private const val PrivacyPolicyUrl = "https://www.single-chat.net/datenschutz"
private data class GenderOption(val value: String, val label: String)
private data class SmileyItem(val token: String, val hexCode: String, val tooltip: String)
@@ -574,6 +582,7 @@ private fun MoreScreen(state: ChatState, viewModel: ChatViewModel, section: More
MoreSection.Faq -> StaticContentScreen(stringResource(R.string.faq_title), stringResource(R.string.faq_body)) { onSectionChange(MoreSection.Overview) }
MoreSection.Rules -> StaticContentScreen(stringResource(R.string.rules_title), stringResource(R.string.rules_body)) { onSectionChange(MoreSection.Overview) }
MoreSection.Safety -> StaticContentScreen(stringResource(R.string.safety_title), stringResource(R.string.safety_body)) { onSectionChange(MoreSection.Overview) }
MoreSection.Privacy -> PrivacyScreen { onSectionChange(MoreSection.Overview) }
MoreSection.Imprint -> StaticContentScreen(stringResource(R.string.imprint_title), stringResource(R.string.imprint_body)) { onSectionChange(MoreSection.Overview) }
}
}
@@ -587,6 +596,7 @@ private fun MoreOverviewScreen(onSectionChange: (MoreSection) -> Unit) {
item { MoreLinkCard(stringResource(R.string.more_faq), stringResource(R.string.faq_intro)) { onSectionChange(MoreSection.Faq) } }
item { MoreLinkCard(stringResource(R.string.more_rules), stringResource(R.string.rules_intro)) { onSectionChange(MoreSection.Rules) } }
item { MoreLinkCard(stringResource(R.string.more_safety), stringResource(R.string.safety_intro)) { onSectionChange(MoreSection.Safety) } }
item { MoreLinkCard(stringResource(R.string.more_privacy), stringResource(R.string.privacy_intro)) { onSectionChange(MoreSection.Privacy) } }
item { MoreLinkCard(stringResource(R.string.more_imprint), stringResource(R.string.imprint_intro)) { onSectionChange(MoreSection.Imprint) } }
}
}
@@ -712,6 +722,29 @@ private fun PartnersScreen(links: List<PartnerLinkDto>, error: String?, onBack:
}
}
@Composable
private fun PrivacyScreen(onBack: () -> Unit) {
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
BackHeader(stringResource(R.string.privacy_title), onBack)
Text(stringResource(R.string.privacy_body), color = TextStrong)
Button(
onClick = { uriHandler.openUri(PrivacyPolicyUrl) },
colors = ButtonDefaults.buttonColors(containerColor = Primary600),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.privacy_open_policy))
}
}
}
@Composable
private fun StaticContentScreen(title: String, body: String, onBack: () -> Unit) {
Column(
@@ -743,6 +776,25 @@ private fun ChatScreen(state: ChatState, viewModel: ChatViewModel) {
val imagePicker = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) viewModel.sendImage(context, uri)
}
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
if (bitmap != null) {
val uri = saveCameraBitmap(context, bitmap)
if (uri != null) {
viewModel.sendImage(context, uri)
} else {
viewModel.setImageUploadMessage("Camera capture failed")
}
} else {
viewModel.setImageUploadMessage("Camera capture failed")
}
}
val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
cameraLauncher.launch(null)
} else {
viewModel.setImageUploadMessage("Camera permission denied")
}
}
Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
@@ -782,6 +834,25 @@ private fun ChatScreen(state: ChatState, viewModel: ChatViewModel) {
enabled = !state.isUploadingImage
)
Spacer(Modifier.width(8.dp))
TextButton(
onClick = {
if (context.checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(null)
} else {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
},
enabled = !state.isUploadingImage,
colors = ButtonDefaults.textButtonColors(
containerColor = Primary100,
contentColor = Primary700,
disabledContainerColor = SurfaceSubtle,
disabledContentColor = TextMuted
)
) {
Text(stringResource(R.string.button_camera), fontWeight = FontWeight.Bold)
}
Spacer(Modifier.width(8.dp))
Button(
onClick = { viewModel.sendMessage(draft); draft = "" },
colors = ButtonDefaults.buttonColors(containerColor = Primary600),
@@ -813,6 +884,16 @@ private fun ChatScreen(state: ChatState, viewModel: ChatViewModel) {
}
}
private fun saveCameraBitmap(context: Context, bitmap: Bitmap): Uri? {
return runCatching {
val file = File(context.cacheDir, "singlechat-photo-${System.currentTimeMillis()}.jpg")
FileOutputStream(file).use { output ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 88, output)
}
Uri.fromFile(file)
}.getOrNull()
}
@Composable
private fun UploadStatusBanner(state: ChatState) {
val message = when {
@@ -1007,6 +1088,10 @@ private fun localizeRuntimeMessage(message: String): String {
stringResource(R.string.image_upload_too_large)
message == "Image could not be opened" ->
stringResource(R.string.image_upload_open_failed)
message == "Camera permission denied" ->
stringResource(R.string.camera_permission_denied)
message == "Camera capture failed" ->
stringResource(R.string.camera_capture_failed)
message.endsWith(" blocked") ->
stringResource(R.string.user_blocked, message.removeSuffix(" blocked"))
message.endsWith(" unblocked") ->

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">Bitte gib deinen Nicknamen für den Chat ein:</string>
<string name="label_gender">Geschlecht:</string>
<string name="label_age">Alter:</string>
<string name="label_country">Land:</string>
<string name="button_start_chat">Chat starten</string>
<string name="gender_female">Weiblich</string>
<string name="gender_male">Männlich</string>
<string name="gender_pair">Paar</string>
<string name="gender_trans_mf">Transgender (M-&gt;F)</string>
<string name="gender_trans_fm">Transgender (F-&gt;M)</string>
<string name="tab_search">Suchen</string>
<string name="tab_inbox">Posteingang</string>
<string name="tab_history">Verlauf</string>
<string name="button_send">Senden</string>
<string name="search_username_includes">Benutzername enthält</string>
<string name="search_from_age">Von Alter</string>
<string name="search_to_age">Bis Alter</string>
<string name="search_country">Land</string>
<string name="search_genders">Geschlechter</string>
<string name="search_all">Alle</string>
<string name="search_button">Suchen</string>
<string name="search_no_results">Keine Ergebnisse.</string>
<string name="search_min_age_error">Das Mindestalter muss mindestens so groß sein wie das Höchstalter.</string>
<string name="history_empty">Keine vorherigen Unterhaltungen verfügbar.</string>
<string name="block">Benutzer blockieren</string>
<string name="unblock">Benutzer entsperren</string>
<string name="button_image">Ein Bild senden</string>
</resources>

View File

@@ -49,6 +49,7 @@
<string name="unblock">Entsperren</string>
<string name="message_placeholder">Nachricht</string>
<string name="button_image">Bild</string>
<string name="button_camera">Foto</string>
<string name="button_send">Senden</string>
<string name="button_smileys">Smileys</string>
<string name="image_message">Bildnachricht</string>
@@ -57,6 +58,8 @@
<string name="image_upload_failed">Bild-Upload fehlgeschlagen.</string>
<string name="image_upload_too_large">Das Bild ist größer als 5 MB.</string>
<string name="image_upload_open_failed">Das Bild konnte nicht geöffnet werden.</string>
<string name="camera_permission_denied">Die Kameraberechtigung wurde abgelehnt.</string>
<string name="camera_capture_failed">Das Foto konnte nicht aufgenommen werden.</string>
<string name="feedback_created_at">Eingegangen %1$s</string>
<string name="feedback_meta_separator"></string>
<string name="countries_load_error">Länderliste konnte nicht geladen werden: %1$s</string>
@@ -83,21 +86,26 @@
<string name="more_faq">FAQ</string>
<string name="more_rules">Regeln</string>
<string name="more_safety">Sicherheit</string>
<string name="more_privacy">Datenschutz</string>
<string name="more_imprint">Impressum</string>
<string name="more_back">Zur Übersicht</string>
<string name="partners_intro">Empfehlungen und befreundete Projekte für unsere Community.</string>
<string name="faq_intro">Antworten auf häufige Fragen zum Chat.</string>
<string name="rules_intro">Grundregeln für respektvollen Chat.</string>
<string name="safety_intro">Tipps für Privatsphäre und sichere Nutzung.</string>
<string name="privacy_intro">Datenschutzerklärung, verarbeitete Daten und Kontakt für Datenschutzanfragen.</string>
<string name="imprint_intro">Rechtliche Hinweise und Kontaktdaten.</string>
<string name="external_link">Externer Link</string>
<string name="faq_title">Häufige Fragen</string>
<string name="rules_title">Chat-Regeln</string>
<string name="safety_title">Sicherheit und Privatsphäre</string>
<string name="privacy_title">Datenschutzerklärung</string>
<string name="imprint_title">Impressum</string>
<string name="partners_title">Partner</string>
<string name="faq_body">Wähle einen Nicknamen, gib deine Profildaten an und starte den Chat. Teile keine sensiblen Daten wie Telefonnummern, Adressen, Passwörter oder Zahlungsinformationen. Du kannst Bilder senden, Benutzer blockieren und Feedback für ernste Vorfälle nutzen.</string>
<string name="rules_body">Keine Beleidigungen, Hassrede, illegalen Inhalte, Spam oder unerwünschte Belästigung. Sende nur Bilder, die du teilen darfst, und respektiere die Privatsphäre anderer.</string>
<string name="safety_body">Nutze einen Nicknamen, der dich nicht identifiziert. Teile keine privaten Kontakt- oder Zahlungsdaten. Sei vorsichtig mit Links von Unbekannten und beende Gespräche, die sich falsch anfühlen. Nutze Blockieren und Feedback bei schweren Vorfällen.</string>
<string name="privacy_body">SingleChat verarbeitet den von dir gewählten Nickname, Profildaten wie Alter, Geschlecht und Land, Chat-Nachrichten, von dir aktiv gesendete Bilder, Feedback-Nachrichten sowie technisch notwendige Sitzungsdaten. Die Android-App fragt den Kamerazugriff nur an, wenn du in der App aktiv ein Foto aufnehmen möchtest. Die vollständige Datenschutzerklärung für Website und App ist auf single-chat.net veröffentlicht.</string>
<string name="privacy_open_policy">Datenschutzerklärung öffnen</string>
<string name="imprint_body">Torsten Schulz, Friedrich-Stampfer-Str. 21, 60437 Frankfurt. Kontakt: tsschulz@tsschulz.de. Für externe Links sind deren Betreiber verantwortlich.</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">Por favor, escribe tu apodo para el chat:</string>
<string name="label_gender">Género:</string>
<string name="label_age">Edad:</string>
<string name="label_country">País:</string>
<string name="button_start_chat">Iniciar chat</string>
<string name="gender_female">Femenino</string>
<string name="gender_male">Masculino</string>
<string name="gender_pair">Pareja</string>
<string name="gender_trans_mf">Transgénero (M-&gt;F)</string>
<string name="gender_trans_fm">Transgénero (F-&gt;M)</string>
<string name="tab_search">Buscar</string>
<string name="tab_inbox">Bandeja de entrada</string>
<string name="tab_history">Historial</string>
<string name="button_send">Enviar</string>
<string name="search_username_includes">El nombre de usuario incluye</string>
<string name="search_from_age">Desde la edad</string>
<string name="search_to_age">Hasta la edad</string>
<string name="search_country">País</string>
<string name="search_genders">Géneros</string>
<string name="search_all">Todos</string>
<string name="search_button">Buscar</string>
<string name="search_no_results">Sin resultados.</string>
<string name="search_min_age_error">La edad mínima debe ser al menos tan grande como la edad máxima.</string>
<string name="history_empty">No hay conversaciones anteriores disponibles.</string>
<string name="block">Bloquear usuario</string>
<string name="unblock">Desbloquear usuario</string>
<string name="button_image">Enviar una imagen</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">Veuillez saisir votre pseudo pour le chat:</string>
<string name="label_gender">Genre:</string>
<string name="label_age">Âge:</string>
<string name="label_country">Pays:</string>
<string name="button_start_chat">Démarrer le chat</string>
<string name="gender_female">Féminin</string>
<string name="gender_male">Masculin</string>
<string name="gender_pair">Couple</string>
<string name="gender_trans_mf">Transgenre (M-&gt;F)</string>
<string name="gender_trans_fm">Transgenre (F-&gt;M)</string>
<string name="tab_search">Rechercher</string>
<string name="tab_inbox">Boîte de réception</string>
<string name="tab_history">Historique</string>
<string name="button_send">Envoyer</string>
<string name="search_username_includes">Le nom d\'utilisateur contient</string>
<string name="search_from_age">À partir de l\'âge</string>
<string name="search_to_age">Jusqu\'à l\'âge</string>
<string name="search_country">Pays</string>
<string name="search_genders">Genres</string>
<string name="search_all">Tous</string>
<string name="search_button">Rechercher</string>
<string name="search_no_results">Aucun résultat.</string>
<string name="search_min_age_error">L\'âge minimum doit être au moins aussi grand que l\'âge maximum.</string>
<string name="history_empty">Aucune conversation précédente disponible.</string>
<string name="block">Bloquer l\'utilisateur</string>
<string name="unblock">Débloquer l\'utilisateur</string>
<string name="button_image">Envoyer une image</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">Inserisci il tuo nickname per la chat:</string>
<string name="label_gender">Genere:</string>
<string name="label_age">Età:</string>
<string name="label_country">Paese:</string>
<string name="button_start_chat">Inizia chat</string>
<string name="gender_female">Femmina</string>
<string name="gender_male">Maschio</string>
<string name="gender_pair">Coppia</string>
<string name="gender_trans_mf">Transgender (M-&gt;F)</string>
<string name="gender_trans_fm">Transgender (F-&gt;M)</string>
<string name="tab_search">Cerca</string>
<string name="tab_inbox">Posta in arrivo</string>
<string name="tab_history">Cronologia</string>
<string name="button_send">Invia</string>
<string name="search_username_includes">Il nome utente include</string>
<string name="search_from_age">Dall\'età</string>
<string name="search_to_age">Fino all\'età</string>
<string name="search_country">Paese</string>
<string name="search_genders">Generi</string>
<string name="search_all">Tutti</string>
<string name="search_button">Cerca</string>
<string name="search_no_results">Nessun risultato.</string>
<string name="search_min_age_error">L\'età minima deve essere almeno grande quanto l\'età massima.</string>
<string name="history_empty">Nessuna conversazione precedente disponibile.</string>
<string name="block">Blocca utente</string>
<string name="unblock">Sblocca utente</string>
<string name="button_image">Invia un\'immagine</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">チャット用のニックネームを入力してください:</string>
<string name="label_gender">性別:</string>
<string name="label_age">年齢:</string>
<string name="label_country">国:</string>
<string name="button_start_chat">チャットを開始</string>
<string name="gender_female">女性</string>
<string name="gender_male">男性</string>
<string name="gender_pair">カップル</string>
<string name="gender_trans_mf">トランスジェンダー (M-&gt;F)</string>
<string name="gender_trans_fm">トランスジェンダー (F-&gt;M)</string>
<string name="tab_search">検索</string>
<string name="tab_inbox">受信トレイ</string>
<string name="tab_history">履歴</string>
<string name="button_send">送信</string>
<string name="search_username_includes">ユーザー名に含まれる</string>
<string name="search_from_age">年齢から</string>
<string name="search_to_age">年齢まで</string>
<string name="search_country"></string>
<string name="search_genders">性別</string>
<string name="search_all">すべて</string>
<string name="search_button">検索</string>
<string name="search_no_results">結果がありません。</string>
<string name="search_min_age_error">最小年齢は最大年齢以上でなければなりません。</string>
<string name="history_empty">以前の会話はありません。</string>
<string name="block">ユーザーをブロック</string>
<string name="unblock">ユーザーのブロックを解除</string>
<string name="button_image">画像を送信</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">กรุณาพิมพ์ชื่อเล่นของคุณสำหรับแชท:</string>
<string name="label_gender">เพศ:</string>
<string name="label_age">อายุ:</string>
<string name="label_country">ประเทศ:</string>
<string name="button_start_chat">เริ่มแชท</string>
<string name="gender_female">หญิง</string>
<string name="gender_male">ชาย</string>
<string name="gender_pair">คู่</string>
<string name="gender_trans_mf">ทรานส์เจนเดอร์ (M-&gt;F)</string>
<string name="gender_trans_fm">ทรานส์เจนเดอร์ (F-&gt;M)</string>
<string name="tab_search">ค้นหา</string>
<string name="tab_inbox">กล่องจดหมาย</string>
<string name="tab_history">ประวัติ</string>
<string name="button_send">ส่ง</string>
<string name="search_username_includes">ชื่อผู้ใช้รวมถึง</string>
<string name="search_from_age">จากอายุ</string>
<string name="search_to_age">ถึงอายุ</string>
<string name="search_country">ประเทศ</string>
<string name="search_genders">เพศ</string>
<string name="search_all">ทั้งหมด</string>
<string name="search_button">ค้นหา</string>
<string name="search_no_results">ไม่มีผลลัพธ์</string>
<string name="search_min_age_error">อายุขั้นต่ำต้องมากกว่าหรือเท่ากับอายุสูงสุด</string>
<string name="history_empty">ไม่มีการสนทนาก่อนหน้านี้</string>
<string name="block">บล็อกผู้ใช้</string>
<string name="unblock">ยกเลิกการบล็อกผู้ใช้</string>
<string name="button_image">ส่งรูปภาพ</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">Mangyaring i-type ang iyong nickname para sa chat:</string>
<string name="label_gender">Kasarian:</string>
<string name="label_age">Edad:</string>
<string name="label_country">Bansa:</string>
<string name="button_start_chat">Simulan ang chat</string>
<string name="gender_female">Babae</string>
<string name="gender_male">Lalaki</string>
<string name="gender_pair">Mag-asawa</string>
<string name="gender_trans_mf">Transgender (M-&gt;F)</string>
<string name="gender_trans_fm">Transgender (F-&gt;M)</string>
<string name="tab_search">Maghanap</string>
<string name="tab_inbox">Inbox</string>
<string name="tab_history">Kasaysayan</string>
<string name="button_send">Ipadala</string>
<string name="search_username_includes">Kasama sa username</string>
<string name="search_from_age">Mula sa edad</string>
<string name="search_to_age">Hanggang edad</string>
<string name="search_country">Bansa</string>
<string name="search_genders">Kasarian</string>
<string name="search_all">Lahat</string>
<string name="search_button">Maghanap</string>
<string name="search_no_results">Walang resulta.</string>
<string name="search_min_age_error">Ang minimum na edad ay dapat na hindi bababa sa maximum na edad.</string>
<string name="history_empty">Walang nakaraang pag-uusap na available.</string>
<string name="block">I-block ang user</string>
<string name="unblock">I-unblock ang user</string>
<string name="button_image">Magpadala ng larawan</string>
</resources>

View File

@@ -1,29 +0,0 @@
<resources>
<string name="label_nick">请输入您的聊天昵称:</string>
<string name="label_gender">性别:</string>
<string name="label_age">年龄:</string>
<string name="label_country">国家:</string>
<string name="button_start_chat">开始聊天</string>
<string name="gender_female">女性</string>
<string name="gender_male">男性</string>
<string name="gender_pair">情侣</string>
<string name="gender_trans_mf">跨性别 (M-&gt;F)</string>
<string name="gender_trans_fm">跨性别 (F-&gt;M)</string>
<string name="tab_search">搜索</string>
<string name="tab_inbox">收件箱</string>
<string name="tab_history">历史记录</string>
<string name="button_send">发送</string>
<string name="search_username_includes">用户名包含</string>
<string name="search_from_age">从年龄</string>
<string name="search_to_age">到年龄</string>
<string name="search_country">国家</string>
<string name="search_genders">性别</string>
<string name="search_all">全部</string>
<string name="search_button">搜索</string>
<string name="search_no_results">没有结果。</string>
<string name="search_min_age_error">最小年龄必须至少等于或大于最大年龄。</string>
<string name="history_empty">没有可用的历史对话。</string>
<string name="block">屏蔽用户</string>
<string name="unblock">取消屏蔽用户</string>
<string name="button_image">发送图片</string>
</resources>

View File

@@ -49,6 +49,7 @@
<string name="unblock">Unblock</string>
<string name="message_placeholder">Message</string>
<string name="button_image">Image</string>
<string name="button_camera">Photo</string>
<string name="button_send">Send</string>
<string name="button_smileys">Smileys</string>
<string name="image_message">Image message</string>
@@ -57,6 +58,8 @@
<string name="image_upload_failed">Image upload failed.</string>
<string name="image_upload_too_large">Image is larger than 5 MB.</string>
<string name="image_upload_open_failed">Image could not be opened.</string>
<string name="camera_permission_denied">Camera permission was denied.</string>
<string name="camera_capture_failed">Photo could not be captured.</string>
<string name="feedback_created_at">Received %1$s</string>
<string name="feedback_meta_separator"></string>
<string name="countries_load_error">Country list could not be loaded: %1$s</string>
@@ -83,21 +86,26 @@
<string name="more_faq">FAQ</string>
<string name="more_rules">Rules</string>
<string name="more_safety">Safety</string>
<string name="more_privacy">Privacy</string>
<string name="more_imprint">Imprint</string>
<string name="more_back">Back to overview</string>
<string name="partners_intro">Recommended and friendly projects for our community.</string>
<string name="faq_intro">Answers to common questions about the chat.</string>
<string name="rules_intro">Basic rules for respectful chatting.</string>
<string name="safety_intro">Tips for privacy and safer usage.</string>
<string name="privacy_intro">Privacy policy, processed data and contact for privacy requests.</string>
<string name="imprint_intro">Legal notice and contact details.</string>
<string name="external_link">External link</string>
<string name="faq_title">Frequently Asked Questions</string>
<string name="rules_title">Chat Rules</string>
<string name="safety_title">Safety and Privacy</string>
<string name="privacy_title">Privacy Policy</string>
<string name="imprint_title">Imprint</string>
<string name="partners_title">Partners</string>
<string name="faq_body">Choose a nickname, enter your profile details and start chatting. Do not share sensitive data like phone numbers, addresses, passwords or payment information. You can send images, block users and use feedback for serious issues.</string>
<string name="rules_body">No insults, hate speech, illegal content, spam or unwanted harassment. Only send images you are allowed to share and respect the privacy of others.</string>
<string name="safety_body">Use a nickname that does not identify you. Do not share private contact or payment data. Be careful with links from strangers and end conversations that feel wrong. Use block and feedback for serious incidents.</string>
<string name="privacy_body">SingleChat processes the nickname you choose, profile details such as age, gender and country, chat messages, images you actively send, feedback messages and technically necessary session data. The Android app requests camera access only if you actively want to take a photo in the app. The full privacy policy for website and app is published on single-chat.net.</string>
<string name="privacy_open_policy">Open privacy policy</string>
<string name="imprint_body">Torsten Schulz, Friedrich-Stampfer-Str. 21, 60437 Frankfurt. Contact: tsschulz@tsschulz.de. External links are the responsibility of their operators.</string>
</resources>

View File

@@ -25,6 +25,15 @@
>
<img src="/image.png" alt="Image" />
</button>
<button
class="camera-button"
type="button"
@click="openCamera"
title="Foto aufnehmen"
:disabled="!hasConversation || isCameraStarting"
>
<span aria-hidden="true">📷</span>
</button>
<div v-if="showSmileys" class="smiley-bar">
<span
@@ -36,16 +45,86 @@
@click="insertSmiley(code)"
></span>
</div>
<div v-if="cameraModalOpen" class="camera-modal-overlay" @click="closeCamera">
<div class="camera-modal" @click.stop>
<div class="camera-modal-header">
<h3>Foto aufnehmen</h3>
<button type="button" class="camera-close" @click="closeCamera" title="Schließen">×</button>
</div>
<div v-if="cameraError" class="camera-error">
{{ cameraError }}
</div>
<div class="camera-preview">
<video
v-show="!capturedImageUrl && !cameraError"
ref="videoRef"
autoplay
playsinline
muted
></video>
<img
v-if="capturedImageUrl"
:src="capturedImageUrl"
alt="Aufgenommenes Foto"
/>
<div v-if="isCameraStarting && !cameraError" class="camera-loading">
Kamera wird gestartet ...
</div>
</div>
<canvas ref="canvasRef" class="camera-canvas" aria-hidden="true"></canvas>
<div class="camera-actions">
<button
v-if="!capturedImageUrl"
type="button"
@click="capturePhoto"
:disabled="isCameraStarting || !!cameraError"
>
Foto machen
</button>
<button
v-if="capturedImageUrl"
type="button"
class="secondary"
@click="retakePhoto"
:disabled="isUploadingPhoto"
>
Neu aufnehmen
</button>
<button
v-if="capturedImageUrl"
type="button"
@click="sendCapturedPhoto"
:disabled="isUploadingPhoto"
>
{{ isUploadingPhoto ? 'Sende ...' : 'Foto senden' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onBeforeUnmount } from 'vue';
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
const message = ref('');
const showSmileys = ref(false);
const cameraModalOpen = ref(false);
const isCameraStarting = ref(false);
const isUploadingPhoto = ref(false);
const cameraError = ref('');
const videoRef = ref(null);
const canvasRef = ref(null);
const cameraStream = ref(null);
const capturedImageUrl = ref('');
const capturedPhotoBlob = ref(null);
const hasConversation = computed(() => !!chatStore.currentConversation);
const isAwaitingUsername = computed(() => chatStore.awaitingLoginUsername);
const isAwaitingPassword = computed(() => chatStore.awaitingLoginPassword);
@@ -115,55 +194,339 @@ function insertSmiley(code) {
showSmileys.value = false;
}
function showTemporaryError(text) {
chatStore.errorMessage = text;
setTimeout(() => {
if (chatStore.errorMessage === text) {
chatStore.errorMessage = null;
}
}, 4000);
}
async function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!chatStore.currentConversation) {
console.error('Keine Konversation ausgewählt');
return;
}
// Prüfe Dateigröße (max. 5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
alert('Bild ist zu groß. Maximale Größe: 5MB');
return;
}
try {
// Erstelle FormData für Upload
const formData = new FormData();
formData.append('image', file);
// Lade Bild hoch
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData,
credentials: 'include' // Wichtig für Session-Cookies
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }));
throw new Error(errorData.error || 'Fehler beim Hochladen des Bildes');
}
const data = await response.json();
if (data.success && data.code) {
// Sende nur den Code über Socket.IO
chatStore.sendImage(chatStore.currentConversation, data.code, data.url);
} else {
throw new Error('Ungültige Antwort vom Server');
}
await uploadAndSendImage(file);
} catch (error) {
console.error('Fehler beim Bild-Upload:', error);
alert('Fehler beim Bild-Upload: ' + error.message);
}
// Input zurücksetzen, damit das gleiche Bild erneut ausgewählt werden kann
event.target.value = '';
}
async function uploadAndSendImage(file) {
if (!chatStore.currentConversation) {
throw new Error('Keine Konversation ausgewählt');
}
// Prüfe Dateigröße (max. 5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
throw new Error('Bild ist zu groß. Maximale Größe: 5MB');
}
const formData = new FormData();
formData.append('image', file);
const response = await fetch('/api/upload-image', {
method: 'POST',
body: formData,
credentials: 'include' // Wichtig für Session-Cookies
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }));
throw new Error(errorData.error || 'Fehler beim Hochladen des Bildes');
}
const data = await response.json();
if (data.success && data.code) {
chatStore.sendImage(chatStore.currentConversation, data.code, data.url);
} else {
throw new Error('Ungültige Antwort vom Server');
}
}
async function openCamera() {
if (!hasConversation.value) {
showTemporaryError('Bitte zuerst eine Unterhaltung auswählen.');
return;
}
if (!navigator.mediaDevices?.getUserMedia) {
showTemporaryError('Kamera wird von diesem Browser nicht unterstützt.');
return;
}
cameraModalOpen.value = true;
cameraError.value = '';
capturedImageUrl.value = '';
capturedPhotoBlob.value = null;
isCameraStarting.value = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 1280 },
height: { ideal: 1280 }
},
audio: false
});
cameraStream.value = stream;
if (videoRef.value) {
videoRef.value.srcObject = stream;
await videoRef.value.play();
}
} catch (error) {
console.error('Kamera konnte nicht gestartet werden:', error);
cameraError.value = 'Kamera konnte nicht gestartet werden. Bitte Berechtigung prüfen.';
stopCameraStream();
} finally {
isCameraStarting.value = false;
}
}
function stopCameraStream() {
if (cameraStream.value) {
cameraStream.value.getTracks().forEach(track => track.stop());
cameraStream.value = null;
}
if (videoRef.value) {
videoRef.value.srcObject = null;
}
}
function closeCamera() {
stopCameraStream();
cameraModalOpen.value = false;
cameraError.value = '';
isCameraStarting.value = false;
isUploadingPhoto.value = false;
clearCapturedPhoto();
}
function clearCapturedPhoto() {
if (capturedImageUrl.value) {
URL.revokeObjectURL(capturedImageUrl.value);
}
capturedImageUrl.value = '';
capturedPhotoBlob.value = null;
}
function capturePhoto() {
if (!videoRef.value || !canvasRef.value) return;
const video = videoRef.value;
const canvas = canvasRef.value;
const sourceWidth = video.videoWidth || 1280;
const sourceHeight = video.videoHeight || 720;
const maxSide = 1280;
const scale = Math.min(1, maxSide / Math.max(sourceWidth, sourceHeight));
const targetWidth = Math.round(sourceWidth * scale);
const targetHeight = Math.round(sourceHeight * scale);
canvas.width = targetWidth;
canvas.height = targetHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, targetWidth, targetHeight);
canvas.toBlob((blob) => {
if (!blob) {
cameraError.value = 'Foto konnte nicht verarbeitet werden.';
return;
}
clearCapturedPhoto();
capturedPhotoBlob.value = blob;
capturedImageUrl.value = URL.createObjectURL(blob);
stopCameraStream();
}, 'image/jpeg', 0.86);
}
async function retakePhoto() {
clearCapturedPhoto();
await openCamera();
}
async function sendCapturedPhoto() {
if (!capturedPhotoBlob.value) return;
isUploadingPhoto.value = true;
try {
const file = new File([capturedPhotoBlob.value], `singlechat-photo-${Date.now()}.jpg`, {
type: 'image/jpeg'
});
await uploadAndSendImage(file);
closeCamera();
} catch (error) {
console.error('Fehler beim Foto-Versand:', error);
cameraError.value = 'Foto konnte nicht gesendet werden: ' + error.message;
} finally {
isUploadingPhoto.value = false;
}
}
onBeforeUnmount(() => {
stopCameraStream();
clearCapturedPhoto();
});
</script>
<style scoped>
.camera-button {
width: 35px;
height: 35px;
border: 1px solid #cdd8d0;
border-radius: 8px;
background: #ffffff;
display: inline-grid;
place-items: center;
cursor: pointer;
font-size: 18px;
}
.camera-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.camera-modal-overlay {
position: fixed;
inset: 0;
z-index: 1200;
padding: 18px;
background: rgba(12, 18, 14, 0.78);
display: flex;
align-items: center;
justify-content: center;
}
.camera-modal {
width: min(560px, 100%);
max-height: calc(100vh - 36px);
border-radius: 10px;
border: 1px solid #d7dfd9;
background: #ffffff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.camera-modal-header {
min-height: 54px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e4ebe6;
}
.camera-modal-header h3 {
margin: 0;
color: #18201b;
font-size: 17px;
}
.camera-close {
width: 36px;
height: 36px;
border: 0;
border-radius: 8px;
background: #edf2ee;
color: #253027;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.camera-error {
margin: 14px 14px 0;
border: 1px solid #e5b7b7;
border-radius: 8px;
padding: 10px 12px;
background: #fff0f0;
color: #7d2525;
font-size: 14px;
}
.camera-preview {
position: relative;
margin: 14px;
aspect-ratio: 4 / 3;
border-radius: 10px;
background: #101510;
overflow: hidden;
display: grid;
place-items: center;
}
.camera-preview video,
.camera-preview img {
width: 100%;
height: 100%;
object-fit: contain;
}
.camera-preview video {
transform: scaleX(-1);
}
.camera-loading {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #ffffff;
background: rgba(0, 0, 0, 0.35);
font-weight: 700;
}
.camera-canvas {
display: none;
}
.camera-actions {
padding: 0 14px 14px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.camera-actions button {
min-height: 40px;
border: 0;
border-radius: 8px;
padding: 0 16px;
background: #245c3a;
color: #ffffff;
font-weight: 800;
cursor: pointer;
}
.camera-actions button.secondary {
background: #edf2ee;
color: #245c3a;
border: 1px solid #bfd5c4;
}
.camera-actions button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
@media (max-width: 620px) {
.camera-modal-overlay {
padding: 10px;
}
.camera-actions {
flex-direction: column;
}
}
</style>

View File

@@ -17,6 +17,11 @@
Diese Datenschutzerklärung gilt für die Website und die Android-App von SingleChat unter
der Domain <strong>www.single-chat.net</strong>.
</p>
<p>
Sie beschreibt die Verarbeitung personenbezogener Daten im Zusammenhang mit der Nutzung der
Chat-Funktionen, der Bildfreigabe, von Feedback-Meldungen und der technisch notwendigen
Sitzungsverwaltung.
</p>
<h3>1. Verantwortlicher</h3>
<p>
@@ -48,59 +53,88 @@
<li>Bearbeitung von Feedback und Missbrauchshinweisen</li>
</ul>
<h3>4. Chat-Nachrichten und Profilangaben</h3>
<h3>4. Rechtsgrundlagen</h3>
<p>
Soweit personenbezogene Daten verarbeitet werden, erfolgt dies in der Regel zur Erfüllung
der angeforderten Chat-Funktionen und auf Grundlage berechtigter Interessen an einem
sicheren, stabilen und missbrauchsarmen Betrieb des Angebots.
</p>
<h3>5. Chat-Nachrichten und Profilangaben</h3>
<p>
Wenn du den Dienst nutzt, werden von dir eingegebene Profilangaben wie Nickname, Alter, Geschlecht und Land für
die Chat-Funktion verwendet. Chat-Nachrichten werden technisch verarbeitet, damit Unterhaltungen in Echtzeit
zugestellt werden können.
</p>
<h3>5. Bilder</h3>
<h3>6. Bilder und Kamerazugriff</h3>
<p>
Bilder werden nur verarbeitet, wenn du sie aktiv auswählst und hochlädst. Nach aktuellem Systemstand werden
hochgeladene Bilder serverseitig temporär gespeichert und nach Ablauf einer begrenzten Zeit wieder entfernt.
</p>
<p>
Die Android-App fordert die Kameraberechtigung nur an, wenn du in der App aktiv ein Foto aufnehmen möchtest.
Ohne deine Auslösung erfolgt kein Kamerazugriff.
</p>
<h3>6. Sitzungen, Cookies und technische Protokolle</h3>
<h3>7. Sitzungen, Cookies und technische Protokolle</h3>
<p>
Für den Betrieb des Dienstes werden Sitzungsdaten verwendet. Dazu gehören insbesondere technisch notwendige
Session-Informationen, damit ein Login erhalten bleibt und Socket- sowie API-Anfragen korrekt zugeordnet werden
können. Zusätzlich können im Rahmen des Serverbetriebs technische Protokolldaten anfallen.
</p>
<h3>7. Feedback und Missbrauchsmeldungen</h3>
<h3>8. Feedback und Missbrauchsmeldungen</h3>
<p>
Wenn du Feedback sendest, werden die von dir eingetragenen Inhalte verarbeitet, um Hinweise, Fehlermeldungen oder
Missbrauchsmeldungen zu bearbeiten.
</p>
<h3>8. Weitergabe an Dritte</h3>
<h3>9. Weitergabe an Dritte</h3>
<p>
Eine Weitergabe personenbezogener Daten an Dritte erfolgt nicht zu Werbezwecken. Soweit externe technische
Dienstleister oder Hosting-Anbieter eingebunden sind, kann eine Verarbeitung im Rahmen des technischen Betriebs
erforderlich sein.
</p>
<h3>9. Verschlüsselung</h3>
<h3>10. Werbung, Standort und weitere sensible Daten</h3>
<p>
Die Android-App verwendet nach aktuellem Stand kein Werbe-SDK und verarbeitet keine Standortdaten, Kontaktlisten,
Gesundheitsdaten oder Zahlungsdaten. Solche Daten werden weder angefordert noch fuer die Kernfunktion des Chats
benoetigt.
</p>
<h3>11. Verschlüsselung</h3>
<p>
Die produktive Bereitstellung der Website und der App erfolgt über verschlüsselte Verbindungen, damit Daten bei der
Übertragung geschützt sind.
</p>
<h3>10. Deine Rechte</h3>
<h3>12. Speicherdauer</h3>
<p>
Personenbezogene Daten werden nicht länger gespeichert, als es für den technischen Betrieb, die Bereitstellung der
Funktionen und die Bearbeitung von Missbrauchs- oder Supportanfragen erforderlich ist. Bilder sind für eine
begrenzte Verfügbarkeit im Chat gedacht und werden nicht dauerhaft als öffentliches Archiv bereitgestellt.
</p>
<h3>13. Deine Rechte</h3>
<p>
Du hast im Rahmen der gesetzlichen Vorschriften insbesondere das Recht auf Auskunft, Berichtigung, Löschung,
Einschränkung der Verarbeitung sowie Beschwerde bei einer zuständigen Aufsichtsbehörde.
</p>
<h3>11. Kontakt zum Datenschutz</h3>
<h3>14. Kontakt zum Datenschutz</h3>
<p>
Bei Fragen zum Datenschutz oder wenn du eine datenschutzbezogene Anfrage stellen möchtest, kontaktiere bitte:
<a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>.
</p>
<p>
Wenn du die Löschung von Daten anfragen möchtest, teile bitte den verwendeten Nickname, den ungefähren Zeitraum der
Nutzung und - soweit vorhanden - weitere zur Zuordnung notwendige Angaben mit.
</p>
<h3>12. Stand</h3>
<p>Stand dieser Datenschutzerklärung: 22. April 2026</p>
<h3>15. Stand</h3>
<p>Stand dieser Datenschutzerklärung: 16. Juni 2026</p>
</main>
<ImprintContainer />