feat: Add confirmation modals for video and collection actions
This commit is contained in:
@@ -17,8 +17,85 @@
|
|||||||
--spacing-md: 15px;
|
--spacing-md: 15px;
|
||||||
--spacing-lg: 20px;
|
--spacing-lg: 20px;
|
||||||
--spacing-xl: 30px;
|
--spacing-xl: 30px;
|
||||||
|
--input-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ... (lines 22-131 unchanged) ... */
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
width: 100%;
|
||||||
|
height: var(--input-height);
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 24px 0 0 24px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--background-light);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 0;
|
||||||
|
/* Prevents flex item from overflowing */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... (lines 145-156 unchanged) ... */
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
height: var(--input-height);
|
||||||
|
padding: 0 24px;
|
||||||
|
background-color: var(--background-light);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 24px 24px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ... (lines 170-198 unchanged) ... */
|
||||||
|
|
||||||
|
.downloads-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: var(--background-light);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0 16px;
|
||||||
|
height: var(--input-height);
|
||||||
|
border-radius: 23px;
|
||||||
|
/* Half of 46px */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode {
|
||||||
|
--primary-color: #ff3e3e;
|
||||||
|
--primary-hover: #ff1a1a;
|
||||||
|
--secondary-color: #e0e0e0;
|
||||||
|
--text-color: #121212;
|
||||||
|
--text-secondary: #555555;
|
||||||
|
--background-dark: #f9f9f9;
|
||||||
|
--background-darker: #ffffff;
|
||||||
|
--background-lighter: #f0f0f0;
|
||||||
|
--card-background: #ffffff;
|
||||||
|
--border-color: #e0e0e0;
|
||||||
|
--hover-color: #f0f0f0;
|
||||||
|
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
--background-light: #f0f0f0;
|
||||||
|
/* Added for input backgrounds */
|
||||||
|
--text-primary: #121212;
|
||||||
|
/* Added for input text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default dark mode variables that were missing or hardcoded */
|
||||||
|
:root {
|
||||||
|
--background-light: #2a2a2a;
|
||||||
|
--text-primary: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -93,7 +170,7 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-md);
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -105,7 +182,8 @@ body {
|
|||||||
|
|
||||||
.url-input {
|
.url-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 20px;
|
height: var(--input-height);
|
||||||
|
padding: 0 20px;
|
||||||
border-radius: 24px 0 0 24px;
|
border-radius: 24px 0 0 24px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
background-color: var(--background-light);
|
background-color: var(--background-light);
|
||||||
@@ -129,6 +207,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
|
height: var(--input-height);
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
background-color: var(--background-light);
|
background-color: var(--background-light);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -164,8 +243,10 @@ body {
|
|||||||
/* Downloads Indicator */
|
/* Downloads Indicator */
|
||||||
.downloads-indicator-container {
|
.downloads-indicator-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: 10px;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads-summary {
|
.downloads-summary {
|
||||||
@@ -174,8 +255,10 @@ body {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
background-color: var(--background-light);
|
background-color: var(--background-light);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 8px 16px;
|
padding: 0 16px;
|
||||||
border-radius: 20px;
|
height: var(--input-height);
|
||||||
|
border-radius: 23px;
|
||||||
|
/* Half of 46px */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -208,7 +291,7 @@ body {
|
|||||||
top: calc(100% + 10px);
|
top: calc(100% + 10px);
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background-color: var(--background-card);
|
background-color: var(--card-background);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
@@ -456,7 +539,7 @@ body {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-actions-row {
|
.video-actions-row {
|
||||||
@@ -489,25 +572,37 @@ body {
|
|||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-mode .btn-secondary {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode .btn-secondary:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--text-color);
|
background-color: var(--text-color);
|
||||||
color: #000;
|
color: var(--background-darker);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: #e0e0e0;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
color: #ff3e3e;
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode .btn-danger {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
@@ -516,7 +611,7 @@ body {
|
|||||||
|
|
||||||
/* Channel/Description Box */
|
/* Channel/Description Box */
|
||||||
.channel-desc-container {
|
.channel-desc-container {
|
||||||
background-color: #2a2a2a;
|
background-color: var(--background-light);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -549,7 +644,7 @@ body {
|
|||||||
.channel-name {
|
.channel-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +660,7 @@ body {
|
|||||||
.description-text {
|
.description-text {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +677,7 @@ body {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.related-videos-list {
|
.related-videos-list {
|
||||||
@@ -623,7 +718,7 @@ body {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: #fff;
|
color: var(--text-color);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@@ -1134,6 +1229,21 @@ body {
|
|||||||
border-color: #ff3e3e;
|
border-color: #ff3e3e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-mode .glass-panel:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(62, 166, 255, 0.5);
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-mode .glass-panel:focus {
|
||||||
|
border-color: rgba(62, 166, 255, 0.5);
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.video-link,
|
.video-link,
|
||||||
.author-link {
|
.author-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -1505,8 +1615,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%);
|
background: var(--card-background);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
@@ -1515,9 +1625,13 @@ body {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-mode .modal-content {
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
padding: 24px 24px 20px;
|
padding: 24px 24px 20px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -1630,8 +1744,8 @@ body {
|
|||||||
padding: 20px 24px 24px;
|
padding: 20px 24px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: var(--background-lighter);
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .btn {
|
.modal-footer .btn {
|
||||||
@@ -1670,14 +1784,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .secondary-btn {
|
.modal-footer .secondary-btn {
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
background-color: var(--background-light);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .secondary-btn:hover:not(:disabled) {
|
.modal-footer .secondary-btn:hover:not(:disabled) {
|
||||||
background-color: rgba(255, 255, 255, 0.12);
|
background-color: var(--hover-color);
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive modal styles */
|
/* Responsive modal styles */
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [isCheckingParts, setIsCheckingParts] = useState<boolean>(false);
|
const [isCheckingParts, setIsCheckingParts] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Theme state
|
||||||
|
const [theme, setTheme] = useState<string>(() => {
|
||||||
|
return localStorage.getItem('theme') || 'dark';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply theme to body
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.className = theme === 'light' ? 'light-mode' : '';
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
// Reference to the current search request's abort controller
|
// Reference to the current search request's abort controller
|
||||||
const searchAbortController = useRef<AbortController | null>(null);
|
const searchAbortController = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
@@ -636,6 +651,8 @@ function App() {
|
|||||||
isSearchMode={isSearchMode}
|
isSearchMode={isSearchMode}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
onResetSearch={resetSearch}
|
onResetSearch={resetSearch}
|
||||||
|
theme={theme}
|
||||||
|
toggleTheme={toggleTheme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Bilibili Parts Modal */}
|
{/* Bilibili Parts Modal */}
|
||||||
|
|||||||
55
frontend/src/components/ConfirmationModal.tsx
Normal file
55
frontend/src/components/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ConfirmationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
isDanger?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmText = 'Confirm',
|
||||||
|
cancelText = 'Cancel',
|
||||||
|
isDanger = false
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<button className="close-btn" onClick={onClose}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button className="btn secondary-btn" onClick={onClose}>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn ${isDanger ? 'danger-btn' : 'primary-btn'}`}
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationModal;
|
||||||
@@ -35,21 +35,17 @@ const DeleteCollectionModal: React.FC<DeleteCollectionModalProps> = ({
|
|||||||
|
|
||||||
<div className="modal-actions" style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div className="modal-actions" style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
<button
|
<button
|
||||||
className="btn secondary-btn"
|
className="btn secondary-btn glass-panel"
|
||||||
onClick={onDeleteCollectionOnly}
|
onClick={onDeleteCollectionOnly}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
background: 'rgba(255, 255, 255, 0.05)',
|
|
||||||
color: 'var(--text-color)',
|
color: 'var(--text-color)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s ease'
|
|
||||||
}}
|
}}
|
||||||
onMouseOver={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)'}
|
|
||||||
onMouseOut={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
Delete Collection Only
|
Delete Collection Only
|
||||||
</button>
|
</button>
|
||||||
{videoCount > 0 && (
|
{videoCount > 0 && (
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface HeaderProps {
|
|||||||
isSearchMode?: boolean;
|
isSearchMode?: boolean;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
onResetSearch?: () => void;
|
onResetSearch?: () => void;
|
||||||
|
theme: string;
|
||||||
|
toggleTheme: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({
|
const Header: React.FC<HeaderProps> = ({
|
||||||
@@ -23,7 +25,9 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
activeDownloads = [],
|
activeDownloads = [],
|
||||||
isSearchMode = false,
|
isSearchMode = false,
|
||||||
searchTerm = '',
|
searchTerm = '',
|
||||||
onResetSearch
|
onResetSearch,
|
||||||
|
theme,
|
||||||
|
toggleTheme
|
||||||
}) => {
|
}) => {
|
||||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
@@ -98,10 +102,35 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<Link to="/" className="logo">
|
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
||||||
<img src={logo} alt="MyTube Logo" className="logo-icon" />
|
<Link to="/" className="logo">
|
||||||
<span style={{ color: '#f0f0f0' }}>MyTube</span>
|
<img src={logo} alt="MyTube Logo" className="logo-icon" />
|
||||||
</Link>
|
<span style={{ color: 'var(--text-color)' }}>MyTube</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="theme-toggle-btn"
|
||||||
|
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'var(--text-color)',
|
||||||
|
transition: 'background-color 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(128, 128, 128, 0.1)'}
|
||||||
|
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? '☀️' : '🌙'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form className="url-form" onSubmit={handleSubmit}>
|
<form className="url-form" onSubmit={handleSubmit}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Collection, Video } from '../types';
|
import { Collection, Video } from '../types';
|
||||||
|
import ConfirmationModal from './ConfirmationModal';
|
||||||
|
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
|
||||||
// Format the date (assuming format YYYYMMDD from youtube-dl)
|
// Format the date (assuming format YYYYMMDD from youtube-dl)
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
@@ -44,22 +46,26 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
navigate(`/author/${encodeURIComponent(video.author)}`);
|
navigate(`/author/${encodeURIComponent(video.author)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete click
|
// Handle confirm delete
|
||||||
const handleDeleteClick = async (e: React.MouseEvent) => {
|
const confirmDelete = async () => {
|
||||||
e.stopPropagation();
|
|
||||||
if (!onDeleteVideo) return;
|
if (!onDeleteVideo) return;
|
||||||
|
|
||||||
if (window.confirm(`Are you sure you want to delete "${video.title}"?`)) {
|
setIsDeleting(true);
|
||||||
setIsDeleting(true);
|
try {
|
||||||
try {
|
await onDeleteVideo(video.id);
|
||||||
await onDeleteVideo(video.id);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('Error deleting video:', error);
|
||||||
console.error('Error deleting video:', error);
|
setIsDeleting(false);
|
||||||
setIsDeleting(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle delete click
|
||||||
|
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!onDeleteVideo) return;
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
// Find collections this video belongs to
|
// Find collections this video belongs to
|
||||||
const videoCollections = collections.filter(collection =>
|
const videoCollections = collections.filter(collection =>
|
||||||
collection.videos.includes(video.id)
|
collection.videos.includes(video.id)
|
||||||
@@ -108,82 +114,95 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`video-card ${isFirstInAnyCollection ? 'collection-first' : ''}`}>
|
<>
|
||||||
<div
|
<div className={`video-card ${isFirstInAnyCollection ? 'collection-first' : ''}`}>
|
||||||
className="thumbnail-container clickable"
|
{/* ... (rest of the video card JSX) ... */}
|
||||||
onClick={handleVideoNavigation}
|
<div
|
||||||
aria-label={isFirstInAnyCollection
|
className="thumbnail-container clickable"
|
||||||
? `View collection: ${firstInCollectionNames[0]}${firstInCollectionNames.length > 1 ? ' and others' : ''}`
|
|
||||||
: `Play ${video.title}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={thumbnailSrc || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
|
||||||
alt={`${video.title} thumbnail`}
|
|
||||||
className="thumbnail"
|
|
||||||
loading="lazy"
|
|
||||||
onError={(e) => {
|
|
||||||
const target = e.target as HTMLImageElement;
|
|
||||||
target.onerror = null;
|
|
||||||
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{getSourceIcon()}
|
|
||||||
|
|
||||||
{/* Show part number for multi-part videos */}
|
|
||||||
{video.partNumber && video.totalParts && video.totalParts > 1 && (
|
|
||||||
<div className="part-badge">
|
|
||||||
Part {video.partNumber}/{video.totalParts}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show collection badge if this is the first video in a collection */}
|
|
||||||
{isFirstInAnyCollection && (
|
|
||||||
<div className="collection-badge" title={`Collection${firstInCollectionNames.length > 1 ? 's' : ''}: ${firstInCollectionNames.join(', ')}`}>
|
|
||||||
<span className="collection-icon">📁</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Delete button overlay */}
|
|
||||||
{showDeleteButton && onDeleteVideo && (
|
|
||||||
<button
|
|
||||||
className="card-delete-btn"
|
|
||||||
onClick={handleDeleteClick}
|
|
||||||
disabled={isDeleting}
|
|
||||||
title="Delete video"
|
|
||||||
>
|
|
||||||
{isDeleting ? '...' : '×'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="video-info">
|
|
||||||
<h3
|
|
||||||
className="video-title clickable"
|
|
||||||
onClick={handleVideoNavigation}
|
onClick={handleVideoNavigation}
|
||||||
|
aria-label={isFirstInAnyCollection
|
||||||
|
? `View collection: ${firstInCollectionNames[0]}${firstInCollectionNames.length > 1 ? ' and others' : ''}`
|
||||||
|
: `Play ${video.title}`}
|
||||||
>
|
>
|
||||||
{isFirstInAnyCollection ? (
|
<img
|
||||||
<>
|
src={thumbnailSrc || 'https://via.placeholder.com/480x360?text=No+Thumbnail'}
|
||||||
{firstInCollectionNames[0]}
|
alt={`${video.title} thumbnail`}
|
||||||
{firstInCollectionNames.length > 1 && <span className="more-collections"> +{firstInCollectionNames.length - 1}</span>}
|
className="thumbnail"
|
||||||
</>
|
loading="lazy"
|
||||||
) : (
|
onError={(e) => {
|
||||||
video.title
|
const target = e.target as HTMLImageElement;
|
||||||
)}
|
target.onerror = null;
|
||||||
</h3>
|
target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
|
||||||
<div className="video-meta">
|
}}
|
||||||
<span
|
/>
|
||||||
className="author-link"
|
{getSourceIcon()}
|
||||||
onClick={handleAuthorClick}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`View all videos by ${video.author}`}
|
|
||||||
>
|
|
||||||
{video.author}
|
|
||||||
</span>
|
|
||||||
<span className="video-date">{formatDate(video.date)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Show part number for multi-part videos */}
|
||||||
|
{video.partNumber && video.totalParts && video.totalParts > 1 && (
|
||||||
|
<div className="part-badge">
|
||||||
|
Part {video.partNumber}/{video.totalParts}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show collection badge if this is the first video in a collection */}
|
||||||
|
{isFirstInAnyCollection && (
|
||||||
|
<div className="collection-badge" title={`Collection${firstInCollectionNames.length > 1 ? 's' : ''}: ${firstInCollectionNames.join(', ')}`}>
|
||||||
|
<span className="collection-icon">📁</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete button overlay */}
|
||||||
|
{showDeleteButton && onDeleteVideo && (
|
||||||
|
<button
|
||||||
|
className="card-delete-btn"
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
disabled={isDeleting}
|
||||||
|
title="Delete video"
|
||||||
|
>
|
||||||
|
{isDeleting ? '...' : '×'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="video-info">
|
||||||
|
<h3
|
||||||
|
className="video-title clickable"
|
||||||
|
onClick={handleVideoNavigation}
|
||||||
|
>
|
||||||
|
{isFirstInAnyCollection ? (
|
||||||
|
<>
|
||||||
|
{firstInCollectionNames[0]}
|
||||||
|
{firstInCollectionNames.length > 1 && <span className="more-collections"> +{firstInCollectionNames.length - 1}</span>}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
video.title
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div className="video-meta">
|
||||||
|
<span
|
||||||
|
className="author-link"
|
||||||
|
onClick={handleAuthorClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`View all videos by ${video.author}`}
|
||||||
|
>
|
||||||
|
{video.author}
|
||||||
|
</span>
|
||||||
|
<span className="video-date">{formatDate(video.date)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
onClose={() => setShowDeleteModal(false)}
|
||||||
|
onConfirm={confirmDelete}
|
||||||
|
title="Delete Video"
|
||||||
|
message={`Are you sure you want to delete "${video.title}"?`}
|
||||||
|
confirmText="Delete"
|
||||||
|
isDanger={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
/* CSS Reset */
|
/* CSS Reset */
|
||||||
*, *::before, *::after {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, #root {
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: #121212;
|
background-color: var(--background-dark);
|
||||||
color: #f0f0f0;
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -29,7 +33,10 @@ a {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, select, textarea {
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
@@ -42,11 +49,13 @@ button {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul,
|
||||||
|
ol {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
img, video {
|
img,
|
||||||
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -93,4 +102,4 @@ button:focus-visible {
|
|||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #444;
|
background: #444;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import ConfirmationModal from '../components/ConfirmationModal';
|
||||||
import { Collection, Video } from '../types';
|
import { Collection, Video } from '../types';
|
||||||
|
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||||
@@ -16,18 +17,27 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
|||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [collectionToDelete, setCollectionToDelete] = useState<Collection | null>(null);
|
const [collectionToDelete, setCollectionToDelete] = useState<Collection | null>(null);
|
||||||
const [isDeletingCollection, setIsDeletingCollection] = useState<boolean>(false);
|
const [isDeletingCollection, setIsDeletingCollection] = useState<boolean>(false);
|
||||||
|
const [videoToDelete, setVideoToDelete] = useState<string | null>(null);
|
||||||
|
const [showVideoDeleteModal, setShowVideoDeleteModal] = useState<boolean>(false);
|
||||||
|
|
||||||
const filteredVideos = videos.filter(video =>
|
const filteredVideos = videos.filter(video =>
|
||||||
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
video.author.toLowerCase().includes(searchTerm.toLowerCase())
|
video.author.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const confirmDeleteVideo = async () => {
|
||||||
if (window.confirm('Are you sure you want to delete this video?')) {
|
if (!videoToDelete) return;
|
||||||
setDeletingId(id);
|
|
||||||
await onDeleteVideo(id);
|
setDeletingId(videoToDelete);
|
||||||
setDeletingId(null);
|
await onDeleteVideo(videoToDelete);
|
||||||
}
|
setDeletingId(null);
|
||||||
|
setVideoToDelete(null);
|
||||||
|
setShowVideoDeleteModal(false); // Close the modal after deletion
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
setVideoToDelete(id);
|
||||||
|
setShowVideoDeleteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDeleteCollection = (collection: Collection) => {
|
const confirmDeleteCollection = (collection: Collection) => {
|
||||||
@@ -96,7 +106,8 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
|||||||
disabled={isDeletingCollection}
|
disabled={isDeletingCollection}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: 'linear-gradient(135deg, #ff3e3e 0%, #ff6b6b 100%)'
|
background: 'linear-gradient(135deg, #ff3e3e 0%, #ff6b6b 100%)',
|
||||||
|
color: 'white'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDeletingCollection ? 'Deleting...' : '⚠️ Delete Collection & Videos'}
|
{isDeletingCollection ? 'Deleting...' : '⚠️ Delete Collection & Videos'}
|
||||||
@@ -118,6 +129,19 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showVideoDeleteModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowVideoDeleteModal(false);
|
||||||
|
setVideoToDelete(null);
|
||||||
|
}}
|
||||||
|
onConfirm={confirmDeleteVideo}
|
||||||
|
title="Delete Video"
|
||||||
|
message="Are you sure you want to delete this video?"
|
||||||
|
confirmText="Delete"
|
||||||
|
isDanger={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="manage-section">
|
<div className="manage-section">
|
||||||
<h2>Collections ({collections.length})</h2>
|
<h2>Collections ({collections.length})</h2>
|
||||||
<div className="manage-list">
|
<div className="manage-list">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import ConfirmationModal from '../components/ConfirmationModal';
|
||||||
import { Collection, Video } from '../types';
|
import { Collection, Video } from '../types';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||||
|
|
||||||
@@ -35,6 +35,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const [selectedCollection, setSelectedCollection] = useState<string>('');
|
const [selectedCollection, setSelectedCollection] = useState<string>('');
|
||||||
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
|
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
|
||||||
|
|
||||||
|
// Confirmation Modal State
|
||||||
|
const [confirmationModal, setConfirmationModal] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
onConfirm: () => { },
|
||||||
|
confirmText: 'Confirm',
|
||||||
|
isDanger: false
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't try to fetch the video if it's being deleted
|
// Don't try to fetch the video if it's being deleted
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
@@ -110,13 +120,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
navigate(`/collection/${collectionId}`);
|
navigate(`/collection/${collectionId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const executeDelete = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
if (!window.confirm('Are you sure you want to delete this video?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
setDeleteError(null);
|
setDeleteError(null);
|
||||||
|
|
||||||
@@ -137,6 +143,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setConfirmationModal({
|
||||||
|
isOpen: true,
|
||||||
|
title: 'Delete Video',
|
||||||
|
message: 'Are you sure you want to delete this video?',
|
||||||
|
onConfirm: executeDelete,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
isDanger: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddToCollection = () => {
|
const handleAddToCollection = () => {
|
||||||
setShowCollectionModal(true);
|
setShowCollectionModal(true);
|
||||||
};
|
};
|
||||||
@@ -173,13 +190,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFromCollection = async () => {
|
const executeRemoveFromCollection = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
if (!window.confirm('Are you sure you want to remove this video from the collection?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onRemoveFromCollection(id);
|
await onRemoveFromCollection(id);
|
||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
@@ -188,6 +201,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromCollection = () => {
|
||||||
|
setConfirmationModal({
|
||||||
|
isOpen: true,
|
||||||
|
title: 'Remove from Collection',
|
||||||
|
message: 'Are you sure you want to remove this video from the collection?',
|
||||||
|
onConfirm: executeRemoveFromCollection,
|
||||||
|
confirmText: 'Remove',
|
||||||
|
isDanger: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="loading">Loading video...</div>;
|
return <div className="loading">Loading video...</div>;
|
||||||
}
|
}
|
||||||
@@ -255,24 +279,24 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deleteError && (
|
{deleteError && (
|
||||||
<div className="error-message" style={{ color: '#ff4d4d', marginTop: '10px' }}>
|
<div className="error-message" style={{ color: 'var(--primary-color)', marginTop: '10px' }}>
|
||||||
{deleteError}
|
{deleteError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="channel-desc-container">
|
<div className="channel-desc-container">
|
||||||
<div className="video-stats" style={{ marginBottom: '8px', color: '#fff', fontWeight: 'bold' }}>
|
<div className="video-stats" style={{ marginBottom: '8px', color: 'var(--text-secondary)', fontWeight: 'bold' }}>
|
||||||
{/* Views would go here */}
|
{/* Views would go here */}
|
||||||
{formatDate(video.date)}
|
{formatDate(video.date)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="description-text">
|
<div className="description-text" style={{ color: 'var(--text-color)' }}>
|
||||||
{/* We don't have a real description, so we'll show some metadata */}
|
{/* We don't have a real description, so we'll show some metadata */}
|
||||||
<p>Source: {video.source === 'bilibili' ? 'Bilibili' : 'YouTube'}</p>
|
<p>Source: {video.source === 'bilibili' ? 'Bilibili' : 'YouTube'}</p>
|
||||||
{video.sourceUrl && (
|
{video.sourceUrl && (
|
||||||
<p>
|
<p>
|
||||||
Original Link: <a href={video.sourceUrl} target="_blank" rel="noopener noreferrer" style={{ color: '#3ea6ff' }}>{video.sourceUrl}</a>
|
Original Link: <a href={video.sourceUrl} target="_blank" rel="noopener noreferrer" style={{ color: 'var(--primary-color)' }}>{video.sourceUrl}</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -375,34 +399,31 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
<select
|
<select
|
||||||
value={selectedCollection}
|
value={selectedCollection}
|
||||||
onChange={(e) => setSelectedCollection(e.target.value)}
|
onChange={(e) => setSelectedCollection(e.target.value)}
|
||||||
|
className="glass-panel"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
paddingRight: '40px',
|
paddingRight: '40px',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: 'var(--text-color)',
|
color: 'var(--text-color)',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
marginBottom: '0.8rem',
|
marginBottom: '0.8rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
appearance: 'none',
|
appearance: 'none',
|
||||||
backgroundImage: `url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e")`,
|
backgroundImage: `url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='var(--text-color)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e")`,
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: 'right 12px center',
|
backgroundPosition: 'right 12px center',
|
||||||
backgroundSize: '16px',
|
backgroundSize: '16px',
|
||||||
transition: 'all 0.2s ease'
|
|
||||||
}}
|
}}
|
||||||
onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'}
|
|
||||||
onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.05)'}
|
|
||||||
>
|
>
|
||||||
<option value="" style={{ color: 'black' }}>Select a collection</option>
|
|
||||||
|
<option value="" style={{ color: 'var(--text-color)', backgroundColor: 'var(--background-card)' }}>Select a collection</option>
|
||||||
{collections.map(collection => (
|
{collections.map(collection => (
|
||||||
<option
|
<option
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
value={collection.id}
|
value={collection.id}
|
||||||
disabled={videoCollections.length > 0 && videoCollections[0].id === collection.id}
|
disabled={videoCollections.length > 0 && videoCollections[0].id === collection.id}
|
||||||
style={{ color: 'black' }}
|
style={{ color: 'var(--text-color)', backgroundColor: 'var(--background-card)' }}
|
||||||
>
|
>
|
||||||
{collection.name} {videoCollections.length > 0 && videoCollections[0].id === collection.id ? '(Current)' : ''}
|
{collection.name} {videoCollections.length > 0 && videoCollections[0].id === collection.id ? '(Current)' : ''}
|
||||||
</option>
|
</option>
|
||||||
@@ -437,7 +458,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="collection-input"
|
className="collection-input glass-panel"
|
||||||
placeholder="Collection name"
|
placeholder="Collection name"
|
||||||
value={newCollectionName}
|
value={newCollectionName}
|
||||||
onChange={(e) => setNewCollectionName(e.target.value)}
|
onChange={(e) => setNewCollectionName(e.target.value)}
|
||||||
@@ -445,16 +466,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
color: 'var(--text-color)',
|
color: 'var(--text-color)',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
marginBottom: '0.8rem',
|
marginBottom: '0.8rem',
|
||||||
transition: 'all 0.2s ease'
|
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => e.currentTarget.style.borderColor = 'rgba(62, 166, 255, 0.5)'}
|
|
||||||
onBlur={(e) => e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)'}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="modal-btn primary-btn"
|
className="modal-btn primary-btn"
|
||||||
@@ -487,6 +503,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={confirmationModal.isOpen}
|
||||||
|
onClose={() => setConfirmationModal({ ...confirmationModal, isOpen: false })}
|
||||||
|
onConfirm={confirmationModal.onConfirm}
|
||||||
|
title={confirmationModal.title}
|
||||||
|
message={confirmationModal.message}
|
||||||
|
confirmText={confirmationModal.confirmText}
|
||||||
|
isDanger={confirmationModal.isDanger}
|
||||||
|
/>
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user