core/torrent-handler/ui-components.js
2025-02-27 19:18:54 +01:00

489 lines
15 KiB
JavaScript

// core/torrent-handler/components/TorrentDashboard.js
import React, { useState, useEffect } from 'react';
export const TorrentDashboard = ({ core }) => {
const [torrents, setTorrents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const loadTorrents = async () => {
setLoading(true);
try {
const torrentList = await core.getTorrents();
setTorrents(torrentList);
setError(null);
} catch (err) {
setError('Failed to load torrents: ' + err.message);
} finally {
setLoading(false);
}
};
// Load torrents on component mount
useEffect(() => {
loadTorrents();
// Set up refresh interval
const interval = setInterval(loadTorrents, 3000);
// Clean up interval on unmount
return () => clearInterval(interval);
}, []);
const handleStart = async (torrentId) => {
try {
await core.startTorrent(torrentId);
loadTorrents();
} catch (err) {
setError('Failed to start torrent: ' + err.message);
}
};
const handleStop = async (torrentId) => {
try {
await core.stopTorrent(torrentId);
loadTorrents();
} catch (err) {
setError('Failed to stop torrent: ' + err.message);
}
};
const handleRemove = async (torrentId, deleteData) => {
if (confirm(`Are you sure you want to remove this torrent${deleteData ? ' and its data' : ''}?`)) {
try {
await core.removeTorrent(torrentId, deleteData);
loadTorrents();
} catch (err) {
setError('Failed to remove torrent: ' + err.message);
}
}
};
// Helper function to format size
const formatSize = (bytes) => {
if (bytes === 0) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
};
// Helper function to format status
const getStatusText = (status) => {
const statusMap = {
0: 'Stopped',
1: 'Queued to check',
2: 'Checking',
3: 'Queued to download',
4: 'Downloading',
5: 'Queued to seed',
6: 'Seeding'
};
return statusMap[status] || 'Unknown';
};
return (
<div className="torrent-dashboard">
<h1>Torrent Dashboard</h1>
{error && (
<div className="form-actions">
<button type="button" onClick={() => navigate('/torrents')}>Cancel</button>
<button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Settings'}
</button>
</div>
</form>
</div>
);
};
// core/torrent-handler/components/index.js
// Export all components for easier imports
export { TorrentDashboard } from './TorrentDashboard';
export { AddTorrentForm } from './AddTorrentForm';
export { TorrentSettings } from './TorrentSettings';
error-message">
{error}
<button onClick={() => setError(null)}>Dismiss</button>
</div>
)}
<div className="dashboard-actions">
<button onClick={loadTorrents} disabled={loading}>
{loading ? 'Refreshing...' : 'Refresh'}
</button>
<a href="#/torrents/add" className="button">Add Torrent</a>
</div>
{loading && torrents.length === 0 ? (
<div className="loading">Loading torrents...</div>
) : torrents.length === 0 ? (
<div className="empty-state">
<p>No torrents found.</p>
<a href="#/torrents/add" className="button">Add a torrent</a>
</div>
) : (
<table className="torrents-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Progress</th>
<th>Size</th>
<th>Down</th>
<th>Up</th>
<th>Ratio</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{torrents.map(torrent => (
<tr key={torrent.id}>
<td>{torrent.name}</td>
<td>{getStatusText(torrent.status)}</td>
<td>
<div className="form-group">
<label htmlFor="default_seed_ratio">Default Seed Ratio:</label>
<input
type="number"
id="default_seed_ratio"
name="default_seed_ratio"
value={settings.default_seed_ratio}
onChange={handleChange}
min="0"
step="0.1"
/>
<p className="help-text">Set to 0 for unlimited</p>
</div>
<div className="form-group">
<label htmlFor="default_seed_time">Default Seed Time (minutes):</label>
<input
type="number"
id="default_seed_time"
name="default_seed_time"
value={settings.default_seed_time}
onChange={handleChange}
min="0"
/>
<p className="help-text">Set to 0 for unlimited</p>
</div>
<div className="form-group">
<label htmlFor="auto_start_torrents">
<input
type="checkbox"
id="auto_start_torrents"
name="auto_start_torrents"
checked={settings.auto_start_torrents}
onChange={handleChange}
/>
Start torrents automatically when added
</label>
</div>
</fieldset>
<div className="form-group">
<label htmlFor="transmission_username">Username:</label>
<input
type="text"
id="transmission_username"
name="transmission_username"
value={settings.transmission_username}
onChange={handleChange}
/>
<p className="help-text">Leave empty if authentication is not required</p>
</div>
<div className="form-group">
<label htmlFor="transmission_password">Password:</label>
<input
type="password"
id="transmission_password"
name="transmission_password"
value={settings.transmission_password}
onChange={handleChange}
/>
</div>
<div className="form-actions">
<button type="button" onClick={handleTestConnection} disabled={loading}>
{loading ? 'Testing...' : 'Test Connection'}
</button>
</div>
</fieldset>
<fieldset>
<legend>Default Torrent Settings</legend>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${Math.round(torrent.percentDone * 100)}%` }}
></div>
<span>{Math.round(torrent.percentDone * 100)}%</span>
</div>
</td>
<td>{formatSize(torrent.totalSize)}</td>
<td>{formatSize(torrent.rateDownload)}/s</td>
<td>{formatSize(torrent.rateUpload)}/s</td>
<td>{torrent.uploadRatio.toFixed(2)}</td>
<td className="actions">
{[0, 3, 5].includes(torrent.status) ? (
<button onClick={() => handleStart(torrent.id)}>Start</button>
) : (
<button onClick={() => handleStop(torrent.id)}>Stop</button>
)}
<button onClick={() => handleRemove(torrent.id, false)}>Remove</button>
<button onClick={() => handleRemove(torrent.id, true)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
// core/torrent-handler/components/AddTorrentForm.js
import React, { useState } from 'react';
export const AddTorrentForm = ({ core, navigate }) => {
const [torrentFile, setTorrentFile] = useState(null);
const [seedRatio, setSeedRatio] = useState(core.config.default_seed_ratio);
const [seedTime, setSeedTime] = useState(core.config.default_seed_time);
const [autoStart, setAutoStart] = useState(core.config.auto_start_torrents);
const [downloadDir, setDownloadDir] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file && (file.name.endsWith('.torrent') || file.type === 'application/x-bittorrent')) {
setTorrentFile(file);
setError(null);
} else {
setTorrentFile(null);
setError('Please select a valid .torrent file');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!torrentFile) {
setError('Please select a torrent file');
return;
}
setLoading(true);
setError(null);
try {
await core.addTorrent(torrentFile, {
seedRatio: parseFloat(seedRatio),
seedTime: parseInt(seedTime, 10),
autoStart: autoStart,
downloadDirectory: downloadDir || undefined
});
// Redirect to dashboard
navigate('/torrents');
} catch (err) {
setError('Failed to add torrent: ' + err.message);
setLoading(false);
}
};
return (
<div className="add-torrent-form">
<h1>Add New Torrent</h1>
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>Dismiss</button>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="torrent-file">Torrent File:</label>
<input
type="file"
id="torrent-file"
accept=".torrent,application/x-bittorrent"
onChange={handleFileChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="seed-ratio">Seed Ratio:</label>
<input
type="number"
id="seed-ratio"
value={seedRatio}
onChange={e => setSeedRatio(e.target.value)}
min="0"
step="0.1"
/>
<p className="help-text">Set to 0 for unlimited</p>
</div>
<div className="form-group">
<label htmlFor="seed-time">Seed Time (minutes):</label>
<input
type="number"
id="seed-time"
value={seedTime}
onChange={e => setSeedTime(e.target.value)}
min="0"
/>
<p className="help-text">Set to 0 for unlimited</p>
</div>
<div className="form-group">
<label htmlFor="auto-start">
<input
type="checkbox"
id="auto-start"
checked={autoStart}
onChange={e => setAutoStart(e.target.checked)}
/>
Start torrent automatically
</label>
</div>
<div className="form-group">
<label htmlFor="download-dir">Download Directory (optional):</label>
<input
type="text"
id="download-dir"
value={downloadDir}
onChange={e => setDownloadDir(e.target.value)}
placeholder="Leave empty for default"
/>
</div>
<div className="form-actions">
<button type="button" onClick={() => navigate('/torrents')}>Cancel</button>
<button type="submit" disabled={loading || !torrentFile}>
{loading ? 'Adding...' : 'Add Torrent'}
</button>
</div>
</form>
</div>
);
};
// core/torrent-handler/components/TorrentSettings.js
import React, { useState } from 'react';
export const TorrentSettings = ({ core, navigate }) => {
const [settings, setSettings] = useState({
default_seed_ratio: core.config.default_seed_ratio,
default_seed_time: core.config.default_seed_time,
auto_start_torrents: core.config.auto_start_torrents,
transmission_url: core.config.transmission_url,
transmission_username: core.config.transmission_username,
transmission_password: core.config.transmission_password
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setSettings({
...settings,
[name]: type === 'checkbox' ? checked : value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);
try {
// Update core configuration
core.config = { ...core.config, ...settings };
// Save configuration to framework persistence
await core.framework.saveConfiguration(core.id, core.config);
setSuccess(true);
} catch (err) {
setError('Failed to save settings: ' + err.message);
} finally {
setLoading(false);
}
};
const handleTestConnection = async () => {
setLoading(true);
setError(null);
setSuccess(false);
try {
// Temporarily update connection settings
const originalConfig = { ...core.config };
core.config = {
...core.config,
transmission_url: settings.transmission_url,
transmission_username: settings.transmission_username,
transmission_password: settings.transmission_password
};
// Test connection
await core.connectToTransmission();
setSuccess('Connection successful!');
} catch (err) {
setError('Connection test failed: ' + err.message);
} finally {
setLoading(false);
}
};
return (
<div className="torrent-settings">
<h1>Torrent Settings</h1>
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>Dismiss</button>
</div>
)}
{success && (
<div className="success-message">
{typeof success === 'string' ? success : 'Settings saved successfully!'}
<button onClick={() => setSuccess(false)}>Dismiss</button>
</div>
)}
<form onSubmit={handleSubmit}>
<fieldset>
<legend>Connection Settings</legend>
<div className="form-group">
<label htmlFor="transmission_url">Transmission URL:</label>
<input
type="text"
id="transmission_url"
name="transmission_url"
value={settings.transmission_url}
onChange={handleChange}
required
/>
<p className="help-text">URL to Transmission RPC interface (e.g. http://localhost:9091/transmission/rpc)</p>
</div>
<div className="