cli-browser 1.0.0
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 +309 -0
- package/bin/cli-browser.js +8 -0
- package/package.json +50 -0
- package/src/bangs.js +53 -0
- package/src/bookmarks.js +45 -0
- package/src/browser.js +1277 -0
- package/src/config.js +51 -0
- package/src/developer.js +246 -0
- package/src/download.js +80 -0
- package/src/history.js +58 -0
- package/src/index.js +45 -0
- package/src/network.js +56 -0
- package/src/plugins.js +74 -0
- package/src/renderer.js +259 -0
- package/src/search.js +113 -0
- package/src/sessions.js +37 -0
- package/src/stats.js +66 -0
- package/src/tabs.js +147 -0
- package/src/themes.js +137 -0
- package/src/ui.js +202 -0
package/src/renderer.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
import { theme } from './themes.js';
|
|
3
|
+
|
|
4
|
+
class Renderer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.links = [];
|
|
7
|
+
this.images = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
render(html, url = '') {
|
|
11
|
+
this.links = [];
|
|
12
|
+
this.images = [];
|
|
13
|
+
|
|
14
|
+
const $ = cheerio.load(html);
|
|
15
|
+
|
|
16
|
+
// Remove unwanted elements
|
|
17
|
+
$('script, style, noscript, iframe, svg, nav, footer, header, .ad, .ads, .advertisement, .sidebar, .menu, .nav, .cookie, .popup, .modal, .banner').remove();
|
|
18
|
+
|
|
19
|
+
const title = $('title').text().trim() || 'Untitled Page';
|
|
20
|
+
const body = $('body');
|
|
21
|
+
|
|
22
|
+
let output = '';
|
|
23
|
+
output += this._processElement($, body, 0);
|
|
24
|
+
|
|
25
|
+
// Deduplicate links
|
|
26
|
+
const uniqueLinks = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const link of this.links) {
|
|
29
|
+
const key = link.url;
|
|
30
|
+
if (!seen.has(key) && link.url && !link.url.startsWith('#') && !link.url.startsWith('javascript:')) {
|
|
31
|
+
seen.add(key);
|
|
32
|
+
// Resolve relative URLs
|
|
33
|
+
if (link.url.startsWith('/') && url) {
|
|
34
|
+
try {
|
|
35
|
+
const base = new URL(url);
|
|
36
|
+
link.url = `${base.protocol}//${base.host}${link.url}`;
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
uniqueLinks.push(link);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.links = uniqueLinks;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
title,
|
|
46
|
+
content: this._cleanOutput(output),
|
|
47
|
+
links: this.links,
|
|
48
|
+
images: this.images,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
renderReader(html, url = '') {
|
|
53
|
+
const $ = cheerio.load(html);
|
|
54
|
+
|
|
55
|
+
// Remove navigation, ads, sidebars, etc
|
|
56
|
+
$('script, style, noscript, iframe, svg, nav, footer, header, aside, .ad, .ads, .advertisement, .sidebar, .menu, .nav, .cookie, .popup, .modal, .banner, .share, .social, .related, .comments, .comment, form, button, input, select, textarea').remove();
|
|
57
|
+
|
|
58
|
+
const title = $('title').text().trim() ||
|
|
59
|
+
$('h1').first().text().trim() ||
|
|
60
|
+
'Untitled Page';
|
|
61
|
+
|
|
62
|
+
// Try to find main content
|
|
63
|
+
let mainContent = $('article, [role="main"], main, .post-content, .article-content, .entry-content, .content, #content, #main').first();
|
|
64
|
+
if (!mainContent.length) {
|
|
65
|
+
mainContent = $('body');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.links = [];
|
|
69
|
+
this.images = [];
|
|
70
|
+
|
|
71
|
+
let output = this._processElement($, mainContent, 0);
|
|
72
|
+
|
|
73
|
+
// Resolve relative URLs
|
|
74
|
+
const uniqueLinks = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const link of this.links) {
|
|
77
|
+
if (!seen.has(link.url) && link.url && !link.url.startsWith('#') && !link.url.startsWith('javascript:')) {
|
|
78
|
+
seen.add(link.url);
|
|
79
|
+
if (link.url.startsWith('/') && url) {
|
|
80
|
+
try {
|
|
81
|
+
const base = new URL(url);
|
|
82
|
+
link.url = `${base.protocol}//${base.host}${link.url}`;
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
uniqueLinks.push(link);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.links = uniqueLinks;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
title,
|
|
92
|
+
content: this._cleanOutput(output),
|
|
93
|
+
links: this.links,
|
|
94
|
+
images: this.images,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getPageInfo(html) {
|
|
99
|
+
const $ = cheerio.load(html);
|
|
100
|
+
return {
|
|
101
|
+
title: $('title').text().trim(),
|
|
102
|
+
description: $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '',
|
|
103
|
+
keywords: $('meta[name="keywords"]').attr('content') || '',
|
|
104
|
+
language: $('html').attr('lang') || '',
|
|
105
|
+
author: $('meta[name="author"]').attr('content') || '',
|
|
106
|
+
ogTitle: $('meta[property="og:title"]').attr('content') || '',
|
|
107
|
+
ogImage: $('meta[property="og:image"]').attr('content') || '',
|
|
108
|
+
ogType: $('meta[property="og:type"]').attr('content') || '',
|
|
109
|
+
canonical: $('link[rel="canonical"]').attr('href') || '',
|
|
110
|
+
favicon: $('link[rel="icon"]').attr('href') || $('link[rel="shortcut icon"]').attr('href') || '',
|
|
111
|
+
charset: $('meta[charset]').attr('charset') || '',
|
|
112
|
+
viewport: $('meta[name="viewport"]').attr('content') || '',
|
|
113
|
+
generator: $('meta[name="generator"]').attr('content') || '',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_processElement($, el, depth) {
|
|
118
|
+
let output = '';
|
|
119
|
+
|
|
120
|
+
$(el).contents().each((_, node) => {
|
|
121
|
+
if (node.type === 'text') {
|
|
122
|
+
const text = $(node).text();
|
|
123
|
+
if (text.trim()) {
|
|
124
|
+
output += text.replace(/\s+/g, ' ');
|
|
125
|
+
}
|
|
126
|
+
} else if (node.type === 'tag') {
|
|
127
|
+
const tagName = node.tagName?.toLowerCase();
|
|
128
|
+
const $node = $(node);
|
|
129
|
+
|
|
130
|
+
switch (tagName) {
|
|
131
|
+
case 'h1':
|
|
132
|
+
output += '\n\n' + theme.title(' ══ ' + $node.text().trim() + ' ══') + '\n\n';
|
|
133
|
+
break;
|
|
134
|
+
case 'h2':
|
|
135
|
+
output += '\n\n' + theme.title(' ── ' + $node.text().trim() + ' ──') + '\n';
|
|
136
|
+
break;
|
|
137
|
+
case 'h3':
|
|
138
|
+
output += '\n\n' + theme.accent(' ▸ ' + $node.text().trim()) + '\n';
|
|
139
|
+
break;
|
|
140
|
+
case 'h4':
|
|
141
|
+
case 'h5':
|
|
142
|
+
case 'h6':
|
|
143
|
+
output += '\n' + theme.accent(' ' + $node.text().trim()) + '\n';
|
|
144
|
+
break;
|
|
145
|
+
case 'p':
|
|
146
|
+
output += '\n ' + this._processElement($, $node, depth).trim() + '\n';
|
|
147
|
+
break;
|
|
148
|
+
case 'br':
|
|
149
|
+
output += '\n';
|
|
150
|
+
break;
|
|
151
|
+
case 'hr':
|
|
152
|
+
output += '\n' + theme.muted(' ' + '─'.repeat(60)) + '\n';
|
|
153
|
+
break;
|
|
154
|
+
case 'a': {
|
|
155
|
+
const href = $node.attr('href') || '';
|
|
156
|
+
const text = $node.text().trim();
|
|
157
|
+
if (text && href) {
|
|
158
|
+
const linkIdx = this.links.length + 1;
|
|
159
|
+
this.links.push({ text, url: href });
|
|
160
|
+
output += theme.link(text) + theme.muted(` [${linkIdx}]`);
|
|
161
|
+
} else if (text) {
|
|
162
|
+
output += text;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'img': {
|
|
167
|
+
const src = $node.attr('src') || '';
|
|
168
|
+
const alt = $node.attr('alt') || 'image';
|
|
169
|
+
if (src) {
|
|
170
|
+
this.images.push({ src, alt });
|
|
171
|
+
output += theme.muted(`[IMG: ${alt}]`);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'strong':
|
|
176
|
+
case 'b':
|
|
177
|
+
output += theme.highlight($node.text().trim());
|
|
178
|
+
break;
|
|
179
|
+
case 'em':
|
|
180
|
+
case 'i':
|
|
181
|
+
output += theme.info($node.text().trim());
|
|
182
|
+
break;
|
|
183
|
+
case 'code':
|
|
184
|
+
output += theme.warning('`' + $node.text().trim() + '`');
|
|
185
|
+
break;
|
|
186
|
+
case 'pre': {
|
|
187
|
+
const code = $node.text().trim();
|
|
188
|
+
output += '\n' + theme.muted(' ┌─ code ────────────────────') + '\n';
|
|
189
|
+
code.split('\n').forEach(line => {
|
|
190
|
+
output += theme.muted(' │ ') + theme.secondary(line) + '\n';
|
|
191
|
+
});
|
|
192
|
+
output += theme.muted(' └─────────────────────────────') + '\n';
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'ul':
|
|
196
|
+
$node.children('li').each((_, li) => {
|
|
197
|
+
output += '\n ' + theme.accent(' • ') + this._processElement($, $(li), depth + 1).trim();
|
|
198
|
+
});
|
|
199
|
+
output += '\n';
|
|
200
|
+
break;
|
|
201
|
+
case 'ol':
|
|
202
|
+
$node.children('li').each((i, li) => {
|
|
203
|
+
output += '\n ' + theme.accent(` ${i + 1}. `) + this._processElement($, $(li), depth + 1).trim();
|
|
204
|
+
});
|
|
205
|
+
output += '\n';
|
|
206
|
+
break;
|
|
207
|
+
case 'li':
|
|
208
|
+
output += this._processElement($, $node, depth + 1);
|
|
209
|
+
break;
|
|
210
|
+
case 'blockquote':
|
|
211
|
+
output += '\n' + theme.muted(' │ ') + theme.info(this._processElement($, $node, depth + 1).trim()) + '\n';
|
|
212
|
+
break;
|
|
213
|
+
case 'table': {
|
|
214
|
+
output += '\n';
|
|
215
|
+
$node.find('tr').each((_, tr) => {
|
|
216
|
+
const cells = [];
|
|
217
|
+
$(tr).find('th, td').each((__, cell) => {
|
|
218
|
+
cells.push($(cell).text().trim());
|
|
219
|
+
});
|
|
220
|
+
output += ' ' + theme.muted('│') + cells.map(c => ` ${c} `).join(theme.muted('│')) + theme.muted('│') + '\n';
|
|
221
|
+
});
|
|
222
|
+
output += '\n';
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'div':
|
|
226
|
+
case 'section':
|
|
227
|
+
case 'article':
|
|
228
|
+
case 'main':
|
|
229
|
+
case 'span':
|
|
230
|
+
case 'figure':
|
|
231
|
+
case 'figcaption':
|
|
232
|
+
output += this._processElement($, $node, depth);
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
output += this._processElement($, $node, depth);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return output;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_cleanOutput(text) {
|
|
244
|
+
return text
|
|
245
|
+
.replace(/\n{4,}/g, '\n\n\n')
|
|
246
|
+
.replace(/[ \t]+$/gm, '')
|
|
247
|
+
.trim();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getLinks() {
|
|
251
|
+
return this.links;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getImages() {
|
|
255
|
+
return this.images;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default new Renderer();
|
package/src/search.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import config from './config.js';
|
|
3
|
+
import { theme } from './themes.js';
|
|
4
|
+
|
|
5
|
+
class SearchEngine {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.baseUrl = config.get('searxng');
|
|
8
|
+
this.lastResults = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async search(query, options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
engine = null,
|
|
14
|
+
type = 'general',
|
|
15
|
+
time = null,
|
|
16
|
+
page = 1,
|
|
17
|
+
language = 'en',
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
const params = {
|
|
21
|
+
q: query,
|
|
22
|
+
format: 'json',
|
|
23
|
+
categories: type,
|
|
24
|
+
pageno: page,
|
|
25
|
+
language,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (engine) {
|
|
29
|
+
params.engines = engine;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (time) {
|
|
33
|
+
const timeMap = {
|
|
34
|
+
day: 'day',
|
|
35
|
+
week: 'week',
|
|
36
|
+
month: 'month',
|
|
37
|
+
year: 'year',
|
|
38
|
+
};
|
|
39
|
+
params.time_range = timeMap[time] || time;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const proxy = config.get('proxy');
|
|
43
|
+
const axiosConfig = {
|
|
44
|
+
params,
|
|
45
|
+
headers: {
|
|
46
|
+
'User-Agent': config.get('userAgent'),
|
|
47
|
+
Accept: 'application/json',
|
|
48
|
+
},
|
|
49
|
+
timeout: 15000,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (proxy) {
|
|
53
|
+
axiosConfig.proxy = {
|
|
54
|
+
host: proxy.split(':')[0],
|
|
55
|
+
port: parseInt(proxy.split(':')[1]),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const response = await axios.get(`${this.baseUrl}/search`, axiosConfig);
|
|
60
|
+
const data = response.data;
|
|
61
|
+
|
|
62
|
+
this.lastResults = (data.results || []).map((r, i) => ({
|
|
63
|
+
index: i + 1,
|
|
64
|
+
title: r.title || 'Untitled',
|
|
65
|
+
url: r.url || '',
|
|
66
|
+
content: r.content || '',
|
|
67
|
+
engine: r.engine || '',
|
|
68
|
+
category: r.category || type,
|
|
69
|
+
publishedDate: r.publishedDate || null,
|
|
70
|
+
thumbnail: r.thumbnail || null,
|
|
71
|
+
img_src: r.img_src || null,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
results: this.lastResults,
|
|
76
|
+
query: data.query || query,
|
|
77
|
+
number_of_results: data.number_of_results || this.lastResults.length,
|
|
78
|
+
suggestions: data.suggestions || [],
|
|
79
|
+
infoboxes: data.infoboxes || [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async multiSearch(query, options = {}) {
|
|
84
|
+
const categories = ['general', 'images', 'videos', 'news'];
|
|
85
|
+
const results = {};
|
|
86
|
+
|
|
87
|
+
const promises = categories.map(async (cat) => {
|
|
88
|
+
try {
|
|
89
|
+
const res = await this.search(query, { ...options, type: cat });
|
|
90
|
+
results[cat] = res.results.slice(0, 5);
|
|
91
|
+
} catch {
|
|
92
|
+
results[cat] = [];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await Promise.all(promises);
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async imageSearch(query, options = {}) {
|
|
101
|
+
return this.search(query, { ...options, type: 'images' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getLastResults() {
|
|
105
|
+
return this.lastResults;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getResultByIndex(idx) {
|
|
109
|
+
return this.lastResults[idx - 1] || null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default new SearchEngine();
|
package/src/sessions.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import config from './config.js';
|
|
2
|
+
|
|
3
|
+
class SessionManager {
|
|
4
|
+
getSessions() {
|
|
5
|
+
return config.get('sessions') || {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
saveSession(domain, cookies) {
|
|
9
|
+
const sessions = this.getSessions();
|
|
10
|
+
sessions[domain] = {
|
|
11
|
+
cookies,
|
|
12
|
+
savedAt: new Date().toISOString(),
|
|
13
|
+
};
|
|
14
|
+
config.set('sessions', sessions);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getSession(domain) {
|
|
18
|
+
const sessions = this.getSessions();
|
|
19
|
+
return sessions[domain] || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeSession(domain) {
|
|
23
|
+
const sessions = this.getSessions();
|
|
24
|
+
delete sessions[domain];
|
|
25
|
+
config.set('sessions', sessions);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clearAll() {
|
|
29
|
+
config.set('sessions', {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
listDomains() {
|
|
33
|
+
return Object.keys(this.getSessions());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default new SessionManager();
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import config from './config.js';
|
|
2
|
+
|
|
3
|
+
class StatsManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.sessionStart = Date.now();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
incrementSearch() {
|
|
9
|
+
const stats = config.get('stats');
|
|
10
|
+
stats.searchCount = (stats.searchCount || 0) + 1;
|
|
11
|
+
config.set('stats', stats);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
incrementPageVisit() {
|
|
15
|
+
const stats = config.get('stats');
|
|
16
|
+
stats.pagesVisited = (stats.pagesVisited || 0) + 1;
|
|
17
|
+
config.set('stats', stats);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getStats() {
|
|
21
|
+
const stats = config.get('stats');
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const sessionTime = now - this.sessionStart;
|
|
24
|
+
const totalTime = (stats.totalTime || 0) + sessionTime;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
searchCount: stats.searchCount || 0,
|
|
28
|
+
pagesVisited: stats.pagesVisited || 0,
|
|
29
|
+
sessionTime: this.formatTime(sessionTime),
|
|
30
|
+
totalTime: this.formatTime(totalTime),
|
|
31
|
+
bookmarkCount: (config.get('bookmarks') || []).length,
|
|
32
|
+
historyCount: (config.get('history') || []).length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
formatTime(ms) {
|
|
37
|
+
const seconds = Math.floor(ms / 1000);
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
const hours = Math.floor(minutes / 60);
|
|
40
|
+
|
|
41
|
+
if (hours > 0) {
|
|
42
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
43
|
+
}
|
|
44
|
+
if (minutes > 0) {
|
|
45
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
46
|
+
}
|
|
47
|
+
return `${seconds}s`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
saveTotalTime() {
|
|
51
|
+
const stats = config.get('stats');
|
|
52
|
+
stats.totalTime = (stats.totalTime || 0) + (Date.now() - this.sessionStart);
|
|
53
|
+
config.set('stats', stats);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reset() {
|
|
57
|
+
config.set('stats', {
|
|
58
|
+
searchCount: 0,
|
|
59
|
+
pagesVisited: 0,
|
|
60
|
+
startTime: Date.now(),
|
|
61
|
+
totalTime: 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default new StatsManager();
|
package/src/tabs.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import config from './config.js';
|
|
2
|
+
|
|
3
|
+
class TabManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.tabs = [this._createTab('New Tab')];
|
|
6
|
+
this.activeTab = 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
_createTab(title = 'New Tab') {
|
|
10
|
+
return {
|
|
11
|
+
id: Date.now(),
|
|
12
|
+
title,
|
|
13
|
+
url: null,
|
|
14
|
+
html: null,
|
|
15
|
+
content: null,
|
|
16
|
+
links: [],
|
|
17
|
+
history: [],
|
|
18
|
+
historyIndex: -1,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
newTab(title) {
|
|
23
|
+
if (this.tabs.length >= config.get('maxTabs')) {
|
|
24
|
+
throw new Error(`Maximum tabs (${config.get('maxTabs')}) reached`);
|
|
25
|
+
}
|
|
26
|
+
const tab = this._createTab(title);
|
|
27
|
+
this.tabs.push(tab);
|
|
28
|
+
this.activeTab = this.tabs.length - 1;
|
|
29
|
+
return this.activeTab;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
closeTab(index) {
|
|
33
|
+
if (index === undefined) index = this.activeTab;
|
|
34
|
+
if (this.tabs.length <= 1) {
|
|
35
|
+
throw new Error('Cannot close the last tab');
|
|
36
|
+
}
|
|
37
|
+
if (index < 0 || index >= this.tabs.length) {
|
|
38
|
+
throw new Error('Invalid tab index');
|
|
39
|
+
}
|
|
40
|
+
this.tabs.splice(index, 1);
|
|
41
|
+
if (this.activeTab >= this.tabs.length) {
|
|
42
|
+
this.activeTab = this.tabs.length - 1;
|
|
43
|
+
}
|
|
44
|
+
return this.activeTab;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switchTab(index) {
|
|
48
|
+
if (index < 0 || index >= this.tabs.length) {
|
|
49
|
+
throw new Error(`Tab ${index + 1} does not exist`);
|
|
50
|
+
}
|
|
51
|
+
this.activeTab = index;
|
|
52
|
+
return this.getCurrentTab();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getCurrentTab() {
|
|
56
|
+
return this.tabs[this.activeTab];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setCurrentPage(url, title, html, content, links) {
|
|
60
|
+
const tab = this.getCurrentTab();
|
|
61
|
+
|
|
62
|
+
// Add to tab history
|
|
63
|
+
if (tab.url) {
|
|
64
|
+
tab.history = tab.history.slice(0, tab.historyIndex + 1);
|
|
65
|
+
tab.history.push({
|
|
66
|
+
url: tab.url,
|
|
67
|
+
title: tab.title,
|
|
68
|
+
html: tab.html,
|
|
69
|
+
content: tab.content,
|
|
70
|
+
links: tab.links,
|
|
71
|
+
});
|
|
72
|
+
tab.historyIndex = tab.history.length - 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
tab.url = url;
|
|
76
|
+
tab.title = title || 'Untitled';
|
|
77
|
+
tab.html = html;
|
|
78
|
+
tab.content = content;
|
|
79
|
+
tab.links = links || [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
goBack() {
|
|
83
|
+
const tab = this.getCurrentTab();
|
|
84
|
+
if (tab.historyIndex < 0) {
|
|
85
|
+
throw new Error('No previous page');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const prev = tab.history[tab.historyIndex];
|
|
89
|
+
tab.historyIndex--;
|
|
90
|
+
|
|
91
|
+
// Save current to forward
|
|
92
|
+
const currentPage = {
|
|
93
|
+
url: tab.url,
|
|
94
|
+
title: tab.title,
|
|
95
|
+
html: tab.html,
|
|
96
|
+
content: tab.content,
|
|
97
|
+
links: tab.links,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
tab.url = prev.url;
|
|
101
|
+
tab.title = prev.title;
|
|
102
|
+
tab.html = prev.html;
|
|
103
|
+
tab.content = prev.content;
|
|
104
|
+
tab.links = prev.links;
|
|
105
|
+
|
|
106
|
+
// Store current page for forward
|
|
107
|
+
if (tab.historyIndex + 2 < tab.history.length) {
|
|
108
|
+
tab.history[tab.historyIndex + 2] = currentPage;
|
|
109
|
+
} else {
|
|
110
|
+
tab.history.push(currentPage);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return tab;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
goForward() {
|
|
117
|
+
const tab = this.getCurrentTab();
|
|
118
|
+
if (tab.historyIndex + 2 >= tab.history.length) {
|
|
119
|
+
throw new Error('No next page');
|
|
120
|
+
}
|
|
121
|
+
tab.historyIndex += 2;
|
|
122
|
+
|
|
123
|
+
const next = tab.history[tab.historyIndex];
|
|
124
|
+
tab.url = next.url;
|
|
125
|
+
tab.title = next.title;
|
|
126
|
+
tab.html = next.html;
|
|
127
|
+
tab.content = next.content;
|
|
128
|
+
tab.links = next.links;
|
|
129
|
+
|
|
130
|
+
return tab;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getAllTabs() {
|
|
134
|
+
return this.tabs.map((tab, i) => ({
|
|
135
|
+
index: i + 1,
|
|
136
|
+
title: tab.title,
|
|
137
|
+
url: tab.url,
|
|
138
|
+
active: i === this.activeTab,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getActiveIndex() {
|
|
143
|
+
return this.activeTab;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default new TabManager();
|