feature: generate magnet links

This commit is contained in:
Jesse Chan
2020-12-01 22:56:37 +08:00
parent a99a64ff1c
commit 00cbc6537c
11 changed files with 248 additions and 24 deletions
@@ -6,6 +6,7 @@ import {useKeyPressEvent} from 'react-use';
import AddTorrentsModal from './add-torrents-modal/AddTorrentsModal';
import ConfirmModal from './confirm-modal/ConfirmModal';
import FeedsModal from './feeds-modal/FeedsModal';
import GenerateMagnetModal from './generate-magnet-modal/GenerateMagnetModal';
import MoveTorrentsModal from './move-torrents-modal/MoveTorrentsModal';
import RemoveTorrentsModal from './remove-torrents-modal/RemoveTorrentsModal';
import SetTagsModal from './set-tags-modal/SetTagsModal';
@@ -25,6 +26,8 @@ const createModal = (id: Modal['id']): React.ReactNode => {
return <ConfirmModal />;
case 'feeds':
return <FeedsModal />;
case 'generate-magnet':
return <GenerateMagnetModal />;
case 'move-torrents':
return <MoveTorrentsModal />;
case 'remove-torrents':
@@ -0,0 +1,145 @@
import {FC, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {TorrentTrackerType} from '@shared/types/TorrentTracker';
import Checkmark from '../../icons/Checkmark';
import ClipboardIcon from '../../icons/ClipboardIcon';
import {Form, FormElementAddon, FormError, FormRow, Textbox} from '../../../ui';
import Modal from '../Modal';
import TorrentActions from '../../../actions/TorrentActions';
import TorrentStore from '../../../stores/TorrentStore';
const generateMagnet = (hash: string, trackers?: Array<string>): string => {
let result = `magnet:?xt=urn:btih:${hash}`;
if (trackers?.length) {
trackers.forEach((tracker) => {
result = `${result}&tr=${encodeURI(tracker)}`;
});
}
return result;
};
const GenerateMagnetModal: FC = () => {
const magnetTextboxRef = useRef<HTMLInputElement>(null);
const magnetTrackersTextboxRef = useRef<HTMLInputElement>(null);
const intl = useIntl();
const [isMagnetCopied, setIsMagnetCopied] = useState<boolean>(false);
const [isMagnetTrackersCopied, setIsMagnetTrackersCopied] = useState<boolean>(false);
const [trackerState, setTrackerState] = useState<{
isLoadingTrackers: boolean;
magnetTrackersLink: string;
}>({
isLoadingTrackers: true,
magnetTrackersLink: '',
});
useEffect(() => {
TorrentActions.fetchTorrentTrackers(TorrentStore.selectedTorrents[0]).then((trackers) => {
if (trackers != null) {
setTrackerState({
isLoadingTrackers: false,
magnetTrackersLink: generateMagnet(
TorrentStore.selectedTorrents[0],
trackers.filter((tracker) => tracker.type !== TorrentTrackerType.DHT).map((tracker) => tracker.url),
),
});
}
});
}, []);
const magnetLink = generateMagnet(TorrentStore.selectedTorrents[0]);
return (
<Modal
heading={intl.formatMessage({
id: 'torrents.generate.magnet.heading',
})}
content={
<div className="modal__content inverse">
<Form>
{TorrentStore.torrents[TorrentStore.selectedTorrents[0]]?.isPrivate ? (
<FormRow>
<FormError>{intl.formatMessage({id: 'torrents.generate.magnet.private.torrent'})}</FormError>
</FormRow>
) : null}
<FormRow>
<Textbox
id="magnet"
ref={magnetTextboxRef}
addonPlacement="after"
label={intl.formatMessage({id: 'torrents.generate.magnet.magnet'})}
defaultValue={magnetLink}
readOnly>
<FormElementAddon
onClick={() => {
if (typeof navigator.clipboard?.writeText === 'function') {
navigator.clipboard.writeText(magnetLink).then(() => {
setIsMagnetCopied(true);
});
} else if (magnetTextboxRef.current != null) {
magnetTextboxRef.current?.select();
document.execCommand('copy');
setIsMagnetCopied(true);
}
}}>
{isMagnetCopied ? <Checkmark /> : <ClipboardIcon />}
</FormElementAddon>
</Textbox>
</FormRow>
<FormRow>
{trackerState.isLoadingTrackers ? (
<Textbox
id="loading"
label={intl.formatMessage({id: 'torrents.generate.magnet.magnet.with.trackers'})}
placeholder={intl.formatMessage({
id: 'torrents.generate.magnet.loading.trackers',
})}
disabled
/>
) : (
<Textbox
id="magnet-trackers"
ref={magnetTrackersTextboxRef}
addonPlacement="after"
label={intl.formatMessage({id: 'torrents.generate.magnet.magnet.with.trackers'})}
defaultValue={trackerState.magnetTrackersLink}
readOnly>
<FormElementAddon
onClick={() => {
if (typeof navigator.clipboard?.writeText === 'function') {
navigator.clipboard.writeText(trackerState.magnetTrackersLink).then(() => {
setIsMagnetTrackersCopied(true);
});
} else if (magnetTrackersTextboxRef.current != null) {
magnetTrackersTextboxRef.current?.select();
document.execCommand('copy');
setIsMagnetTrackersCopied(true);
}
}}>
{isMagnetTrackersCopied ? <Checkmark /> : <ClipboardIcon />}
</FormElementAddon>
</Textbox>
)}
</FormRow>
</Form>
</div>
}
actions={[
{
clickHandler: null,
content: intl.formatMessage({
id: 'button.close',
}),
triggerDismiss: true,
type: 'tertiary',
},
]}
/>
);
};
export default GenerateMagnetModal;
@@ -30,8 +30,13 @@ class TorrentContextMenuActionsList extends Component<
constructor(props: TorrentContextMenuActionsListProps) {
super(props);
const {torrentContextMenuActions} = SettingStore.floodSettings;
this.state = {
torrentContextMenuActions: SettingStore.floodSettings.torrentContextMenuActions,
torrentContextMenuActions: Object.keys(TorrentContextMenuActions).map((key) => ({
id: key,
visible: torrentContextMenuActions.some((setting) => setting.id === key && setting.visible),
})) as FloodSettings['torrentContextMenuActions'],
};
}
@@ -92,16 +97,11 @@ class TorrentContextMenuActionsList extends Component<
};
render() {
const torrentContextMenuActions = Object.keys(TorrentContextMenuActions).map((key) => ({
id: key,
visible: this.state.torrentContextMenuActions.some((setting) => setting.id === key && setting.visible),
}));
return (
<SortableList
id="torrent-context-menu-items"
className="sortable-list--torrent-context-menu-items"
items={torrentContextMenuActions}
items={this.state.torrentContextMenuActions}
lockedIDs={lockedIDs}
isDraggable={false}
onMouseDown={this.handleMouseDown}
@@ -28,8 +28,25 @@ class TorrentListColumnsList extends React.Component<TorrentListColumnsListProps
constructor(props: TorrentListColumnsListProps) {
super(props);
const {torrentListColumns} = SettingStore.floodSettings;
const torrentListColumnItems: ListItem[] = torrentListColumns
.filter((column) => TorrentListColumns[column.id] != null)
.slice();
const newTorrentListColumnItems: ListItem[] = Object.keys(TorrentListColumns)
.filter((key) => torrentListColumns.every((column) => column.id !== key))
.map((newColumn) => {
return {
id: newColumn,
visible: false,
};
});
this.state = {
torrentListColumns: SettingStore.floodSettings.torrentListColumns,
torrentListColumns: torrentListColumnItems.concat(
newTorrentListColumnItems,
) as FloodSettings['torrentListColumns'],
};
}
@@ -125,24 +142,11 @@ class TorrentListColumnsList extends React.Component<TorrentListColumnsListProps
render(): React.ReactNode {
const lockedIDs = this.getLockedIDs();
const torrentListColumnItems: ListItem[] = this.state.torrentListColumns
.filter((column) => TorrentListColumns[column.id] != null)
.slice();
const newTorrentListColumnItems: ListItem[] = Object.keys(TorrentListColumns)
.filter((key) => this.state.torrentListColumns.every((column) => column.id !== key))
.map((newColumn) => {
return {
id: newColumn,
visible: false,
};
});
return (
<SortableList
id="torrent-details"
className="sortable-list--torrent-details"
items={torrentListColumnItems.concat(newTorrentListColumnItems)}
items={this.state.torrentListColumns}
lockedIDs={lockedIDs}
onMouseDown={this.handleMouseDown}
onDrop={this.handleMove}
@@ -118,6 +118,14 @@ const getContextMenuItems = (torrent: TorrentProperties): Array<ContextMenuItem>
handleTorrentDownload(selectedTorrents[selectedTorrents.length - 1]);
},
},
{
type: 'action',
action: 'generateMagnet',
label: TorrentContextMenuActions.generateMagnet.id,
clickHandler: () => {
UIActions.displayModal({id: 'generate-magnet'});
},
},
{
type: 'action',
action: 'setPriority',
@@ -26,6 +26,9 @@ const TorrentContextMenuActions = {
torrentDownload: {
id: 'torrents.list.context.download',
},
generateMagnet: {
id: 'torrents.list.context.generate.magnet',
},
setPriority: {
id: 'torrents.list.context.priority',
},
@@ -389,6 +389,12 @@
"value": "Cancel"
}
],
"button.close": [
{
"type": 0,
"value": "Close"
}
],
"button.download": [
{
"type": 0,
@@ -2137,6 +2143,36 @@
"value": "Type"
}
],
"torrents.generate.magnet.heading": [
{
"type": 0,
"value": "Generate Magnet Link"
}
],
"torrents.generate.magnet.loading.trackers": [
{
"type": 0,
"value": "Loading trackers..."
}
],
"torrents.generate.magnet.magnet": [
{
"type": 0,
"value": "Magnet Link"
}
],
"torrents.generate.magnet.magnet.with.trackers": [
{
"type": 0,
"value": "Magnet Link with Trackers"
}
],
"torrents.generate.magnet.private.torrent": [
{
"type": 0,
"value": "This is a private torrent."
}
],
"torrents.list.cannot.connect": [
{
"type": 0,
@@ -2167,6 +2203,12 @@
"value": "Download"
}
],
"torrents.list.context.generate.magnet": [
{
"type": 0,
"value": "Generate Magnet Link"
}
],
"torrents.list.context.move": [
{
"type": 0,
+7
View File
@@ -28,6 +28,7 @@
"auth.message.not.admin": "User is not Admin",
"button.add": "Add",
"button.cancel": "Cancel",
"button.close": "Close",
"button.download": "Download",
"button.no": "No",
"button.ok": "OK",
@@ -302,9 +303,15 @@
"torrents.details.trackers.no.data": "There is no tracker data for this torrent.",
"torrents.details.trackers.type": "Type",
"torrents.details.trackers": "Trackers",
"torrents.generate.magnet.heading": "Generate Magnet Link",
"torrents.generate.magnet.loading.trackers": "Loading trackers...",
"torrents.generate.magnet.private.torrent": "This is a private torrent.",
"torrents.generate.magnet.magnet": "Magnet Link",
"torrents.generate.magnet.magnet.with.trackers": "Magnet Link with Trackers",
"torrents.list.clear.filters": "Clear Filters",
"torrents.list.context.check.hash": "Check Hash",
"torrents.list.context.details": "Torrent Details",
"torrents.list.context.generate.magnet": "Generate Magnet Link",
"torrents.list.context.move": "Set Torrent Location",
"torrents.list.context.pause": "Pause",
"torrents.list.context.download": "Download",
+8 -1
View File
@@ -57,7 +57,14 @@ export type ModalAction = CheckboxModalAction | ButtonModalAction;
export type Modal =
| {
id: 'feeds' | 'move-torrents' | 'remove-torrents' | 'set-taxonomy' | 'set-trackers' | 'settings';
id:
| 'feeds'
| 'generate-magnet'
| 'move-torrents'
| 'remove-torrents'
| 'set-taxonomy'
| 'set-trackers'
| 'settings';
}
| {
id: 'add-torrents';
@@ -8,7 +8,7 @@ import type {FormRowItemProps} from './FormRowItem';
type TextboxProps = Pick<
React.InputHTMLAttributes<HTMLInputElement>,
'children' | 'defaultValue' | 'placeholder' | 'onChange' | 'onClick' | 'autoComplete'
'children' | 'disabled' | 'defaultValue' | 'placeholder' | 'readOnly' | 'onChange' | 'onClick' | 'autoComplete'
> & {
id: string;
label?: React.ReactNode;
@@ -33,6 +33,8 @@ const Textbox = forwardRef<HTMLInputElement, TextboxProps>(
placeholder,
autoComplete,
type,
disabled,
readOnly,
onChange,
onClick,
}: TextboxProps,
@@ -78,6 +80,8 @@ const Textbox = forwardRef<HTMLInputElement, TextboxProps>(
tabIndex={0}
type={type}
autoComplete={autoComplete}
disabled={disabled}
readOnly={readOnly}
/>
{childElements}
</div>
+1
View File
@@ -58,6 +58,7 @@ const defaultFloodSettings: Readonly<FloodSettings> = {
{id: 'setTrackers', visible: false},
{id: 'torrentDetails', visible: true},
{id: 'torrentDownload', visible: true},
{id: 'generateMagnet', visible: false},
{id: 'setPriority', visible: false},
],
torrentListViewSize: 'condensed',