feat: Add support for deleting cookies
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# MyTube
|
||||
|
||||
一个 YouTube/Bilibili/MissAV 视频下载和播放应用,支持频道订阅与自动下载,允许您将视频及其缩略图本地保存。将您的视频整理到收藏夹中,以便轻松访问和管理。现已支持[yt-dlp所有网址](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md##),包括微博,小红书,x.com等。
|
||||
一个 YouTube/Bilibili/MissAV 视频下载和播放应用,支持频道订阅与自动下载,允许您将视频及其缩略图本地保存。将您的视频整理到收藏夹中,以便轻松访问和管理。现已支持[yt-dlp所有网址](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md##),包括微博,小红书,X.com等。
|
||||
|
||||
[](https://discord.gg/tqucdh7a)
|
||||
|
||||
@@ -23,12 +23,10 @@
|
||||
- **并发下载限制**:设置同时下载的数量限制以管理带宽。
|
||||
- **本地库**:自动保存视频缩略图和元数据,提供丰富的浏览体验。
|
||||
- **视频播放器**:自定义播放器,支持播放/暂停、循环、快进/快退、全屏和调光控制。
|
||||
- **字幕**:自动下载 YouTube 默认语言字幕。
|
||||
- **字幕**:自动下载 YouTube / Bilibili 默认语言字幕。
|
||||
- **搜索功能**:支持在本地库中搜索视频,或在线搜索 YouTube 视频。
|
||||
- **收藏夹**:创建自定义收藏夹以整理您的视频。
|
||||
- **订阅功能**:订阅您喜爱的频道,并在新视频发布时自动下载。
|
||||
- **现代化 UI**:响应式深色主题界面,包含“返回主页”功能和玻璃拟态效果。
|
||||
- **主题支持**:支持在明亮和深色模式之间切换,支持平滑过渡。
|
||||
- **登录保护**:通过密码登录页面保护您的应用。
|
||||
- **国际化**:支持多种语言,包括英语、中文、西班牙语、法语、德语、日语、韩语、阿拉伯语和葡萄牙语。
|
||||
- **分页功能**:支持分页浏览,高效管理大量视频。
|
||||
|
||||
@@ -23,11 +23,9 @@ A YouTube/Bilibili/MissAV video downloader and player that supports channel subs
|
||||
- **Concurrent Download Limit**: Set a limit on the number of simultaneous downloads to manage bandwidth.
|
||||
- **Local Library**: Automatically save video thumbnails and metadata for a rich browsing experience.
|
||||
- **Video Player**: Custom player with Play/Pause, Loop, Seek, Full-screen, and Dimming controls.
|
||||
- **Auto Subtitles**: Automatically download YouTube default language subtitles.
|
||||
- **Auto Subtitles**: Automatically download YouTube / Bilibili default language subtitles.
|
||||
- **Search**: Search for videos locally in your library or online via YouTube.
|
||||
- **Collections**: Organize videos into custom collections for easy access.
|
||||
- **Modern UI**: Responsive, dark-themed interface with a "Back to Home" feature and glassmorphism effects.
|
||||
- **Theme Support**: Toggle between Light and Dark modes with smooth transitions.
|
||||
- **Login Protection**: Secure your application with a password login page.
|
||||
- **Internationalization**: Support for multiple languages including English, Chinese, Spanish, French, German, Japanese, Korean, Arabic, and Portuguese.
|
||||
- **Pagination**: Efficiently browse large libraries with pagination support.
|
||||
|
||||
@@ -158,6 +158,21 @@ export const updateSettings = async (req: Request, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getPasswordEnabled = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const settings = storageService.getSettings();
|
||||
const mergedSettings = { ...defaultSettings, ...settings };
|
||||
|
||||
// Return true only if login is enabled AND a password is set
|
||||
const isEnabled = mergedSettings.loginEnabled && !!mergedSettings.password;
|
||||
|
||||
res.json({ enabled: isEnabled });
|
||||
} catch (error) {
|
||||
console.error('Error checking password status:', error);
|
||||
res.status(500).json({ error: 'Failed to check password status' });
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
@@ -210,3 +225,32 @@ export const uploadCookies = async (req: Request, res: Response) => {
|
||||
res.status(500).json({ error: 'Failed to upload cookies', details: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const checkCookies = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { DATA_DIR } = require('../config/paths');
|
||||
const cookiesPath = path.join(DATA_DIR, 'cookies.txt');
|
||||
const exists = fs.existsSync(cookiesPath);
|
||||
res.json({ exists });
|
||||
} catch (error) {
|
||||
console.error('Error checking cookies:', error);
|
||||
res.status(500).json({ error: 'Failed to check cookies' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCookies = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { DATA_DIR } = require('../config/paths');
|
||||
const cookiesPath = path.join(DATA_DIR, 'cookies.txt');
|
||||
|
||||
if (fs.existsSync(cookiesPath)) {
|
||||
fs.unlinkSync(cookiesPath);
|
||||
res.json({ success: true, message: 'Cookies deleted successfully' });
|
||||
} else {
|
||||
res.status(404).json({ error: 'Cookies file not found' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting cookies:', error);
|
||||
res.status(500).json({ error: 'Failed to delete cookies' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import os from 'os';
|
||||
import { deleteLegacyData, getSettings, migrateData, updateSettings, uploadCookies, verifyPassword } from '../controllers/settingsController';
|
||||
import { checkCookies, deleteCookies, deleteLegacyData, getPasswordEnabled, getSettings, migrateData, updateSettings, uploadCookies, verifyPassword } from '../controllers/settingsController';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({ dest: os.tmpdir() });
|
||||
|
||||
router.get('/', getSettings);
|
||||
router.post('/', updateSettings);
|
||||
router.get('/password-enabled', getPasswordEnabled);
|
||||
router.post('/verify-password', verifyPassword);
|
||||
router.post('/migrate', migrateData);
|
||||
router.post('/delete-legacy', deleteLegacyData);
|
||||
router.post('/upload-cookies', upload.single('file'), uploadCookies);
|
||||
router.post('/delete-cookies', deleteCookies);
|
||||
router.get('/check-cookies', checkCookies);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { Box, CircularProgress, CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
|
||||
import './App.css';
|
||||
@@ -72,7 +72,17 @@ function AppContent() {
|
||||
<CssBaseline />
|
||||
{!isAuthenticated && loginRequired ? (
|
||||
checkingAuth ? (
|
||||
<div className="loading">Loading...</div>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
bgcolor: 'background.default'
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
) : (
|
||||
<LoginPage />
|
||||
)
|
||||
|
||||
@@ -15,17 +15,17 @@ import {
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { Collection } from '../../types';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Collection } from '../types';
|
||||
|
||||
interface CollectionModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
videoCollections: Collection[];
|
||||
collections: Collection[];
|
||||
onAddToCollection: (collectionId: string) => Promise<void>;
|
||||
onCreateCollection: (name: string) => Promise<void>;
|
||||
onRemoveFromCollection: () => void;
|
||||
videoCollections?: Collection[];
|
||||
collections?: Collection[];
|
||||
onAddToCollection?: (collectionId: string) => Promise<void>;
|
||||
onCreateCollection?: (name: string) => Promise<void>;
|
||||
onRemoveFromCollection?: () => void;
|
||||
}
|
||||
|
||||
const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
@@ -48,13 +48,13 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newCollectionName.trim()) return;
|
||||
if (!newCollectionName.trim() || !onCreateCollection) return;
|
||||
await onCreateCollection(newCollectionName);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!selectedCollection) return;
|
||||
if (!selectedCollection || !onAddToCollection) return;
|
||||
await onAddToCollection(selectedCollection);
|
||||
handleClose();
|
||||
};
|
||||
@@ -63,7 +63,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('addToCollection')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{videoCollections.length > 0 && (
|
||||
{videoCollections && videoCollections.length > 0 && onRemoveFromCollection && (
|
||||
<Alert severity="info" sx={{ mb: 3 }} action={
|
||||
<Button color="error" size="small" onClick={() => {
|
||||
onRemoveFromCollection();
|
||||
@@ -79,7 +79,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{collections && collections.length > 0 && (
|
||||
{collections && collections.length > 0 && onAddToCollection && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('addToExistingCollection')}</Typography>
|
||||
<Stack direction="row" spacing={2}>
|
||||
@@ -94,9 +94,9 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
<MenuItem
|
||||
key={collection.id}
|
||||
value={collection.id}
|
||||
disabled={videoCollections.length > 0 && videoCollections[0].id === collection.id}
|
||||
disabled={videoCollections && videoCollections.length > 0 && videoCollections[0].id === collection.id}
|
||||
>
|
||||
{collection.name} {videoCollections.length > 0 && videoCollections[0].id === collection.id ? t('current') : ''}
|
||||
{collection.name} {videoCollections && videoCollections.length > 0 && videoCollections[0].id === collection.id ? t('current') : ''}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -112,6 +112,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{onCreateCollection && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>{t('createNewCollection')}</Typography>
|
||||
<Stack direction="row" spacing={2}>
|
||||
@@ -132,6 +133,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="inherit">{t('cancel')}</Button>
|
||||
@@ -20,7 +20,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ debugMode, onDebugM
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>{t('debugMode')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('debugModeDescription')}
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { CloudUpload } from '@mui/icons-material';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { CheckCircle, CloudUpload, Delete, ErrorOutline } from '@mui/icons-material';
|
||||
import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import ConfirmationModal from '../ConfirmationModal';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -32,20 +34,57 @@ const CookieSettings: React.FC<CookieSettingsProps> = ({ onSuccess, onError }) =
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
onSuccess(t('cookiesUploadedSuccess') || 'Cookies uploaded successfully');
|
||||
handleSuccess(t('cookiesUploadedSuccess') || 'Cookies uploaded successfully');
|
||||
} catch (error) {
|
||||
console.error('Error uploading cookies:', error);
|
||||
onError(t('cookiesUploadFailed') || 'Failed to upload cookies');
|
||||
}
|
||||
|
||||
// Reset input
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const { data: cookieStatus, refetch: refetchCookieStatus, isLoading } = useQuery({
|
||||
queryKey: ['cookieStatus'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get(`${API_URL}/settings/check-cookies`);
|
||||
return response.data;
|
||||
}
|
||||
});
|
||||
|
||||
const handleSuccess = (msg: string) => {
|
||||
onSuccess(msg);
|
||||
refetchCookieStatus();
|
||||
};
|
||||
|
||||
// Delete mutation
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const response = await axios.post(`${API_URL}/settings/delete-cookies`);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess(t('cookiesDeletedSuccess') || 'Cookies deleted successfully');
|
||||
refetchCookieStatus();
|
||||
},
|
||||
onError: () => {
|
||||
onError(t('cookiesDeleteFailed') || 'Failed to delete cookies');
|
||||
}
|
||||
});
|
||||
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
deleteMutation.mutate();
|
||||
setShowDeleteConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>{t('cookieSettings') || 'Cookie Settings'}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('cookieUploadDescription') || 'Upload cookies.txt to pass YouTube bot checks and enable Bilibili subtitle downloads. The file will be renamed to cookies.txt automatically. (Example: use "Get cookies.txt LOCALLY" extension to export cookies)'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
@@ -62,7 +101,44 @@ const CookieSettings: React.FC<CookieSettingsProps> = ({ onSuccess, onError }) =
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{cookieStatus?.exists && (
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<Delete />}
|
||||
onClick={handleDelete}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{t('deleteCookies') || 'Delete Cookies'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} />
|
||||
) : cookieStatus?.exists ? (
|
||||
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
|
||||
{t('cookiesFound') || 'cookies.txt found'}
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert icon={<ErrorOutline fontSize="inherit" />} severity="warning">
|
||||
{t('cookiesNotFound') || 'cookies.txt not found'}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
onConfirm={confirmDelete}
|
||||
title={t('deleteCookies') || 'Delete Cookies'}
|
||||
message={t('confirmDeleteCookies') || 'Are you sure you want to delete the cookies file? This may affect downloading capabilities.'}
|
||||
confirmText={t('delete') || 'Delete'}
|
||||
cancelText={t('cancel') || 'Cancel'}
|
||||
isDanger={true}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ onMigrate, onDelete
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>{t('database')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('migrateDataDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
@@ -28,7 +28,7 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ onMigrate, onDelete
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>{t('removeLegacyData')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('removeLegacyDataDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
|
||||
@@ -40,7 +40,7 @@ const DownloadSettings: React.FC<DownloadSettingsProps> = ({
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>{t('cleanupTempFiles')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('cleanupTempFilesDescription')}
|
||||
</Typography>
|
||||
{activeDownloadsCount > 0 && (
|
||||
|
||||
@@ -25,7 +25,7 @@ const TagsSettings: React.FC<TagsSettingsProps> = ({ tags, onTagsChange }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>{t('tagsManagement') || 'Tags Management'}</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('tagsManagementNote') || 'Please remember to click "Save Settings" after adding or removing tags to apply changes.'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Add,
|
||||
Delete,
|
||||
Folder
|
||||
} from '@mui/icons-material';
|
||||
@@ -16,9 +17,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Collection, Video } from '../types';
|
||||
import { formatDuration, parseDuration } from '../utils/formatUtils';
|
||||
import CollectionModal from './CollectionModal';
|
||||
import ConfirmationModal from './ConfirmationModal';
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
@@ -140,6 +143,33 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
|
||||
|
||||
|
||||
// Collections Logic (State and Handlers)
|
||||
const { collections: allCollections, addToCollection, createCollection, removeFromCollection } = useCollection();
|
||||
const [showCollectionModal, setShowCollectionModal] = useState(false);
|
||||
|
||||
const handleAddToCollection = async (collectionId: string) => {
|
||||
if (!video.id) return;
|
||||
await addToCollection(collectionId, video.id);
|
||||
};
|
||||
|
||||
const handleCreateCollection = async (name: string) => {
|
||||
if (!video.id) return;
|
||||
await createCollection(name, video.id);
|
||||
};
|
||||
|
||||
const handleRemoveFromCollection = async () => {
|
||||
if (!video.id) return;
|
||||
await removeFromCollection(video.id);
|
||||
};
|
||||
|
||||
const handleAddClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setShowCollectionModal(true);
|
||||
};
|
||||
|
||||
// Calculate collections that contain THIS video
|
||||
const currentVideoCollections = allCollections.filter(c => c.videos.includes(video.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
@@ -154,6 +184,9 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
boxShadow: theme.shadows[8],
|
||||
'& .delete-btn': {
|
||||
opacity: 1
|
||||
},
|
||||
'& .add-btn': {
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
border: isFirstInAnyCollection ? `1px solid ${theme.palette.primary.main}` : 'none'
|
||||
@@ -263,6 +296,8 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
sx={{ position: 'absolute', top: 8, left: 8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ flexGrow: 1, p: 2 }}>
|
||||
@@ -318,12 +353,36 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
transition: 'opacity 0.2s',
|
||||
'&:hover': {
|
||||
bgcolor: 'error.main',
|
||||
}
|
||||
},
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Delete fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<IconButton
|
||||
className="add-btn"
|
||||
onClick={handleAddClick}
|
||||
size="small"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: showDeleteButton ? 40 : 8, // Shift left if delete button is present
|
||||
bgcolor: 'rgba(0,0,0,0.6)',
|
||||
color: 'white',
|
||||
opacity: 0, // Hidden by default, shown on hover
|
||||
transition: 'opacity 0.2s',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.main',
|
||||
},
|
||||
zIndex: 2
|
||||
}}
|
||||
>
|
||||
<Add fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<ConfirmationModal
|
||||
@@ -336,6 +395,16 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
||||
cancelText={t('cancel')}
|
||||
isDanger={true}
|
||||
/>
|
||||
|
||||
<CollectionModal
|
||||
open={showCollectionModal}
|
||||
onClose={() => setShowCollectionModal(false)}
|
||||
videoCollections={currentVideoCollections}
|
||||
collections={allCollections}
|
||||
onAddToCollection={handleAddToCollection}
|
||||
onCreateCollection={handleCreateCollection}
|
||||
onRemoveFromCollection={handleRemoveFromCollection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -324,7 +324,7 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
|
||||
<Tooltip title={t('addToCollection')}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onAddToCollection}
|
||||
onClick={() => onAddToCollection()}
|
||||
sx={{ minWidth: 'auto', p: 1 }}
|
||||
>
|
||||
<Add />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Alert, AlertColor, Snackbar } from '@mui/material';
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { SNACKBAR_AUTO_HIDE_DURATION } from '../utils/constants';
|
||||
|
||||
interface SnackbarContextType {
|
||||
showSnackbar: (message: string, severity?: AlertColor) => void;
|
||||
@@ -42,7 +43,7 @@ export const SnackbarProvider: React.FC<SnackbarProviderProps> = ({ children })
|
||||
{children}
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
autoHideDuration={SNACKBAR_AUTO_HIDE_DURATION}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
|
||||
@@ -49,9 +49,18 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const { data: videos = [], isLoading: videosLoading, error: videosError, refetch: refetchVideos } = useQuery({
|
||||
queryKey: ['videos'],
|
||||
queryFn: async () => {
|
||||
console.log('Fetching videos from:', `${API_URL}/videos`);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/videos`);
|
||||
console.log('Videos fetch success');
|
||||
return response.data as Video[];
|
||||
} catch (err) {
|
||||
console.error('Videos fetch failed:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
retry: 10,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
|
||||
// Tags Query
|
||||
@@ -61,6 +70,8 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
return response.data.tags || [];
|
||||
},
|
||||
retry: 10,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
@@ -239,8 +239,8 @@ const Home: React.FC = () => {
|
||||
minWidth: 'auto',
|
||||
p: 1,
|
||||
display: { xs: 'none', md: 'inline-flex' },
|
||||
color: 'text.primary',
|
||||
borderColor: 'text.primary',
|
||||
color: 'text.secondary',
|
||||
borderColor: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
<ViewSidebar sx={{ transform: 'rotate(180deg)' }} />
|
||||
|
||||
@@ -13,7 +13,7 @@ const InstructionPage: React.FC = () => {
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
{t('instructionSection1Title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph color="text.secondary">
|
||||
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
|
||||
{t('instructionSection1Desc')}
|
||||
</Typography>
|
||||
|
||||
@@ -80,7 +80,7 @@ const InstructionPage: React.FC = () => {
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
{t('instructionSection2Title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph color="text.secondary">
|
||||
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
|
||||
{t('instructionSection2Desc')}
|
||||
</Typography>
|
||||
|
||||
@@ -108,7 +108,7 @@ const InstructionPage: React.FC = () => {
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
{t('instructionSection3Title')}
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph color="text.secondary">
|
||||
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
|
||||
{t('instructionSection3Desc')}
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { LockOutlined } from '@mui/icons-material';
|
||||
import { ErrorOutline, LockOutlined } from '@mui/icons-material';
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
CssBaseline,
|
||||
TextField,
|
||||
ThemeProvider,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import getTheme from '../theme';
|
||||
@@ -25,6 +26,24 @@ const LoginPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { login } = useAuth();
|
||||
|
||||
// Check backend connection and password status
|
||||
const { data: statusData, isLoading: isCheckingConnection, isError: isConnectionError, refetch: retryConnection } = useQuery({
|
||||
queryKey: ['healthCheck'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get(`${API_URL}/settings/password-enabled`, { timeout: 5000 });
|
||||
return response.data;
|
||||
},
|
||||
retry: 1,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
|
||||
// Auto-login if password is not enabled
|
||||
useEffect(() => {
|
||||
if (statusData && statusData.enabled === false) {
|
||||
login();
|
||||
}
|
||||
}, [statusData, login]);
|
||||
|
||||
// Use dark theme for login page to match app style
|
||||
const theme = getTheme('dark');
|
||||
|
||||
@@ -68,6 +87,37 @@ const LoginPage: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{isCheckingConnection ? (
|
||||
// Loading state while checking backend connection
|
||||
<>
|
||||
<CircularProgress sx={{ mb: 2 }} />
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t('checkingConnection') || 'Checking connection...'}
|
||||
</Typography>
|
||||
</>
|
||||
) : isConnectionError ? (
|
||||
// Backend connection error state
|
||||
<>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'error.main', width: 56, height: 56 }}>
|
||||
<ErrorOutline fontSize="large" />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5" sx={{ mt: 2, mb: 1 }}>
|
||||
{t('connectionError') || 'Connection Error'}
|
||||
</Typography>
|
||||
<Alert severity="error" sx={{ mt: 2, mb: 2, width: '100%' }}>
|
||||
{t('backendConnectionFailed') || 'Unable to connect to the server. Please check if the backend is running and port is open, then try again.'}
|
||||
</Alert>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => retryConnection()}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{t('retry') || 'Retry'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
// Normal login form
|
||||
<>
|
||||
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||
<LockOutlined />
|
||||
</Avatar>
|
||||
@@ -104,6 +154,8 @@ const LoginPage: React.FC = () => {
|
||||
{loginMutation.isPending ? t('verifying') : t('signIn')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useDownload } from '../contexts/DownloadContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Settings } from '../types';
|
||||
import ConsoleManager from '../utils/consoleManager';
|
||||
import { SNACKBAR_AUTO_HIDE_DURATION } from '../utils/constants';
|
||||
import { Language } from '../utils/translations';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
@@ -363,7 +364,7 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
<Snackbar
|
||||
open={!!message}
|
||||
autoHideDuration={6000}
|
||||
autoHideDuration={SNACKBAR_AUTO_HIDE_DURATION}
|
||||
onClose={() => setMessage(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import CollectionModal from '../components/CollectionModal';
|
||||
import ConfirmationModal from '../components/ConfirmationModal';
|
||||
import CollectionModal from '../components/VideoPlayer/CollectionModal';
|
||||
import CommentsSection from '../components/VideoPlayer/CommentsSection';
|
||||
import UpNextSidebar from '../components/VideoPlayer/UpNextSidebar';
|
||||
import VideoControls from '../components/VideoPlayer/VideoControls';
|
||||
|
||||
1
frontend/src/utils/constants.ts
Normal file
1
frontend/src/utils/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SNACKBAR_AUTO_HIDE_DURATION = 6000;
|
||||
@@ -95,6 +95,12 @@ export const ar = {
|
||||
onlyTxtFilesAllowed: "يسمح فقط بملفات .txt",
|
||||
cookiesUploadedSuccess: "تم تحميل ملفات تعريف الارتباط بنجاح",
|
||||
cookiesUploadFailed: "فشل تحميل ملفات تعريف الارتباط",
|
||||
cookiesFound: "تم العثور على cookies.txt",
|
||||
cookiesNotFound: "لم يتم العثور على cookies.txt",
|
||||
deleteCookies: "حذف ملفات تعريف الارتباط",
|
||||
confirmDeleteCookies: "هل أنت متأكد أنك تريد حذف ملف تعريف الارتباط؟ سيؤثر هذا على قدرتك على تنزيل مقاطع الفيديو المقيدة بالعمر أو الخاصة بالأعضاء فقط.",
|
||||
cookiesDeletedSuccess: "تم حذف ملفات تعريف الارتباط بنجاح",
|
||||
cookiesDeleteFailed: "فشل حذف ملفات تعريف الارتباط",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "التخزين السحابي (OpenList)",
|
||||
@@ -168,6 +174,7 @@ export const ar = {
|
||||
rateThisVideo: "قيم هذا الفيديو",
|
||||
enterFullscreen: "ملء الشاشة",
|
||||
exitFullscreen: "خروج من ملء الشاشة",
|
||||
share: "مشاركة",
|
||||
editTitle: "تعديل العنوان",
|
||||
titleUpdated: "تم تحديث العنوان بنجاح",
|
||||
titleUpdateFailed: "فشل تحديث العنوان",
|
||||
@@ -189,6 +196,10 @@ export const ar = {
|
||||
incorrectPassword: "كلمة المرور غير صحيحة",
|
||||
loginFailed: "فشل التحقق من كلمة المرور",
|
||||
defaultPasswordHint: "كلمة المرور الافتراضية: 123",
|
||||
checkingConnection: "جارٍ التحقق من الاتصال...",
|
||||
connectionError: "خطأ في الاتصال",
|
||||
backendConnectionFailed: "تعذر الاتصال بالخادم. يرجى التحقق من تشغيل الخادم الخلفي ومن أن المنفذ مفتوح، ثم أعد المحاولة.",
|
||||
retry: "إعادة المحاولة",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "جاري تحميل المجموعة...",
|
||||
|
||||
@@ -54,6 +54,12 @@ export const de = {
|
||||
onlyTxtFilesAllowed: "Nur .txt-Dateien erlaubt",
|
||||
cookiesUploadedSuccess: "Cookies erfolgreich hochgeladen",
|
||||
cookiesUploadFailed: "Fehler beim Hochladen der Cookies",
|
||||
cookiesFound: "cookies.txt gefunden",
|
||||
cookiesNotFound: "cookies.txt nicht gefunden",
|
||||
deleteCookies: "Cookies löschen",
|
||||
confirmDeleteCookies: "Sind Sie sicher, dass Sie die Cookie-Datei löschen möchten? Dies beeinträchtigt Ihre Fähigkeit, altersbeschränkte oder nur für Mitglieder zugängliche Videos herunterzuladen.",
|
||||
cookiesDeletedSuccess: "Cookies erfolgreich gelöscht",
|
||||
cookiesDeleteFailed: "Fehler beim Löschen der Cookies",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Cloud-Speicher (OpenList)",
|
||||
@@ -85,7 +91,8 @@ export const de = {
|
||||
create: "Erstellen", removeFromCollection: "Aus Sammlung Entfernen",
|
||||
confirmRemoveFromCollection: "Sind Sie sicher, dass Sie dieses Video aus der Sammlung entfernen möchten?", remove: "Entfernen",
|
||||
loadingVideo: "Video wird geladen...", current: "(Aktuell)", rateThisVideo: "Dieses Video bewerten",
|
||||
enterFullscreen: "Vollbild", exitFullscreen: "Vollbild Verlassen", editTitle: "Titel Bearbeiten",
|
||||
enterFullscreen: "Vollbild", exitFullscreen: "Vollbild Verlassen",
|
||||
share: "Teilen", editTitle: "Titel Bearbeiten",
|
||||
titleUpdated: "Titel erfolgreich aktualisiert", titleUpdateFailed: "Fehler beim Aktualisieren des Titels",
|
||||
refreshThumbnail: "Vorschaubild aktualisieren",
|
||||
thumbnailRefreshed: "Vorschaubild erfolgreich aktualisiert",
|
||||
@@ -100,6 +107,10 @@ export const de = {
|
||||
searchCancelled: "Suche abgebrochen",
|
||||
signIn: "Anmelden", verifying: "Überprüfen...", incorrectPassword: "Falsches Passwort",
|
||||
loginFailed: "Fehler beim Überprüfen des Passworts", defaultPasswordHint: "Standardpasswort: 123",
|
||||
checkingConnection: "Verbindung wird überprüft...",
|
||||
connectionError: "Verbindungsfehler",
|
||||
backendConnectionFailed: "Verbindung zum Server nicht möglich. Bitte überprüfen Sie, ob das Backend läuft und der Port geöffnet ist, und versuchen Sie es erneut.",
|
||||
retry: "Erneut versuchen",
|
||||
loadingCollection: "Sammlung wird geladen...", collectionNotFound: "Sammlung nicht gefunden",
|
||||
noVideosInCollection: "Keine Videos in dieser Sammlung.", back: "Zurück", loadVideosError: "Fehler beim Laden der Videos. Bitte versuchen Sie es später erneut.",
|
||||
unknownAuthor: "Unbekannt", noVideosForAuthor: "Keine Videos für diesen Autor gefunden.",
|
||||
|
||||
@@ -96,6 +96,12 @@ export const en = {
|
||||
onlyTxtFilesAllowed: "Only .txt files are allowed",
|
||||
cookiesUploadedSuccess: "Cookies uploaded successfully",
|
||||
cookiesUploadFailed: "Failed to upload cookies",
|
||||
cookiesFound: "cookies.txt found",
|
||||
cookiesNotFound: "cookies.txt not found",
|
||||
deleteCookies: "Delete Cookies",
|
||||
confirmDeleteCookies: "Are you sure you want to delete the cookies file? This will affect your ability to download age-restricted or member-only videos.",
|
||||
cookiesDeletedSuccess: "Cookies deleted successfully",
|
||||
cookiesDeleteFailed: "Failed to delete cookies",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Cloud Drive (OpenList)",
|
||||
@@ -191,6 +197,10 @@ export const en = {
|
||||
incorrectPassword: "Incorrect password",
|
||||
loginFailed: "Failed to verify password",
|
||||
defaultPasswordHint: "Default password: 123",
|
||||
checkingConnection: "Checking connection...",
|
||||
connectionError: "Connection Error",
|
||||
backendConnectionFailed: "Unable to connect to the server. Please check if the backend is running and port is open, then try again.",
|
||||
retry: "Retry",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "Loading collection...",
|
||||
|
||||
@@ -51,7 +51,13 @@ export const es = {
|
||||
uploadCookies: "Subir Cookies",
|
||||
onlyTxtFilesAllowed: "Solo se permiten archivos .txt",
|
||||
cookiesUploadedSuccess: "Cookies subidas con éxito",
|
||||
cookiesUploadFailed: "Error al subir cookies",
|
||||
cookiesUploadFailed: "Error al cargar cookies",
|
||||
cookiesFound: "cookies.txt encontrado",
|
||||
cookiesNotFound: "cookies.txt no encontrado",
|
||||
deleteCookies: "Eliminar Cookies",
|
||||
confirmDeleteCookies: "¿Estás seguro de que deseas eliminar el archivo de cookies? Esto afectará tu capacidad para descargar videos con restricción de edad o solo para miembros.",
|
||||
cookiesDeletedSuccess: "Cookies eliminadas con éxito",
|
||||
cookiesDeleteFailed: "Error al eliminar cookies",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Almacenamiento en la Nube (OpenList)",
|
||||
@@ -93,7 +99,8 @@ export const es = {
|
||||
create: "Crear", removeFromCollection: "Eliminar de la Colección",
|
||||
confirmRemoveFromCollection: "¿Está seguro de que desea eliminar este video de la colección?", remove: "Eliminar",
|
||||
loadingVideo: "Cargando video...", current: "(Actual)", rateThisVideo: "Calificar este video",
|
||||
enterFullscreen: "Pantalla Completa", exitFullscreen: "Salir de Pantalla Completa", editTitle: "Editar Título",
|
||||
enterFullscreen: "Pantalla Completa", exitFullscreen: "Salir de Pantalla Completa",
|
||||
share: "Compartir", editTitle: "Editar Título",
|
||||
titleUpdated: "Título actualizado exitosamente", titleUpdateFailed: "Error al actualizar el título",
|
||||
refreshThumbnail: "Actualizar miniatura",
|
||||
thumbnailRefreshed: "Miniatura actualizada con éxito",
|
||||
@@ -108,6 +115,10 @@ export const es = {
|
||||
searchCancelled: "Búsqueda cancelada",
|
||||
signIn: "Iniciar Sesión", verifying: "Verificando...", incorrectPassword: "Contraseña incorrecta",
|
||||
loginFailed: "Error al verificar la contraseña", defaultPasswordHint: "Contraseña predeterminada: 123",
|
||||
checkingConnection: "Comprobando conexión...",
|
||||
connectionError: "Error de Conexión",
|
||||
backendConnectionFailed: "No se puede conectar al servidor. Verifique que el backend esté en ejecución y que el puerto esté abierto, luego intente de nuevo.",
|
||||
retry: "Reintentar",
|
||||
loadingCollection: "Cargando colección...", collectionNotFound: "Colección no encontrada",
|
||||
noVideosInCollection: "No hay videos en esta colección.", back: "Volver", loadVideosError: "Error al cargar los videos. Por favor, inténtelo más tarde.",
|
||||
unknownAuthor: "Desconocido", noVideosForAuthor: "No se encontraron videos para este autor.",
|
||||
|
||||
@@ -95,6 +95,12 @@ export const fr = {
|
||||
onlyTxtFilesAllowed: "Seuls les fichiers .txt sont autorisés",
|
||||
cookiesUploadedSuccess: "Cookies téléchargés avec succès",
|
||||
cookiesUploadFailed: "Échec du téléchargement des cookies",
|
||||
cookiesFound: "cookies.txt trouvé",
|
||||
cookiesNotFound: "cookies.txt introuvable",
|
||||
deleteCookies: "Supprimer les cookies",
|
||||
confirmDeleteCookies: "Êtes-vous sûr de vouloir supprimer le fichier de cookies ? Cela affectera votre capacité à télécharger des vidéos soumises à une limite d'âge ou réservées aux membres.",
|
||||
cookiesDeletedSuccess: "Cookies supprimés avec succès",
|
||||
cookiesDeleteFailed: "Échec de la suppression des cookies",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Stockage Cloud (OpenList)",
|
||||
@@ -177,6 +183,7 @@ export const fr = {
|
||||
rateThisVideo: "Noter cette vidéo",
|
||||
enterFullscreen: "Plein écran",
|
||||
exitFullscreen: "Quitter le plein écran",
|
||||
share: "Partager",
|
||||
editTitle: "Modifier le titre",
|
||||
titleUpdated: "Titre mis à jour avec succès",
|
||||
titleUpdateFailed: "Échec de la mise à jour du titre",
|
||||
@@ -197,6 +204,10 @@ export const fr = {
|
||||
incorrectPassword: "Mot de passe incorrect",
|
||||
loginFailed: "Échec de la vérification du mot de passe",
|
||||
defaultPasswordHint: "Mot de passe par défaut : 123",
|
||||
checkingConnection: "Vérification de la connexion...",
|
||||
connectionError: "Erreur de Connexion",
|
||||
backendConnectionFailed: "Impossible de se connecter au serveur. Veuillez vérifier que le backend est en cours d'exécution et que le port est ouvert, puis réessayez.",
|
||||
retry: "Réessayer",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "Chargement de la collection...",
|
||||
|
||||
@@ -94,7 +94,13 @@ export const ja = {
|
||||
uploadCookies: "Cookieをアップロード",
|
||||
onlyTxtFilesAllowed: ".txtファイルのみ許可されています",
|
||||
cookiesUploadedSuccess: "Cookieが正常にアップロードされました",
|
||||
cookiesUploadFailed: "Cookieのアップロードに失敗しました",
|
||||
cookiesUploadFailed: "Cookiesのアップロードに失敗しました",
|
||||
cookiesFound: "cookies.txtが見つかりました",
|
||||
cookiesNotFound: "cookies.txtが見つかりません",
|
||||
deleteCookies: "Cookiesを削除",
|
||||
confirmDeleteCookies: "cookiesファイルを削除してもよろしいですか?年齢制限のある動画やメンバー限定動画のダウンロードに影響する可能性があります。",
|
||||
cookiesDeletedSuccess: "Cookiesが正常に削除されました",
|
||||
cookiesDeleteFailed: "Cookiesの削除に失敗しました",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "クラウドストレージ (OpenList)",
|
||||
@@ -166,6 +172,7 @@ export const ja = {
|
||||
rateThisVideo: "この動画を評価",
|
||||
enterFullscreen: "全画面表示",
|
||||
exitFullscreen: "全画面表示を終了",
|
||||
share: "共有",
|
||||
editTitle: "タイトルを編集",
|
||||
titleUpdated: "タイトルが正常に更新されました",
|
||||
titleUpdateFailed: "タイトルの更新に失敗しました",
|
||||
@@ -187,6 +194,10 @@ export const ja = {
|
||||
incorrectPassword: "パスワードが間違っています",
|
||||
loginFailed: "パスワードの確認に失敗しました",
|
||||
defaultPasswordHint: "デフォルトのパスワード: 123",
|
||||
checkingConnection: "接続を確認中...",
|
||||
connectionError: "接続エラー",
|
||||
backendConnectionFailed: "サーバーに接続できません。バックエンドが実行されていること、およびポートが開いていることを確認してから、再試行してください。",
|
||||
retry: "再試行",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "コレクションを読み込み中...",
|
||||
|
||||
@@ -95,6 +95,12 @@ export const ko = {
|
||||
onlyTxtFilesAllowed: ".txt 파일만 허용됩니다",
|
||||
cookiesUploadedSuccess: "쿠키가 성공적으로 업로드되었습니다",
|
||||
cookiesUploadFailed: "쿠키 업로드 실패",
|
||||
cookiesFound: "cookies.txt를 찾았습니다",
|
||||
cookiesNotFound: "cookies.txt를 찾을 수 없습니다",
|
||||
deleteCookies: "쿠키 삭제",
|
||||
confirmDeleteCookies: "쿠키 파일을 삭제하시겠습니까? 연령 제한 또는 회원 전용 비디오를 다운로드하는 데 영향을 줄 수 있습니다.",
|
||||
cookiesDeletedSuccess: "쿠키가 성공적으로 삭제되었습니다",
|
||||
cookiesDeleteFailed: "쿠키 삭제 실패",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "클라우드 드라이브 (OpenList)",
|
||||
@@ -168,6 +174,7 @@ export const ko = {
|
||||
rateThisVideo: "이 동영상 평가",
|
||||
enterFullscreen: "전체 화면",
|
||||
exitFullscreen: "전체 화면 종료",
|
||||
share: "공유",
|
||||
editTitle: "제목 편집",
|
||||
titleUpdated: "제목이 성공적으로 업데이트됨",
|
||||
titleUpdateFailed: "제목 업데이트 실패",
|
||||
@@ -189,6 +196,10 @@ export const ko = {
|
||||
incorrectPassword: "비밀번호가 올바르지 않습니다",
|
||||
loginFailed: "비밀번호 확인 실패",
|
||||
defaultPasswordHint: "기본 비밀번호: 123",
|
||||
checkingConnection: "연결 확인 중...",
|
||||
connectionError: "연결 오류",
|
||||
backendConnectionFailed: "서버에 연결할 수 없습니다. 백엔드가 실행 중이고 포트가 열려 있는지 확인한 후 다시 시도하세요.",
|
||||
retry: "다시 시도",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "컬렉션 로드 중...",
|
||||
|
||||
@@ -95,6 +95,12 @@ export const pt = {
|
||||
onlyTxtFilesAllowed: "Apenas arquivos .txt são permitidos",
|
||||
cookiesUploadedSuccess: "Cookies enviados com sucesso",
|
||||
cookiesUploadFailed: "Falha ao enviar cookies",
|
||||
cookiesFound: "cookies.txt encontrado",
|
||||
cookiesNotFound: "cookies.txt não encontrado",
|
||||
deleteCookies: "Excluir Cookies",
|
||||
confirmDeleteCookies: "Tem certeza de que deseja excluir o arquivo de cookies? Isso afetará sua capacidade de baixar vídeos com restrição de idade ou exclusivos para membros.",
|
||||
cookiesDeletedSuccess: "Cookies excluídos com sucesso",
|
||||
cookiesDeleteFailed: "Falha ao excluir cookies",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Armazenamento em Nuvem (OpenList)",
|
||||
@@ -168,6 +174,7 @@ export const pt = {
|
||||
rateThisVideo: "Avaliar este vídeo",
|
||||
enterFullscreen: "Tela Cheia",
|
||||
exitFullscreen: "Sair da Tela Cheia",
|
||||
share: "Compartilhar",
|
||||
editTitle: "Editar Título",
|
||||
titleUpdated: "Título atualizado com sucesso",
|
||||
titleUpdateFailed: "Falha ao atualizar título",
|
||||
@@ -197,6 +204,10 @@ export const pt = {
|
||||
incorrectPassword: "Senha incorreta",
|
||||
loginFailed: "Falha ao verificar senha",
|
||||
defaultPasswordHint: "Senha padrão: 123",
|
||||
checkingConnection: "Verificando conexão...",
|
||||
connectionError: "Erro de Conexão",
|
||||
backendConnectionFailed: "Não foi possível conectar ao servidor. Verifique se o backend está em execução e a porta está aberta, depois tente novamente.",
|
||||
retry: "Tentar Novamente",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "Carregando coleção...",
|
||||
|
||||
@@ -95,6 +95,12 @@ export const ru = {
|
||||
onlyTxtFilesAllowed: "Разрешены только файлы .txt",
|
||||
cookiesUploadedSuccess: "Cookie успешно загружены",
|
||||
cookiesUploadFailed: "Не удалось загрузить cookie",
|
||||
cookiesFound: "cookies.txt найден",
|
||||
cookiesNotFound: "cookies.txt не найден",
|
||||
deleteCookies: "Удалить Cookie",
|
||||
confirmDeleteCookies: "Вы уверены, что хотите удалить файл cookie? Это повлияет на возможность скачивания видео с возрастными ограничениями или только для участников.",
|
||||
cookiesDeletedSuccess: "Cookie успешно удалены",
|
||||
cookiesDeleteFailed: "Не удалось удалить cookie",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "Облачное хранилище (OpenList)",
|
||||
@@ -168,6 +174,7 @@ export const ru = {
|
||||
rateThisVideo: "Оценить это видео",
|
||||
enterFullscreen: "На весь экран",
|
||||
exitFullscreen: "Выйти из полноэкранного режима",
|
||||
share: "Поделиться",
|
||||
editTitle: "Редактировать название",
|
||||
titleUpdated: "Название успешно обновлено",
|
||||
titleUpdateFailed: "Не удалось обновить название",
|
||||
@@ -189,6 +196,10 @@ export const ru = {
|
||||
incorrectPassword: "Неверный пароль",
|
||||
loginFailed: "Ошибка проверки пароля",
|
||||
defaultPasswordHint: "Пароль по умолчанию: 123",
|
||||
checkingConnection: "Проверка соединения...",
|
||||
connectionError: "Ошибка соединения",
|
||||
backendConnectionFailed: "Не удалось подключиться к серверу. Убедитесь, что сервер запущен и порт открыт, затем повторите попытку.",
|
||||
retry: "Повторить",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "Загрузка коллекции...",
|
||||
|
||||
@@ -95,7 +95,13 @@ export const zh = {
|
||||
uploadCookies: "上传 Cookie",
|
||||
onlyTxtFilesAllowed: "仅允许 .txt 文件",
|
||||
cookiesUploadedSuccess: "Cookie 上传成功",
|
||||
cookiesUploadFailed: "Cookie 上传失败",
|
||||
cookiesUploadFailed: "上传 Cookies 失败",
|
||||
cookiesFound: "已找到 cookies.txt",
|
||||
cookiesNotFound: "未找到 cookies.txt",
|
||||
deleteCookies: "删除 Cookies",
|
||||
confirmDeleteCookies: "您确定要删除 cookies 文件吗?这将影响您下载有年龄限制或仅限会员视频的能力。",
|
||||
cookiesDeletedSuccess: "Cookies 删除成功",
|
||||
cookiesDeleteFailed: "删除 Cookies 失败",
|
||||
|
||||
// Cloud Drive
|
||||
cloudDriveSettings: "云端存储 (OpenList)",
|
||||
@@ -169,6 +175,7 @@ export const zh = {
|
||||
rateThisVideo: "给视频评分",
|
||||
enterFullscreen: "全屏",
|
||||
exitFullscreen: "退出全屏",
|
||||
share: "分享",
|
||||
editTitle: "编辑标题",
|
||||
titleUpdated: "标题更新成功",
|
||||
titleUpdateFailed: "更新标题失败",
|
||||
@@ -190,6 +197,10 @@ export const zh = {
|
||||
incorrectPassword: "密码错误",
|
||||
loginFailed: "验证密码失败",
|
||||
defaultPasswordHint: "默认密码:123",
|
||||
checkingConnection: "正在检查连接...",
|
||||
connectionError: "连接错误",
|
||||
backendConnectionFailed: "无法连接到服务器。请检查后端是否正在运行并确保端口已开放,然后重试。",
|
||||
retry: "重试",
|
||||
|
||||
// Collection Page
|
||||
loadingCollection: "加载合集中...",
|
||||
|
||||
Reference in New Issue
Block a user