Files
yourpart3/frontend/src/views/social/EroticPicturesView.vue
Torsten Schulz (local) 5556beffef feat(socialnetwork): enhance folder and video management with user visibility options
- Added functionality to manage selected users for adult folders and erotic videos, allowing for more granular visibility control.
- Introduced new endpoints and methods in the SocialNetworkController and SocialNetworkService to handle selected users.
- Updated the frontend components to include input fields for selected users in CreateFolderDialog, EditImageDialog, and EroticPicturesView.
- Enhanced the routing to support fetching erotic folders and videos by username, improving user experience in profile views.
2026-03-27 16:56:45 +01:00

418 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="gallery-page erotic-gallery-page">
<section class="gallery-hero erotic-gallery-hero surface-card">
<div>
<span class="gallery-kicker">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
<h2>{{ $t('socialnetwork.erotic.picturesTitle') }}</h2>
<p>{{ $t('socialnetwork.erotic.picturesIntro') }}</p>
</div>
</section>
<div class="gallery-view">
<div class="sidebar surface-card">
<h3>{{ $t('socialnetwork.gallery.folders') }}</h3>
<ul class="tree">
<folder-item
v-for="folder in [folders]"
:key="folder.id"
:folder="folder"
:selected-folder="selectedFolder"
@select-folder="selectFolder"
:isLastItem="true"
:depth="0"
:parentsWithChildren="[false]"
:noActionItems="isForeignView"
@edit-folder="openEditFolderDialog"
@delete-folder="deleteFolder"
/>
</ul>
<button v-if="!isForeignView" @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
</div>
<div class="content">
<div v-if="!isForeignView" class="upload-section surface-card">
<div class="upload-header" @click="toggleUploadSection">
<span><i class="icon-upload-toggle">{{ isUploadVisible ? '&#9650;' : '&#9660;' }}</i></span>
<h3>{{ $t('socialnetwork.erotic.uploadTitle') }}</h3>
</div>
<div v-if="isUploadVisible" class="upload-content">
<form @submit.prevent="handleUpload">
<div class="form-group">
<label for="imageTitle">{{ $t('socialnetwork.gallery.upload.image_title') }}</label>
<input v-model="imageTitle" type="text" :placeholder="$t('socialnetwork.gallery.upload.image_title')" />
</div>
<div class="form-group">
<label for="imageFile">{{ $t('socialnetwork.gallery.upload.image_file') }}</label>
<input type="file" accept="image/*" required @change="onFileChange" />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview" style="max-width: 150px; max-height: 150px;" />
</div>
</div>
<div class="form-group">
<label for="visibility">{{ $t('socialnetwork.gallery.upload.visibility') }}</label>
<multiselect
v-model="selectedVisibilities"
:options="visibilityOptions"
:multiple="true"
:close-on-select="false"
label="description"
:placeholder="$t('socialnetwork.gallery.upload.selectvisibility')"
:track-by="'value'"
>
<template #option="{ option }">
<span v-if="option && option.description">
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
</span>
</template>
<template #tag="{ option, remove }">
<span v-if="option && option.description" class="multiselect__tag">
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
<span @click="remove(option)">×</span>
</span>
</template>
</multiselect>
</div>
<div v-if="requiresSelectedUsers" class="form-group">
<label for="selectedUsers">{{ $t('socialnetwork.gallery.visibility.selected-users') }}</label>
<input
id="selectedUsers"
v-model="selectedUsernamesText"
type="text"
placeholder="anna, bert, clara"
/>
</div>
<button type="submit" class="upload-button">
{{ $t('socialnetwork.gallery.upload.upload_button') }}
</button>
</form>
</div>
</div>
<div class="image-list surface-card">
<h3>{{ $t('socialnetwork.gallery.images') }}</h3>
<ul v-if="images.length > 0" class="image-grid">
<li v-for="image in images" :key="image.id" class="erotic-image-card">
<div class="erotic-image-card__preview" @click="!image.isModeratedHidden && openImageDialog(image)">
<img v-if="!image.isModeratedHidden" :src="image.url || image.placeholder" alt="Loading..." />
<div v-else class="erotic-image-card__hidden">
{{ $t('socialnetwork.erotic.hiddenByModeration') }}
</div>
</div>
<p>{{ image.title }}</p>
<span v-if="image.isModeratedHidden" class="erotic-image-card__badge">
{{ $t('socialnetwork.erotic.moderationHidden') }}
</span>
<div class="erotic-image-card__actions">
<button type="button" class="secondary" @click="startReport('image', image.id)">
{{ $t('socialnetwork.erotic.reportAction') }}
</button>
</div>
<div v-if="reportTarget.type === 'image' && reportTarget.id === image.id" class="erotic-report-form">
<select v-model="reportReason">
<option v-for="option in reportReasonOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<textarea v-model="reportNote" rows="3" :placeholder="$t('socialnetwork.erotic.reportNote')" />
<div class="erotic-report-form__actions">
<button type="button" @click="submitReport">{{ $t('socialnetwork.erotic.submitReport') }}</button>
<button type="button" class="secondary" @click="resetReport">{{ $t('general.cancel') }}</button>
</div>
</div>
</li>
</ul>
<span v-else>{{ $t('socialnetwork.erotic.noimages') }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
import Multiselect from 'vue-multiselect';
import FolderItem from '../../components/FolderItem.vue';
import 'vue-multiselect/dist/vue-multiselect.min.css';
import { EventBus } from '@/utils/eventBus.js';
import { showApiError, showSuccess } from '@/utils/feedback.js';
import { mapGetters } from 'vuex';
export default {
components: {
FolderItem,
Multiselect,
},
data() {
return {
folders: { children: [] },
images: [],
selectedFolder: null,
imageTitle: '',
fileToUpload: null,
isUploadVisible: true,
visibilityOptions: [],
selectedVisibilities: [],
selectedUsernamesText: '',
imagePreview: null,
reportTarget: { type: null, id: null },
reportReason: 'other',
reportNote: '',
viewUsername: '',
};
},
computed: {
...mapGetters(['user']),
reportReasonOptions() {
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
value,
label: this.$t(`socialnetwork.erotic.reportReasons.${value}`)
}));
},
isForeignView() {
return Boolean(this.viewUsername && this.viewUsername !== this.user?.username);
},
requiresSelectedUsers() {
return this.selectedVisibilities.some(option => option?.description === 'selected-users');
}
},
async mounted() {
await this.initializeView();
await this.loadImageVisibilities();
if (this.folders) {
this.selectFolder(this.folders);
}
EventBus.on('folderCreated', this.loadFolders);
},
beforeUnmount() {
EventBus.off('folderCreated', this.loadFolders);
},
methods: {
async initializeView() {
this.viewUsername = String(this.$route.query.username || '').trim();
await this.loadFolders();
},
async loadFolders() {
const response = this.isForeignView
? await apiClient.get(`/api/socialnetwork/profile/erotic/folders/${this.viewUsername}`)
: await apiClient.get('/api/socialnetwork/erotic/folders');
this.folders = response.data;
},
async loadImageVisibilities() {
const response = await apiClient.get('/api/socialnetwork/imagevisibilities');
this.visibilityOptions = response.data.filter(option => option.description !== 'everyone');
if (!this.selectedVisibilities.length) {
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
}
},
async selectFolder(folder) {
this.selectedFolder = folder;
await this.loadImages(folder.id);
},
async loadImages(folderId) {
const response = await apiClient.get(`/api/socialnetwork/erotic/folder/${folderId}`);
this.images = response.data.map((image) => ({
...image,
placeholder: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3C/svg%3E',
url: null,
}));
await this.fetchImages();
},
async fetchImages() {
this.images.forEach((image) => {
this.fetchImage(image);
});
},
openCreateFolderDialog() {
const parentFolder = this.selectedFolder || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
Object.assign(this.$root.$refs.createFolderDialog, {
parentFolder,
folderId: 0,
eroticMode: true,
});
this.$root.$refs.createFolderDialog.open();
},
onFileChange(event) {
this.fileToUpload = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
this.imagePreview = e.target.result;
};
reader.readAsDataURL(this.fileToUpload);
},
async handleUpload() {
if (!this.fileToUpload || !this.selectedFolder?.id) return;
const formData = new FormData();
formData.append('image', this.fileToUpload);
formData.append('folderId', this.selectedFolder.id);
formData.append('title', this.imageTitle);
formData.append('visibility', JSON.stringify(this.selectedVisibilities.map((v) => v.id)));
formData.append('selectedUsers', JSON.stringify(
this.selectedUsernamesText.split(',').map(value => value.trim()).filter(Boolean)
));
await apiClient.post('/api/socialnetwork/erotic/images', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
await this.loadImages(this.selectedFolder.id);
this.imageTitle = '';
this.fileToUpload = null;
this.imagePreview = null;
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
this.selectedUsernamesText = '';
},
async fetchImage(image) {
if (image.isModeratedHidden) {
return;
}
const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
const response = await apiClient.get(`/api/socialnetwork/erotic/image/${image.hash}`, {
headers: { userid: userId },
responseType: 'blob',
});
image.url = URL.createObjectURL(response.data);
},
toggleUploadSection() {
this.isUploadVisible = !this.isUploadVisible;
},
openImageDialog(image) {
if (this.isForeignView) {
this.$root.$refs.showImageDialog.open(image);
return;
}
this.$root.$refs.editImageDialog.open(image);
},
startReport(type, id) {
this.reportTarget = { type, id };
this.reportReason = 'other';
this.reportNote = '';
},
resetReport() {
this.reportTarget = { type: null, id: null };
this.reportReason = 'other';
this.reportNote = '';
},
async submitReport() {
try {
await apiClient.post('/api/socialnetwork/erotic/report', {
targetType: this.reportTarget.type,
targetId: this.reportTarget.id,
reason: this.reportReason,
note: this.reportNote
});
showSuccess(this, this.$t('socialnetwork.erotic.reportSubmitted'));
this.resetReport();
} catch (error) {
showApiError(this, error, this.$t('socialnetwork.erotic.reportError'));
}
},
async saveImage(updatedImage) {
const response = await apiClient.put(`/api/socialnetwork/erotic/images/${updatedImage.id}`, {
title: updatedImage.title,
visibilities: updatedImage.visibilities,
selectedUsers: updatedImage.selectedUsers || [],
});
this.images = response.data.map((image) => ({
...image,
placeholder: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3C/svg%3E',
url: null,
}));
await this.fetchImages();
},
openEditFolderDialog(folder) {
const parentFolder = folder.parent || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
Object.assign(this.$root.$refs.createFolderDialog, {
parentFolder,
folderId: folder.id,
eroticMode: true,
});
this.$root.$refs.createFolderDialog.open(folder);
},
async deleteFolder() {
// Separate delete flow for adult folders is intentionally not enabled yet.
},
},
watch: {
'$route.query.username': {
async handler() {
await this.initializeView();
if (this.folders) {
await this.selectFolder(this.folders);
}
}
}
}
};
</script>
<style scoped>
.erotic-gallery-page {
display: grid;
gap: 1.25rem;
}
.erotic-gallery-hero {
padding: 1.4rem 1.5rem;
}
.gallery-kicker {
display: inline-flex;
margin-bottom: 0.5rem;
padding: 0.25rem 0.7rem;
border-radius: 999px;
background: rgba(120, 195, 138, 0.14);
color: #42634e;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.erotic-gallery-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.erotic-image-card {
display: grid;
gap: 0.55rem;
}
.erotic-image-card__preview {
cursor: pointer;
}
.erotic-image-card__hidden {
display: grid;
place-items: center;
min-height: 180px;
border-radius: var(--radius-md);
background: rgba(96, 32, 48, 0.18);
color: var(--color-text-secondary);
text-align: center;
padding: 1rem;
}
.erotic-image-card__badge {
display: inline-flex;
width: fit-content;
padding: 0.2rem 0.65rem;
border-radius: var(--radius-pill);
background: rgba(176, 88, 88, 0.14);
color: #8b3340;
font-size: 0.78rem;
font-weight: 700;
}
.erotic-image-card__actions,
.erotic-report-form,
.erotic-report-form__actions {
display: grid;
gap: 0.5rem;
}
</style>