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

@@ -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 />