für playstore änderungen
All checks were successful
Deploy SingleChat / deploy (push) Successful in 23s
All checks were successful
Deploy SingleChat / deploy (push) Successful in 23s
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user