mirror of
https://github.com/zoriya/flood.git
synced 2026-06-06 20:12:19 +00:00
feature: generate magnet links
This commit is contained in:
@@ -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':
|
||||
|
||||
+145
@@ -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;
|
||||
+7
-7
@@ -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}
|
||||
|
||||
+19
-15
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user