BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 1 | import React, { useRef, useState, useEffect } from 'react'; |
| 2 | import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined, DownloadOutlined, CommentOutlined } from '@ant-design/icons'; |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 3 | import { |
| 4 | Button, |
| 5 | Modal, |
| 6 | message, |
| 7 | Drawer, |
| 8 | Form, |
| 9 | Input, |
| 10 | InputNumber, |
| 11 | DatePicker, |
| 12 | Card, |
| 13 | Layout, |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 14 | Upload, |
| 15 | UploadProps, |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 16 | Select, |
| 17 | Tag, // 导入Tag组件用于显示标签 |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 18 | } from 'antd'; |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 19 | import { ProTable, ActionType, ProColumns, ProDescriptions, ProDescriptionsItemProps } from '@ant-design/pro-components'; |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 20 | import { useNavigate } from 'react-router-dom'; |
| 21 | import type { BtTorrent, BtTorrentTag } from './data'; |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 22 | import { |
| 23 | listBtTorrent, |
| 24 | getBtTorrent, |
| 25 | addBtTorrent, |
| 26 | updateBtTorrent, |
| 27 | removeBtTorrent, |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 28 | uploadTorrent, |
| 29 | downloadTorrent, |
| 30 | addBtTorrentTag, |
| 31 | listBtTorrentTags, |
| 32 | getBtTorrentTag |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 33 | } from './service'; |
| 34 | |
| 35 | const { Content } = Layout; |
| 36 | |
| 37 | const BtTorrentPage: React.FC = () => { |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 38 | const navigate = useNavigate(); |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 39 | const actionRef = useRef<ActionType>(); |
| 40 | const [form] = Form.useForm(); |
| 41 | const [modalVisible, setModalVisible] = useState(false); |
| 42 | const [drawerVisible, setDrawerVisible] = useState(false); |
| 43 | const [current, setCurrent] = useState<Partial<BtTorrent>>({}); |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 44 | const [uploadModalVisible, setUploadModalVisible] = useState(false); // State for upload modal |
| 45 | const [uploadFile, setUploadFile] = useState<File | null>(null); // State to store selected file |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 46 | const [uploadForm] = Form.useForm(); // Form for upload modal |
| 47 | const [torrentTags, setTorrentTags] = useState<BtTorrentTag[]>([]); // 修改为数组类型来存储多个标签 |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 48 | |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 49 | // Columns for the ProTable (the table displaying torrents) |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 50 | const columns: ProColumns<BtTorrent>[] = [ |
| 51 | { |
| 52 | title: '种子ID', |
| 53 | dataIndex: 'torrentId', |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 54 | render: (dom, entity) => ( |
| 55 | <a |
| 56 | onClick={async () => { |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 57 | try { |
| 58 | // 获取详细信息 |
| 59 | const res = await getBtTorrent(entity.torrentId!); |
| 60 | console.log('获取的种子详情:', res); // 调试用 |
| 61 | |
| 62 | // 确保res是对象类型并且包含数据 |
| 63 | if (res && typeof res === 'object') { |
| 64 | // 如果API返回了data包装,则提取data |
| 65 | const torrentData = res.data ? res.data : res; |
| 66 | setCurrent(torrentData); |
| 67 | |
| 68 | // 先设置当前种子,然后获取标签 |
| 69 | await handleGetTags(entity.torrentId!); |
| 70 | setDrawerVisible(true); |
| 71 | } else { |
| 72 | message.error('获取种子详情格式错误'); |
| 73 | } |
| 74 | } catch (error) { |
| 75 | console.error('获取种子详情出错:', error); |
| 76 | message.error('获取种子详情失败'); |
| 77 | } |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 78 | }} |
| 79 | > |
| 80 | {dom} |
| 81 | </a> |
| 82 | ), |
| 83 | }, |
| 84 | { title: '名称', dataIndex: 'name' }, |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 85 | // { title: 'infoHash', dataIndex: 'infoHash' }, |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 86 | { title: '大小 (bytes)', dataIndex: 'length', valueType: 'digit' }, |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 87 | // { title: '分片大小', dataIndex: 'pieceLength', valueType: 'digit' }, |
| 88 | // { title: '片段数', dataIndex: 'piecesCount', valueType: 'digit' }, |
| 89 | { title: '创建人', dataIndex: 'createdBy', hideInSearch: true }, |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 90 | { title: '上传时间', dataIndex: 'uploadTime', valueType: 'dateTime', hideInSearch: true }, |
| 91 | { |
| 92 | title: '操作', |
| 93 | valueType: 'option', |
| 94 | render: (_, record) => [ |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 95 | <Button key="view" type="link" icon={<EyeOutlined />} onClick={async () => { |
| 96 | try { |
| 97 | // 获取详细信息 |
| 98 | const res = await getBtTorrent(record.torrentId!); |
| 99 | console.log('获取的种子详情:', res); // 调试用 |
| 100 | |
| 101 | // 确保res是对象类型并且包含数据 |
| 102 | if (res && typeof res === 'object') { |
| 103 | // 如果API返回了data包装,则提取data |
| 104 | const torrentData = res.data ? res.data : res; |
| 105 | setCurrent(torrentData); |
| 106 | |
| 107 | // 获取标签 |
| 108 | await handleGetTags(record.torrentId!); |
| 109 | setDrawerVisible(true); |
| 110 | } else { |
| 111 | message.error('获取种子详情格式错误'); |
| 112 | } |
| 113 | } catch (error) { |
| 114 | console.error('获取种子详情出错:', error); |
| 115 | message.error('获取详情失败'); |
| 116 | } |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 117 | }}>查看</Button>, |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 118 | <Button key="download" type="link" icon={<DownloadOutlined />} onClick={async () => { |
| 119 | try { |
| 120 | const blob = await downloadTorrent(record.torrentId!); |
| 121 | const url = window.URL.createObjectURL(blob); |
| 122 | const link = document.createElement('a'); |
| 123 | link.href = url; |
| 124 | link.download = `${record.name}.torrent`; |
| 125 | document.body.appendChild(link); |
| 126 | link.click(); |
| 127 | document.body.removeChild(link); |
| 128 | window.URL.revokeObjectURL(url); |
| 129 | message.success('下载成功'); |
| 130 | } catch (error: any) { |
| 131 | message.error('下载失败'); |
| 132 | } |
| 133 | }}>下载</Button>, |
| 134 | |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 135 | ], |
| 136 | }, |
| 137 | ]; |
| 138 | |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 139 | // Handle the submit for adding or updating a torrent |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 140 | const handleSubmit = async () => { |
| 141 | const values = await form.validateFields(); |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 142 | try { |
| 143 | if (current?.torrentId) { |
| 144 | await updateBtTorrent({ ...current, ...values }); |
| 145 | message.success('更新成功'); |
| 146 | } else { |
| 147 | await addBtTorrent(values as BtTorrent); |
| 148 | message.success('新增成功'); |
| 149 | } |
| 150 | setModalVisible(false); |
| 151 | form.resetFields(); |
| 152 | actionRef.current?.reload(); |
| 153 | } catch (error) { |
| 154 | message.error('操作失败'); |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 155 | } |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 156 | }; |
| 157 | |
| 158 | // Handle file upload |
| 159 | const handleFileUpload = async (file: File) => { |
| 160 | try { |
| 161 | if (!file) { |
| 162 | throw new Error('请选择一个文件'); |
| 163 | } |
| 164 | |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 165 | const values = await uploadForm.validateFields(); |
| 166 | console.log(file); |
| 167 | // Call the uploadTorrent function to upload the file with additional info |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 168 | await uploadTorrent(file); |
| 169 | |
| 170 | // Show a success message |
| 171 | message.success('文件上传成功'); |
| 172 | |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 173 | // Close the upload modal and reset form |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 174 | setUploadModalVisible(false); |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 175 | uploadForm.resetFields(); |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 176 | |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 177 | // Reload the table |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 178 | actionRef.current?.reload(); |
| 179 | |
| 180 | } catch (error) { |
| 181 | message.error(error.message || '文件上传失败'); |
| 182 | } |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 183 | }; |
| 184 | |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 185 | // 修改获取标签的函数,处理API特定的响应格式 |
| 186 | const handleGetTags = async (id: number) => { |
| 187 | try { |
| 188 | // 根据API的响应格式,获取rows数组中的标签 |
| 189 | const response = await listBtTorrentTags({ torrentId: id }); |
| 190 | console.log('API标签响应:', response); |
| 191 | |
| 192 | // 检查响应格式并提取rows数组 |
| 193 | if (response && response.rows && Array.isArray(response.rows)) { |
| 194 | setTorrentTags(response.rows); |
| 195 | console.log('设置标签:', response.rows); |
| 196 | } else { |
| 197 | console.log('未找到标签或格式不符'); |
| 198 | setTorrentTags([]); |
| 199 | } |
| 200 | } catch (error) { |
| 201 | console.error('获取标签失败:', error); |
| 202 | message.error('获取标签失败'); |
| 203 | setTorrentTags([]); |
| 204 | } |
| 205 | }; |
| 206 | |
| 207 | useEffect(() => { |
| 208 | if (current?.torrentId) { |
| 209 | handleGetTags(current.torrentId); |
| 210 | } else { |
| 211 | setTorrentTags([]); // 清空标签当没有选中种子时 |
| 212 | } |
| 213 | }, [current]); |
| 214 | |
| 215 | // 渲染标签列表的函数 |
| 216 | const renderTags = () => { |
| 217 | if (!torrentTags || torrentTags.length === 0) { |
| 218 | return <span style={{ color: '#999' }}>暂无标签</span>; |
| 219 | } |
| 220 | |
| 221 | // 定义一些可能的标签颜色 |
| 222 | const tagColors = ['blue', 'green', 'cyan', 'purple', 'magenta', 'orange', 'gold', 'lime']; |
| 223 | |
| 224 | return ( |
| 225 | <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}> |
| 226 | {torrentTags.map((tag, index) => { |
| 227 | // 根据索引轮换颜色 |
| 228 | const colorIndex = index % tagColors.length; |
| 229 | return ( |
| 230 | <Tag |
| 231 | key={tag.id || tag.torrentId || index} |
| 232 | color={tagColors[colorIndex]} |
| 233 | style={{ margin: '0 4px 4px 0', padding: '2px 8px' }} |
| 234 | > |
| 235 | {tag.tag} |
| 236 | </Tag> |
| 237 | ); |
| 238 | })} |
| 239 | </div> |
| 240 | ); |
| 241 | }; |
| 242 | |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 243 | return ( |
| 244 | <Content> |
| 245 | <Card bordered={false}> |
| 246 | <ProTable<BtTorrent> |
| 247 | headerTitle="种子列表" |
| 248 | actionRef={actionRef} |
| 249 | rowKey="torrentId" |
| 250 | search={{ labelWidth: 100 }} |
| 251 | toolBarRender={() => [ |
| 252 | <Button |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 253 | key="upload" |
| 254 | type="primary" |
| 255 | icon={<UploadOutlined />} |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 256 | onClick={() => setUploadModalVisible(true)} |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 257 | > |
| 258 | 上传种子文件 |
| 259 | </Button> |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 260 | ]} |
| 261 | request={async (params) => { |
| 262 | const res = await listBtTorrent(params); |
| 263 | return { data: res.rows || res.data || [], success: true }; |
| 264 | }} |
| 265 | columns={columns} |
| 266 | /> |
| 267 | |
| 268 | {/* 编辑/新增弹窗 */} |
| 269 | <Modal |
| 270 | title={current?.torrentId ? '编辑种子' : '新增种子'} |
| 271 | open={modalVisible} |
| 272 | onOk={handleSubmit} |
| 273 | onCancel={() => setModalVisible(false)} |
| 274 | destroyOnClose |
| 275 | > |
| 276 | <Form form={form} layout="vertical"> |
| 277 | <Form.Item name="name" label="名称" rules={[{ required: true }]}> |
| 278 | <Input /> |
| 279 | </Form.Item> |
| 280 | <Form.Item name="infoHash" label="infoHash" rules={[{ required: true }]}> |
| 281 | <Input /> |
| 282 | </Form.Item> |
| 283 | <Form.Item name="length" label="总大小 (bytes)" rules={[{ required: true }]}> |
| 284 | <InputNumber style={{ width: '100%' }} /> |
| 285 | </Form.Item> |
| 286 | <Form.Item name="pieceLength" label="分片大小" rules={[{ required: true }]}> |
| 287 | <InputNumber style={{ width: '100%' }} /> |
| 288 | </Form.Item> |
| 289 | <Form.Item name="piecesCount" label="片段数"> |
| 290 | <InputNumber style={{ width: '100%' }} /> |
| 291 | </Form.Item> |
| 292 | <Form.Item name="createdBy" label="创建工具"> |
| 293 | <Input /> |
| 294 | </Form.Item> |
| 295 | </Form> |
| 296 | </Modal> |
| 297 | |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 298 | {/* 上传种子文件的Modal */} |
| 299 | <Modal |
| 300 | title="上传种子文件" |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 301 | open={uploadModalVisible} |
| 302 | onCancel={() => { |
| 303 | setUploadModalVisible(false); |
| 304 | uploadForm.resetFields(); |
| 305 | }} |
| 306 | onOk={() => { |
| 307 | if (uploadFile) { |
| 308 | handleFileUpload(uploadFile); |
| 309 | } else { |
| 310 | message.error('请选择文件'); |
| 311 | } |
| 312 | }} |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 313 | > |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 314 | <Form form={uploadForm} layout="vertical"> |
| 315 | <Form.Item |
| 316 | name="file" |
| 317 | label="种子文件" |
| 318 | rules={[{ required: true, message: '请选择种子文件' }]} |
| 319 | > |
| 320 | <Upload |
| 321 | customRequest={({ file, onSuccess }) => { |
| 322 | setUploadFile(file as File); |
| 323 | onSuccess?.(); |
| 324 | }} |
| 325 | showUploadList={true} |
| 326 | maxCount={1} |
| 327 | accept=".torrent" |
| 328 | onRemove={() => setUploadFile(null)} |
| 329 | > |
| 330 | <Button icon={<UploadOutlined />}>选择 .torrent 文件</Button> |
| 331 | </Upload> |
| 332 | </Form.Item> |
| 333 | <Form.Item |
| 334 | name="description" |
| 335 | label="介绍" |
| 336 | rules={[{ required: true, message: '请输入种子介绍' }]} |
| 337 | > |
| 338 | <Input.TextArea rows={4} placeholder="请输入种子文件的详细介绍" /> |
| 339 | </Form.Item> |
| 340 | <Form.Item |
| 341 | name="tags" |
| 342 | label="标签" |
| 343 | rules={[{ required: true, message: '请输入标签' }]} |
| 344 | > |
| 345 | <Select |
| 346 | mode="tags" |
| 347 | style={{ width: '100%' }} |
| 348 | placeholder="请输入标签,按回车键确认" |
| 349 | tokenSeparators={[',']} |
| 350 | /> |
| 351 | </Form.Item> |
| 352 | </Form> |
86133 | 2d0a179 | 2025-04-21 20:45:00 +0800 | [diff] [blame] | 353 | </Modal> |
| 354 | |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 355 | {/* 详情抽屉 */} |
| 356 | <Drawer |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 357 | width={600} |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 358 | open={drawerVisible} |
| 359 | onClose={() => setDrawerVisible(false)} |
| 360 | title="种子详情" |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 361 | extra={ |
| 362 | <Button |
| 363 | type="primary" |
| 364 | icon={<CommentOutlined />} |
| 365 | onClick={() => navigate(`/torrent/comments/${current.torrentId}`)} |
| 366 | > |
| 367 | 查看评论 |
| 368 | </Button> |
| 369 | } |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 370 | > |
| 371 | {current && ( |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 372 | <> |
| 373 | {/* 不要使用request属性,直接使用dataSource */} |
| 374 | <ProDescriptions<BtTorrent> |
| 375 | column={1} |
| 376 | title={current.name} |
| 377 | dataSource={current} |
| 378 | columns={[ |
| 379 | { |
| 380 | title: '种子ID', |
| 381 | dataIndex: 'torrentId', |
| 382 | valueType: 'text' |
| 383 | }, |
| 384 | { |
| 385 | title: '名称', |
| 386 | dataIndex: 'name', |
| 387 | valueType: 'text' |
| 388 | }, |
| 389 | { |
| 390 | title: 'infoHash', |
| 391 | dataIndex: 'infoHash', |
| 392 | valueType: 'text', |
| 393 | copyable: true |
| 394 | }, |
| 395 | { |
| 396 | title: '大小 (bytes)', |
| 397 | dataIndex: 'length', |
| 398 | valueType: 'digit', |
| 399 | |
| 400 | }, |
| 401 | { |
| 402 | title: '分片大小', |
| 403 | dataIndex: 'pieceLength', |
| 404 | valueType: 'digit' |
| 405 | }, |
| 406 | { |
| 407 | title: '片段数', |
| 408 | dataIndex: 'piecesCount', |
| 409 | valueType: 'digit' |
| 410 | }, |
| 411 | { |
| 412 | title: '创建人', |
| 413 | dataIndex: 'createdBy', |
| 414 | valueType: 'text' |
| 415 | }, |
| 416 | { |
| 417 | title: '上传时间', |
| 418 | dataIndex: 'uploadTime', |
| 419 | valueType: 'dateTime' |
| 420 | }, |
| 421 | { |
| 422 | title: '标签', |
| 423 | dataIndex: 'tags', |
| 424 | render: () => renderTags() |
| 425 | } |
| 426 | ] as ProDescriptionsItemProps<BtTorrent>[]} |
| 427 | /> |
| 428 | |
| 429 | {/* 如果需要显示额外信息,可以在这里添加 */} |
| 430 | {current.description && ( |
| 431 | <div style={{ marginTop: 16 }}> |
| 432 | <h3>介绍</h3> |
| 433 | <p>{current.description}</p> |
| 434 | </div> |
| 435 | )} |
| 436 | |
| 437 | {/* 添加调试信息 - 开发时使用,生产环境可以移除 */} |
| 438 | {/* <div style={{ marginTop: 20, background: '#f5f5f5', padding: 10, borderRadius: 4 }}> |
| 439 | <h4>调试信息:</h4> |
| 440 | <p>当前种子ID: {current.torrentId}</p> |
| 441 | <p>标签数量: {torrentTags?.length || 0}</p> |
| 442 | <details> |
| 443 | <summary>查看完整数据</summary> |
| 444 | <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(current, null, 2)}</pre> |
| 445 | <h5>标签数据:</h5> |
| 446 | <pre style={{ maxHeight: 200, overflow: 'auto' }}>{JSON.stringify(torrentTags, null, 2)}</pre> |
| 447 | </details> |
| 448 | </div> */} |
| 449 | </> |
86133 | ec55c54 | 2025-04-21 11:51:32 +0800 | [diff] [blame] | 450 | )} |
| 451 | </Drawer> |
| 452 | </Card> |
| 453 | </Content> |
| 454 | ); |
| 455 | }; |
| 456 | |
BirdNETM | b0f7153 | 2025-05-26 17:37:33 +0800 | [diff] [blame] | 457 | export default BtTorrentPage; |