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.
This commit is contained in:
@@ -16,13 +16,16 @@ class SocialNetworkController {
|
|||||||
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
||||||
this.deleteFolder = this.deleteFolder.bind(this);
|
this.deleteFolder = this.deleteFolder.bind(this);
|
||||||
this.getAdultFolders = this.getAdultFolders.bind(this);
|
this.getAdultFolders = this.getAdultFolders.bind(this);
|
||||||
|
this.getAdultFoldersByUsername = this.getAdultFoldersByUsername.bind(this);
|
||||||
this.createAdultFolder = this.createAdultFolder.bind(this);
|
this.createAdultFolder = this.createAdultFolder.bind(this);
|
||||||
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
||||||
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
||||||
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
||||||
this.changeAdultImage = this.changeAdultImage.bind(this);
|
this.changeAdultImage = this.changeAdultImage.bind(this);
|
||||||
this.listEroticVideos = this.listEroticVideos.bind(this);
|
this.listEroticVideos = this.listEroticVideos.bind(this);
|
||||||
|
this.getEroticVideosByUsername = this.getEroticVideosByUsername.bind(this);
|
||||||
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
||||||
|
this.changeEroticVideo = this.changeEroticVideo.bind(this);
|
||||||
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
||||||
this.reportEroticContent = this.reportEroticContent.bind(this);
|
this.reportEroticContent = this.reportEroticContent.bind(this);
|
||||||
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
||||||
@@ -157,8 +160,8 @@ class SocialNetworkController {
|
|||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
const { imageId } = req.params;
|
const { imageId } = req.params;
|
||||||
const { title, visibilities } = req.body;
|
const { title, visibilities, selectedUsers } = req.body;
|
||||||
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities);
|
const folderId = await this.socialNetworkService.changeImage(userId, imageId, title, visibilities, selectedUsers);
|
||||||
console.log('--->', folderId);
|
console.log('--->', folderId);
|
||||||
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
res.status(201).json(await this.socialNetworkService.getFolderImageList(userId, folderId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -208,6 +211,21 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAdultFoldersByUsername(req, res) {
|
||||||
|
try {
|
||||||
|
const requestingUserId = req.headers.userid;
|
||||||
|
const { username } = req.params;
|
||||||
|
const folders = await this.socialNetworkService.getAdultFoldersByUsername(username, requestingUserId);
|
||||||
|
if (!folders) {
|
||||||
|
return res.status(404).json({ error: 'No folders found or access denied.' });
|
||||||
|
}
|
||||||
|
res.status(200).json(folders);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getAdultFoldersByUsername:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createAdultFolder(req, res) {
|
async createAdultFolder(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
@@ -267,8 +285,8 @@ class SocialNetworkController {
|
|||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
const { imageId } = req.params;
|
const { imageId } = req.params;
|
||||||
const { title, visibilities } = req.body;
|
const { title, visibilities, selectedUsers } = req.body;
|
||||||
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities);
|
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities, selectedUsers);
|
||||||
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in changeAdultImage:', error);
|
console.error('Error in changeAdultImage:', error);
|
||||||
@@ -287,6 +305,18 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEroticVideosByUsername(req, res) {
|
||||||
|
try {
|
||||||
|
const userId = req.headers.userid;
|
||||||
|
const { username } = req.params;
|
||||||
|
const videos = await this.socialNetworkService.getEroticVideosByUsername(username, userId);
|
||||||
|
res.status(200).json(videos);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getEroticVideosByUsername:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async uploadEroticVideo(req, res) {
|
async uploadEroticVideo(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
@@ -300,6 +330,18 @@ class SocialNetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeEroticVideo(req, res) {
|
||||||
|
try {
|
||||||
|
const userId = req.headers.userid;
|
||||||
|
const { videoId } = req.params;
|
||||||
|
const updatedVideo = await this.socialNetworkService.changeEroticVideo(userId, videoId, req.body);
|
||||||
|
res.status(200).json(updatedVideo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in changeEroticVideo:', error);
|
||||||
|
res.status(error.status || 500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getEroticVideoByHash(req, res) {
|
async getEroticVideoByHash(req, res) {
|
||||||
try {
|
try {
|
||||||
const userId = req.headers.userid;
|
const userId = req.headers.userid;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.createTable(
|
||||||
|
{ schema: 'community', tableName: 'erotic_video_image_visibility' },
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
erotic_video_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'erotic_video' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
visibility_type_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'type', tableName: 'image_visibility' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryInterface.createTable(
|
||||||
|
{ schema: 'community', tableName: 'erotic_video_visibility_user' },
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
erotic_video_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'erotic_video' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: { schema: 'community', tableName: 'user' },
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
INSERT INTO community.erotic_video_image_visibility (erotic_video_id, visibility_type_id)
|
||||||
|
SELECT ev.id, iv.id
|
||||||
|
FROM community.erotic_video ev
|
||||||
|
CROSS JOIN type.image_visibility iv
|
||||||
|
WHERE iv.description = 'adults'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM community.erotic_video_image_visibility eviv
|
||||||
|
WHERE eviv.erotic_video_id = ev.id
|
||||||
|
AND eviv.visibility_type_id = iv.id
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_visibility_user' });
|
||||||
|
await queryInterface.dropTable({ schema: 'community', tableName: 'erotic_video_image_visibility' });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
|||||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||||
|
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
|
||||||
|
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
|
||||||
import GuestbookEntry from './community/guestbook.js';
|
import GuestbookEntry from './community/guestbook.js';
|
||||||
import Forum from './forum/forum.js';
|
import Forum from './forum/forum.js';
|
||||||
import Title from './forum/title.js';
|
import Title from './forum/title.js';
|
||||||
@@ -242,6 +244,17 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'imageId'
|
otherKey: 'imageId'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EroticVideo.belongsToMany(ImageVisibilityType, {
|
||||||
|
through: EroticVideoImageVisibility,
|
||||||
|
foreignKey: 'eroticVideoId',
|
||||||
|
otherKey: 'visibilityTypeId'
|
||||||
|
});
|
||||||
|
ImageVisibilityType.belongsToMany(EroticVideo, {
|
||||||
|
through: EroticVideoImageVisibility,
|
||||||
|
foreignKey: 'visibilityTypeId',
|
||||||
|
otherKey: 'eroticVideoId'
|
||||||
|
});
|
||||||
|
|
||||||
Folder.belongsToMany(ImageVisibilityUser, {
|
Folder.belongsToMany(ImageVisibilityUser, {
|
||||||
through: FolderVisibilityUser,
|
through: FolderVisibilityUser,
|
||||||
foreignKey: 'folderId',
|
foreignKey: 'folderId',
|
||||||
@@ -253,6 +266,19 @@ export default function setupAssociations() {
|
|||||||
otherKey: 'folderId'
|
otherKey: 'folderId'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EroticVideo.belongsToMany(User, {
|
||||||
|
through: EroticVideoVisibilityUser,
|
||||||
|
foreignKey: 'eroticVideoId',
|
||||||
|
otherKey: 'userId',
|
||||||
|
as: 'selectedVisibilityUsers'
|
||||||
|
});
|
||||||
|
User.belongsToMany(EroticVideo, {
|
||||||
|
through: EroticVideoVisibilityUser,
|
||||||
|
foreignKey: 'userId',
|
||||||
|
otherKey: 'eroticVideoId',
|
||||||
|
as: 'visibleEroticVideos'
|
||||||
|
});
|
||||||
|
|
||||||
// Guestbook related associations
|
// Guestbook related associations
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'recipientId', as: 'receivedEntries' });
|
||||||
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
User.hasMany(GuestbookEntry, { foreignKey: 'senderId', as: 'sentEntries' });
|
||||||
|
|||||||
26
backend/models/community/erotic_video_image_visibility.js
Normal file
26
backend/models/community/erotic_video_image_visibility.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
const EroticVideoImageVisibility = sequelize.define('erotic_video_image_visibility', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
eroticVideoId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
visibilityTypeId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'erotic_video_image_visibility',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
schema: 'community'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EroticVideoImageVisibility;
|
||||||
26
backend/models/community/erotic_video_visibility_user.js
Normal file
26
backend/models/community/erotic_video_visibility_user.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { sequelize } from '../../utils/sequelize.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
const EroticVideoVisibilityUser = sequelize.define('erotic_video_visibility_user', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
eroticVideoId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'erotic_video_visibility_user',
|
||||||
|
timestamps: false,
|
||||||
|
underscored: true,
|
||||||
|
schema: 'community'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EroticVideoVisibilityUser;
|
||||||
@@ -25,6 +25,8 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
|
|||||||
import FolderImageVisibility from './community/folder_image_visibility.js';
|
import FolderImageVisibility from './community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from './community/image_image_visibility.js';
|
import ImageImageVisibility from './community/image_image_visibility.js';
|
||||||
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
import FolderVisibilityUser from './community/folder_visibility_user.js';
|
||||||
|
import EroticVideoImageVisibility from './community/erotic_video_image_visibility.js';
|
||||||
|
import EroticVideoVisibilityUser from './community/erotic_video_visibility_user.js';
|
||||||
import GuestbookEntry from './community/guestbook.js';
|
import GuestbookEntry from './community/guestbook.js';
|
||||||
import DiaryHistory from './community/diary_history.js';
|
import DiaryHistory from './community/diary_history.js';
|
||||||
import Diary from './community/diary.js';
|
import Diary from './community/diary.js';
|
||||||
@@ -179,6 +181,8 @@ const models = {
|
|||||||
FolderImageVisibility,
|
FolderImageVisibility,
|
||||||
ImageImageVisibility,
|
ImageImageVisibility,
|
||||||
FolderVisibilityUser,
|
FolderVisibilityUser,
|
||||||
|
EroticVideoImageVisibility,
|
||||||
|
EroticVideoVisibilityUser,
|
||||||
GuestbookEntry,
|
GuestbookEntry,
|
||||||
DiaryHistory,
|
DiaryHistory,
|
||||||
Diary,
|
Diary,
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ router.get('/folder/:folderId', socialNetworkController.getFolderImageList);
|
|||||||
router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
|
router.post('/images', upload.single('image'), socialNetworkController.uploadImage);
|
||||||
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
|
router.post('/erotic/folders/:folderId', socialNetworkController.createAdultFolder);
|
||||||
router.get('/erotic/folders', socialNetworkController.getAdultFolders);
|
router.get('/erotic/folders', socialNetworkController.getAdultFolders);
|
||||||
|
router.get('/profile/erotic/folders/:username', socialNetworkController.getAdultFoldersByUsername);
|
||||||
|
router.get('/profile/erotic/videos/:username', socialNetworkController.getEroticVideosByUsername);
|
||||||
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
|
router.get('/erotic/folder/:folderId', socialNetworkController.getAdultFolderImageList);
|
||||||
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
|
router.post('/erotic/images', upload.single('image'), socialNetworkController.uploadAdultImage);
|
||||||
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
|
router.put('/erotic/images/:imageId', socialNetworkController.changeAdultImage);
|
||||||
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
|
router.get('/erotic/image/:hash', socialNetworkController.getAdultImageByHash);
|
||||||
router.get('/erotic/videos', socialNetworkController.listEroticVideos);
|
router.get('/erotic/videos', socialNetworkController.listEroticVideos);
|
||||||
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
|
router.post('/erotic/videos', upload.single('video'), socialNetworkController.uploadEroticVideo);
|
||||||
|
router.put('/erotic/videos/:videoId', socialNetworkController.changeEroticVideo);
|
||||||
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
|
router.get('/erotic/video/:hash', socialNetworkController.getEroticVideoByHash);
|
||||||
router.post('/erotic/report', socialNetworkController.reportEroticContent);
|
router.post('/erotic/report', socialNetworkController.reportEroticContent);
|
||||||
router.get('/images/:imageId', socialNetworkController.getImage);
|
router.get('/images/:imageId', socialNetworkController.getImage);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import EroticContentReport from '../models/community/erotic_content_report.js';
|
|||||||
import ImageVisibilityType from '../models/type/image_visibility.js';
|
import ImageVisibilityType from '../models/type/image_visibility.js';
|
||||||
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
|
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
|
||||||
import ImageImageVisibility from '../models/community/image_image_visibility.js';
|
import ImageImageVisibility from '../models/community/image_image_visibility.js';
|
||||||
|
import EroticVideoImageVisibility from '../models/community/erotic_video_image_visibility.js';
|
||||||
|
import EroticVideoVisibilityUser from '../models/community/erotic_video_visibility_user.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
@@ -74,6 +76,372 @@ class SocialNetworkService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseSelectedUsers(selectedUsers) {
|
||||||
|
if (!selectedUsers) return [];
|
||||||
|
if (Array.isArray(selectedUsers)) {
|
||||||
|
return selectedUsers.map(value => String(value || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (typeof selectedUsers === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(selectedUsers);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map(value => String(value || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to comma-separated values below.
|
||||||
|
}
|
||||||
|
return selectedUsers
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveFriendIds(userId) {
|
||||||
|
const friendships = await Friendship.findAll({
|
||||||
|
where: {
|
||||||
|
accepted: true,
|
||||||
|
denied: false,
|
||||||
|
withdrawn: false,
|
||||||
|
[Op.or]: [
|
||||||
|
{ user1Id: userId },
|
||||||
|
{ user2Id: userId }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return friendships.map(friendship => (
|
||||||
|
friendship.user1Id === userId ? friendship.user2Id : friendship.user1Id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async areUsersFriends(userId, otherUserId) {
|
||||||
|
if (!userId || !otherUserId) return false;
|
||||||
|
const friendship = await Friendship.findOne({
|
||||||
|
where: {
|
||||||
|
accepted: true,
|
||||||
|
denied: false,
|
||||||
|
withdrawn: false,
|
||||||
|
[Op.or]: [
|
||||||
|
{ user1Id: userId, user2Id: otherUserId },
|
||||||
|
{ user1Id: otherUserId, user2Id: userId }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Boolean(friendship);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly = false } = {}) {
|
||||||
|
const usernames = [...new Set(this.parseSelectedUsers(selectedUsers))];
|
||||||
|
if (!usernames.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.or]: usernames.map(username => ({
|
||||||
|
username: {
|
||||||
|
[Op.iLike]: username
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchedUsers = [];
|
||||||
|
for (const requestedName of usernames) {
|
||||||
|
const user = users.find(candidate => (
|
||||||
|
String(candidate.username || '').toLowerCase() === requestedName.toLowerCase()
|
||||||
|
));
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`User "${requestedName}" not found`);
|
||||||
|
}
|
||||||
|
if (user.id === ownerId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (adultOnly) {
|
||||||
|
const access = await this.getAdultAccessState(user.id);
|
||||||
|
if (!access.adultAccessEnabled) {
|
||||||
|
throw new Error(`User "${user.username}" is not approved for the adult area`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matchedUsers.push(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(matchedUsers)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFolderSelectedUsers(folderId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
||||||
|
await FolderVisibilityUser.destroy({ where: { folderId } });
|
||||||
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
||||||
|
for (const userId of selectedUserIds) {
|
||||||
|
await FolderVisibilityUser.create({ folderId, visibilityUserId: userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveImageSelectedUsers(imageId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
||||||
|
await ImageVisibilityUser.destroy({ where: { imageId } });
|
||||||
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
||||||
|
for (const userId of selectedUserIds) {
|
||||||
|
await ImageVisibilityUser.create({ imageId, userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFolderSelectedUsernames(folderId) {
|
||||||
|
const selectedUserLinks = await FolderVisibilityUser.findAll({ where: { folderId } });
|
||||||
|
if (!selectedUserLinks.length) return [];
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: { id: selectedUserLinks.map(link => link.visibilityUserId) },
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
});
|
||||||
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getImageSelectedUsernames(imageId) {
|
||||||
|
const selectedUsers = await ImageVisibilityUser.findAll({ where: { imageId } });
|
||||||
|
if (!selectedUsers.length) return [];
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: { id: selectedUsers.map(entry => entry.userId) },
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
});
|
||||||
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEroticVideoSelectedUsers(videoId, selectedUsers, ownerId, { adultOnly = false } = {}) {
|
||||||
|
await EroticVideoVisibilityUser.destroy({ where: { eroticVideoId: videoId } });
|
||||||
|
const selectedUserIds = await this.resolveSelectedUserIds(ownerId, selectedUsers, { adultOnly });
|
||||||
|
for (const userId of selectedUserIds) {
|
||||||
|
await EroticVideoVisibilityUser.create({ eroticVideoId: videoId, userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEroticVideoSelectedUsernames(videoId) {
|
||||||
|
const selectedUsers = await EroticVideoVisibilityUser.findAll({ where: { eroticVideoId: videoId } });
|
||||||
|
if (!selectedUsers.length) return [];
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: { id: selectedUsers.map(entry => entry.userId) },
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
});
|
||||||
|
return users.map(user => user.username).sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEroticVideoVisibilities(videoId, visibilities) {
|
||||||
|
let normalizedVisibilities = visibilities;
|
||||||
|
if (typeof normalizedVisibilities === 'string') {
|
||||||
|
normalizedVisibilities = JSON.parse(normalizedVisibilities);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(normalizedVisibilities) || !normalizedVisibilities.length) {
|
||||||
|
throw new Error('Invalid visibilities provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
await EroticVideoImageVisibility.destroy({ where: { eroticVideoId: videoId } });
|
||||||
|
for (const visibility of normalizedVisibilities) {
|
||||||
|
const visibilityTypeId = typeof visibility === 'object' ? visibility.id : visibility;
|
||||||
|
await EroticVideoImageVisibility.create({ eroticVideoId: videoId, visibilityTypeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEroticVideoVisibilityEntries(videoId) {
|
||||||
|
return await ImageVisibilityType.findAll({
|
||||||
|
include: [{
|
||||||
|
model: EroticVideo,
|
||||||
|
where: { id: videoId },
|
||||||
|
attributes: [],
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async enrichEroticVideoVisibilityMetadata(videos) {
|
||||||
|
const enrichedVideos = [];
|
||||||
|
for (const videoRecord of videos) {
|
||||||
|
const video = videoRecord.get ? videoRecord.get() : { ...videoRecord };
|
||||||
|
const visibilities = await this.getEroticVideoVisibilityEntries(video.id);
|
||||||
|
video.visibilities = visibilities.map(entry => ({ id: entry.id, description: entry.description }));
|
||||||
|
video.selectedUsers = await this.getEroticVideoSelectedUsernames(video.id);
|
||||||
|
enrichedVideos.push(video);
|
||||||
|
}
|
||||||
|
return enrichedVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
async enrichImageVisibilityMetadata(images) {
|
||||||
|
const enrichedImages = [];
|
||||||
|
for (const imageRecord of images) {
|
||||||
|
const image = imageRecord.get ? imageRecord.get() : { ...imageRecord };
|
||||||
|
const visibilities = await ImageVisibilityType.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Image,
|
||||||
|
where: { id: image.id },
|
||||||
|
attributes: [],
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
image.visibilities = visibilities.map(entry => ({ id: entry.id, description: entry.description }));
|
||||||
|
image.selectedUsers = await this.getImageSelectedUsernames(image.id);
|
||||||
|
enrichedImages.push(image);
|
||||||
|
}
|
||||||
|
return enrichedImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
async canRequesterAccessAdultFolder(folder, requesterId) {
|
||||||
|
if (!folder || !requesterId) return false;
|
||||||
|
if (folder.userId === requesterId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
||||||
|
if (!adultAccess.adultAccessEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderVisibilities = await ImageVisibilityType.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Folder,
|
||||||
|
where: { id: folder.id },
|
||||||
|
attributes: [],
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const descriptions = folderVisibilities.map(entry => entry.description);
|
||||||
|
if (!descriptions.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
||||||
|
await this.areUsersFriends(folder.userId, requesterId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('selected-users')) {
|
||||||
|
const selectedLink = await FolderVisibilityUser.findOne({
|
||||||
|
where: {
|
||||||
|
folderId: folder.id,
|
||||||
|
visibilityUserId: requesterId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (selectedLink) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async canRequesterAccessAdultImage(image, requesterId) {
|
||||||
|
if (!image || !requesterId) return false;
|
||||||
|
if (image.userId === requesterId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (image.isModeratedHidden) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await Folder.findOne({
|
||||||
|
where: {
|
||||||
|
id: image.folderId,
|
||||||
|
userId: image.userId,
|
||||||
|
isAdultArea: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!folder) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderAccess = await this.canRequesterAccessAdultFolder(folder, requesterId);
|
||||||
|
if (!folderAccess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
||||||
|
if (!adultAccess.adultAccessEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageVisibilities = await ImageVisibilityType.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Image,
|
||||||
|
where: { id: image.id },
|
||||||
|
attributes: [],
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const descriptions = imageVisibilities.map(entry => entry.description);
|
||||||
|
if (!descriptions.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
||||||
|
await this.areUsersFriends(image.userId, requesterId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('selected-users')) {
|
||||||
|
const selectedLink = await ImageVisibilityUser.findOne({
|
||||||
|
where: {
|
||||||
|
imageId: image.id,
|
||||||
|
userId: requesterId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (selectedLink) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async canRequesterAccessEroticVideo(video, requesterId) {
|
||||||
|
if (!video || !requesterId) return false;
|
||||||
|
if (video.userId === requesterId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (video.isModeratedHidden) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adultAccess = await this.getAdultAccessState(requesterId);
|
||||||
|
if (!adultAccess.adultAccessEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoVisibilities = await this.getEroticVideoVisibilityEntries(video.id);
|
||||||
|
const descriptions = videoVisibilities.map(entry => entry.description);
|
||||||
|
if (!descriptions.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('adults') || descriptions.includes('everyone')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((descriptions.includes('friends') || descriptions.includes('friends-and-adults')) &&
|
||||||
|
await this.areUsersFriends(video.userId, requesterId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.includes('selected-users')) {
|
||||||
|
const selectedLink = await EroticVideoVisibilityUser.findOne({
|
||||||
|
where: {
|
||||||
|
eroticVideoId: video.id,
|
||||||
|
userId: requesterId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (selectedLink) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async resolveEroticTarget(targetType, targetId) {
|
async resolveEroticTarget(targetType, targetId) {
|
||||||
if (targetType === 'image') {
|
if (targetType === 'image') {
|
||||||
const image = await Image.findOne({
|
const image = await Image.findOne({
|
||||||
@@ -240,6 +608,9 @@ class SocialNetworkService extends BaseService {
|
|||||||
visibilityTypeId: visibilityId
|
visibilityTypeId: visibilityId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await this.saveFolderSelectedUsers(newFolder.id, data.selectedUsers || data.selectedUsernames || [], user.id, {
|
||||||
|
adultOnly: isAdultArea
|
||||||
|
});
|
||||||
return newFolder;
|
return newFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +641,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
const children = await this.getSubFolders(folder.id, userId, isAdultArea);
|
const children = await this.getSubFolders(folder.id, userId, isAdultArea);
|
||||||
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
|
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
|
||||||
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
|
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
|
||||||
|
folder.setDataValue('selectedUsers', await this.getFolderSelectedUsernames(folder.id));
|
||||||
folder.setDataValue('children', children);
|
folder.setDataValue('children', children);
|
||||||
folder.setDataValue('image_visibility_types', undefined);
|
folder.setDataValue('image_visibility_types', undefined);
|
||||||
}
|
}
|
||||||
@@ -286,7 +658,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!folder) throw new Error('Folder not found');
|
if (!folder) throw new Error('Folder not found');
|
||||||
return await Image.findAll({
|
const images = await Image.findAll({
|
||||||
where: {
|
where: {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
isAdultContent: false
|
isAdultContent: false
|
||||||
@@ -295,6 +667,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
['title', 'asc']
|
['title', 'asc']
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
return this.enrichImageVisibilityMetadata(images);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImage(hashedId, file, formData) {
|
async uploadImage(hashedId, file, formData) {
|
||||||
@@ -302,6 +675,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
const processedImageName = await this.processAndUploadUserImage(file, 'user');
|
const processedImageName = await this.processAndUploadUserImage(file, 'user');
|
||||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: false });
|
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: false });
|
||||||
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
||||||
|
await this.saveImageSelectedUsers(newImage.id, formData.selectedUsers || formData.selectedUsernames || [], userId);
|
||||||
return newImage;
|
return newImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +780,11 @@ class SocialNetworkService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadUserByName(userName) {
|
async loadUserByName(userName) {
|
||||||
return await User.findOne({ username: userName});
|
return await User.findOne({
|
||||||
|
where: {
|
||||||
|
username: userName
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateFolderData(data) {
|
validateFolderData(data) {
|
||||||
@@ -601,27 +979,78 @@ class SocialNetworkService extends BaseService {
|
|||||||
const children = await this.getSubFolders(rootFolder.id, userId, true);
|
const children = await this.getSubFolders(rootFolder.id, userId, true);
|
||||||
const data = rootFolder.get();
|
const data = rootFolder.get();
|
||||||
data.visibilityTypeIds = data.image_visibility_types.map(v => v.id);
|
data.visibilityTypeIds = data.image_visibility_types.map(v => v.id);
|
||||||
|
data.selectedUsers = await this.getFolderSelectedUsernames(rootFolder.id);
|
||||||
delete data.image_visibility_types;
|
delete data.image_visibility_types;
|
||||||
data.children = children;
|
data.children = children;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAdultFoldersByUsername(username, hashedUserId) {
|
||||||
|
const requestingUserId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
||||||
|
const owner = await this.loadUserByName(username);
|
||||||
|
if (!owner) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerRoot = await Folder.findOne({
|
||||||
|
where: {
|
||||||
|
userId: owner.id,
|
||||||
|
isAdultArea: true,
|
||||||
|
name: 'Erotik'
|
||||||
|
},
|
||||||
|
include: [{
|
||||||
|
model: ImageVisibilityType,
|
||||||
|
through: { model: FolderImageVisibility },
|
||||||
|
attributes: ['id']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
if (!ownerRoot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.canRequesterAccessAdultFolder(ownerRoot, requestingUserId))) {
|
||||||
|
const error = new Error('Adult folder access denied');
|
||||||
|
error.status = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = await this.getAccessibleAdultFolders(ownerRoot.id, owner.id, requestingUserId);
|
||||||
|
const rootFolder = ownerRoot.get();
|
||||||
|
rootFolder.visibilityTypeIds = ownerRoot.image_visibility_types.map(v => v.id);
|
||||||
|
rootFolder.selectedUsers = await this.getFolderSelectedUsernames(ownerRoot.id);
|
||||||
|
delete rootFolder.image_visibility_types;
|
||||||
|
rootFolder.children = children;
|
||||||
|
return rootFolder;
|
||||||
|
}
|
||||||
|
|
||||||
async getAdultFolderImageList(hashedId, folderId) {
|
async getAdultFolderImageList(hashedId, folderId) {
|
||||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||||
const folder = await Folder.findOne({
|
const folder = await Folder.findOne({
|
||||||
where: { id: folderId, userId, isAdultArea: true }
|
where: { id: folderId, isAdultArea: true }
|
||||||
});
|
});
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw new Error('Folder not found');
|
throw new Error('Folder not found');
|
||||||
}
|
}
|
||||||
return await Image.findAll({
|
if (!(await this.canRequesterAccessAdultFolder(folder, userId))) {
|
||||||
|
const error = new Error('Access denied');
|
||||||
|
error.status = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const images = await Image.findAll({
|
||||||
where: {
|
where: {
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
isAdultContent: true,
|
isAdultContent: true,
|
||||||
userId
|
userId: folder.userId
|
||||||
},
|
},
|
||||||
order: [['title', 'asc']]
|
order: [['title', 'asc']]
|
||||||
});
|
});
|
||||||
|
const visibleImages = [];
|
||||||
|
for (const image of images) {
|
||||||
|
if (await this.canRequesterAccessAdultImage(image, userId)) {
|
||||||
|
visibleImages.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.enrichImageVisibilityMetadata(visibleImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAdultFolder(hashedId, data, folderId) {
|
async createAdultFolder(hashedId, data, folderId) {
|
||||||
@@ -650,6 +1079,9 @@ class SocialNetworkService extends BaseService {
|
|||||||
const processedImageName = await this.processAndUploadUserImage(file, 'erotic');
|
const processedImageName = await this.processAndUploadUserImage(file, 'erotic');
|
||||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: true });
|
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: true });
|
||||||
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
||||||
|
await this.saveImageSelectedUsers(newImage.id, formData.selectedUsers || formData.selectedUsernames || [], userId, {
|
||||||
|
adultOnly: true
|
||||||
|
});
|
||||||
return newImage;
|
return newImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,13 +1090,17 @@ class SocialNetworkService extends BaseService {
|
|||||||
const image = await Image.findOne({
|
const image = await Image.findOne({
|
||||||
where: {
|
where: {
|
||||||
hash,
|
hash,
|
||||||
userId,
|
|
||||||
isAdultContent: true
|
isAdultContent: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!image) {
|
if (!image) {
|
||||||
throw new Error('Image not found');
|
throw new Error('Image not found');
|
||||||
}
|
}
|
||||||
|
if (!(await this.canRequesterAccessAdultImage(image, userId))) {
|
||||||
|
const error = new Error('Access denied');
|
||||||
|
error.status = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (image.isModeratedHidden) {
|
if (image.isModeratedHidden) {
|
||||||
throw new Error('Image hidden by moderation');
|
throw new Error('Image hidden by moderation');
|
||||||
}
|
}
|
||||||
@@ -677,10 +1113,33 @@ class SocialNetworkService extends BaseService {
|
|||||||
|
|
||||||
async listEroticVideos(hashedId) {
|
async listEroticVideos(hashedId) {
|
||||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||||
return await EroticVideo.findAll({
|
const videos = await EroticVideo.findAll({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
return this.enrichEroticVideoVisibilityMetadata(videos);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEroticVideosByUsername(username, hashedId) {
|
||||||
|
const requestingUserId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||||
|
const owner = await this.loadUserByName(username);
|
||||||
|
if (!owner) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const videos = await EroticVideo.findAll({
|
||||||
|
where: { userId: owner.id },
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleVideos = [];
|
||||||
|
for (const video of videos) {
|
||||||
|
if (await this.canRequesterAccessEroticVideo(video, requestingUserId)) {
|
||||||
|
visibleVideos.push(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.enrichEroticVideoVisibilityMetadata(visibleVideos);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadEroticVideo(hashedId, file, formData) {
|
async uploadEroticVideo(hashedId, file, formData) {
|
||||||
@@ -698,7 +1157,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
const filePath = this.buildFilePath(fileName, 'erotic-video');
|
const filePath = this.buildFilePath(fileName, 'erotic-video');
|
||||||
await this.saveFile(file.buffer, filePath);
|
await this.saveFile(file.buffer, filePath);
|
||||||
|
|
||||||
return await EroticVideo.create({
|
const video = await EroticVideo.create({
|
||||||
title: formData.title || file.originalname,
|
title: formData.title || file.originalname,
|
||||||
description: formData.description || null,
|
description: formData.description || null,
|
||||||
originalFileName: file.originalname,
|
originalFileName: file.originalname,
|
||||||
@@ -706,16 +1165,33 @@ class SocialNetworkService extends BaseService {
|
|||||||
mimeType: file.mimetype,
|
mimeType: file.mimetype,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const visibility = formData.visibility || JSON.stringify(
|
||||||
|
(await this.getPossibleImageVisibilities())
|
||||||
|
.filter(entry => entry.description === 'adults')
|
||||||
|
.map(entry => entry.id)
|
||||||
|
);
|
||||||
|
await this.saveEroticVideoVisibilities(video.id, visibility);
|
||||||
|
await this.saveEroticVideoSelectedUsers(video.id, formData.selectedUsers || formData.selectedUsernames || [], userId, {
|
||||||
|
adultOnly: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return video;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEroticVideoFilePath(hashedId, hash) {
|
async getEroticVideoFilePath(hashedId, hash) {
|
||||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||||
const video = await EroticVideo.findOne({
|
const video = await EroticVideo.findOne({
|
||||||
where: { hash, userId }
|
where: { hash }
|
||||||
});
|
});
|
||||||
if (!video) {
|
if (!video) {
|
||||||
throw new Error('Video not found');
|
throw new Error('Video not found');
|
||||||
}
|
}
|
||||||
|
if (!(await this.canRequesterAccessEroticVideo(video, userId))) {
|
||||||
|
const error = new Error('Access denied');
|
||||||
|
error.status = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (video.isModeratedHidden) {
|
if (video.isModeratedHidden) {
|
||||||
throw new Error('Video hidden by moderation');
|
throw new Error('Video hidden by moderation');
|
||||||
}
|
}
|
||||||
@@ -726,6 +1202,29 @@ class SocialNetworkService extends BaseService {
|
|||||||
return { filePath: videoPath, mimeType: video.mimeType };
|
return { filePath: videoPath, mimeType: video.mimeType };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeEroticVideo(hashedUserId, videoId, payload) {
|
||||||
|
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
||||||
|
const video = await EroticVideo.findOne({
|
||||||
|
where: {
|
||||||
|
id: videoId,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!video) {
|
||||||
|
throw new Error('Video not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await video.update({
|
||||||
|
title: payload.title || video.title,
|
||||||
|
description: payload.description ?? video.description
|
||||||
|
});
|
||||||
|
await this.saveEroticVideoVisibilities(videoId, payload.visibilities);
|
||||||
|
await this.saveEroticVideoSelectedUsers(videoId, payload.selectedUsers || [], userId, {
|
||||||
|
adultOnly: true
|
||||||
|
});
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
||||||
async createEroticContentReport(hashedId, payload) {
|
async createEroticContentReport(hashedId, payload) {
|
||||||
const reporterId = await this.requireAdultAreaAccessByHash(hashedId);
|
const reporterId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||||
const targetType = String(payload.targetType || '').trim().toLowerCase();
|
const targetType = String(payload.targetType || '').trim().toLowerCase();
|
||||||
@@ -807,7 +1306,7 @@ class SocialNetworkService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeImage(hashedUserId, imageId, title, visibilities) {
|
async changeImage(hashedUserId, imageId, title, visibilities, selectedUsers = []) {
|
||||||
const userId = await this.checkUserAccess(hashedUserId);
|
const userId = await this.checkUserAccess(hashedUserId);
|
||||||
await this.checkUserImageAccess(userId, imageId);
|
await this.checkUserImageAccess(userId, imageId);
|
||||||
const image = await Image.findOne({ where: { id: imageId, isAdultContent: false } });
|
const image = await Image.findOne({ where: { id: imageId, isAdultContent: false } });
|
||||||
@@ -819,10 +1318,11 @@ class SocialNetworkService extends BaseService {
|
|||||||
for (const visibility of visibilities) {
|
for (const visibility of visibilities) {
|
||||||
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
||||||
}
|
}
|
||||||
|
await this.saveImageSelectedUsers(imageId, selectedUsers, userId);
|
||||||
return image.folderId;
|
return image.folderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeAdultImage(hashedUserId, imageId, title, visibilities) {
|
async changeAdultImage(hashedUserId, imageId, title, visibilities, selectedUsers = []) {
|
||||||
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
||||||
const image = await Image.findOne({
|
const image = await Image.findOne({
|
||||||
where: {
|
where: {
|
||||||
@@ -839,9 +1339,38 @@ class SocialNetworkService extends BaseService {
|
|||||||
for (const visibility of visibilities) {
|
for (const visibility of visibilities) {
|
||||||
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
||||||
}
|
}
|
||||||
|
await this.saveImageSelectedUsers(imageId, selectedUsers, userId, {
|
||||||
|
adultOnly: true
|
||||||
|
});
|
||||||
return image.folderId;
|
return image.folderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAccessibleAdultFolders(parentId, ownerUserId, requestingUserId) {
|
||||||
|
const folders = await Folder.findAll({
|
||||||
|
where: { parentId, userId: ownerUserId, isAdultArea: true },
|
||||||
|
include: [{
|
||||||
|
model: ImageVisibilityType,
|
||||||
|
through: { model: FolderImageVisibility },
|
||||||
|
attributes: ['id']
|
||||||
|
}],
|
||||||
|
order: [['name', 'asc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const folderRecord of folders) {
|
||||||
|
if (!(await this.canRequesterAccessAdultFolder(folderRecord, requestingUserId))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const folder = folderRecord.get();
|
||||||
|
folder.visibilityTypeIds = folderRecord.image_visibility_types.map(v => v.id);
|
||||||
|
folder.selectedUsers = await this.getFolderSelectedUsernames(folder.id);
|
||||||
|
delete folder.image_visibility_types;
|
||||||
|
folder.children = await this.getAccessibleAdultFolders(folder.id, ownerUserId, requestingUserId);
|
||||||
|
result.push(folder);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async getFoldersByUsername(username, hashedUserId) {
|
async getFoldersByUsername(username, hashedUserId) {
|
||||||
const user = await this.loadUserByName(username);
|
const user = await this.loadUserByName(username);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -30,6 +30,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="requiresSelectedUsers" class="form-group">
|
||||||
|
<label for="selectedUsers">{{ $t("socialnetwork.gallery.visibility.selected-users") }}</label>
|
||||||
|
<input
|
||||||
|
id="selectedUsers"
|
||||||
|
type="text"
|
||||||
|
v-model="selectedUsernamesText"
|
||||||
|
placeholder="anna, bert, clara"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogWidget>
|
</DialogWidget>
|
||||||
</template>
|
</template>
|
||||||
@@ -55,6 +64,7 @@ export default {
|
|||||||
visibilityOptions: [],
|
visibilityOptions: [],
|
||||||
allVisibilityOptions: [],
|
allVisibilityOptions: [],
|
||||||
selectedVisibility: [],
|
selectedVisibility: [],
|
||||||
|
selectedUsernamesText: '',
|
||||||
parentFolder: {id: null, name: ''},
|
parentFolder: {id: null, name: ''},
|
||||||
folderId: 0,
|
folderId: 0,
|
||||||
eroticMode: false
|
eroticMode: false
|
||||||
@@ -65,6 +75,9 @@ export default {
|
|||||||
buttons() {
|
buttons() {
|
||||||
return [{ text: this.$t("socialnetwork.gallery.create_folder"), action: this.createFolder }];
|
return [{ text: this.$t("socialnetwork.gallery.create_folder"), action: this.createFolder }];
|
||||||
},
|
},
|
||||||
|
requiresSelectedUsers() {
|
||||||
|
return this.selectedVisibility.some(option => option?.description === 'selected-users');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadVisibilityOptions();
|
await this.loadVisibilityOptions();
|
||||||
@@ -79,9 +92,11 @@ export default {
|
|||||||
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
||||||
folder.visibilityTypeIds.includes(option.id)
|
folder.visibilityTypeIds.includes(option.id)
|
||||||
);
|
);
|
||||||
|
this.selectedUsernamesText = (folder.selectedUsers || []).join(', ');
|
||||||
} else {
|
} else {
|
||||||
this.folderTitle = '';
|
this.folderTitle = '';
|
||||||
this.selectedVisibility = [];
|
this.selectedVisibility = [];
|
||||||
|
this.selectedUsernamesText = '';
|
||||||
}
|
}
|
||||||
this.$refs.dialog.open();
|
this.$refs.dialog.open();
|
||||||
},
|
},
|
||||||
@@ -109,6 +124,10 @@ export default {
|
|||||||
name: this.folderTitle,
|
name: this.folderTitle,
|
||||||
parentId: this.parentFolder.id,
|
parentId: this.parentFolder.id,
|
||||||
visibilities: this.selectedVisibility.map(item => item.id),
|
visibilities: this.selectedVisibility.map(item => item.id),
|
||||||
|
selectedUsers: this.selectedUsernamesText
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const basePath = this.eroticMode ? '/api/socialnetwork/erotic/folders' : '/api/socialnetwork/folders';
|
const basePath = this.eroticMode ? '/api/socialnetwork/erotic/folders' : '/api/socialnetwork/folders';
|
||||||
|
|||||||
@@ -28,6 +28,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="requiresSelectedUsers" class="form-group">
|
||||||
|
<label for="selectedUsers">{{ $t('socialnetwork.gallery.visibility.selected-users') }}</label>
|
||||||
|
<input
|
||||||
|
id="selectedUsers"
|
||||||
|
type="text"
|
||||||
|
v-model="selectedUsernamesText"
|
||||||
|
placeholder="anna, bert, clara"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogWidget>
|
</DialogWidget>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,6 +57,7 @@ export default {
|
|||||||
imageTitle: '',
|
imageTitle: '',
|
||||||
selectedVisibilities: [],
|
selectedVisibilities: [],
|
||||||
visibilityOptions: [],
|
visibilityOptions: [],
|
||||||
|
selectedUsernamesText: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -57,12 +67,16 @@ export default {
|
|||||||
{ text: this.$t('socialnetwork.gallery.imagedialog.close'), action: this.closeDialog }
|
{ text: this.$t('socialnetwork.gallery.imagedialog.close'), action: this.closeDialog }
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
requiresSelectedUsers() {
|
||||||
|
return this.selectedVisibilities.some(option => option?.description === 'selected-users');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
open(image) {
|
open(image) {
|
||||||
this.image = image;
|
this.image = image;
|
||||||
this.imageTitle = image.title;
|
this.imageTitle = image.title;
|
||||||
this.selectedVisibilities = image.visibilities || [];
|
this.selectedVisibilities = image.visibilities || [];
|
||||||
|
this.selectedUsernamesText = (image.selectedUsers || []).join(', ');
|
||||||
this.$refs.dialog.open();
|
this.$refs.dialog.open();
|
||||||
},
|
},
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
@@ -73,6 +87,10 @@ export default {
|
|||||||
...this.image,
|
...this.image,
|
||||||
title: this.imageTitle,
|
title: this.imageTitle,
|
||||||
visibilities: this.selectedVisibilities,
|
visibilities: this.selectedVisibilities,
|
||||||
|
selectedUsers: this.selectedUsernamesText
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -23,6 +23,14 @@
|
|||||||
<td>{{ generateValue(key, value) }}</td>
|
<td>{{ generateValue(key, value) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<div v-if="canOpenEroticPictures" class="adult-actions">
|
||||||
|
<button type="button" @click="openEroticPictures">
|
||||||
|
{{ $t('socialnetwork.erotic.picturesTitle') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="openEroticVideos">
|
||||||
|
{{ $t('socialnetwork.erotic.videosTitle') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content images-tab" v-if="activeTab === 'images'">
|
<div class="tab-content images-tab" v-if="activeTab === 'images'">
|
||||||
<div v-if="folders.length === 0">{{ $t('socialnetwork.profile.noFolders') }}</div>
|
<div v-if="folders.length === 0">{{ $t('socialnetwork.profile.noFolders') }}</div>
|
||||||
@@ -98,6 +106,7 @@ import { Editor, EditorContent } from '@tiptap/vue-3'
|
|||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { showError } from '@/utils/feedback.js';
|
import { showError } from '@/utils/feedback.js';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserProfileDialog',
|
name: 'UserProfileDialog',
|
||||||
@@ -106,6 +115,16 @@ export default {
|
|||||||
FolderItem,
|
FolderItem,
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['user']),
|
||||||
|
canOpenEroticPictures() {
|
||||||
|
return Boolean(
|
||||||
|
this.userProfile?.username &&
|
||||||
|
this.user?.username &&
|
||||||
|
this.userProfile.username !== this.user.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isTitleTranslated: true,
|
isTitleTranslated: true,
|
||||||
@@ -251,6 +270,24 @@ export default {
|
|||||||
openImageDialog(image) {
|
openImageDialog(image) {
|
||||||
this.$root.$refs.showImageDialog.open(image);
|
this.$root.$refs.showImageDialog.open(image);
|
||||||
},
|
},
|
||||||
|
openEroticPictures() {
|
||||||
|
this.closeDialog();
|
||||||
|
this.$router.push({
|
||||||
|
name: 'EroticPictures',
|
||||||
|
query: {
|
||||||
|
username: this.userProfile.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openEroticVideos() {
|
||||||
|
this.closeDialog();
|
||||||
|
this.$router.push({
|
||||||
|
name: 'EroticVideos',
|
||||||
|
query: {
|
||||||
|
username: this.userProfile.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
toggleInputSection() {
|
toggleInputSection() {
|
||||||
this.showInputSection = !this.showInputSection;
|
this.showInputSection = !this.showInputSection;
|
||||||
},
|
},
|
||||||
@@ -468,6 +505,10 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.adult-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,16 @@
|
|||||||
:isLastItem="true"
|
:isLastItem="true"
|
||||||
:depth="0"
|
:depth="0"
|
||||||
:parentsWithChildren="[false]"
|
:parentsWithChildren="[false]"
|
||||||
|
:noActionItems="isForeignView"
|
||||||
@edit-folder="openEditFolderDialog"
|
@edit-folder="openEditFolderDialog"
|
||||||
@delete-folder="deleteFolder"
|
@delete-folder="deleteFolder"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
<button @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
|
<button v-if="!isForeignView" @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="upload-section surface-card">
|
<div v-if="!isForeignView" class="upload-section surface-card">
|
||||||
<div class="upload-header" @click="toggleUploadSection">
|
<div class="upload-header" @click="toggleUploadSection">
|
||||||
<span><i class="icon-upload-toggle">{{ isUploadVisible ? '▲' : '▼' }}</i></span>
|
<span><i class="icon-upload-toggle">{{ isUploadVisible ? '▲' : '▼' }}</i></span>
|
||||||
<h3>{{ $t('socialnetwork.erotic.uploadTitle') }}</h3>
|
<h3>{{ $t('socialnetwork.erotic.uploadTitle') }}</h3>
|
||||||
@@ -72,6 +73,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</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">
|
<button type="submit" class="upload-button">
|
||||||
{{ $t('socialnetwork.gallery.upload.upload_button') }}
|
{{ $t('socialnetwork.gallery.upload.upload_button') }}
|
||||||
@@ -127,6 +137,7 @@ import FolderItem from '../../components/FolderItem.vue';
|
|||||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||||
import { EventBus } from '@/utils/eventBus.js';
|
import { EventBus } from '@/utils/eventBus.js';
|
||||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -143,22 +154,31 @@ export default {
|
|||||||
isUploadVisible: true,
|
isUploadVisible: true,
|
||||||
visibilityOptions: [],
|
visibilityOptions: [],
|
||||||
selectedVisibilities: [],
|
selectedVisibilities: [],
|
||||||
|
selectedUsernamesText: '',
|
||||||
imagePreview: null,
|
imagePreview: null,
|
||||||
reportTarget: { type: null, id: null },
|
reportTarget: { type: null, id: null },
|
||||||
reportReason: 'other',
|
reportReason: 'other',
|
||||||
reportNote: '',
|
reportNote: '',
|
||||||
|
viewUsername: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['user']),
|
||||||
reportReasonOptions() {
|
reportReasonOptions() {
|
||||||
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
|
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
|
||||||
value,
|
value,
|
||||||
label: this.$t(`socialnetwork.erotic.reportReasons.${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() {
|
async mounted() {
|
||||||
await this.loadFolders();
|
await this.initializeView();
|
||||||
await this.loadImageVisibilities();
|
await this.loadImageVisibilities();
|
||||||
if (this.folders) {
|
if (this.folders) {
|
||||||
this.selectFolder(this.folders);
|
this.selectFolder(this.folders);
|
||||||
@@ -169,8 +189,14 @@ export default {
|
|||||||
EventBus.off('folderCreated', this.loadFolders);
|
EventBus.off('folderCreated', this.loadFolders);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async initializeView() {
|
||||||
|
this.viewUsername = String(this.$route.query.username || '').trim();
|
||||||
|
await this.loadFolders();
|
||||||
|
},
|
||||||
async loadFolders() {
|
async loadFolders() {
|
||||||
const response = await apiClient.get('/api/socialnetwork/erotic/folders');
|
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;
|
this.folders = response.data;
|
||||||
},
|
},
|
||||||
async loadImageVisibilities() {
|
async loadImageVisibilities() {
|
||||||
@@ -223,6 +249,9 @@ export default {
|
|||||||
formData.append('folderId', this.selectedFolder.id);
|
formData.append('folderId', this.selectedFolder.id);
|
||||||
formData.append('title', this.imageTitle);
|
formData.append('title', this.imageTitle);
|
||||||
formData.append('visibility', JSON.stringify(this.selectedVisibilities.map((v) => v.id)));
|
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, {
|
await apiClient.post('/api/socialnetwork/erotic/images', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -234,6 +263,7 @@ export default {
|
|||||||
this.fileToUpload = null;
|
this.fileToUpload = null;
|
||||||
this.imagePreview = null;
|
this.imagePreview = null;
|
||||||
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
|
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
|
||||||
|
this.selectedUsernamesText = '';
|
||||||
},
|
},
|
||||||
async fetchImage(image) {
|
async fetchImage(image) {
|
||||||
if (image.isModeratedHidden) {
|
if (image.isModeratedHidden) {
|
||||||
@@ -250,6 +280,10 @@ export default {
|
|||||||
this.isUploadVisible = !this.isUploadVisible;
|
this.isUploadVisible = !this.isUploadVisible;
|
||||||
},
|
},
|
||||||
openImageDialog(image) {
|
openImageDialog(image) {
|
||||||
|
if (this.isForeignView) {
|
||||||
|
this.$root.$refs.showImageDialog.open(image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$root.$refs.editImageDialog.open(image);
|
this.$root.$refs.editImageDialog.open(image);
|
||||||
},
|
},
|
||||||
startReport(type, id) {
|
startReport(type, id) {
|
||||||
@@ -280,6 +314,7 @@ export default {
|
|||||||
const response = await apiClient.put(`/api/socialnetwork/erotic/images/${updatedImage.id}`, {
|
const response = await apiClient.put(`/api/socialnetwork/erotic/images/${updatedImage.id}`, {
|
||||||
title: updatedImage.title,
|
title: updatedImage.title,
|
||||||
visibilities: updatedImage.visibilities,
|
visibilities: updatedImage.visibilities,
|
||||||
|
selectedUsers: updatedImage.selectedUsers || [],
|
||||||
});
|
});
|
||||||
this.images = response.data.map((image) => ({
|
this.images = response.data.map((image) => ({
|
||||||
...image,
|
...image,
|
||||||
@@ -301,6 +336,16 @@ export default {
|
|||||||
// Separate delete flow for adult folders is intentionally not enabled yet.
|
// 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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
<section class="erotic-videos-hero surface-card">
|
<section class="erotic-videos-hero surface-card">
|
||||||
<div>
|
<div>
|
||||||
<span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
<span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
||||||
<h2>{{ $t('socialnetwork.erotic.videosTitle') }}</h2>
|
<h2>{{ isForeignView ? `${$t('socialnetwork.erotic.videosTitle')} · ${viewUsername}` : $t('socialnetwork.erotic.videosTitle') }}</h2>
|
||||||
<p>{{ $t('socialnetwork.erotic.videosIntro') }}</p>
|
<p>{{ isForeignView ? 'Freigegebene Videos aus dem Erwachsenenbereich.' : $t('socialnetwork.erotic.videosIntro') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="erotic-videos-workspace">
|
<div class="erotic-videos-workspace">
|
||||||
<aside class="erotic-videos-sidebar">
|
<aside class="erotic-videos-sidebar">
|
||||||
<section class="erotic-videos-upload surface-card">
|
<section v-if="!isForeignView" class="erotic-videos-upload surface-card">
|
||||||
<div class="erotic-videos-upload__header">
|
<div class="erotic-videos-upload__header">
|
||||||
<h3>{{ $t('socialnetwork.erotic.videoUploadTitle') }}</h3>
|
<h3>{{ $t('socialnetwork.erotic.videoUploadTitle') }}</h3>
|
||||||
<p>{{ $t('socialnetwork.erotic.videoUploadHint') }}</p>
|
<p>{{ $t('socialnetwork.erotic.videoUploadHint') }}</p>
|
||||||
@@ -27,6 +27,34 @@
|
|||||||
<span>{{ $t('socialnetwork.erotic.videoDescription') }}</span>
|
<span>{{ $t('socialnetwork.erotic.videoDescription') }}</span>
|
||||||
<textarea v-model="description" rows="4" />
|
<textarea v-model="description" rows="4" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('socialnetwork.gallery.upload.visibility') }}</span>
|
||||||
|
<multiselect
|
||||||
|
v-model="selectedVisibilities"
|
||||||
|
:options="visibilityOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
label="description"
|
||||||
|
:placeholder="$t('socialnetwork.gallery.upload.selectvisibility')"
|
||||||
|
:track-by="'id'"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
<label v-if="requiresSelectedUsers">
|
||||||
|
<span>{{ $t('socialnetwork.gallery.visibility.selected-users') }}</span>
|
||||||
|
<input v-model="selectedUsernamesText" type="text" placeholder="anna, bert, clara" />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>{{ $t('socialnetwork.erotic.videoFile') }}</span>
|
<span>{{ $t('socialnetwork.erotic.videoFile') }}</span>
|
||||||
<input type="file" accept="video/mp4,video/webm,video/ogg,video/quicktime" required @change="onFileChange" />
|
<input type="file" accept="video/mp4,video/webm,video/ogg,video/quicktime" required @change="onFileChange" />
|
||||||
@@ -43,7 +71,7 @@
|
|||||||
<h3>Bibliothek</h3>
|
<h3>Bibliothek</h3>
|
||||||
<div class="erotic-videos-panel__list">
|
<div class="erotic-videos-panel__list">
|
||||||
<div class="erotic-videos-panel__item">
|
<div class="erotic-videos-panel__item">
|
||||||
<span>{{ $t('socialnetwork.erotic.myVideos') }}</span>
|
<span>{{ isForeignView ? 'Freigegebene Videos' : $t('socialnetwork.erotic.myVideos') }}</span>
|
||||||
<strong>{{ videos.length }}</strong>
|
<strong>{{ videos.length }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="erotic-videos-panel__item">
|
<div class="erotic-videos-panel__item">
|
||||||
@@ -64,10 +92,10 @@
|
|||||||
<section class="erotic-videos-panel surface-card">
|
<section class="erotic-videos-panel surface-card">
|
||||||
<h3>Hinweise</h3>
|
<h3>Hinweise</h3>
|
||||||
<ul class="erotic-videos-checklist">
|
<ul class="erotic-videos-checklist">
|
||||||
<li>{{ $t('socialnetwork.erotic.videoUploadHint') }}</li>
|
<li>{{ isForeignView ? 'Du siehst hier nur Videos, die dir für den Erwachsenenbereich freigegeben wurden.' : $t('socialnetwork.erotic.videoUploadHint') }}</li>
|
||||||
<li>Nach dem Upload erscheint dein Video direkt in deiner Bibliothek und kann dort weiter gepflegt werden.</li>
|
<li>Freunde sehen Inhalte nur dann, wenn sie volljährig und für den Erwachsenenbereich freigeschaltet sind.</li>
|
||||||
|
<li>Gezielt freigegebene Personen müssen ebenfalls volljährig und freigeschaltet sein.</li>
|
||||||
<li>Nutze {{ $t('socialnetwork.erotic.reportAction') }} direkt am jeweiligen Eintrag, wenn Inhalte geprüft werden sollen.</li>
|
<li>Nutze {{ $t('socialnetwork.erotic.reportAction') }} direkt am jeweiligen Eintrag, wenn Inhalte geprüft werden sollen.</li>
|
||||||
<li>Präzise Titel und Beschreibungen erleichtern Einordnung, Verwaltung und Moderation.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -75,15 +103,15 @@
|
|||||||
<section class="erotic-videos-library surface-card">
|
<section class="erotic-videos-library surface-card">
|
||||||
<div class="erotic-videos-library__header">
|
<div class="erotic-videos-library__header">
|
||||||
<div>
|
<div>
|
||||||
<h3>{{ $t('socialnetwork.erotic.myVideos') }}</h3>
|
<h3>{{ isForeignView ? 'Freigegebene Videos' : $t('socialnetwork.erotic.myVideos') }}</h3>
|
||||||
<p>Eigene Uploads, Status und Meldungen an einem Ort.</p>
|
<p>{{ isForeignView ? 'Sichtbare Videos aus freigegebenen Erwachsenenbereichen.' : 'Eigene Uploads, Freigaben und Meldungen an einem Ort.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="erotic-videos-library__count">{{ videos.length }}</span>
|
<span class="erotic-videos-library__count">{{ videos.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="videos.length === 0" class="erotic-videos-empty">
|
<div v-if="videos.length === 0" class="erotic-videos-empty">
|
||||||
<strong>{{ $t('socialnetwork.erotic.noVideos') }}</strong>
|
<strong>{{ $t('socialnetwork.erotic.noVideos') }}</strong>
|
||||||
<span>Lege links dein erstes Video an und verwalte es danach hier in der Bibliothek.</span>
|
<span>{{ isForeignView ? 'Für dich sind aktuell keine freigegebenen Videos vorhanden.' : 'Lege links dein erstes Video an und verwalte es danach hier in der Bibliothek.' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="erotic-videos-library__scroll">
|
<div v-else class="erotic-videos-library__scroll">
|
||||||
@@ -97,10 +125,61 @@
|
|||||||
<strong>{{ video.title || 'Ohne Titel' }}</strong>
|
<strong>{{ video.title || 'Ohne Titel' }}</strong>
|
||||||
<span v-if="video.createdAtLabel">{{ video.createdAtLabel }}</span>
|
<span v-if="video.createdAtLabel">{{ video.createdAtLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="video.visibilities?.length" class="erotic-videos-card__visibility">
|
||||||
|
<span v-for="visibility in video.visibilities" :key="`${video.id}-${visibility.id}`" class="erotic-videos-card__visibility-badge">
|
||||||
|
{{ $t(`socialnetwork.gallery.visibility.${visibility.description}`) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<span v-if="video.isModeratedHidden" class="erotic-videos-card__badge">
|
<span v-if="video.isModeratedHidden" class="erotic-videos-card__badge">
|
||||||
{{ $t('socialnetwork.erotic.moderationHidden') }}
|
{{ $t('socialnetwork.erotic.moderationHidden') }}
|
||||||
</span>
|
</span>
|
||||||
<p v-if="video.description">{{ video.description }}</p>
|
<p v-if="video.description">{{ video.description }}</p>
|
||||||
|
<div v-if="!isForeignView" class="erotic-videos-card__edit">
|
||||||
|
<button type="button" class="secondary" @click="toggleEditor(video.id)">
|
||||||
|
{{ editingVideoId === video.id ? 'Bearbeitung schließen' : 'Freigaben bearbeiten' }}
|
||||||
|
</button>
|
||||||
|
<div v-if="editingVideoId === video.id" class="erotic-videos-editor">
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('socialnetwork.gallery.upload.image_title') }}</span>
|
||||||
|
<input v-model="editForm.title" type="text" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('socialnetwork.erotic.videoDescription') }}</span>
|
||||||
|
<textarea v-model="editForm.description" rows="3" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>{{ $t('socialnetwork.gallery.upload.visibility') }}</span>
|
||||||
|
<multiselect
|
||||||
|
v-model="editForm.visibilities"
|
||||||
|
:options="visibilityOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:close-on-select="false"
|
||||||
|
label="description"
|
||||||
|
:placeholder="$t('socialnetwork.gallery.upload.selectvisibility')"
|
||||||
|
:track-by="'id'"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
<label v-if="editRequiresSelectedUsers">
|
||||||
|
<span>{{ $t('socialnetwork.gallery.visibility.selected-users') }}</span>
|
||||||
|
<input v-model="editForm.selectedUsernamesText" type="text" placeholder="anna, bert, clara" />
|
||||||
|
</label>
|
||||||
|
<div class="erotic-videos-editor__actions">
|
||||||
|
<button type="button" @click="saveVideo(video.id)">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="erotic-videos-card__actions">
|
<div class="erotic-videos-card__actions">
|
||||||
<button type="button" class="secondary" @click="startReport('video', video.id)">
|
<button type="button" class="secondary" @click="startReport('video', video.id)">
|
||||||
{{ $t('socialnetwork.erotic.reportAction') }}
|
{{ $t('socialnetwork.erotic.reportAction') }}
|
||||||
@@ -130,28 +209,55 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import apiClient from '@/utils/axios.js';
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EroticVideosView',
|
name: 'EroticVideosView',
|
||||||
|
components: {
|
||||||
|
Multiselect
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
videos: [],
|
videos: [],
|
||||||
fileToUpload: null,
|
fileToUpload: null,
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
visibilityOptions: [],
|
||||||
|
selectedVisibilities: [],
|
||||||
|
selectedUsernamesText: '',
|
||||||
|
editingVideoId: null,
|
||||||
|
editForm: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
visibilities: [],
|
||||||
|
selectedUsernamesText: ''
|
||||||
|
},
|
||||||
reportTarget: { type: null, id: null },
|
reportTarget: { type: null, id: null },
|
||||||
reportReason: 'other',
|
reportReason: 'other',
|
||||||
reportNote: ''
|
reportNote: '',
|
||||||
|
viewUsername: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['user']),
|
||||||
reportReasonOptions() {
|
reportReasonOptions() {
|
||||||
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
|
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
|
||||||
value,
|
value,
|
||||||
label: this.$t(`socialnetwork.erotic.reportReasons.${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');
|
||||||
|
},
|
||||||
|
editRequiresSelectedUsers() {
|
||||||
|
return this.editForm.visibilities.some(option => option?.description === 'selected-users');
|
||||||
|
},
|
||||||
visibleVideosCount() {
|
visibleVideosCount() {
|
||||||
return this.videos.filter((video) => !video.isModeratedHidden).length;
|
return this.videos.filter((video) => !video.isModeratedHidden).length;
|
||||||
},
|
},
|
||||||
@@ -163,15 +269,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadVideos();
|
await this.initializeView();
|
||||||
|
await this.loadImageVisibilities();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.releaseVideoUrls();
|
this.releaseVideoUrls();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async initializeView() {
|
||||||
|
this.viewUsername = String(this.$route.query.username || '').trim();
|
||||||
|
await this.loadVideos();
|
||||||
|
},
|
||||||
|
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 loadVideos() {
|
async loadVideos() {
|
||||||
this.releaseVideoUrls();
|
this.releaseVideoUrls();
|
||||||
const response = await apiClient.get('/api/socialnetwork/erotic/videos');
|
const response = this.isForeignView
|
||||||
|
? await apiClient.get(`/api/socialnetwork/profile/erotic/videos/${this.viewUsername}`)
|
||||||
|
: await apiClient.get('/api/socialnetwork/erotic/videos');
|
||||||
this.videos = await Promise.all(response.data.map(async (video) => ({
|
this.videos = await Promise.all(response.data.map(async (video) => ({
|
||||||
...video,
|
...video,
|
||||||
url: video.isModeratedHidden ? null : await this.fetchVideoUrl(video.hash),
|
url: video.isModeratedHidden ? null : await this.fetchVideoUrl(video.hash),
|
||||||
@@ -200,12 +320,45 @@ export default {
|
|||||||
formData.append('video', this.fileToUpload);
|
formData.append('video', this.fileToUpload);
|
||||||
formData.append('title', this.title);
|
formData.append('title', this.title);
|
||||||
formData.append('description', this.description);
|
formData.append('description', this.description);
|
||||||
|
formData.append('visibility', JSON.stringify(this.selectedVisibilities.map((visibility) => visibility.id)));
|
||||||
|
formData.append('selectedUsers', JSON.stringify(
|
||||||
|
this.selectedUsernamesText.split(',').map(value => value.trim()).filter(Boolean)
|
||||||
|
));
|
||||||
await apiClient.post('/api/socialnetwork/erotic/videos', formData, {
|
await apiClient.post('/api/socialnetwork/erotic/videos', formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
});
|
});
|
||||||
this.fileToUpload = null;
|
this.fileToUpload = null;
|
||||||
this.title = '';
|
this.title = '';
|
||||||
this.description = '';
|
this.description = '';
|
||||||
|
this.selectedUsernamesText = '';
|
||||||
|
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
|
||||||
|
await this.loadVideos();
|
||||||
|
},
|
||||||
|
toggleEditor(videoId) {
|
||||||
|
if (this.editingVideoId === videoId) {
|
||||||
|
this.editingVideoId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const video = this.videos.find(entry => entry.id === videoId);
|
||||||
|
if (!video) return;
|
||||||
|
this.editingVideoId = videoId;
|
||||||
|
this.editForm = {
|
||||||
|
title: video.title || '',
|
||||||
|
description: video.description || '',
|
||||||
|
visibilities: this.visibilityOptions.filter(option =>
|
||||||
|
(video.visibilities || []).some(visibility => visibility.id === option.id)
|
||||||
|
),
|
||||||
|
selectedUsernamesText: (video.selectedUsers || []).join(', ')
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async saveVideo(videoId) {
|
||||||
|
await apiClient.put(`/api/socialnetwork/erotic/videos/${videoId}`, {
|
||||||
|
title: this.editForm.title,
|
||||||
|
description: this.editForm.description,
|
||||||
|
visibilities: this.editForm.visibilities,
|
||||||
|
selectedUsers: this.editForm.selectedUsernamesText.split(',').map(value => value.trim()).filter(Boolean)
|
||||||
|
});
|
||||||
|
this.editingVideoId = null;
|
||||||
await this.loadVideos();
|
await this.loadVideos();
|
||||||
},
|
},
|
||||||
startReport(type, id) {
|
startReport(type, id) {
|
||||||
@@ -232,6 +385,13 @@ export default {
|
|||||||
showApiError(this, error, this.$t('socialnetwork.erotic.reportError'));
|
showApiError(this, error, this.$t('socialnetwork.erotic.reportError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.query.username': {
|
||||||
|
async handler() {
|
||||||
|
await this.initializeView();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -301,7 +461,8 @@ export default {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.erotic-videos-form label {
|
.erotic-videos-form label,
|
||||||
|
.erotic-videos-editor label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
@@ -405,6 +566,22 @@ export default {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.erotic-videos-card__visibility {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.erotic-videos-card__visibility-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(66, 99, 78, 0.12);
|
||||||
|
color: #42634e;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.erotic-videos-card__hidden {
|
.erotic-videos-card__hidden {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -434,6 +611,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.erotic-videos-card__actions,
|
.erotic-videos-card__actions,
|
||||||
|
.erotic-videos-card__edit,
|
||||||
|
.erotic-videos-editor,
|
||||||
|
.erotic-videos-editor__actions,
|
||||||
.erotic-report-form,
|
.erotic-report-form,
|
||||||
.erotic-report-form__actions {
|
.erotic-report-form__actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -455,9 +635,6 @@ export default {
|
|||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.erotic-videos-workspace {
|
.erotic-videos-workspace {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
|
||||||
|
|
||||||
.erotic-videos-workspace {
|
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user