diff options
author | André Oliveira <37463445+SpecialAro@users.noreply.github.com> | 2023-09-02 16:28:04 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-02 15:28:04 +0000 |
commit | d1c623f4c3d72c859f9ad9cb985be127d6a3eb62 (patch) | |
tree | e102da856ae328c70e822d60ac53909acd4627b9 /src/components | |
parent | Downgrade 'electron' to 25.x (diff) | |
download | ferdium-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.tsx | 288 | ||||
-rw-r--r-- | src/components/downloadManager/DownloadManagerLayout.tsx | 81 | ||||
-rw-r--r-- | src/components/layout/Sidebar.tsx | 27 | ||||
-rw-r--r-- | src/components/settings/settings/EditSettingsForm.tsx | 2 |
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 @@ | |||
1 | import { Component } from 'react'; | ||
2 | import { observer } from 'mobx-react'; | ||
3 | import { IntlShape, defineMessages, injectIntl } from 'react-intl'; | ||
4 | import { shell } from 'electron'; | ||
5 | import prettyBytes from 'pretty-bytes'; | ||
6 | import { | ||
7 | Typography, | ||
8 | Card, | ||
9 | CardContent, | ||
10 | LinearProgress, | ||
11 | Box, | ||
12 | IconButton, | ||
13 | ListItemButton, | ||
14 | ListItemIcon, | ||
15 | ListItemText, | ||
16 | } from '@mui/material'; | ||
17 | import { mdiDownload } from '@mdi/js'; | ||
18 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | ||
19 | import PauseIcon from '@mui/icons-material/Pause'; | ||
20 | import CancelIcon from '@mui/icons-material/Cancel'; | ||
21 | import FolderIcon from '@mui/icons-material/Folder'; | ||
22 | import DeleteIcon from '@mui/icons-material/Delete'; | ||
23 | import ClearAllIcon from '@mui/icons-material/ClearAll'; | ||
24 | import { round } from 'lodash'; | ||
25 | import { RealStores } from '../../stores'; | ||
26 | import { Actions } from '../../actions/lib/actions'; | ||
27 | import Icon from '../ui/icon'; | ||
28 | |||
29 | const 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 | |||
40 | interface IProps { | ||
41 | intl: IntlShape; | ||
42 | stores?: RealStores; | ||
43 | actions?: Actions; | ||
44 | } | ||
45 | |||
46 | interface IState { | ||
47 | data: string; | ||
48 | } | ||
49 | |||
50 | // eslint-disable-next-line react/prefer-stateless-function | ||
51 | class 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 | |||
288 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import { inject, observer } from 'mobx-react'; | ||
3 | import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; | ||
4 | |||
5 | import { mdiClose } from '@mdi/js'; | ||
6 | import { Outlet } from 'react-router-dom'; | ||
7 | import { Actions } from '../../actions/lib/actions'; | ||
8 | import { isEscKeyPress } from '../../jsUtils'; | ||
9 | import Appear from '../ui/effects/Appear'; | ||
10 | import ErrorBoundary from '../util/ErrorBoundary'; | ||
11 | import Icon from '../ui/icon'; | ||
12 | |||
13 | const messages = defineMessages({ | ||
14 | closeSettings: { | ||
15 | id: 'settings.app.closeSettings', | ||
16 | defaultMessage: 'Close settings', | ||
17 | }, | ||
18 | }); | ||
19 | |||
20 | interface 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 | ||
28 | class 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 | |||
81 | export 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 | |||
19 | import Tabbar from '../services/tabs/Tabbar'; | 19 | import Tabbar from '../services/tabs/Tabbar'; |
20 | import { | 20 | import { |
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 | )} |