@vacantthinker/firefox-addon-framework-easy 2026.530.1431 → 2026.602.1003
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -12
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/browserDownload.js +6 -1
- package/src/browserTab.js +60 -0
- package/src/serviceCommon.js +0 -349
- package/src/serviceOpContent.js +17 -14
- package/src/serviceOpJavascript.js +349 -0
package/README.md
CHANGED
|
@@ -25,7 +25,8 @@ export class BaseORM { }
|
|
|
25
25
|
|
|
26
26
|
### 📄 File: `src/browserDownload.js`
|
|
27
27
|
```javascript
|
|
28
|
-
export async function browserDownloadByDownlink(
|
|
28
|
+
export async function browserDownloadByDownlink(
|
|
29
|
+
{ }
|
|
29
30
|
|
|
30
31
|
```
|
|
31
32
|
|
|
@@ -58,6 +59,14 @@ export function browserRuntimeManifestName() { }
|
|
|
58
59
|
|
|
59
60
|
```
|
|
60
61
|
|
|
62
|
+
### 📄 File: `src/browserTab.js`
|
|
63
|
+
```javascript
|
|
64
|
+
export async function browserTabCreateNearSendMessageToContentJs(message) { }
|
|
65
|
+
|
|
66
|
+
export function browserTabWaitReloadThenRemoveIt({ }
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
61
70
|
### 📄 File: `src/generate.js`
|
|
62
71
|
```javascript
|
|
63
72
|
export function generateHtmlByUserSettings(
|
|
@@ -129,17 +138,6 @@ export async function tabOpRemoveCssCode(tabId, code) { }
|
|
|
129
138
|
|
|
130
139
|
### 📄 File: `src/serviceCommon.js`
|
|
131
140
|
```javascript
|
|
132
|
-
export async function serviceTakeScreenshot(
|
|
133
|
-
{ }
|
|
134
|
-
|
|
135
|
-
export async function serviceElementPicker(message) { }
|
|
136
|
-
|
|
137
|
-
export async function serviceGetFullPageRectData(message) { }
|
|
138
|
-
|
|
139
|
-
export async function serviceFindAllMagnetLink(message) { }
|
|
140
|
-
|
|
141
|
-
export async function serviceDealWithMagnetLink(message) { }
|
|
142
|
-
|
|
143
141
|
```
|
|
144
142
|
|
|
145
143
|
### 📄 File: `src/serviceFetch.js`
|
|
@@ -174,6 +172,21 @@ export function serviceRemoveIllegalWord(value) { }
|
|
|
174
172
|
|
|
175
173
|
```
|
|
176
174
|
|
|
175
|
+
### 📄 File: `src/serviceOpJavascript.js`
|
|
176
|
+
```javascript
|
|
177
|
+
export async function serviceTakeScreenshot(
|
|
178
|
+
{ }
|
|
179
|
+
|
|
180
|
+
export async function serviceElementPicker(message) { }
|
|
181
|
+
|
|
182
|
+
export async function serviceGetFullPageRectData(message) { }
|
|
183
|
+
|
|
184
|
+
export async function serviceFindAllMagnetLink(message) { }
|
|
185
|
+
|
|
186
|
+
export async function serviceDealWithMagnetLink(message) { }
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
|
|
177
190
|
### 📄 File: `src/servicePureVideolink.js`
|
|
178
191
|
```javascript
|
|
179
192
|
export function servicePureVideolinkYTB(videolinkOrigin) { }
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from './src/baseORM.js'
|
|
|
2
2
|
export * from './src/browserDownload.js'
|
|
3
3
|
export * from './src/browserNotification.js'
|
|
4
4
|
export * from './src/browserRuntime.js'
|
|
5
|
+
export * from './src/browserTab.js'
|
|
5
6
|
export * from './src/generate.js'
|
|
6
7
|
export * from './src/opStorage.js'
|
|
7
8
|
export * from './src/opTab.js'
|
|
@@ -9,6 +10,7 @@ export * from './src/serviceCommon.js'
|
|
|
9
10
|
export * from './src/serviceFetch.js'
|
|
10
11
|
export * from './src/serviceGet.js'
|
|
11
12
|
export * from './src/serviceOpContent.js'
|
|
13
|
+
export * from './src/serviceOpJavascript.js'
|
|
12
14
|
export * from './src/servicePureVideolink.js'
|
|
13
15
|
export * from './src/serviceUpdateTabStyle.js'
|
|
14
16
|
export * from './src/serviceUserSettings.js'
|
package/package.json
CHANGED
package/src/browserDownload.js
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* @param param0.filename{string}
|
|
6
6
|
* @returns {Promise<void>}
|
|
7
7
|
*/
|
|
8
|
-
export async function browserDownloadByDownlink(
|
|
8
|
+
export async function browserDownloadByDownlink(
|
|
9
|
+
{
|
|
10
|
+
downlink,
|
|
11
|
+
filename,
|
|
12
|
+
}) {
|
|
13
|
+
|
|
9
14
|
let url = downlink;
|
|
10
15
|
await browser.downloads.download({url, filename});
|
|
11
16
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {browserNotificationCreate} from './browserNotification.js';
|
|
4
|
+
import {tabOpCreateNear, tabOpRemove} from './opTab.js';
|
|
5
|
+
|
|
6
|
+
async function browserTabSendMessage(tabId, message) {
|
|
7
|
+
await browser.tabs.sendMessage(tabId, message);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* @param message{{
|
|
13
|
+
* tabId:number,
|
|
14
|
+
* title:string,
|
|
15
|
+
* url:string,
|
|
16
|
+
* focusNewTab:boolean,
|
|
17
|
+
* }}
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
export async function browserTabCreateNearSendMessageToContentJs(message) {
|
|
21
|
+
let {title, url} = message;
|
|
22
|
+
await browserNotificationCreate(`new tab! ${title}`);
|
|
23
|
+
|
|
24
|
+
let {focusNewTab} = message;
|
|
25
|
+
let properties = {
|
|
26
|
+
url, tabId: message.tabId,
|
|
27
|
+
active: focusNewTab,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let {tabId} = await tabOpCreateNear(properties);
|
|
31
|
+
browser.tabs.onUpdated.addListener(
|
|
32
|
+
async function lis(tabId, changeInfo) {
|
|
33
|
+
if (changeInfo.status === 'complete') {
|
|
34
|
+
browser.tabs.onUpdated.removeListener(lis);
|
|
35
|
+
// todo code here
|
|
36
|
+
await browserTabSendMessage(
|
|
37
|
+
tabId, Object.assign({}, message, {tabId}));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
, {tabId, properties: ['status']});
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param param0
|
|
47
|
+
* @param param0.tabId{number}
|
|
48
|
+
*/
|
|
49
|
+
export function browserTabWaitReloadThenRemoveIt({tabId}) {
|
|
50
|
+
browser.tabs.onUpdated.addListener(
|
|
51
|
+
async function lis(tabId, changeInfo) {
|
|
52
|
+
if (changeInfo.status === 'complete') {
|
|
53
|
+
browser.tabs.onUpdated.removeListener(lis);
|
|
54
|
+
// todo code here
|
|
55
|
+
await tabOpRemove(tabId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
, {tabId, properties: ['status']});
|
|
59
|
+
|
|
60
|
+
}
|
package/src/serviceCommon.js
CHANGED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import {serviceGetCurrentDateYYYYMMDDHHMMSS} from './serviceGet.js';
|
|
2
|
-
import {
|
|
3
|
-
serviceCopyContentToClipboard,
|
|
4
|
-
serviceRemoveIllegalWord,
|
|
5
|
-
serviceSaveContentToLocal,
|
|
6
|
-
} from './serviceOpContent.js';
|
|
7
|
-
import {browserNotificationCreate} from './browserNotification.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
* @param param0
|
|
12
|
-
* @param param0.tabId {number}
|
|
13
|
-
* @param param0.filename {string}
|
|
14
|
-
* @param param0.rect {{
|
|
15
|
-
* x,
|
|
16
|
-
* y,
|
|
17
|
-
* width,
|
|
18
|
-
* height,
|
|
19
|
-
* }}
|
|
20
|
-
* @returns {Promise<void>}
|
|
21
|
-
*/
|
|
22
|
-
export async function serviceTakeScreenshot(
|
|
23
|
-
{
|
|
24
|
-
tabId,
|
|
25
|
-
filename,
|
|
26
|
-
rect,
|
|
27
|
-
}) {
|
|
28
|
-
|
|
29
|
-
const tag = 'actTakeScreenshot()';
|
|
30
|
-
console.info(tag, `rect=\n`, rect);
|
|
31
|
-
let dataURI = await browser.tabs.captureTab(tabId, {
|
|
32
|
-
rect: rect,
|
|
33
|
-
});
|
|
34
|
-
let assign = Object.assign(
|
|
35
|
-
{},
|
|
36
|
-
{dataURI, filename},
|
|
37
|
-
);
|
|
38
|
-
await browser.scripting.executeScript({
|
|
39
|
-
target: {tabId},
|
|
40
|
-
args: [assign],
|
|
41
|
-
func: function(message) {
|
|
42
|
-
if (message) {
|
|
43
|
-
let {dataURI, filename} = message;
|
|
44
|
-
|
|
45
|
-
imageDataToLocalFile({dataURI, filename});
|
|
46
|
-
|
|
47
|
-
function imageDataToLocalFile({dataURI, filename}) {
|
|
48
|
-
let a = document.createElement('a');
|
|
49
|
-
a.href = dataURI;
|
|
50
|
-
a.download = filename;
|
|
51
|
-
a.click();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// todo end if(message)
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* middle ware, output => {rect, uniqueSelector,}
|
|
63
|
-
* @param message{{
|
|
64
|
-
* tabId:number,
|
|
65
|
-
* act:string
|
|
66
|
-
* }}
|
|
67
|
-
* @returns {Promise<void>}
|
|
68
|
-
*/
|
|
69
|
-
export async function serviceElementPicker(message) {
|
|
70
|
-
let {tabId} = message;
|
|
71
|
-
await browser.scripting.executeScript({
|
|
72
|
-
target: {tabId},
|
|
73
|
-
args: [message],
|
|
74
|
-
func: async function(message) {
|
|
75
|
-
if (!message) return;
|
|
76
|
-
console.log('picker.js initialized', message);
|
|
77
|
-
|
|
78
|
-
// 1. Create a dedicated "Always On Top" highlighter overlay
|
|
79
|
-
const overlayId = 'extension-element-highlighter-overlay';
|
|
80
|
-
let overlay = document.getElementById(overlayId);
|
|
81
|
-
|
|
82
|
-
if (!overlay) {
|
|
83
|
-
overlay = document.createElement('div');
|
|
84
|
-
overlay.id = overlayId;
|
|
85
|
-
// !important and max z-index guarantees it will never be hidden by site CSS
|
|
86
|
-
overlay.style.cssText = `
|
|
87
|
-
position: fixed !important;
|
|
88
|
-
top: 0 !important;
|
|
89
|
-
left: 0 !important;
|
|
90
|
-
width: 0 !important;
|
|
91
|
-
height: 0 !important;
|
|
92
|
-
background-color: rgba(255, 0, 85, 0.2) !important;
|
|
93
|
-
outline: 2px dashed #ff0055 !important;
|
|
94
|
-
outline-offset: -2px !important;
|
|
95
|
-
z-index: 2147483647 !important;
|
|
96
|
-
pointer-events: none !important;
|
|
97
|
-
transition: all 0.05s ease-out !important;
|
|
98
|
-
display: none !important;
|
|
99
|
-
`;
|
|
100
|
-
// Appending to documentElement (<html>) avoids <body> layout restrictions
|
|
101
|
-
document.documentElement.appendChild(overlay);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getUniqueSelector(el) {
|
|
105
|
-
if (!(el instanceof Element)) return '';
|
|
106
|
-
const path = [];
|
|
107
|
-
while (el.nodeType === Node.ELEMENT_NODE) {
|
|
108
|
-
let selector = el.nodeName.toLowerCase();
|
|
109
|
-
if (el.id) {
|
|
110
|
-
selector += '#' + el.id;
|
|
111
|
-
path.unshift(selector);
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
let sib = el, nth = 1;
|
|
116
|
-
while (sib = sib.previousElementSibling) {
|
|
117
|
-
if (sib.nodeName.toLowerCase() === selector) nth++;
|
|
118
|
-
}
|
|
119
|
-
if (nth !== 1) selector += `:nth-of-type(${nth})`;
|
|
120
|
-
}
|
|
121
|
-
path.unshift(selector);
|
|
122
|
-
el = el.parentNode;
|
|
123
|
-
}
|
|
124
|
-
return path.join(' > ');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function handleMouseOver(e) {
|
|
128
|
-
const target = e.target;
|
|
129
|
-
|
|
130
|
-
// Safety check to ensure we don't try to highlight our own UI
|
|
131
|
-
if (target.id === overlayId) return;
|
|
132
|
-
|
|
133
|
-
// Get exact coordinates of the hovered element
|
|
134
|
-
const clientRect = target.getBoundingClientRect();
|
|
135
|
-
|
|
136
|
-
// Move the floating overlay exactly over the target
|
|
137
|
-
overlay.style.setProperty('display', 'block', 'important');
|
|
138
|
-
overlay.style.setProperty('top', `${clientRect.top}px`, 'important');
|
|
139
|
-
overlay.style.setProperty('left', `${clientRect.left}px`, 'important');
|
|
140
|
-
overlay.style.setProperty('width', `${clientRect.width}px`,
|
|
141
|
-
'important');
|
|
142
|
-
overlay.style.setProperty('height', `${clientRect.height}px`,
|
|
143
|
-
'important');
|
|
144
|
-
|
|
145
|
-
// Change mouse cursor to indicate picking mode
|
|
146
|
-
document.body.style.setProperty('cursor', 'crosshair', 'important');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function handleElementClick(e) {
|
|
150
|
-
e.preventDefault();
|
|
151
|
-
e.stopPropagation();
|
|
152
|
-
|
|
153
|
-
const target = e.target;
|
|
154
|
-
|
|
155
|
-
// Clean up the picker completely
|
|
156
|
-
stopPickingMode();
|
|
157
|
-
|
|
158
|
-
// Calculate screenshot coordinates
|
|
159
|
-
let clientRect = target.getBoundingClientRect();
|
|
160
|
-
let rect = {
|
|
161
|
-
height: clientRect.height,
|
|
162
|
-
width: clientRect.width,
|
|
163
|
-
x: clientRect.left + window.scrollX,
|
|
164
|
-
y: clientRect.top + window.scrollY,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Assuming 'target' is your clicked element (e.g., from e.target)
|
|
168
|
-
let messageTakeScreenshot = Object.assign(
|
|
169
|
-
{}, // Start with a fresh, empty object
|
|
170
|
-
message, // Put the original message first so it doesn't overwrite your new data
|
|
171
|
-
{rect},
|
|
172
|
-
{
|
|
173
|
-
// The guaranteed unique CSS path (e.g., "div#wrap > ul > li:nth-of-type(2)")
|
|
174
|
-
uniqueSelector: getUniqueSelector(target),
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
await browser.runtime.sendMessage(messageTakeScreenshot);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function startPickingMode() {
|
|
182
|
-
document.addEventListener('mouseover', handleMouseOver, true);
|
|
183
|
-
document.addEventListener('click', handleElementClick, true);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function stopPickingMode() {
|
|
187
|
-
document.removeEventListener('mouseover', handleMouseOver, true);
|
|
188
|
-
document.removeEventListener('click', handleElementClick, true);
|
|
189
|
-
|
|
190
|
-
// Restore normal mouse cursor
|
|
191
|
-
document.body.style.removeProperty('cursor');
|
|
192
|
-
|
|
193
|
-
// Remove the overlay from the DOM entirely
|
|
194
|
-
if (overlay) {
|
|
195
|
-
overlay.remove();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Initialize the picker mode
|
|
200
|
-
startPickingMode();
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* middle ware, output: {rect}
|
|
207
|
-
*
|
|
208
|
-
* @param message{{
|
|
209
|
-
* tabId:number,
|
|
210
|
-
* act:string
|
|
211
|
-
* }}
|
|
212
|
-
* @returns {Promise<void>}
|
|
213
|
-
*/
|
|
214
|
-
export async function serviceGetFullPageRectData(message) {
|
|
215
|
-
let {tabId} = message;
|
|
216
|
-
|
|
217
|
-
await browser.scripting.executeScript({
|
|
218
|
-
target: {tabId},
|
|
219
|
-
args: [message],
|
|
220
|
-
func: (message) => {
|
|
221
|
-
|
|
222
|
-
let x = 0, y = 0;
|
|
223
|
-
let width = document.documentElement.scrollWidth;
|
|
224
|
-
let height = document.documentElement.scrollHeight;
|
|
225
|
-
let rect = {
|
|
226
|
-
x, y, width, height,
|
|
227
|
-
};
|
|
228
|
-
browser.runtime.sendMessage(Object.assign(
|
|
229
|
-
{},
|
|
230
|
-
message,
|
|
231
|
-
{rect},
|
|
232
|
-
));
|
|
233
|
-
// todo end if (message)
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* middle ware, output: Object.assign({}, message, {data})
|
|
240
|
-
*
|
|
241
|
-
* serviceFindAllMagnetLink({
|
|
242
|
-
* tabId, title, act, ...
|
|
243
|
-
* })
|
|
244
|
-
*
|
|
245
|
-
* @param message{{
|
|
246
|
-
* tabId:number,
|
|
247
|
-
* title:string,
|
|
248
|
-
* act: string,
|
|
249
|
-
* }}
|
|
250
|
-
*/
|
|
251
|
-
export async function serviceFindAllMagnetLink(message) {
|
|
252
|
-
let {tabId} = message;
|
|
253
|
-
const assign = Object.assign({}, message);
|
|
254
|
-
|
|
255
|
-
await browser.scripting.executeScript({
|
|
256
|
-
target: {tabId},
|
|
257
|
-
args: [assign],
|
|
258
|
-
func: async (message) => {
|
|
259
|
-
if (message) {
|
|
260
|
-
function findAllMagnetLinks() {
|
|
261
|
-
const magnets = new Set(); // Prevents duplicates
|
|
262
|
-
|
|
263
|
-
// --- Type 1: Find inside ANY element's attributes ---
|
|
264
|
-
const attributeSelector = '*[href*="magnet:"], *[data-url*="magnet:"], *[data-magnet*="magnet:"], *[data-href*="magnet:"]';
|
|
265
|
-
const attrElements = document.querySelectorAll(attributeSelector);
|
|
266
|
-
|
|
267
|
-
attrElements.forEach(el => {
|
|
268
|
-
// Check every attribute of the element to find the one holding the magnet string
|
|
269
|
-
for (let attr of el.attributes) {
|
|
270
|
-
if (attr.value.includes('magnet:?xt=')) {
|
|
271
|
-
magnets.add(attr.value.trim());
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// --- Type 2: Find inside raw text (for <div>, <span>, <td>, etc.) ---
|
|
277
|
-
// We target elements that don't have children to avoid grabbing huge parent container blocks
|
|
278
|
-
const allElements = document.querySelectorAll(
|
|
279
|
-
'div, span, td, p, a, button');
|
|
280
|
-
allElements.forEach(el => {
|
|
281
|
-
if (el.children.length === 0) { // Deepest element
|
|
282
|
-
const text = el.textContent.trim();
|
|
283
|
-
if (text.includes('magnet:?xt=')) {
|
|
284
|
-
// Extract just the magnet link using Regex in case there is surrounding text
|
|
285
|
-
const match = text.match(/magnet:\?xt=[^\s"'<>]+/);
|
|
286
|
-
if (match) {
|
|
287
|
-
magnets.add(match[0]);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
return Array.from(magnets);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
await browser.runtime.sendMessage(Object.assign(
|
|
297
|
-
{},
|
|
298
|
-
message,
|
|
299
|
-
{
|
|
300
|
-
data: findAllMagnetLinks(),
|
|
301
|
-
},
|
|
302
|
-
));
|
|
303
|
-
|
|
304
|
-
// todo end if(message)
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
*
|
|
312
|
-
* @param message{{
|
|
313
|
-
* title:string,
|
|
314
|
-
* data: [string],
|
|
315
|
-
* handleOption: 'clipboard'|'txt'|'clipboardAndTxt'
|
|
316
|
-
* }}
|
|
317
|
-
* @returns {Promise<void>}
|
|
318
|
-
*/
|
|
319
|
-
export async function serviceDealWithMagnetLink(message) {
|
|
320
|
-
|
|
321
|
-
let {title, data, handleOption} = message;
|
|
322
|
-
let titleCleaned = serviceRemoveIllegalWord(title);
|
|
323
|
-
console.info(`data.length=\n`, data.length);
|
|
324
|
-
|
|
325
|
-
if (Array.isArray(data) && data.length >= 1) {
|
|
326
|
-
let content = `${data.join('\n')}\n`;
|
|
327
|
-
console.info(`content=\n`, content);
|
|
328
|
-
|
|
329
|
-
let filename = [
|
|
330
|
-
'magnet-link',
|
|
331
|
-
titleCleaned,
|
|
332
|
-
serviceGetCurrentDateYYYYMMDDHHMMSS()].join(' ');
|
|
333
|
-
|
|
334
|
-
if (handleOption === 'clipboard') {
|
|
335
|
-
await serviceCopyContentToClipboard(content);
|
|
336
|
-
}
|
|
337
|
-
else if (handleOption === 'txt') {
|
|
338
|
-
serviceSaveContentToLocal(content, filename);
|
|
339
|
-
}
|
|
340
|
-
else if (handleOption === 'clipboardAndTxt') {
|
|
341
|
-
await serviceCopyContentToClipboard(content);
|
|
342
|
-
serviceSaveContentToLocal(content, filename);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
// todo notification => magnet link not found!
|
|
347
|
-
await browserNotificationCreate('magnet link not found!');
|
|
348
|
-
}
|
|
349
|
-
}
|
package/src/serviceOpContent.js
CHANGED
|
@@ -65,28 +65,31 @@ export async function serviceGenerateMkvToolNixScript({vid, title}) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* @param {string} value -
|
|
71
|
-
* @returns {string} -
|
|
68
|
+
* 终极清理函数:过滤所有中英文标点、全角符号、以及非法/不可见字符。
|
|
69
|
+
* (100% 预防下载报错和复制粘贴导致的乱码)
|
|
70
|
+
* @param {string} value - 原始视频标题
|
|
71
|
+
* @returns {string} - 干净、安全的纯文本/字母数字文件名
|
|
72
72
|
*/
|
|
73
73
|
export function serviceRemoveIllegalWord(value) {
|
|
74
74
|
if (!value) return '';
|
|
75
75
|
|
|
76
|
-
// 1.
|
|
76
|
+
// 1. 获取第一行并去除两端空格
|
|
77
77
|
let name = value.trim().split(/\r?\n/).shift();
|
|
78
78
|
|
|
79
|
-
// 2.
|
|
80
|
-
//
|
|
81
|
-
//
|
|
79
|
+
// 2. 使用 Unicode 属性移除所有危险字符:
|
|
80
|
+
// \p{P} = 所有标点符号
|
|
81
|
+
// \p{S} = 所有符号(Emoji、数学符号、货币符号)
|
|
82
|
+
// \p{C} = 所有控制/格式化/代理字符(完美修复不可见的 U+202A/U+202C 导致崩溃的 Bug!)
|
|
82
83
|
name = name.replace(/[\p{P}\p{S}\p{C}]/gu, ' ');
|
|
83
84
|
|
|
84
|
-
// 3.
|
|
85
|
-
|
|
85
|
+
// 3. Firefox WebExtension 严格拦截的特殊字符。
|
|
86
|
+
// (大部分已经被 \p{P} 和 \p{S} 处理,但显式移除可以确保彻底杜绝边缘报错)
|
|
87
|
+
name = name.replace(/[~"#%&*:<>?/\\{|}]/g, ' ');
|
|
86
88
|
|
|
87
|
-
// 4.
|
|
88
|
-
name = name.replace(/[\
|
|
89
|
+
// 4. 将连续的空格(包括全角/Unicode空格)合并为一个标准空格
|
|
90
|
+
name = name.replace(/[\s\u3000]+/g, ' ').trim();
|
|
89
91
|
|
|
90
|
-
// 5.
|
|
91
|
-
|
|
92
|
+
// 5. Firefox 下载 API 会因为文件名以点(.)或连字符(-)开头/结尾而报错
|
|
93
|
+
// 再次修剪以确保绝对安全
|
|
94
|
+
return name.replace(/^[-.]+|[-.]+$/g, '');
|
|
92
95
|
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {serviceGetCurrentDateYYYYMMDDHHMMSS} from './serviceGet.js';
|
|
2
|
+
import {
|
|
3
|
+
serviceCopyContentToClipboard,
|
|
4
|
+
serviceRemoveIllegalWord,
|
|
5
|
+
serviceSaveContentToLocal,
|
|
6
|
+
} from './serviceOpContent.js';
|
|
7
|
+
import {browserNotificationCreate} from './browserNotification.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param param0
|
|
12
|
+
* @param param0.tabId {number}
|
|
13
|
+
* @param param0.filename {string}
|
|
14
|
+
* @param param0.rect {{
|
|
15
|
+
* x,
|
|
16
|
+
* y,
|
|
17
|
+
* width,
|
|
18
|
+
* height,
|
|
19
|
+
* }}
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
export async function serviceTakeScreenshot(
|
|
23
|
+
{
|
|
24
|
+
tabId,
|
|
25
|
+
filename,
|
|
26
|
+
rect,
|
|
27
|
+
}) {
|
|
28
|
+
|
|
29
|
+
const tag = 'actTakeScreenshot()';
|
|
30
|
+
console.info(tag, `rect=\n`, rect);
|
|
31
|
+
let dataURI = await browser.tabs.captureTab(tabId, {
|
|
32
|
+
rect: rect,
|
|
33
|
+
});
|
|
34
|
+
let assign = Object.assign(
|
|
35
|
+
{},
|
|
36
|
+
{dataURI, filename},
|
|
37
|
+
);
|
|
38
|
+
await browser.scripting.executeScript({
|
|
39
|
+
target: {tabId},
|
|
40
|
+
args: [assign],
|
|
41
|
+
func: function(message) {
|
|
42
|
+
if (message) {
|
|
43
|
+
let {dataURI, filename} = message;
|
|
44
|
+
|
|
45
|
+
imageDataToLocalFile({dataURI, filename});
|
|
46
|
+
|
|
47
|
+
function imageDataToLocalFile({dataURI, filename}) {
|
|
48
|
+
let a = document.createElement('a');
|
|
49
|
+
a.href = dataURI;
|
|
50
|
+
a.download = filename;
|
|
51
|
+
a.click();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// todo end if(message)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* middle ware, output => {rect, uniqueSelector,}
|
|
63
|
+
* @param message{{
|
|
64
|
+
* tabId:number,
|
|
65
|
+
* act:string
|
|
66
|
+
* }}
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
export async function serviceElementPicker(message) {
|
|
70
|
+
let {tabId} = message;
|
|
71
|
+
await browser.scripting.executeScript({
|
|
72
|
+
target: {tabId},
|
|
73
|
+
args: [message],
|
|
74
|
+
func: async function(message) {
|
|
75
|
+
if (!message) return;
|
|
76
|
+
console.log('picker.js initialized', message);
|
|
77
|
+
|
|
78
|
+
// 1. Create a dedicated "Always On Top" highlighter overlay
|
|
79
|
+
const overlayId = 'extension-element-highlighter-overlay';
|
|
80
|
+
let overlay = document.getElementById(overlayId);
|
|
81
|
+
|
|
82
|
+
if (!overlay) {
|
|
83
|
+
overlay = document.createElement('div');
|
|
84
|
+
overlay.id = overlayId;
|
|
85
|
+
// !important and max z-index guarantees it will never be hidden by site CSS
|
|
86
|
+
overlay.style.cssText = `
|
|
87
|
+
position: fixed !important;
|
|
88
|
+
top: 0 !important;
|
|
89
|
+
left: 0 !important;
|
|
90
|
+
width: 0 !important;
|
|
91
|
+
height: 0 !important;
|
|
92
|
+
background-color: rgba(255, 0, 85, 0.2) !important;
|
|
93
|
+
outline: 2px dashed #ff0055 !important;
|
|
94
|
+
outline-offset: -2px !important;
|
|
95
|
+
z-index: 2147483647 !important;
|
|
96
|
+
pointer-events: none !important;
|
|
97
|
+
transition: all 0.05s ease-out !important;
|
|
98
|
+
display: none !important;
|
|
99
|
+
`;
|
|
100
|
+
// Appending to documentElement (<html>) avoids <body> layout restrictions
|
|
101
|
+
document.documentElement.appendChild(overlay);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getUniqueSelector(el) {
|
|
105
|
+
if (!(el instanceof Element)) return '';
|
|
106
|
+
const path = [];
|
|
107
|
+
while (el.nodeType === Node.ELEMENT_NODE) {
|
|
108
|
+
let selector = el.nodeName.toLowerCase();
|
|
109
|
+
if (el.id) {
|
|
110
|
+
selector += '#' + el.id;
|
|
111
|
+
path.unshift(selector);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
let sib = el, nth = 1;
|
|
116
|
+
while (sib = sib.previousElementSibling) {
|
|
117
|
+
if (sib.nodeName.toLowerCase() === selector) nth++;
|
|
118
|
+
}
|
|
119
|
+
if (nth !== 1) selector += `:nth-of-type(${nth})`;
|
|
120
|
+
}
|
|
121
|
+
path.unshift(selector);
|
|
122
|
+
el = el.parentNode;
|
|
123
|
+
}
|
|
124
|
+
return path.join(' > ');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleMouseOver(e) {
|
|
128
|
+
const target = e.target;
|
|
129
|
+
|
|
130
|
+
// Safety check to ensure we don't try to highlight our own UI
|
|
131
|
+
if (target.id === overlayId) return;
|
|
132
|
+
|
|
133
|
+
// Get exact coordinates of the hovered element
|
|
134
|
+
const clientRect = target.getBoundingClientRect();
|
|
135
|
+
|
|
136
|
+
// Move the floating overlay exactly over the target
|
|
137
|
+
overlay.style.setProperty('display', 'block', 'important');
|
|
138
|
+
overlay.style.setProperty('top', `${clientRect.top}px`, 'important');
|
|
139
|
+
overlay.style.setProperty('left', `${clientRect.left}px`, 'important');
|
|
140
|
+
overlay.style.setProperty('width', `${clientRect.width}px`,
|
|
141
|
+
'important');
|
|
142
|
+
overlay.style.setProperty('height', `${clientRect.height}px`,
|
|
143
|
+
'important');
|
|
144
|
+
|
|
145
|
+
// Change mouse cursor to indicate picking mode
|
|
146
|
+
document.body.style.setProperty('cursor', 'crosshair', 'important');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleElementClick(e) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
e.stopPropagation();
|
|
152
|
+
|
|
153
|
+
const target = e.target;
|
|
154
|
+
|
|
155
|
+
// Clean up the picker completely
|
|
156
|
+
stopPickingMode();
|
|
157
|
+
|
|
158
|
+
// Calculate screenshot coordinates
|
|
159
|
+
let clientRect = target.getBoundingClientRect();
|
|
160
|
+
let rect = {
|
|
161
|
+
height: clientRect.height,
|
|
162
|
+
width: clientRect.width,
|
|
163
|
+
x: clientRect.left + window.scrollX,
|
|
164
|
+
y: clientRect.top + window.scrollY,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Assuming 'target' is your clicked element (e.g., from e.target)
|
|
168
|
+
let messageTakeScreenshot = Object.assign(
|
|
169
|
+
{}, // Start with a fresh, empty object
|
|
170
|
+
message, // Put the original message first so it doesn't overwrite your new data
|
|
171
|
+
{rect},
|
|
172
|
+
{
|
|
173
|
+
// The guaranteed unique CSS path (e.g., "div#wrap > ul > li:nth-of-type(2)")
|
|
174
|
+
uniqueSelector: getUniqueSelector(target),
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await browser.runtime.sendMessage(messageTakeScreenshot);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function startPickingMode() {
|
|
182
|
+
document.addEventListener('mouseover', handleMouseOver, true);
|
|
183
|
+
document.addEventListener('click', handleElementClick, true);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stopPickingMode() {
|
|
187
|
+
document.removeEventListener('mouseover', handleMouseOver, true);
|
|
188
|
+
document.removeEventListener('click', handleElementClick, true);
|
|
189
|
+
|
|
190
|
+
// Restore normal mouse cursor
|
|
191
|
+
document.body.style.removeProperty('cursor');
|
|
192
|
+
|
|
193
|
+
// Remove the overlay from the DOM entirely
|
|
194
|
+
if (overlay) {
|
|
195
|
+
overlay.remove();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Initialize the picker mode
|
|
200
|
+
startPickingMode();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* middle ware, output: {rect}
|
|
207
|
+
*
|
|
208
|
+
* @param message{{
|
|
209
|
+
* tabId:number,
|
|
210
|
+
* act:string
|
|
211
|
+
* }}
|
|
212
|
+
* @returns {Promise<void>}
|
|
213
|
+
*/
|
|
214
|
+
export async function serviceGetFullPageRectData(message) {
|
|
215
|
+
let {tabId} = message;
|
|
216
|
+
|
|
217
|
+
await browser.scripting.executeScript({
|
|
218
|
+
target: {tabId},
|
|
219
|
+
args: [message],
|
|
220
|
+
func: (message) => {
|
|
221
|
+
|
|
222
|
+
let x = 0, y = 0;
|
|
223
|
+
let width = document.documentElement.scrollWidth;
|
|
224
|
+
let height = document.documentElement.scrollHeight;
|
|
225
|
+
let rect = {
|
|
226
|
+
x, y, width, height,
|
|
227
|
+
};
|
|
228
|
+
browser.runtime.sendMessage(Object.assign(
|
|
229
|
+
{},
|
|
230
|
+
message,
|
|
231
|
+
{rect},
|
|
232
|
+
));
|
|
233
|
+
// todo end if (message)
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* middle ware, output: Object.assign({}, message, {data})
|
|
240
|
+
*
|
|
241
|
+
* serviceFindAllMagnetLink({
|
|
242
|
+
* tabId, title, act, ...
|
|
243
|
+
* })
|
|
244
|
+
*
|
|
245
|
+
* @param message{{
|
|
246
|
+
* tabId:number,
|
|
247
|
+
* title:string,
|
|
248
|
+
* act: string,
|
|
249
|
+
* }}
|
|
250
|
+
*/
|
|
251
|
+
export async function serviceFindAllMagnetLink(message) {
|
|
252
|
+
let {tabId} = message;
|
|
253
|
+
const assign = Object.assign({}, message);
|
|
254
|
+
|
|
255
|
+
await browser.scripting.executeScript({
|
|
256
|
+
target: {tabId},
|
|
257
|
+
args: [assign],
|
|
258
|
+
func: async (message) => {
|
|
259
|
+
if (message) {
|
|
260
|
+
function findAllMagnetLinks() {
|
|
261
|
+
const magnets = new Set(); // Prevents duplicates
|
|
262
|
+
|
|
263
|
+
// --- Type 1: Find inside ANY element's attributes ---
|
|
264
|
+
const attributeSelector = '*[href*="magnet:"], *[data-url*="magnet:"], *[data-magnet*="magnet:"], *[data-href*="magnet:"]';
|
|
265
|
+
const attrElements = document.querySelectorAll(attributeSelector);
|
|
266
|
+
|
|
267
|
+
attrElements.forEach(el => {
|
|
268
|
+
// Check every attribute of the element to find the one holding the magnet string
|
|
269
|
+
for (let attr of el.attributes) {
|
|
270
|
+
if (attr.value.includes('magnet:?xt=')) {
|
|
271
|
+
magnets.add(attr.value.trim());
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// --- Type 2: Find inside raw text (for <div>, <span>, <td>, etc.) ---
|
|
277
|
+
// We target elements that don't have children to avoid grabbing huge parent container blocks
|
|
278
|
+
const allElements = document.querySelectorAll(
|
|
279
|
+
'div, span, td, p, a, button');
|
|
280
|
+
allElements.forEach(el => {
|
|
281
|
+
if (el.children.length === 0) { // Deepest element
|
|
282
|
+
const text = el.textContent.trim();
|
|
283
|
+
if (text.includes('magnet:?xt=')) {
|
|
284
|
+
// Extract just the magnet link using Regex in case there is surrounding text
|
|
285
|
+
const match = text.match(/magnet:\?xt=[^\s"'<>]+/);
|
|
286
|
+
if (match) {
|
|
287
|
+
magnets.add(match[0]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return Array.from(magnets);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await browser.runtime.sendMessage(Object.assign(
|
|
297
|
+
{},
|
|
298
|
+
message,
|
|
299
|
+
{
|
|
300
|
+
data: findAllMagnetLinks(),
|
|
301
|
+
},
|
|
302
|
+
));
|
|
303
|
+
|
|
304
|
+
// todo end if(message)
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
*
|
|
312
|
+
* @param message{{
|
|
313
|
+
* title:string,
|
|
314
|
+
* data: [string],
|
|
315
|
+
* handleOption: 'clipboard'|'txt'|'clipboardAndTxt'
|
|
316
|
+
* }}
|
|
317
|
+
* @returns {Promise<void>}
|
|
318
|
+
*/
|
|
319
|
+
export async function serviceDealWithMagnetLink(message) {
|
|
320
|
+
|
|
321
|
+
let {title, data, handleOption} = message;
|
|
322
|
+
let titleCleaned = serviceRemoveIllegalWord(title);
|
|
323
|
+
console.info(`data.length=\n`, data.length);
|
|
324
|
+
|
|
325
|
+
if (Array.isArray(data) && data.length >= 1) {
|
|
326
|
+
let content = `${data.join('\n')}\n`;
|
|
327
|
+
console.info(`content=\n`, content);
|
|
328
|
+
|
|
329
|
+
let filename = [
|
|
330
|
+
'magnet-link',
|
|
331
|
+
titleCleaned,
|
|
332
|
+
serviceGetCurrentDateYYYYMMDDHHMMSS()].join(' ');
|
|
333
|
+
|
|
334
|
+
if (handleOption === 'clipboard') {
|
|
335
|
+
await serviceCopyContentToClipboard(content);
|
|
336
|
+
}
|
|
337
|
+
else if (handleOption === 'txt') {
|
|
338
|
+
serviceSaveContentToLocal(content, filename);
|
|
339
|
+
}
|
|
340
|
+
else if (handleOption === 'clipboardAndTxt') {
|
|
341
|
+
await serviceCopyContentToClipboard(content);
|
|
342
|
+
serviceSaveContentToLocal(content, filename);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// todo notification => magnet link not found!
|
|
347
|
+
await browserNotificationCreate('magnet link not found!');
|
|
348
|
+
}
|
|
349
|
+
}
|