489 lines
15 KiB
JavaScript
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=" |