Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion client/src/javascript/components/modals/settings-modal/UITab.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {FC, useState} from 'react';
import {FC, useRef, useState} from 'react';
import {Trans, useLingui} from '@lingui/react';

import {Form, FormRow, Select, SelectItem, Radio} from '@client/ui';
import Languages from '@client/constants/Languages';
import SettingStore from '@client/stores/SettingStore';
import ToggleList from '@client/components/general/ToggleList';

import type {Language} from '@client/constants/Languages';

Expand All @@ -23,6 +24,9 @@
const [torrentListViewSize, setTorrentListViewSize] = useState(SettingStore.floodSettings.torrentListViewSize);
const [selectedLanguage, setSelectedLanguage] = useState(SettingStore.floodSettings.language);
const [UITagSelectorMode, setUITagSelectorMode] = useState(SettingStore.floodSettings.UITagSelectorMode);
const UITorrentDetailsPanelRef = useRef<FloodSettings['UITorrentDetailsPanel']>(
SettingStore.floodSettings.UITorrentDetailsPanel ?? true,
);

return (
<Form
Expand Down Expand Up @@ -70,6 +74,23 @@
<Trans id="settings.ui.tag.selector.mode.multi" />
</Radio>
</FormRow>
<ModalFormSectionHeader>
<Trans id="settings.ui.torrent.details" />
</ModalFormSectionHeader>
<FormRow>
<ToggleList
items={[
{
label: 'settings.ui.torrent.details.panel',
defaultChecked: UITorrentDetailsPanelRef.current,

Check failure on line 85 in client/src/javascript/components/modals/settings-modal/UITab.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, check-types)

Type 'boolean | undefined' is not assignable to type 'boolean'.

Check failure on line 85 in client/src/javascript/components/modals/settings-modal/UITab.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, check-types)

Type 'boolean | undefined' is not assignable to type 'boolean'.
onClick: () => {
UITorrentDetailsPanelRef.current = !UITorrentDetailsPanelRef.current;
onSettingsChange({UITorrentDetailsPanel: UITorrentDetailsPanelRef.current});
},
},
]}
/>
</FormRow>
<ModalFormSectionHeader>
<Trans id="settings.ui.torrent.list" />
</ModalFormSectionHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,25 @@
const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
const {i18n} = useLingui();

const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);

useEffect(() => {
if (UIStore.activeModal?.id === 'torrent-details') {
TorrentActions.fetchTorrentContents(UIStore.activeModal?.hash).then((fetchedContents) => {
if (torrentHash) {
TorrentActions.fetchTorrentContents(torrentHash).then((fetchedContents) => {
if (fetchedContents != null) {
setContents(fetchedContents);
setItemsTree(selectionTree.getSelectionTree(fetchedContents));
}
});
}
}, []);
}, [torrentHash]);

if (UIStore.activeModal?.id !== 'torrent-details') {
if (!torrentHash) {
return null;
}

const {hash} = UIStore.activeModal;

Check failure on line 42 in client/src/javascript/components/modals/torrent-details-modal/TorrentContents.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, check-types)

Property 'hash' does not exist on type 'Modal | null'.

Check failure on line 42 in client/src/javascript/components/modals/torrent-details-modal/TorrentContents.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, check-types)

Property 'hash' does not exist on type 'Modal | null'.

let directoryHeadingIconContent = null;
let fileDetailContent = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ const getTags = (tags: TorrentProperties['tags']) =>
const TorrentGeneralInfo: FC = observer(() => {
const {i18n} = useLingui();

if (UIStore.activeModal?.id !== 'torrent-details') {
return null;
}
const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);
const torrent = torrentHash ? TorrentStore.torrents[torrentHash] : undefined;

const torrent = TorrentStore.torrents[UIStore.activeModal.hash];
if (torrent == null) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import Size from '../../general/Size';

const TorrentHeading: FC = observer(() => {
const {i18n} = useLingui();
const torrent =
UIStore.activeModal?.id === 'torrent-details' ? TorrentStore.torrents[UIStore.activeModal.hash] : undefined;
const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);
const torrent = torrentHash ? TorrentStore.torrents[torrentHash] : undefined;
const [torrentStatus, setTorrentStatus] = useState<'start' | 'stop'>('stop');

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ const TorrentMediainfo: FC = () => {
const [isFetchingMediainfo, setIsFetchingMediainfo] = useState<boolean>(true);
const [isCopiedToClipboard, setIsCopiedToClipboard] = useState<boolean>(false);

const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);

useEffect(() => {
const {current: currentCancelToken} = cancelToken;

if (UIStore.activeModal?.id === 'torrent-details') {
TorrentActions.fetchMediainfo(UIStore.activeModal?.hash, cancelToken.current.token).then(
if (torrentHash) {
TorrentActions.fetchMediainfo(torrentHash, cancelToken.current.token).then(
(fetchedMediainfo) => {
setMediainfo(fetchedMediainfo.output);
setIsFetchingMediainfo(false);
Expand All @@ -39,7 +42,7 @@ const TorrentMediainfo: FC = () => {
return () => {
currentCancelToken.cancel();
};
}, []);
}, [torrentHash]);

let headingMessageId = 'mediainfo.heading';
if (isFetchingMediainfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
const [peers, setPeers] = useState<Array<TorrentPeer>>([]);
const [pollingDelay, setPollingDelay] = useState<number | null>(null);

const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);

const fetchPeers = () => {
setPollingDelay(null);
if (UIStore.activeModal?.id === 'torrent-details') {
TorrentActions.fetchTorrentPeers(UIStore.activeModal?.hash).then((data) => {
if (torrentHash) {
TorrentActions.fetchTorrentPeers(torrentHash).then((data) => {
if (data != null) {
setPeers(data);
}
Expand All @@ -29,7 +32,7 @@
setPollingDelay(ConfigStore.pollInterval);
};

useEffect(() => fetchPeers(), []);
useEffect(() => fetchPeers(), [torrentHash]);

Check warning on line 35 in client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, lint)

React Hook useEffect has a missing dependency: 'fetchPeers'. Either include it or remove the dependency array

Check warning on line 35 in client/src/javascript/components/modals/torrent-details-modal/TorrentPeers.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, lint)

React Hook useEffect has a missing dependency: 'fetchPeers'. Either include it or remove the dependency array
useInterval(() => fetchPeers(), pollingDelay);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import UIStore from '../../../stores/UIStore';
const TorrentTrackers: FC = () => {
const [trackers, setTrackers] = useState<Array<TorrentTracker>>([]);

const torrentHash =
UIStore.detailsPanelHash || (UIStore.activeModal?.id === 'torrent-details' ? UIStore.activeModal.hash : null);

const trackerCount = trackers.length;
const trackerTypes = ['http', 'udp', 'dht'];

Expand All @@ -21,14 +24,14 @@ const TorrentTrackers: FC = () => {
));

useEffect(() => {
if (UIStore.activeModal?.id === 'torrent-details') {
TorrentActions.fetchTorrentTrackers(UIStore.activeModal?.hash).then((data) => {
if (torrentHash) {
TorrentActions.fetchTorrentTrackers(torrentHash).then((data) => {
if (data != null) {
setTrackers(data);
}
});
}
}, []);
}, [torrentHash]);

return (
<div className="torrent-details__trackers torrent-details__section">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {FC, useEffect, useState} from 'react';
import {observer} from 'mobx-react';

Check failure on line 2 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanel.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, check-types)

Cannot find module 'mobx-react' or its corresponding type declarations.

Check failure on line 2 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanel.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, check-types)

Cannot find module 'mobx-react' or its corresponding type declarations.
import {Trans, useLingui} from '@lingui/react';

import ConfigStore from '@client/stores/ConfigStore';

Check failure on line 5 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanel.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, lint)

'ConfigStore' is defined but never used

Check failure on line 5 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanel.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, lint)

'ConfigStore' is defined but never used
import TorrentStore from '@client/stores/TorrentStore';
import UIStore from '@client/stores/UIStore';
import SettingStore from '@client/stores/SettingStore';

import TorrentMediainfo from '../modals/torrent-details-modal/TorrentMediainfo';
import TorrentContents from '../modals/torrent-details-modal/TorrentContents';
import TorrentGeneralInfo from '../modals/torrent-details-modal/TorrentGeneralInfo';
import TorrentHeading from '../modals/torrent-details-modal/TorrentHeading';
import TorrentPeers from '../modals/torrent-details-modal/TorrentPeers';
import TorrentTrackers from '../modals/torrent-details-modal/TorrentTrackers';

const TorrentDetailsPanel: FC = observer(() => {
const {i18n} = useLingui();
const [activeTab, setActiveTab] = useState<string>('torrent-details');

const torrentHash = UIStore.detailsPanelHash;
const torrent = torrentHash ? TorrentStore.torrents[torrentHash] : undefined;

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
UIStore.setDetailsPanelVisible(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

const tabs = [
{
id: 'torrent-details',
label: i18n._('torrents.details.details'),
content: TorrentGeneralInfo,
},
{
id: 'torrent-contents',
label: i18n._('torrents.details.files'),
content: TorrentContents,
},
{
id: 'torrent-peers',
label: i18n._('torrents.details.peers'),
content: TorrentPeers,
},
{
id: 'torrent-trackers',
label: i18n._('torrents.details.trackers'),
content: TorrentTrackers,
},
{
id: 'torrent-mediainfo',
label: i18n._('torrents.details.mediainfo'),
content: TorrentMediainfo,
},
];

const height = SettingStore.floodSettings.detailsPanelHeight || 400;

if (!torrentHash) {
return (
<div className="torrent-details-panel" style={{height: `${height}px`}}>
<div className="torrent-details-panel__empty-state">
<Trans id="torrents.details.select.torrent" />
</div>
</div>
);
}

if (!torrent) {
return (
<div className="torrent-details-panel" style={{height: `${height}px`}}>
<div className="torrent-details-panel__empty-state">
<Trans id="torrents.details.torrent.not.found" />
</div>
</div>
);
}

const ActiveTabContent = tabs.find((tab) => tab.id === activeTab)?.content || TorrentGeneralInfo;

return (
<div className="torrent-details-panel" style={{height: `${height}px`}}>
<div className="torrent-details-panel__header">
<TorrentHeading />
<button
type="button"
className="torrent-details-panel__close-button"
onClick={() => UIStore.setDetailsPanelVisible(false)}
aria-label="Close"
>
×
</button>
</div>
<div className="torrent-details-panel__tabs">
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
className={`torrent-details-panel__tab ${activeTab === tab.id ? 'is-active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
<div className="torrent-details-panel__content">
<ActiveTabContent />
</div>
</div>
);
});

export default TorrentDetailsPanel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {FC, useRef, useState} from 'react';
import {observer} from 'mobx-react';

Check failure on line 2 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanelResizeHandle.tsx

View workflow job for this annotation

GitHub Actions / check-real (22, check-types)

Cannot find module 'mobx-react' or its corresponding type declarations.

Check failure on line 2 in client/src/javascript/components/torrent-details-panel/TorrentDetailsPanelResizeHandle.tsx

View workflow job for this annotation

GitHub Actions / check-real (24, check-types)

Cannot find module 'mobx-react' or its corresponding type declarations.

import SettingActions from '@client/actions/SettingActions';
import SettingStore from '@client/stores/SettingStore';
import UIStore from '@client/stores/UIStore';

const pointerDownStyles = `
body { user-select: none !important; }
* { cursor: row-resize !important; }
`;

const TorrentDetailsPanelResizeHandle: FC = observer(() => {
const [isDragging, setIsDragging] = useState<boolean>(false);
const startY = useRef<number>();
const initialHeight = useRef<number>();
const resizeLine = useRef<HTMLDivElement>(null);

const handlePointerMove = (event: PointerEvent) => {
if (startY.current == null || initialHeight.current == null) {
return;
}

const deltaY = startY.current - event.clientY;
let newHeight = initialHeight.current + deltaY;

// Constraints: Min 200px, max window.innerHeight - 200px
const minHeight = 200;
const maxHeight = window.innerHeight - 200;
newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));

if (resizeLine.current != null) {
resizeLine.current.style.transform = `translateY(${event.clientY}px)`;
}

// Update the height in real-time (just for visual feedback)
document.documentElement.style.setProperty('--details-panel-height', `${newHeight}px`);
};

const handlePointerUp = () => {
UIStore.removeGlobalStyle(pointerDownStyles);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointermove', handlePointerMove);

setIsDragging(false);

if (resizeLine.current != null) {
resizeLine.current.style.opacity = '0';
resizeLine.current.style.transform = '';
}

if (startY.current != null && initialHeight.current != null) {
const deltaY = startY.current - (window.event as PointerEvent).clientY;
let newHeight = initialHeight.current + deltaY;

const minHeight = 200;
const maxHeight = window.innerHeight - 200;
newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));

SettingActions.saveSetting('detailsPanelHeight', newHeight);
}

startY.current = undefined;
initialHeight.current = undefined;
};

const handlePointerDown = (event: React.PointerEvent) => {
if (!isDragging) {
setIsDragging(true);

startY.current = event.clientY;
initialHeight.current = SettingStore.floodSettings.detailsPanelHeight || 400;

window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointermove', handlePointerMove);
UIStore.addGlobalStyle(pointerDownStyles);

if (resizeLine.current != null) {
resizeLine.current.style.transform = `translateY(${event.clientY}px)`;
resizeLine.current.style.opacity = '1';
}
}
};

return (
<>
<div className="torrent-details-panel-resize-handle" onPointerDown={handlePointerDown} />
<div className="torrent-details-panel-resize-line" ref={resizeLine} />
</>
);
});

export default TorrentDetailsPanelResizeHandle;
Loading
Loading