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 | |
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')
-rw-r--r-- | src/@types/stores.types.ts | 1 | ||||
-rw-r--r-- | src/actions/app.ts | 6 | ||||
-rw-r--r-- | src/actions/ui.ts | 3 | ||||
-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 | ||||
-rw-r--r-- | src/config.ts | 1 | ||||
-rw-r--r-- | src/containers/download-manager/DownloadManagerScreen.tsx | 15 | ||||
-rw-r--r-- | src/containers/download-manager/DownloadManagerWindow.tsx | 46 | ||||
-rw-r--r-- | src/containers/layout/AppLayoutContainer.tsx | 4 | ||||
-rw-r--r-- | src/containers/settings/EditSettingsScreen.tsx | 14 | ||||
-rw-r--r-- | src/environment.ts | 2 | ||||
-rw-r--r-- | src/i18n/globalMessages.ts | 4 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 4 | ||||
-rw-r--r-- | src/index.ts | 10 | ||||
-rw-r--r-- | src/lib/Menu.ts | 19 | ||||
-rw-r--r-- | src/models/Service.ts | 81 | ||||
-rw-r--r-- | src/routes.tsx | 11 | ||||
-rw-r--r-- | src/stores/AppStore.ts | 104 | ||||
-rw-r--r-- | src/stores/UIStore.ts | 7 | ||||
-rw-r--r-- | src/styles/layout.scss | 21 |
22 files changed, 748 insertions, 3 deletions
diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts index dc5da563f..973889802 100644 --- a/src/@types/stores.types.ts +++ b/src/@types/stores.types.ts | |||
@@ -125,6 +125,7 @@ export interface AppStore extends TypedStore { | |||
125 | FAILED: 'FAILED'; | 125 | FAILED: 'FAILED'; |
126 | }; | 126 | }; |
127 | universalDarkMode: boolean; | 127 | universalDarkMode: boolean; |
128 | isDownloading: () => boolean; | ||
128 | cacheSize: () => void; | 129 | cacheSize: () => void; |
129 | debugInfo: () => void; | 130 | debugInfo: () => void; |
130 | enableLongPressServiceHint: boolean; | 131 | enableLongPressServiceHint: boolean; |
diff --git a/src/actions/app.ts b/src/actions/app.ts index 2e60c9327..07cc47078 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts | |||
@@ -28,4 +28,10 @@ export default <ActionDefinitions>{ | |||
28 | toggleMuteApp: {}, | 28 | toggleMuteApp: {}, |
29 | toggleCollapseMenu: {}, | 29 | toggleCollapseMenu: {}, |
30 | clearAllCache: {}, | 30 | clearAllCache: {}, |
31 | addDownload: {}, | ||
32 | removeDownload: {}, | ||
33 | updateDownload: {}, | ||
34 | endedDownload: {}, | ||
35 | stopDownload: {}, | ||
36 | togglePauseDownload: {}, | ||
31 | }; | 37 | }; |
diff --git a/src/actions/ui.ts b/src/actions/ui.ts index 7d2dbccfa..f496c5c07 100644 --- a/src/actions/ui.ts +++ b/src/actions/ui.ts | |||
@@ -5,6 +5,9 @@ export default <ActionDefinitions>{ | |||
5 | openSettings: { | 5 | openSettings: { |
6 | path: PropTypes.string, | 6 | path: PropTypes.string, |
7 | }, | 7 | }, |
8 | openDownloads: { | ||
9 | path: PropTypes.string, | ||
10 | }, | ||
8 | closeSettings: {}, | 11 | closeSettings: {}, |
9 | toggleServiceUpdatedInfoBar: { | 12 | toggleServiceUpdatedInfoBar: { |
10 | visible: PropTypes.bool, | 13 | visible: PropTypes.bool, |
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 | )} |
diff --git a/src/config.ts b/src/config.ts index 21a7462b9..d8b028104 100644 --- a/src/config.ts +++ b/src/config.ts | |||
@@ -424,6 +424,7 @@ export const DEFAULT_APP_SETTINGS = { | |||
424 | hideWorkspacesButton: false, | 424 | hideWorkspacesButton: false, |
425 | hideNotificationsButton: false, | 425 | hideNotificationsButton: false, |
426 | hideSettingsButton: false, | 426 | hideSettingsButton: false, |
427 | hideDownloadButton: false, | ||
427 | alwaysShowWorkspaces: false, | 428 | alwaysShowWorkspaces: false, |
428 | liftSingleInstanceLock: false, | 429 | liftSingleInstanceLock: false, |
429 | enableLongPressServiceHint: false, | 430 | enableLongPressServiceHint: false, |
diff --git a/src/containers/download-manager/DownloadManagerScreen.tsx b/src/containers/download-manager/DownloadManagerScreen.tsx new file mode 100644 index 000000000..5d395a180 --- /dev/null +++ b/src/containers/download-manager/DownloadManagerScreen.tsx | |||
@@ -0,0 +1,15 @@ | |||
1 | import { Component, ReactElement } from 'react'; | ||
2 | import ErrorBoundary from '../../components/util/ErrorBoundary'; | ||
3 | import DownloadManager from '../../components/downloadManager/DownloadManagerDashboard'; | ||
4 | |||
5 | class DownloadManagerScreen extends Component { | ||
6 | render(): ReactElement { | ||
7 | return ( | ||
8 | <ErrorBoundary> | ||
9 | <DownloadManager {...this.props} /> | ||
10 | </ErrorBoundary> | ||
11 | ); | ||
12 | } | ||
13 | } | ||
14 | |||
15 | export default DownloadManagerScreen; | ||
diff --git a/src/containers/download-manager/DownloadManagerWindow.tsx b/src/containers/download-manager/DownloadManagerWindow.tsx new file mode 100644 index 000000000..e13e51774 --- /dev/null +++ b/src/containers/download-manager/DownloadManagerWindow.tsx | |||
@@ -0,0 +1,46 @@ | |||
1 | import { inject, observer } from 'mobx-react'; | ||
2 | import { Component, ReactPortal } from 'react'; | ||
3 | import ReactDOM from 'react-dom'; | ||
4 | import { Outlet } from 'react-router-dom'; | ||
5 | |||
6 | import { StoresProps } from '../../@types/ferdium-components.types'; | ||
7 | import Layout from '../../components/downloadManager/DownloadManagerLayout'; | ||
8 | import ErrorBoundary from '../../components/util/ErrorBoundary'; | ||
9 | |||
10 | interface IProps {} | ||
11 | |||
12 | @inject('stores', 'actions') | ||
13 | @observer | ||
14 | class DownloadManagerContainer extends Component<IProps> { | ||
15 | portalRoot: any; | ||
16 | |||
17 | el: HTMLDivElement; | ||
18 | |||
19 | constructor(props: StoresProps) { | ||
20 | super(props); | ||
21 | |||
22 | this.portalRoot = document.querySelector('#portalContainer'); | ||
23 | this.el = document.createElement('div'); | ||
24 | } | ||
25 | |||
26 | componentDidMount(): void { | ||
27 | this.portalRoot.append(this.el); | ||
28 | } | ||
29 | |||
30 | componentWillUnmount(): void { | ||
31 | this.el.remove(); | ||
32 | } | ||
33 | |||
34 | render(): ReactPortal { | ||
35 | return ReactDOM.createPortal( | ||
36 | <ErrorBoundary> | ||
37 | <Layout> | ||
38 | <Outlet /> | ||
39 | </Layout> | ||
40 | </ErrorBoundary>, | ||
41 | this.el, | ||
42 | ); | ||
43 | } | ||
44 | } | ||
45 | |||
46 | export default DownloadManagerContainer; | ||
diff --git a/src/containers/layout/AppLayoutContainer.tsx b/src/containers/layout/AppLayoutContainer.tsx index e30c0e067..8748f1032 100644 --- a/src/containers/layout/AppLayoutContainer.tsx +++ b/src/containers/layout/AppLayoutContainer.tsx | |||
@@ -52,7 +52,8 @@ class AppLayoutContainer extends Component<IProps> { | |||
52 | const { installUpdate, toggleMuteApp, toggleCollapseMenu } = | 52 | const { installUpdate, toggleMuteApp, toggleCollapseMenu } = |
53 | this.props.actions.app; | 53 | this.props.actions.app; |
54 | 54 | ||
55 | const { openSettings, closeSettings } = this.props.actions.ui; | 55 | const { openSettings, closeSettings, openDownloads } = |
56 | this.props.actions.ui; | ||
56 | 57 | ||
57 | const isLoadingFeatures = | 58 | const isLoadingFeatures = |
58 | features.featuresRequest.isExecuting && | 59 | features.featuresRequest.isExecuting && |
@@ -89,6 +90,7 @@ class AppLayoutContainer extends Component<IProps> { | |||
89 | isAppMuted={settings.all.app.isAppMuted} | 90 | isAppMuted={settings.all.app.isAppMuted} |
90 | isMenuCollapsed={settings.all.app.isMenuCollapsed} | 91 | isMenuCollapsed={settings.all.app.isMenuCollapsed} |
91 | openSettings={openSettings} | 92 | openSettings={openSettings} |
93 | openDownloads={openDownloads} | ||
92 | closeSettings={closeSettings} | 94 | closeSettings={closeSettings} |
93 | reorder={reorder} | 95 | reorder={reorder} |
94 | reload={reload} | 96 | reload={reload} |
diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx index b9732ead0..8190bef3e 100644 --- a/src/containers/settings/EditSettingsScreen.tsx +++ b/src/containers/settings/EditSettingsScreen.tsx | |||
@@ -255,6 +255,10 @@ const messages = defineMessages({ | |||
255 | id: 'settings.app.form.hideSettingsButton', | 255 | id: 'settings.app.form.hideSettingsButton', |
256 | defaultMessage: 'Hide Settings button', | 256 | defaultMessage: 'Hide Settings button', |
257 | }, | 257 | }, |
258 | hideDownloadButton: { | ||
259 | id: 'settings.app.form.hideDownloadButton', | ||
260 | defaultMessage: 'Hide Downloads button', | ||
261 | }, | ||
258 | alwaysShowWorkspaces: { | 262 | alwaysShowWorkspaces: { |
259 | id: 'settings.app.form.alwaysShowWorkspaces', | 263 | id: 'settings.app.form.alwaysShowWorkspaces', |
260 | defaultMessage: 'Always show workspace drawer', | 264 | defaultMessage: 'Always show workspace drawer', |
@@ -425,6 +429,7 @@ class EditSettingsScreen extends Component< | |||
425 | hideWorkspacesButton: Boolean(settingsData.hideWorkspacesButton), | 429 | hideWorkspacesButton: Boolean(settingsData.hideWorkspacesButton), |
426 | hideNotificationsButton: Boolean(settingsData.hideNotificationsButton), | 430 | hideNotificationsButton: Boolean(settingsData.hideNotificationsButton), |
427 | hideSettingsButton: Boolean(settingsData.hideSettingsButton), | 431 | hideSettingsButton: Boolean(settingsData.hideSettingsButton), |
432 | hideDownloadButton: Boolean(settingsData.hideDownloadButton), | ||
428 | alwaysShowWorkspaces: Boolean(settingsData.alwaysShowWorkspaces), | 433 | alwaysShowWorkspaces: Boolean(settingsData.alwaysShowWorkspaces), |
429 | accentColor: settingsData.accentColor, | 434 | accentColor: settingsData.accentColor, |
430 | progressbarAccentColor: settingsData.progressbarAccentColor, | 435 | progressbarAccentColor: settingsData.progressbarAccentColor, |
@@ -1081,6 +1086,15 @@ class EditSettingsScreen extends Component< | |||
1081 | default: DEFAULT_APP_SETTINGS.hideSettingsButton, | 1086 | default: DEFAULT_APP_SETTINGS.hideSettingsButton, |
1082 | type: 'checkbox', | 1087 | type: 'checkbox', |
1083 | }, | 1088 | }, |
1089 | hideDownloadButton: { | ||
1090 | label: intl.formatMessage(messages.hideDownloadButton), | ||
1091 | value: ifUndefined<boolean>( | ||
1092 | settings.all.app.hideDownloadButton, | ||
1093 | DEFAULT_APP_SETTINGS.hideDownloadButton, | ||
1094 | ), | ||
1095 | default: DEFAULT_APP_SETTINGS.hideDownloadButton, | ||
1096 | type: 'checkbox', | ||
1097 | }, | ||
1084 | alwaysShowWorkspaces: { | 1098 | alwaysShowWorkspaces: { |
1085 | label: intl.formatMessage(messages.alwaysShowWorkspaces), | 1099 | label: intl.formatMessage(messages.alwaysShowWorkspaces), |
1086 | value: ifUndefined<boolean>( | 1100 | value: ifUndefined<boolean>( |
diff --git a/src/environment.ts b/src/environment.ts index c3b9e7e58..87e2f4f66 100644 --- a/src/environment.ts +++ b/src/environment.ts | |||
@@ -42,5 +42,7 @@ export const splitModeToggleShortcutKey = (isAccelerator = true) => | |||
42 | `${cmdOrCtrlShortcutKey(isAccelerator)}+${altKey(isAccelerator)}+S`; | 42 | `${cmdOrCtrlShortcutKey(isAccelerator)}+${altKey(isAccelerator)}+S`; |
43 | export const settingsShortcutKey = (isAccelerator = true) => | 43 | export const settingsShortcutKey = (isAccelerator = true) => |
44 | `${cmdOrCtrlShortcutKey(isAccelerator)}+${isMac ? ',' : 'P'}`; | 44 | `${cmdOrCtrlShortcutKey(isAccelerator)}+${isMac ? ',' : 'P'}`; |
45 | export const downloadsShortcutKey = (isAccelerator = true) => | ||
46 | `${cmdOrCtrlShortcutKey(isAccelerator)}+J`; | ||
45 | export const toggleFullScreenKey = () => | 47 | export const toggleFullScreenKey = () => |
46 | isMac ? `CTRL + ${cmdKey} + F` : 'F11'; | 48 | isMac ? `CTRL + ${cmdKey} + F` : 'F11'; |
diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts index 9f55da57a..1c5e6a4ba 100644 --- a/src/i18n/globalMessages.ts +++ b/src/i18n/globalMessages.ts | |||
@@ -66,6 +66,10 @@ export default defineMessages({ | |||
66 | id: 'global.quitConfirmation', | 66 | id: 'global.quitConfirmation', |
67 | defaultMessage: 'Do you really want to quit Ferdium?', | 67 | defaultMessage: 'Do you really want to quit Ferdium?', |
68 | }, | 68 | }, |
69 | downloads: { | ||
70 | id: 'global.downloads', | ||
71 | defaultMessage: 'Downloads', | ||
72 | }, | ||
69 | settings: { | 73 | settings: { |
70 | id: 'global.settings', | 74 | id: 'global.settings', |
71 | defaultMessage: 'Settings', | 75 | defaultMessage: 'Settings', |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 82c0f1b02..da6d825b6 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -9,6 +9,8 @@ | |||
9 | "connectionLostBanner.cta": "Reload Service", | 9 | "connectionLostBanner.cta": "Reload Service", |
10 | "connectionLostBanner.informationLink": "What happened?", | 10 | "connectionLostBanner.informationLink": "What happened?", |
11 | "connectionLostBanner.message": "Oh no! Ferdium lost the connection to {name}.", | 11 | "connectionLostBanner.message": "Oh no! Ferdium lost the connection to {name}.", |
12 | "downloadManager.empty": "Your download list is empty.", | ||
13 | "downloadManager.headline": "Download Manager", | ||
12 | "feature.basicAuth.signIn": "Sign In", | 14 | "feature.basicAuth.signIn": "Sign In", |
13 | "feature.publishDebugInfo.error": "There was an error while trying to publish the debug information. Please try again later or view the console for more information.", | 15 | "feature.publishDebugInfo.error": "There was an error while trying to publish the debug information. Please try again later or view the console for more information.", |
14 | "feature.publishDebugInfo.info": "Publishing your debug information helps us find issues and errors in Ferdium. By publishing your debug information you accept Ferdium Debugger's privacy policy and terms of service", | 16 | "feature.publishDebugInfo.info": "Publishing your debug information helps us find issues and errors in Ferdium. By publishing your debug information you accept Ferdium Debugger's privacy policy and terms of service", |
@@ -23,6 +25,7 @@ | |||
23 | "global.api.unhealthy": "Can't connect to {serverNameParse} online services", | 25 | "global.api.unhealthy": "Can't connect to {serverNameParse} online services", |
24 | "global.cancel": "Cancel", | 26 | "global.cancel": "Cancel", |
25 | "global.clearCache": "Clear cache", | 27 | "global.clearCache": "Clear cache", |
28 | "global.downloads": "Downloads", | ||
26 | "global.edit": "Edit", | 29 | "global.edit": "Edit", |
27 | "global.no": "No", | 30 | "global.no": "No", |
28 | "global.notConnectedToTheInternet": "You are not connected to the internet.", | 31 | "global.notConnectedToTheInternet": "You are not connected to the internet.", |
@@ -219,6 +222,7 @@ | |||
219 | "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup", | 222 | "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup", |
220 | "settings.app.form.hibernationStrategy": "Hibernation strategy", | 223 | "settings.app.form.hibernationStrategy": "Hibernation strategy", |
221 | "settings.app.form.hideCollapseButton": "Hide Collapse button", | 224 | "settings.app.form.hideCollapseButton": "Hide Collapse button", |
225 | "settings.app.form.hideDownloadButton": "Hide Downloads button", | ||
222 | "settings.app.form.hideNotificationsButton": "Hide Notifications & Sound button", | 226 | "settings.app.form.hideNotificationsButton": "Hide Notifications & Sound button", |
223 | "settings.app.form.hideRecipesButton": "Hide Recipes button", | 227 | "settings.app.form.hideRecipesButton": "Hide Recipes button", |
224 | "settings.app.form.hideSettingsButton": "Hide Settings button", | 228 | "settings.app.form.hideSettingsButton": "Hide Settings button", |
diff --git a/src/index.ts b/src/index.ts index cbc10bdbe..5accc4570 100644 --- a/src/index.ts +++ b/src/index.ts | |||
@@ -674,6 +674,16 @@ ipcMain.on('window.toolbar-double-clicked', () => { | |||
674 | } | 674 | } |
675 | }); | 675 | }); |
676 | 676 | ||
677 | ipcMain.on('stop-download', (_e, data) => { | ||
678 | debug(`stopping download from main process ${data}`); | ||
679 | mainWindow?.webContents.send('stop-download', data); | ||
680 | }); | ||
681 | |||
682 | ipcMain.on('toggle-pause-download', (_e, data) => { | ||
683 | debug(`stopping download from main process ${data}`); | ||
684 | mainWindow?.webContents.send('toggle-pause-download', data); | ||
685 | }); | ||
686 | |||
677 | // Quit when all windows are closed. | 687 | // Quit when all windows are closed. |
678 | app.on('window-all-closed', () => { | 688 | app.on('window-all-closed', () => { |
679 | // On OS X it is common for applications and their menu bar | 689 | // On OS X it is common for applications and their menu bar |
diff --git a/src/lib/Menu.ts b/src/lib/Menu.ts index a90fd8bea..8ad4441d2 100644 --- a/src/lib/Menu.ts +++ b/src/lib/Menu.ts | |||
@@ -32,6 +32,7 @@ import { | |||
32 | nodeVersion, | 32 | nodeVersion, |
33 | osArch, | 33 | osArch, |
34 | toggleFullScreenKey, | 34 | toggleFullScreenKey, |
35 | downloadsShortcutKey, | ||
35 | } from '../environment'; | 36 | } from '../environment'; |
36 | import { CUSTOM_WEBSITE_RECIPE_ID, LIVE_API_FERDIUM_WEBSITE } from '../config'; | 37 | import { CUSTOM_WEBSITE_RECIPE_ID, LIVE_API_FERDIUM_WEBSITE } from '../config'; |
37 | import { ferdiumVersion } from '../environment-remote'; | 38 | import { ferdiumVersion } from '../environment-remote'; |
@@ -875,6 +876,15 @@ class FranzMenu implements StoresProps { | |||
875 | type: 'separator', | 876 | type: 'separator', |
876 | }, | 877 | }, |
877 | { | 878 | { |
879 | label: intl.formatMessage(globalMessages.downloads), | ||
880 | accelerator: `${downloadsShortcutKey()}`, | ||
881 | click: () => { | ||
882 | this.actions.ui.openDownloads({ path: '/downloadmanager' }); | ||
883 | }, | ||
884 | enabled: this.stores.user.isLoggedIn, | ||
885 | visible: !locked, | ||
886 | }, | ||
887 | { | ||
878 | label: intl.formatMessage(globalMessages.settings), | 888 | label: intl.formatMessage(globalMessages.settings), |
879 | accelerator: `${settingsShortcutKey()}`, | 889 | accelerator: `${settingsShortcutKey()}`, |
880 | click: () => { | 890 | click: () => { |
@@ -991,6 +1001,15 @@ class FranzMenu implements StoresProps { | |||
991 | } else { | 1001 | } else { |
992 | tpl[0].submenu = [ | 1002 | tpl[0].submenu = [ |
993 | { | 1003 | { |
1004 | label: intl.formatMessage(globalMessages.downloads), | ||
1005 | accelerator: `${downloadsShortcutKey()}`, | ||
1006 | click: () => { | ||
1007 | this.actions.ui.openDownloads({ path: '/downloadmanager' }); | ||
1008 | }, | ||
1009 | enabled: this.stores.user.isLoggedIn, | ||
1010 | visible: !locked, | ||
1011 | }, | ||
1012 | { | ||
994 | label: intl.formatMessage(globalMessages.settings), | 1013 | label: intl.formatMessage(globalMessages.settings), |
995 | accelerator: `${settingsShortcutKey()}`, | 1014 | accelerator: `${settingsShortcutKey()}`, |
996 | click: () => { | 1015 | click: () => { |
diff --git a/src/models/Service.ts b/src/models/Service.ts index 265b3e13c..b1f0bc271 100644 --- a/src/models/Service.ts +++ b/src/models/Service.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { join } from 'node:path'; | 1 | import { join, basename } from 'node:path'; |
2 | import { autorun, action, computed, makeObservable, observable } from 'mobx'; | 2 | import { autorun, action, computed, makeObservable, observable } from 'mobx'; |
3 | import { ipcRenderer } from 'electron'; | 3 | import { ipcRenderer } from 'electron'; |
4 | import { webContents } from '@electron/remote'; | 4 | import { webContents } from '@electron/remote'; |
5 | import ElectronWebView from 'react-electron-web-view'; | 5 | import ElectronWebView from 'react-electron-web-view'; |
6 | 6 | ||
7 | import { v4 as uuidV4 } from 'uuid'; | ||
7 | import { todosStore } from '../features/todos'; | 8 | import { todosStore } from '../features/todos'; |
8 | import { isValidExternalURL, normalizedUrl } from '../helpers/url-helpers'; | 9 | import { isValidExternalURL, normalizedUrl } from '../helpers/url-helpers'; |
9 | import UserAgent from './UserAgent'; | 10 | import UserAgent from './UserAgent'; |
@@ -523,6 +524,84 @@ export default class Service { | |||
523 | }); | 524 | }); |
524 | 525 | ||
525 | if (webviewWebContents) { | 526 | if (webviewWebContents) { |
527 | webviewWebContents.session.on('will-download', (event, item) => { | ||
528 | event.preventDefault(); | ||
529 | |||
530 | const downloadId = uuidV4(); | ||
531 | |||
532 | window['ferdium'].actions.app.addDownload({ | ||
533 | id: downloadId, | ||
534 | serviceId: this.id, | ||
535 | filename: item.getFilename(), | ||
536 | url: item.getURL(), | ||
537 | savePath: item.getSavePath(), | ||
538 | }); | ||
539 | |||
540 | item.addListener('updated', (event, state) => { | ||
541 | if (state === 'interrupted') { | ||
542 | debug('Download is interrupted but can be resumed'); | ||
543 | } else if (state === 'progressing') { | ||
544 | if (item.isPaused()) { | ||
545 | debug('Download is paused'); | ||
546 | } else { | ||
547 | debug(`Received bytes: ${item.getReceivedBytes()}`); | ||
548 | } | ||
549 | } | ||
550 | window['ferdium'].actions.app.updateDownload({ | ||
551 | id: downloadId, | ||
552 | serviceId: this.id, | ||
553 | filename: basename(item.getSavePath()), | ||
554 | url: item.getURL(), | ||
555 | savePath: item.getSavePath(), | ||
556 | receivedBytes: item.getReceivedBytes(), | ||
557 | totalBytes: item.getTotalBytes(), | ||
558 | state, | ||
559 | }); | ||
560 | debug('download updated', event, state); | ||
561 | }); | ||
562 | item.addListener('done', (event, state) => { | ||
563 | debug('download done', event, state); | ||
564 | if (state === 'completed') { | ||
565 | debug('Download successfully'); | ||
566 | } else { | ||
567 | if (state === 'cancelled' && item.getSavePath() === '') { | ||
568 | window['ferdium'].actions.app.removeDownload(downloadId); | ||
569 | debug('Download is cancelled'); | ||
570 | } | ||
571 | debug(`Download failed: ${state}`); | ||
572 | } | ||
573 | |||
574 | window['ferdium'].actions.app.endedDownload({ | ||
575 | id: downloadId, | ||
576 | serviceId: this.id, | ||
577 | receivedBytes: item.getReceivedBytes(), | ||
578 | totalBytes: item.getTotalBytes(), | ||
579 | state, | ||
580 | }); | ||
581 | }); | ||
582 | |||
583 | ipcRenderer.on('toggle-pause-download', (_, data) => { | ||
584 | debug('toggle-pause-download', item.isPaused(), item.getState()); | ||
585 | if (data.downloadId === downloadId || data.downloadId === undefined) { | ||
586 | if (item.isPaused()) { | ||
587 | item.resume(); | ||
588 | } else { | ||
589 | item.pause(); | ||
590 | } | ||
591 | } | ||
592 | debug('toggle-pause-download', item.isPaused(), item.getState()); | ||
593 | window['ferdium'].actions.app.updateDownload({ | ||
594 | id: downloadId, | ||
595 | paused: item.isPaused(), | ||
596 | }); | ||
597 | }); | ||
598 | |||
599 | ipcRenderer.on('stop-download', (_, data) => { | ||
600 | if (data === undefined || downloadId === data.downloadId) { | ||
601 | item.cancel(); | ||
602 | } | ||
603 | }); | ||
604 | }); | ||
526 | webviewWebContents.on('login', (event, _, authInfo, callback) => { | 605 | webviewWebContents.on('login', (event, _, authInfo, callback) => { |
527 | // const authCallback = callback; | 606 | // const authCallback = callback; |
528 | debug('browser login event', authInfo); | 607 | debug('browser login event', authInfo); |
diff --git a/src/routes.tsx b/src/routes.tsx index 9f81e46d9..436f25ea4 100644 --- a/src/routes.tsx +++ b/src/routes.tsx | |||
@@ -35,6 +35,8 @@ import { WORKSPACES_ROUTES } from './features/workspaces/constants'; | |||
35 | import { StoresProps } from './@types/ferdium-components.types'; | 35 | import { StoresProps } from './@types/ferdium-components.types'; |
36 | import { Actions } from './actions/lib/actions'; | 36 | import { Actions } from './actions/lib/actions'; |
37 | import { RealStores } from './stores'; | 37 | import { RealStores } from './stores'; |
38 | import DownloadManagerScreen from './containers/download-manager/DownloadManagerScreen'; | ||
39 | import DownloadManagerWindow from './containers/download-manager/DownloadManagerWindow'; | ||
38 | 40 | ||
39 | interface IProps { | 41 | interface IProps { |
40 | history: HashHistory; | 42 | history: HashHistory; |
@@ -116,6 +118,15 @@ class FerdiumRoutes extends Component<IProps> { | |||
116 | /> | 118 | /> |
117 | </Route> | 119 | </Route> |
118 | <Route | 120 | <Route |
121 | path="/downloadmanager" | ||
122 | element={<DownloadManagerWindow {...this.props} />} | ||
123 | > | ||
124 | <Route | ||
125 | path="/downloadmanager" | ||
126 | element={<DownloadManagerScreen {...this.props} />} | ||
127 | /> | ||
128 | </Route> | ||
129 | <Route | ||
119 | path="/settings" | 130 | path="/settings" |
120 | element={<SettingsWindow {...this.props} />} | 131 | element={<SettingsWindow {...this.props} />} |
121 | > | 132 | > |
diff --git a/src/stores/AppStore.ts b/src/stores/AppStore.ts index b317d99fc..1c1336819 100644 --- a/src/stores/AppStore.ts +++ b/src/stores/AppStore.ts | |||
@@ -61,6 +61,22 @@ const CATALINA_NOTIFICATION_HACK_KEY = | |||
61 | 61 | ||
62 | const locales = generatedTranslations(); | 62 | const locales = generatedTranslations(); |
63 | 63 | ||
64 | interface Download { | ||
65 | id: string; | ||
66 | serviceId: string; | ||
67 | filename: string; | ||
68 | url: string; | ||
69 | savePath?: string; | ||
70 | state?: 'progressing' | 'interrupted' | 'completed' | 'cancelled'; | ||
71 | paused?: boolean; | ||
72 | canResume?: boolean; | ||
73 | progress?: number; | ||
74 | totalBytes?: number; | ||
75 | receivedBytes?: number; | ||
76 | startTime?: number; | ||
77 | endTime?: number; | ||
78 | } | ||
79 | |||
64 | export default class AppStore extends TypedStore { | 80 | export default class AppStore extends TypedStore { |
65 | updateStatusTypes = { | 81 | updateStatusTypes = { |
66 | CHECKING: 'CHECKING', | 82 | CHECKING: 'CHECKING', |
@@ -114,6 +130,10 @@ export default class AppStore extends TypedStore { | |||
114 | 130 | ||
115 | fetchDataInterval: null | NodeJS.Timer = null; | 131 | fetchDataInterval: null | NodeJS.Timer = null; |
116 | 132 | ||
133 | @observable downloads: Download[] = []; | ||
134 | |||
135 | @observable justFinishedDownloading: boolean = false; | ||
136 | |||
117 | constructor(stores: Stores, api: ApiInterface, actions: Actions) { | 137 | constructor(stores: Stores, api: ApiInterface, actions: Actions) { |
118 | super(stores, api, actions); | 138 | super(stores, api, actions); |
119 | 139 | ||
@@ -136,6 +156,14 @@ export default class AppStore extends TypedStore { | |||
136 | this._toggleCollapseMenu.bind(this), | 156 | this._toggleCollapseMenu.bind(this), |
137 | ); | 157 | ); |
138 | this.actions.app.clearAllCache.listen(this._clearAllCache.bind(this)); | 158 | this.actions.app.clearAllCache.listen(this._clearAllCache.bind(this)); |
159 | this.actions.app.addDownload.listen(this._addDownload.bind(this)); | ||
160 | this.actions.app.removeDownload.listen(this._removeDownload.bind(this)); | ||
161 | this.actions.app.updateDownload.listen(this._updateDownload.bind(this)); | ||
162 | this.actions.app.endedDownload.listen(this._endedDownload.bind(this)); | ||
163 | this.actions.app.stopDownload.listen(this._stopDownload.bind(this)); | ||
164 | this.actions.app.togglePauseDownload.listen( | ||
165 | this._togglePauseDownload.bind(this), | ||
166 | ); | ||
139 | 167 | ||
140 | this.registerReactions([ | 168 | this.registerReactions([ |
141 | this._offlineCheck.bind(this), | 169 | this._offlineCheck.bind(this), |
@@ -300,6 +328,10 @@ export default class AppStore extends TypedStore { | |||
300 | return this.getAppCacheSizeRequest.execute().result; | 328 | return this.getAppCacheSizeRequest.execute().result; |
301 | } | 329 | } |
302 | 330 | ||
331 | @computed get isDownloading() { | ||
332 | return this.downloads.some(download => download.state === 'progressing'); | ||
333 | } | ||
334 | |||
303 | @computed get debugInfo() { | 335 | @computed get debugInfo() { |
304 | const settings = cleanseJSObject(this.stores.settings.app); | 336 | const settings = cleanseJSObject(this.stores.settings.app); |
305 | settings.lockedPassword = '******'; | 337 | settings.lockedPassword = '******'; |
@@ -518,6 +550,78 @@ export default class AppStore extends TypedStore { | |||
518 | this.locale = value; | 550 | this.locale = value; |
519 | } | 551 | } |
520 | 552 | ||
553 | @action _addDownload(download: Download) { | ||
554 | this.downloads.unshift(download); | ||
555 | debug('Download added', this.downloads); | ||
556 | } | ||
557 | |||
558 | @action _removeDownload(id: string | null) { | ||
559 | debug(`Removed download ${id}`); | ||
560 | if (id === null) { | ||
561 | const indexesToRemove: number[] = []; | ||
562 | this.downloads.map(item => { | ||
563 | if (!item.state) return; | ||
564 | if (item.state === 'completed' || item.state === 'cancelled') { | ||
565 | indexesToRemove.push(this.downloads.indexOf(item)); | ||
566 | } | ||
567 | }); | ||
568 | |||
569 | if (indexesToRemove.length === 0) return; | ||
570 | |||
571 | this.downloads = this.downloads.filter( | ||
572 | (_, index) => !indexesToRemove.includes(index), | ||
573 | ); | ||
574 | |||
575 | debug('Removed all completed downloads'); | ||
576 | return; | ||
577 | } | ||
578 | |||
579 | const index = this.downloads.findIndex(item => item.id === id); | ||
580 | if (index !== -1) { | ||
581 | this.downloads.splice(index, 1); | ||
582 | } | ||
583 | |||
584 | debug(`Removed download ${id}`); | ||
585 | } | ||
586 | |||
587 | @action _updateDownload(download: Download) { | ||
588 | const index = this.downloads.findIndex(item => item.id === download.id); | ||
589 | if (index !== -1) { | ||
590 | this.downloads[index] = { ...this.downloads[index], ...download }; | ||
591 | } | ||
592 | |||
593 | debug('Download updated', this.downloads[index]); | ||
594 | } | ||
595 | |||
596 | @action _endedDownload(download: Download) { | ||
597 | const index = this.downloads.findIndex(item => item.id === download.id); | ||
598 | if (index !== -1) { | ||
599 | this.downloads[index] = { ...this.downloads[index], ...download }; | ||
600 | } | ||
601 | |||
602 | debug('Download ended', this.downloads[index]); | ||
603 | |||
604 | if (!this.isDownloading && download.state === 'completed') { | ||
605 | this.justFinishedDownloading = true; | ||
606 | |||
607 | setTimeout(() => { | ||
608 | this.justFinishedDownloading = false; | ||
609 | }, ms('2s')); | ||
610 | } | ||
611 | } | ||
612 | |||
613 | @action _stopDownload(downloadId: string | undefined) { | ||
614 | ipcRenderer.send('stop-download', { | ||
615 | downloadId, | ||
616 | }); | ||
617 | } | ||
618 | |||
619 | @action _togglePauseDownload(downloadId: string | undefined) { | ||
620 | ipcRenderer.send('toggle-pause-download', { | ||
621 | downloadId, | ||
622 | }); | ||
623 | } | ||
624 | |||
521 | _setLocale() { | 625 | _setLocale() { |
522 | if (this.stores.user?.isLoggedIn && this.stores.user?.data.locale) { | 626 | if (this.stores.user?.isLoggedIn && this.stores.user?.data.locale) { |
523 | this._changeLocale(this.stores.user.data.locale); | 627 | this._changeLocale(this.stores.user.data.locale); |
diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts index 4ed45fc3b..a3330c2e6 100644 --- a/src/stores/UIStore.ts +++ b/src/stores/UIStore.ts | |||
@@ -18,6 +18,7 @@ export default class UIStore extends TypedStore { | |||
18 | makeObservable(this); | 18 | makeObservable(this); |
19 | 19 | ||
20 | // Register action handlers | 20 | // Register action handlers |
21 | this.actions.ui.openDownloads.listen(this._openDownloads.bind(this)); | ||
21 | this.actions.ui.openSettings.listen(this._openSettings.bind(this)); | 22 | this.actions.ui.openSettings.listen(this._openSettings.bind(this)); |
22 | this.actions.ui.closeSettings.listen(this._closeSettings.bind(this)); | 23 | this.actions.ui.closeSettings.listen(this._closeSettings.bind(this)); |
23 | this.actions.ui.toggleServiceUpdatedInfoBar.listen( | 24 | this.actions.ui.toggleServiceUpdatedInfoBar.listen( |
@@ -97,6 +98,12 @@ export default class UIStore extends TypedStore { | |||
97 | } | 98 | } |
98 | 99 | ||
99 | // Actions | 100 | // Actions |
101 | @action _openDownloads({ path = '/downloadmanager' }): void { | ||
102 | const downloadsPath = | ||
103 | path === '/downloadmanager' ? path : `/downloadmanager/${path}`; | ||
104 | this.stores.router.push(downloadsPath); | ||
105 | } | ||
106 | |||
100 | @action _openSettings({ path = '/settings' }): void { | 107 | @action _openSettings({ path = '/settings' }): void { |
101 | const settingsPath = path === '/settings' ? path : `/settings/${path}`; | 108 | const settingsPath = path === '/settings' ? path : `/settings/${path}`; |
102 | this.stores.router.push(settingsPath); | 109 | this.stores.router.push(settingsPath); |
diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 173ca3184..591f7c54b 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss | |||
@@ -204,6 +204,27 @@ body.win32:not(.isFullScreen) .app .app__content { | |||
204 | } | 204 | } |
205 | } | 205 | } |
206 | 206 | ||
207 | @keyframes isDownloadingFade { | ||
208 | 50% { | ||
209 | color: $theme-brand-primary; | ||
210 | } | ||
211 | } | ||
212 | |||
213 | @keyframes isDownloadingDoneFade { | ||
214 | 50% { | ||
215 | color: green; | ||
216 | } | ||
217 | } | ||
218 | |||
219 | .sidebar__button--downloading { | ||
220 | animation: isDownloadingFade 1s cubic-bezier(0.755, 0.05, 0.855, 0.06) | ||
221 | infinite; | ||
222 | } | ||
223 | |||
224 | .sidebar__button--done { | ||
225 | animation: isDownloadingDoneFade 0.2s linear infinite; | ||
226 | } | ||
227 | |||
207 | .grid .grid__row { | 228 | .grid .grid__row { |
208 | display: flex; | 229 | display: flex; |
209 | flex-direction: row; | 230 | flex-direction: row; |