Skip to content

Commit 802115b

Browse files
fix: removed notify, migrtaed to alert, ref: #157
1 parent d8db1bd commit 802115b

7 files changed

Lines changed: 10536 additions & 9262 deletions

File tree

app/scripts/background.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async function handleDownload(downloadItem) {
102102
aria2Service.addUri(downloadUrl, params),
103103
browser.downloads.getFileIcon(downloadItem.id).catch(() => ''),
104104
]);
105+
browser.storage.local.set({ motrixReachable: true }).catch(() => {});
105106

106107
// Mark before erasing so onErased doesn't delete the store entry
107108
redirectedToAria.add(downloadItem.id);
@@ -127,19 +128,15 @@ async function handleDownload(downloadItem) {
127128
}
128129
} catch (error) {
129130
console.error('Motrix WebExtension: failed to send to Motrix:', error);
131+
browser.storage.local.set({ motrixReachable: false }).catch(() => {});
130132

131133
if (settings.downloadFallback !== false) {
132134
await browser.downloads.resume(downloadItem.id).catch(() => {});
133135
trackWithBrowser(downloadItem, downloadStore);
134-
await notify('Motrix not reachable', 'Falling back to browser download');
135136
} else {
136137
redirectedToAria.add(downloadItem.id);
137138
await browser.downloads.cancel(downloadItem.id).catch(() => {});
138139
await browser.downloads.erase({ id: downloadItem.id }).catch(() => {});
139-
await notify(
140-
'Motrix not reachable',
141-
'Download cancelled. Enable fallback in settings to use the browser.'
142-
);
143140
await downloadStore.delete(downloadItem.id);
144141
}
145142
}
@@ -195,6 +192,16 @@ async function init() {
195192
createMenuItem();
196193
}
197194

195+
async function checkMotrixStatus() {
196+
await ensureInitialized();
197+
try {
198+
await aria2Service.ping();
199+
await browser.storage.local.set({ motrixReachable: true });
200+
} catch {
201+
await browser.storage.local.set({ motrixReachable: false });
202+
}
203+
}
204+
198205
// ─── TOP-LEVEL LISTENER REGISTRATION ────────────────────────────────────────
199206
// In MV3, the service worker wakes up fresh for every event. Listeners MUST be
200207
// registered synchronously at the top level so Chrome can dispatch events to
@@ -215,3 +222,9 @@ browser.downloads.onErased.addListener(async (id) => {
215222
// onInstalled / onStartup pre-warm init so the first download is snappier
216223
browser.runtime.onInstalled.addListener(ensureInitialized);
217224
browser.runtime.onStartup.addListener(ensureInitialized);
225+
226+
browser.runtime.onMessage.addListener((message) => {
227+
if (message?.type === 'checkMotrixStatus') {
228+
checkMotrixStatus();
229+
}
230+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useEffect, useState } from 'react';
2+
import * as browser from 'webextension-polyfill';
3+
4+
/**
5+
* Subscribe to browser.storage values and re-render on changes.
6+
*
7+
* @param {'local'|'sync'} area - Storage area to read from.
8+
* @param {string[]} keys - Keys to read from that area.
9+
* @returns {object} - Plain object with the requested keys.
10+
*/
11+
export function useBrowserStorage(area, keys) {
12+
const [values, setValues] = useState({});
13+
14+
useEffect(() => {
15+
const storage = browser.storage[area];
16+
17+
storage.get(keys).then((result) => setValues(result));
18+
19+
const listener = (changes, changedArea) => {
20+
if (changedArea !== area) return;
21+
const relevant = keys.filter((k) => k in changes);
22+
if (relevant.length === 0) return;
23+
setValues((prev) => {
24+
const next = { ...prev };
25+
for (const k of relevant) next[k] = changes[k].newValue;
26+
return next;
27+
});
28+
};
29+
30+
browser.storage.onChanged.addListener(listener);
31+
return () => browser.storage.onChanged.removeListener(listener);
32+
// eslint-disable-next-line react-hooks/exhaustive-deps
33+
}, [area]);
34+
35+
return values;
36+
}

app/scripts/popup.js

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use strict';
2-
import React, { useEffect, useState } from 'react';
2+
import React, { useEffect } from 'react';
33
import ReactDOM from 'react-dom';
44
import { Grid, Paper, IconButton, LinearProgress } from '@mui/material';
55
import SettingsIcon from '@mui/icons-material/Settings';
@@ -10,6 +10,7 @@ import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
1010
import createThemed from './createThemed';
1111
import PropTypes from 'prop-types';
1212
import * as browser from 'webextension-polyfill';
13+
import { useBrowserStorage } from './hooks/useBrowserStorage';
1314

1415
function OptProgress({ status, downloaded, size }) {
1516
if (status !== 'downloading') return null;
@@ -51,44 +52,16 @@ FolderButton.propTypes = {
5152
};
5253

5354
function PopupView() {
54-
const [downloadHistory, setDownloadHistory] = useState([]);
55-
const [extensionStatus, setExtensionStatus] = useState(false);
56-
const [showOnlyAriaDownloads, setShowOnlyAriaDownloads] = useState(false);
55+
const { history: downloadHistory = [], motrixReachable = null } = useBrowserStorage('local', ['history', 'motrixReachable']);
56+
const { extensionStatus = false, showOnlyAria: showOnlyAriaDownloads = false } = useBrowserStorage('sync', ['extensionStatus', 'showOnlyAria']);
5757

5858
useEffect(() => {
59-
browser.storage.local.get(['history']).then(({ history = [] }) => {
60-
setDownloadHistory(history);
61-
});
62-
63-
const listener = (changes, area) => {
64-
if (area !== 'local') return;
65-
if (changes.history) setDownloadHistory(changes.history.newValue ?? []);
66-
};
67-
browser.storage.onChanged.addListener(listener);
68-
return () => browser.storage.onChanged.removeListener(listener);
69-
}, []);
70-
71-
useEffect(() => {
72-
browser.storage.sync
73-
.get(['extensionStatus', 'showOnlyAria'])
74-
.then(({ extensionStatus: status, showOnlyAria }) => {
75-
setExtensionStatus(status ?? false);
76-
setShowOnlyAriaDownloads(showOnlyAria ?? false);
77-
});
78-
79-
const listener = (changes, area) => {
80-
if (area !== 'sync') return;
81-
if (changes.extensionStatus) setExtensionStatus(changes.extensionStatus.newValue);
82-
if (changes.showOnlyAria) setShowOnlyAriaDownloads(changes.showOnlyAria.newValue);
83-
};
84-
browser.storage.onChanged.addListener(listener);
85-
return () => browser.storage.onChanged.removeListener(listener);
59+
browser.runtime.sendMessage({ type: 'checkMotrixStatus' }).catch(() => {});
8660
}, []);
8761

8862
const onExtensionStatusChange = (status) => {
8963
browser.storage.sync.set({ extensionStatus: status });
9064
if (!status) browser.downloads.setShelfEnabled?.(true);
91-
setExtensionStatus(status);
9265
};
9366

9467
const parseName = (name) => {
@@ -128,7 +101,6 @@ function PopupView() {
128101
<IconButton
129102
variant="outlined"
130103
onClick={() => {
131-
setDownloadHistory([]);
132104
browser.storage.local.set({ history: [], downloads: {} });
133105
}}
134106
>
@@ -143,6 +115,31 @@ function PopupView() {
143115
<FolderIcon />
144116
</IconButton>
145117
</Grid>
118+
{motrixReachable === false && (
119+
<Grid item xs={11}>
120+
<Paper
121+
style={{
122+
display: 'flex',
123+
alignItems: 'center',
124+
justifyContent: 'space-between',
125+
padding: '8px 12px',
126+
marginBottom: '8px',
127+
backgroundColor: '#fff3e0',
128+
}}
129+
>
130+
<span style={{ fontSize: '13px', color: '#e65100' }}>
131+
Motrix is not reachable. Please open Motrix.
132+
</span>
133+
<IconButton
134+
size="small"
135+
onClick={() => browser.tabs.create({ url: 'motrix://' })}
136+
style={{ color: '#e65100' }}
137+
>
138+
<PowerSettingsNewIcon fontSize="small" />
139+
</IconButton>
140+
</Paper>
141+
</Grid>
142+
)}
146143
<Grid item xs={11}>
147144
{downloadHistory
148145
.filter((el) => !showOnlyAriaDownloads || el.downloader === 'aria')

app/scripts/services/Aria2Service.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ class Aria2Service {
6363
return this.#connectingPromise;
6464
}
6565

66+
async ping() {
67+
const conn = await this.#getConnection();
68+
await conn.call('getVersion');
69+
}
70+
6671
async addUri(url, params) {
6772
const conn = await this.#getConnection();
6873
return conn.call('addUri', [url], params);

tests/mock-aria2/server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class MockAria2Server {
109109
return this.#onAddUri(ws, id, params);
110110
case 'aria2.tellStatus':
111111
return this.#onTellStatus(ws, id, params);
112+
case 'aria2.getVersion':
113+
return this.#respond(ws, id, { version: '1.36.0', enabledFeatures: [] });
112114
default:
113115
// Acknowledge unknown calls so the client doesn't hang
114116
this.#respond(ws, id, 'OK');

tests/specs/chrome/extension.test.js

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const { DOWNLOAD_DIR, cleanupDownloads, waitFor } = require('../../helpers/exten
2323

2424
// ── Constants ──────────────────────────────────────────────────────────────────
2525
const EXTENSION_PATH = path.resolve(__dirname, '../../../dist/chrome');
26-
const ARIA2_PORT = 16800;
26+
const ARIA2_PORT = 16900;
2727
const FILE_SERVER_PORT = 8080;
2828
const TEST_API_KEY = 'e2e-test-secret';
2929

@@ -130,6 +130,36 @@ async function suppressExtensionNotifications() {
130130
});
131131
}
132132

133+
/** Read a value from chrome.storage.local. */
134+
async function readLocalStorage(key) {
135+
const page = await context.newPage();
136+
try {
137+
await page.goto(`chrome-extension://${extensionId}/pages/config.html`, {
138+
waitUntil: 'domcontentloaded',
139+
});
140+
return await page.evaluate(
141+
(k) => new Promise((r) => chrome.storage.local.get(k, (res) => r(res[k]))),
142+
key
143+
);
144+
} finally {
145+
await page.close();
146+
}
147+
}
148+
149+
/** Write values into chrome.storage.local. */
150+
async function setLocalStorage(obj) {
151+
const page = await context.newPage();
152+
try {
153+
await page.goto(`chrome-extension://${extensionId}/pages/config.html`, {
154+
waitUntil: 'domcontentloaded',
155+
});
156+
await page.evaluate((o) => chrome.storage.local.set(o), obj);
157+
await page.waitForTimeout(200);
158+
} finally {
159+
await page.close();
160+
}
161+
}
162+
133163
/** Open a fresh page pointing at the file server index. */
134164
async function openFileServerPage() {
135165
const page = await context.newPage();
@@ -656,13 +686,13 @@ test.describe('Prompt Before Download', () => {
656686
});
657687

658688
test('download is intercepted when prompt_for_download is enabled', async () => {
659-
const mockAria2Pbd = new MockAria2Server(16_801);
689+
const mockAria2Pbd = new MockAria2Server(16_901);
660690
await mockAria2Pbd.start();
661691

662692
try {
663693
const configPage = await pbdContext.newPage();
664694
await configPage.goto(`chrome-extension://${pbdExtensionId}/pages/config.html`, { waitUntil: 'domcontentloaded' });
665-
await configPage.evaluate(async (s) => { await chrome.storage.sync.set(s); }, { ...DEFAULT_SETTINGS, motrixPort: 16_801 });
695+
await configPage.evaluate(async (s) => { await chrome.storage.sync.set(s); }, { ...DEFAULT_SETTINGS, motrixPort: 16_901 });
666696
await configPage.waitForTimeout(400);
667697
await configPage.close();
668698

@@ -679,6 +709,75 @@ test.describe('Prompt Before Download', () => {
679709
});
680710
});
681711

712+
// ══════════════════════════════════════════════════════════════════════════════
713+
// 8. Motrix Reachability
714+
// ══════════════════════════════════════════════════════════════════════════════
715+
test.describe('Motrix Reachability', () => {
716+
test.afterEach(async () => {
717+
await setLocalStorage({ motrixReachable: null });
718+
});
719+
720+
test('motrixReachable is set to true after a successful intercept', async () => {
721+
const page = await openFileServerPage();
722+
await page.click('#large-download');
723+
await waitFor(() => mockAria2.getCalls('addUri').length > 0, 25_000);
724+
await waitFor(async () => (await readLocalStorage('motrixReachable')) === true, 5_000);
725+
expect(await readLocalStorage('motrixReachable')).toBe(true);
726+
await page.close();
727+
});
728+
729+
test('motrixReachable is set to false when Aria2 is unreachable', async () => {
730+
await configureExtension({ motrixPort: 19_999, downloadFallback: true });
731+
const page = await openFileServerPage();
732+
await page.click('#mini-download');
733+
await waitFor(async () => (await readLocalStorage('motrixReachable')) === false, 15_000, 500);
734+
expect(await readLocalStorage('motrixReachable')).toBe(false);
735+
await page.close();
736+
await restoreDefaults();
737+
});
738+
739+
test('popup shows reachability banner when motrixReachable is false', async () => {
740+
// Reject aria2 connections so the on-open ping also fails and doesn't clear the flag
741+
mockAria2.setRejectConnections(true);
742+
try {
743+
await setLocalStorage({ motrixReachable: false });
744+
const page = await context.newPage();
745+
await page.goto(`chrome-extension://${extensionId}/pages/popup.html`, {
746+
waitUntil: 'networkidle',
747+
});
748+
await sleep(1_000);
749+
await expect(page.locator('text=Motrix is not reachable')).toBeVisible();
750+
await page.close();
751+
} finally {
752+
mockAria2.setRejectConnections(false);
753+
}
754+
});
755+
756+
test('popup does not show banner when motrixReachable is true', async () => {
757+
await setLocalStorage({ motrixReachable: true });
758+
const page = await context.newPage();
759+
await page.goto(`chrome-extension://${extensionId}/pages/popup.html`, {
760+
waitUntil: 'networkidle',
761+
});
762+
await sleep(500);
763+
await expect(page.locator('text=Motrix is not reachable')).toHaveCount(0);
764+
await page.close();
765+
});
766+
767+
test('opening popup with Motrix running clears the unreachable flag', async () => {
768+
await setLocalStorage({ motrixReachable: false });
769+
const page = await context.newPage();
770+
await page.goto(`chrome-extension://${extensionId}/pages/popup.html`, {
771+
waitUntil: 'networkidle',
772+
});
773+
// Popup sends checkMotrixStatus on mount; background pings mock aria2 (running)
774+
await waitFor(async () => (await readLocalStorage('motrixReachable')) === true, 8_000, 300);
775+
expect(await readLocalStorage('motrixReachable')).toBe(true);
776+
await expect(page.locator('text=Motrix is not reachable')).toHaveCount(0);
777+
await page.close();
778+
});
779+
});
780+
682781
// ══════════════════════════════════════════════════════════════════════════════
683782
// 9. File Cleanup
684783
// ══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)