aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorLibravatar André Oliveira <37463445+SpecialAro@users.noreply.github.com>2023-09-02 16:28:04 +0100
committerLibravatar GitHub <noreply@github.com>2023-09-02 15:28:04 +0000
commitd1c623f4c3d72c859f9ad9cb985be127d6a3eb62 (patch)
treee102da856ae328c70e822d60ac53909acd4627b9 /src/components
parentDowngrade 'electron' to 25.x (diff)
downloadferdium-app-d1c623f4c3d72c859f9ad9cb985be127d6a3eb62.tar.gz
ferdium-app-d1c623f4c3d72c859f9ad9cb985be127d6a3eb62.tar.zst
ferdium-app-d1c623f4c3d72c859f9ad9cb985be127d6a3eb62.zip
feat: Add Download Manager (pause, stop, delete) (#1339)
Diffstat (limited to 'src/components')
-rw-r--r--src/components/downloadManager/DownloadManagerDashboard.tsx288
-rw-r--r--src/components/downloadManager/DownloadManagerLayout.tsx81
-rw-r--r--src/components/layout/Sidebar.tsx27
-rw-r--r--src/components/settings/settings/EditSettingsForm.tsx2
4 files changed, 397 insertions, 1 deletions
diff --git a/src/components/downloadManager/DownloadManagerDashboard.tsx b/src/components/downloadManager/DownloadManagerDashboard.tsx
new file mode 100644
index 000000000..86facc476
--- /dev/null
+++ b/src/components/downloadManager/DownloadManagerDashboard.tsx
@@ -0,0 +1,288 @@
1import { Component } from 'react';
2import { observer } from 'mobx-react';
3import { IntlShape, defineMessages, injectIntl } from 'react-intl';
4import { shell } from 'electron';
5import prettyBytes from 'pretty-bytes';
6import {
7 Typography,
8 Card,
9 CardContent,
10 LinearProgress,
11 Box,
12 IconButton,
13 ListItemButton,
14 ListItemIcon,
15 ListItemText,
16} from '@mui/material';
17import { mdiDownload } from '@mdi/js';
18import PlayArrowIcon from '@mui/icons-material/PlayArrow';
19import PauseIcon from '@mui/icons-material/Pause';
20import CancelIcon from '@mui/icons-material/Cancel';
21import FolderIcon from '@mui/icons-material/Folder';
22import DeleteIcon from '@mui/icons-material/Delete';
23import ClearAllIcon from '@mui/icons-material/ClearAll';
24import { round } from 'lodash';
25import { RealStores } from '../../stores';
26import { Actions } from '../../actions/lib/actions';
27import Icon from '../ui/icon';
28
29const messages = defineMessages({
30 headline: {
31 id: 'downloadManager.headline',
32 defaultMessage: 'Download Manager',
33 },
34 empty: {
35 id: 'downloadManager.empty',
36 defaultMessage: 'Your download list is empty.',
37 },
38});
39
40interface IProps {
41 intl: IntlShape;
42 stores?: RealStores;
43 actions?: Actions;
44}
45
46interface IState {
47 data: string;
48}
49
50// eslint-disable-next-line react/prefer-stateless-function
51class DownloadManagerDashboard extends Component<IProps, IState> {
52 render() {
53 const { intl, stores, actions } = this.props;
54
55 const downloads = stores?.app.downloads ?? [];
56
57 return (
58 <div className="settings__main">
59 <div className="settings__header">
60 <span className="settings__header-item">
61 <Box
62 sx={{
63 display: 'flex',
64 justifyContent: 'center',
65 alignItems: 'center',
66 }}
67 gap={1.5}
68 >
69 <Icon icon={mdiDownload} size={1.5} />
70 {intl.formatMessage(messages.headline)}
71 <span className="badge badge--success">beta</span>
72 </Box>
73 </span>
74 </div>
75 <div className="settings__body">
76 {downloads.length === 0 ? (
77 <Box
78 sx={{
79 display: 'flex',
80 flexDirection: 'column',
81 justifyContent: 'center',
82 alignItems: 'center',
83 }}
84 gap={4}
85 >
86 <Icon icon={mdiDownload} size={1.8} />
87 <Typography variant="h4">
88 {intl.formatMessage(messages.empty)}
89 </Typography>
90 </Box>
91 ) : (
92 <Box
93 sx={{
94 display: 'flex',
95 flexDirection: 'row',
96 justifyContent: 'flex-end',
97 height: 'fit-content',
98 }}
99 >
100 <Box
101 sx={{
102 maxWidth: '176px',
103 }}
104 >
105 <ListItemButton
106 onClick={() => {
107 actions?.app.removeDownload(null);
108 }}
109 >
110 <ListItemIcon>
111 <ClearAllIcon />
112 </ListItemIcon>
113 <ListItemText primary="Clear all completed" />
114 </ListItemButton>
115 </Box>
116 </Box>
117 )}
118 {downloads.map(download => {
119 const {
120 totalBytes,
121 receivedBytes,
122 filename,
123 url,
124 savePath,
125 state,
126 id,
127 paused,
128 } = download;
129
130 const downloadPercentage =
131 receivedBytes !== undefined && totalBytes !== undefined
132 ? round((receivedBytes / totalBytes) * 100, 2)
133 : null;
134
135 const stateParse =
136 state === 'progressing'
137 ? paused === false || paused === undefined
138 ? null
139 : 'Paused'
140 : state === 'cancelled'
141 ? 'Cancelled'
142 : state === 'completed'
143 ? null
144 : 'Error';
145
146 return (
147 <Card
148 key={id}
149 style={{
150 marginBottom: '16px',
151 height: 'fit-content',
152 display: 'flex',
153 }}
154 >
155 <Box
156 sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}
157 >
158 <CardContent>
159 <Box
160 sx={{
161 display: 'flex',
162 }}
163 gap={2}
164 >
165 <button
166 type="button"
167 disabled={state !== 'completed'}
168 style={{
169 pointerEvents:
170 state === 'completed' ? undefined : 'none',
171 }}
172 onClick={() => {
173 if (savePath) shell.openPath(savePath);
174 }}
175 >
176 <Typography
177 variant="h6"
178 color={state === 'completed' ? 'primary' : undefined}
179 sx={{
180 textDecoration:
181 stateParse !== null && stateParse !== 'Paused'
182 ? 'line-through'
183 : state === 'completed'
184 ? 'underline'
185 : null,
186 }}
187 >
188 {filename}
189 </Typography>
190 </button>
191 <Typography
192 variant="h6"
193 color={stateParse === 'Paused' ? '#ed6c02' : undefined}
194 >
195 {stateParse !== null && stateParse !== 'Paused'
196 ? stateParse
197 : stateParse === 'Paused'
198 ? stateParse
199 : null}
200 </Typography>
201 </Box>
202 <Typography variant="body2">{url}</Typography>
203 <LinearProgress
204 variant="determinate"
205 value={downloadPercentage || 0}
206 style={{ marginTop: '8px', marginBottom: '8px' }}
207 />
208 <Typography variant="body2">
209 {`${
210 downloadPercentage ? `${downloadPercentage}% - ` : ''
211 }${
212 receivedBytes ? `${prettyBytes(receivedBytes)} of ` : ''
213 }${totalBytes ? prettyBytes(totalBytes) : ''}`}
214 </Typography>
215 </CardContent>
216 </Box>
217
218 <Box
219 sx={{
220 display: 'flex',
221 flexDirection: 'column',
222 justifyContent: 'center',
223 alignItems: 'center',
224 padding: '8px',
225 }}
226 >
227 {state !== 'completed' && state !== 'cancelled' && (
228 <IconButton
229 color="error"
230 size="small"
231 onClick={() => {
232 actions?.app.stopDownload(id);
233 }}
234 >
235 <CancelIcon />
236 </IconButton>
237 )}
238 {state === 'progressing' && (
239 <IconButton
240 color={
241 paused === false || paused === undefined
242 ? 'warning'
243 : 'success'
244 }
245 size="small"
246 onClick={() => {
247 actions?.app.togglePauseDownload(id);
248 }}
249 >
250 {(paused === false || paused === undefined) && (
251 <PauseIcon />
252 )}
253 {paused && <PlayArrowIcon />}
254 </IconButton>
255 )}
256 {(state === 'cancelled' || state === 'completed') && (
257 <IconButton
258 color="error"
259 onClick={() => {
260 actions?.app.removeDownload(id);
261 }}
262 size="small"
263 >
264 <DeleteIcon />
265 </IconButton>
266 )}
267 {state !== 'cancelled' && (
268 <IconButton
269 color="primary"
270 onClick={() => {
271 if (savePath) shell.showItemInFolder(savePath);
272 }}
273 size="small"
274 >
275 <FolderIcon />
276 </IconButton>
277 )}
278 </Box>
279 </Card>
280 );
281 })}
282 </div>
283 </div>
284 );
285 }
286}
287
288export default injectIntl(observer(DownloadManagerDashboard));
diff --git a/src/components/downloadManager/DownloadManagerLayout.tsx b/src/components/downloadManager/DownloadManagerLayout.tsx
new file mode 100644
index 000000000..1e018cfb8
--- /dev/null
+++ b/src/components/downloadManager/DownloadManagerLayout.tsx
@@ -0,0 +1,81 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl';
4
5import { mdiClose } from '@mdi/js';
6import { Outlet } from 'react-router-dom';
7import { Actions } from '../../actions/lib/actions';
8import { isEscKeyPress } from '../../jsUtils';
9import Appear from '../ui/effects/Appear';
10import ErrorBoundary from '../util/ErrorBoundary';
11import Icon from '../ui/icon';
12
13const messages = defineMessages({
14 closeSettings: {
15 id: 'settings.app.closeSettings',
16 defaultMessage: 'Close settings',
17 },
18});
19
20interface IProps extends WrappedComponentProps {
21 actions?: Actions;
22 // eslint-disable-next-line react/no-unused-prop-types
23 children?: React.ReactNode;
24}
25
26@inject('stores', 'actions')
27@observer
28class DownloadManagerLayout extends Component<IProps> {
29 componentDidMount() {
30 document.addEventListener('keydown', this.handleKeyDown.bind(this), false);
31 }
32
33 componentWillUnmount() {
34 document.removeEventListener(
35 'keydown',
36 // eslint-disable-next-line unicorn/no-invalid-remove-event-listener
37 this.handleKeyDown.bind(this),
38 false,
39 );
40 }
41
42 handleKeyDown(e) {
43 if (isEscKeyPress(e.keyCode)) {
44 this.props.actions!.ui.closeSettings();
45 }
46 }
47
48 render() {
49 const { closeSettings } = this.props.actions!.ui;
50
51 const { intl } = this.props;
52
53 return (
54 <Appear transitionName="fadeIn-fast">
55 <div className="settings-wrapper">
56 <ErrorBoundary>
57 <button
58 type="button"
59 className="settings-wrapper__action"
60 onClick={closeSettings}
61 aria-label={intl.formatMessage(messages.closeSettings)}
62 />
63 <div className="settings franz-form">
64 <Outlet />
65 <button
66 type="button"
67 className="settings__close"
68 onClick={closeSettings}
69 aria-label={intl.formatMessage(messages.closeSettings)}
70 >
71 <Icon icon={mdiClose} size={1.35} />
72 </button>
73 </div>
74 </ErrorBoundary>
75 </div>
76 </Appear>
77 );
78 }
79}
80
81export default injectIntl<'intl', IProps>(DownloadManagerLayout);
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index 6fd911a24..7904d7653 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -14,11 +14,12 @@ import {
14 mdiPlusBox, 14 mdiPlusBox,
15 mdiViewGrid, 15 mdiViewGrid,
16 mdiViewSplitVertical, 16 mdiViewSplitVertical,
17 mdiDownload,
17} from '@mdi/js'; 18} from '@mdi/js';
18
19import Tabbar from '../services/tabs/Tabbar'; 19import Tabbar from '../services/tabs/Tabbar';
20import { 20import {
21 addNewServiceShortcutKey, 21 addNewServiceShortcutKey,
22 downloadsShortcutKey,
22 lockFerdiumShortcutKey, 23 lockFerdiumShortcutKey,
23 muteFerdiumShortcutKey, 24 muteFerdiumShortcutKey,
24 settingsShortcutKey, 25 settingsShortcutKey,
@@ -91,6 +92,7 @@ interface IProps extends WrappedComponentProps {
91 toggleCollapseMenu: () => void; 92 toggleCollapseMenu: () => void;
92 toggleWorkspaceDrawer: () => void; 93 toggleWorkspaceDrawer: () => void;
93 openSettings: (args: { path: string }) => void; 94 openSettings: (args: { path: string }) => void;
95 openDownloads: (args: { path: string }) => void;
94 // eslint-disable-next-line react/no-unused-prop-types 96 // eslint-disable-next-line react/no-unused-prop-types
95 closeSettings: () => void; 97 closeSettings: () => void;
96 setActive: (args: { serviceId: string }) => void; 98 setActive: (args: { serviceId: string }) => void;
@@ -141,6 +143,7 @@ class Sidebar extends Component<IProps, IState> {
141 render() { 143 render() {
142 const { 144 const {
143 openSettings, 145 openSettings,
146 openDownloads,
144 toggleMuteApp, 147 toggleMuteApp,
145 toggleCollapseMenu, 148 toggleCollapseMenu,
146 isAppMuted, 149 isAppMuted,
@@ -156,6 +159,7 @@ class Sidebar extends Component<IProps, IState> {
156 hideWorkspacesButton, 159 hideWorkspacesButton,
157 hideNotificationsButton, 160 hideNotificationsButton,
158 hideSettingsButton, 161 hideSettingsButton,
162 hideDownloadButton,
159 hideSplitModeButton, 163 hideSplitModeButton,
160 useHorizontalStyle, 164 useHorizontalStyle,
161 splitMode, 165 splitMode,
@@ -180,6 +184,8 @@ class Sidebar extends Component<IProps, IState> {
180 184
181 const { isMenuCollapsed } = stores!.settings.all.app; 185 const { isMenuCollapsed } = stores!.settings.all.app;
182 186
187 const { isDownloading, justFinishedDownloading } = stores!.app;
188
183 return ( 189 return (
184 <div className="sidebar"> 190 <div className="sidebar">
185 <Tabbar 191 <Tabbar
@@ -340,6 +346,25 @@ class Sidebar extends Component<IProps, IState> {
340 style={{ height: 'auto', overflowY: 'unset' }} 346 style={{ height: 'auto', overflowY: 'unset' }}
341 /> 347 />
342 )} 348 )}
349
350 {!hideDownloadButton && !isMenuCollapsed ? (
351 <button
352 type="button"
353 onClick={() => openDownloads({ path: '/downloadmanager' })}
354 className={
355 'sidebar__button' +
356 `${isDownloading ? ' sidebar__button--downloading' : ''}` +
357 `${justFinishedDownloading ? ' sidebar__button--done' : ''}`
358 }
359 data-tooltip-id="tooltip-sidebar-button"
360 data-tooltip-content={`${intl.formatMessage(
361 globalMessages.downloads,
362 )} (${downloadsShortcutKey(false)})`}
363 >
364 <Icon icon={mdiDownload} size={1.8} />
365 </button>
366 ) : null}
367
343 {!hideSettingsButton && !isMenuCollapsed ? ( 368 {!hideSettingsButton && !isMenuCollapsed ? (
344 <button 369 <button
345 type="button" 370 type="button"
diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx
index 0b5d4374d..210c8d9e9 100644
--- a/src/components/settings/settings/EditSettingsForm.tsx
+++ b/src/components/settings/settings/EditSettingsForm.tsx
@@ -794,6 +794,8 @@ class EditSettingsForm extends Component<IProps, IState> {
794 794
795 <Toggle {...form.$('hideSettingsButton').bind()} /> 795 <Toggle {...form.$('hideSettingsButton').bind()} />
796 796
797 <Toggle {...form.$('hideDownloadButton').bind()} />
798
797 <Toggle {...form.$('alwaysShowWorkspaces').bind()} /> 799 <Toggle {...form.$('alwaysShowWorkspaces').bind()} />
798 </div> 800 </div>
799 )} 801 )}