roxxie-proxy-u-prod 0.2.3 → 0.2.5
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 +6 -6
- package/dist/assets/index-CSCDo1rk.js +3669 -0
- package/dist/assets/inject-CDw4wJ3e.js +43 -0
- package/embed.js +1 -1
- package/examples/single-page.html +2 -2
- package/package.json +1 -1
- package/runtime/index.html +3 -3
- package/scripts/verify.mjs +2 -2
- package/source/packages/chrome/src/Tab/History.test.ts +137 -0
- package/source/packages/chrome/src/Tab/History.ts +8 -3
- package/source/packages/chrome/src/Tab/Tab.tsx +4 -1
- package/source/packages/chrome/src/components/Omnibar/Omnibox.tsx +28 -38
- package/source/packages/chrome/src/components/Omnibar/autocomplete.test.ts +51 -0
- package/source/packages/chrome/src/components/Omnibar/autocomplete.ts +30 -0
- package/source/packages/chrome/src/pages/AdvancedSettings.tsx +225 -0
- package/source/packages/chrome/src/pages/SettingsPage.tsx +11 -0
- package/source/packages/chrome/src/pages/nodes.test.ts +94 -0
- package/source/packages/chrome/src/pages/nodes.ts +108 -0
- package/source/packages/chrome/src/proxy/user-agent.test.ts +78 -0
- package/source/packages/chrome/src/services/SettingsService.ts +8 -1
- package/source/packages/roxxie-transport/package.json +1 -1
- package/source/packages/roxxie-transport/src/transport.ts +51 -7
- package/source/packages/scramjet/packages/core/dist/scramjet.js +14 -13582
- package/source/packages/scramjet/packages/core/dist/scramjet.js.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet.mjs +14 -13632
- package/source/packages/scramjet/packages/core/dist/scramjet.mjs.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.js +14 -13584
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.js.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.mjs +14 -13634
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.mjs.map +1 -1
- package/source/packages/scramjet/packages/core/dist/types/fetch/user-agent.d.ts +31 -0
- package/source/packages/scramjet/packages/core/src/fetch/headers.ts +8 -0
- package/source/packages/scramjet/packages/core/src/fetch/user-agent.ts +51 -0
- package/source/packages/tracker-protocol/package.json +1 -1
- package/source/packages/tracker-protocol/src/index.ts +22 -0
- package/source/pnpm-lock.yaml +1 -1
- package/standalone.html +2 -2
- package/dist/assets/index-BQkXFDet.js +0 -3613
- package/dist/assets/inject-xtH4w5z9.js +0 -43
package/embed.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<div id="roxxie-browser"></div>
|
|
19
19
|
<script
|
|
20
20
|
defer
|
|
21
|
-
src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.
|
|
22
|
-
integrity="sha384-
|
|
21
|
+
src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.5/embed.js"
|
|
22
|
+
integrity="sha384-3fyAZcakc565/Ft23hCqQX1ztazzOiLIr+0aU2iffd1tXw5AIRrYpPFWfE5/MXj8"
|
|
23
23
|
crossorigin="anonymous"
|
|
24
24
|
data-roxxie-target="#roxxie-browser"
|
|
25
25
|
></script>
|
package/package.json
CHANGED
package/runtime/index.html
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/png" sizes="any" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.
|
|
5
|
+
<link rel="icon" type="image/png" sizes="any" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.5/dist/favicon.png" />
|
|
6
6
|
<meta name="theme-color" content="#222230" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Roxxie Proxy U</title>
|
|
9
9
|
|
|
10
10
|
<!--ssr-head-->
|
|
11
|
-
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.
|
|
12
|
-
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.
|
|
11
|
+
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.5/dist/assets/index-CSCDo1rk.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.5/dist/assets/index-EeRhLAof.css">
|
|
13
13
|
</head>
|
|
14
14
|
|
|
15
15
|
<body>
|
package/scripts/verify.mjs
CHANGED
|
@@ -118,8 +118,8 @@ if (
|
|
|
118
118
|
throw new Error("Start screen must remain flat and glow-free");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if (manifest.version !== "0.2.
|
|
122
|
-
throw new Error("This production release must be version 0.2.
|
|
121
|
+
if (manifest.version !== "0.2.5") {
|
|
122
|
+
throw new Error("This production release must be version 0.2.5");
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const embedBytes = await readFile(join(packageRoot, "embed.js"));
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// History records visited pages into the global profile; stub it so the model
|
|
4
|
+
// can be exercised on its own.
|
|
5
|
+
vi.mock("..", () => ({
|
|
6
|
+
profileService: { globalhistory: [] as unknown[] },
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import { History } from "./History";
|
|
10
|
+
|
|
11
|
+
/** Only the surface History touches on its owning tab. */
|
|
12
|
+
function fakeTab() {
|
|
13
|
+
return {
|
|
14
|
+
url: null as URL | null,
|
|
15
|
+
title: null as string | null,
|
|
16
|
+
icon: null as string | null,
|
|
17
|
+
canGoBack: false,
|
|
18
|
+
canGoForward: false,
|
|
19
|
+
navigated: [] as string[],
|
|
20
|
+
_directnavigate(url: URL) {
|
|
21
|
+
this.navigated.push(url.href);
|
|
22
|
+
this.url = url;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeHistory() {
|
|
28
|
+
const tab = fakeTab();
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
const history = new History(tab as any);
|
|
31
|
+
return { tab, history };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const A = new URL("https://a.example/");
|
|
35
|
+
const B = new URL("https://b.example/");
|
|
36
|
+
const C = new URL("https://c.example/");
|
|
37
|
+
const D = new URL("https://d.example/");
|
|
38
|
+
|
|
39
|
+
describe("tab history", () => {
|
|
40
|
+
let tab: ReturnType<typeof fakeTab>;
|
|
41
|
+
let history: History;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
({ tab, history } = makeHistory());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("tracks a linear trail and walks back through it", () => {
|
|
48
|
+
history.push(A);
|
|
49
|
+
history.push(B);
|
|
50
|
+
history.push(C);
|
|
51
|
+
|
|
52
|
+
expect(history.current().url.href).toBe(C.href);
|
|
53
|
+
expect(history.canGoBack()).toBe(true);
|
|
54
|
+
expect(history.canGoForward()).toBe(false);
|
|
55
|
+
|
|
56
|
+
expect(history.go(-1).url.href).toBe(B.href);
|
|
57
|
+
expect(history.go(-1).url.href).toBe(A.href);
|
|
58
|
+
expect(history.canGoBack()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps the current entry when a new navigation forks the trail", () => {
|
|
62
|
+
// Regression: the fork used to splice from the current index rather than
|
|
63
|
+
// after it, deleting the entry the user was on. index then pointed past the
|
|
64
|
+
// end of the array, so states[index] was undefined and going back walked
|
|
65
|
+
// into that hole.
|
|
66
|
+
history.push(A);
|
|
67
|
+
history.push(B);
|
|
68
|
+
history.push(C);
|
|
69
|
+
history.go(-2);
|
|
70
|
+
expect(history.current().url.href).toBe(A.href);
|
|
71
|
+
|
|
72
|
+
history.push(D);
|
|
73
|
+
|
|
74
|
+
expect(history.states.map((s) => s.url.href)).toEqual([A.href, D.href]);
|
|
75
|
+
expect(history.index).toBe(1);
|
|
76
|
+
expect(history.current().url.href).toBe(D.href);
|
|
77
|
+
expect(history.states[history.index]).toBeDefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("goes back to a real page after forking rather than an empty state", () => {
|
|
81
|
+
history.push(A);
|
|
82
|
+
history.push(B);
|
|
83
|
+
history.push(C);
|
|
84
|
+
history.go(-2);
|
|
85
|
+
history.push(D);
|
|
86
|
+
|
|
87
|
+
expect(history.canGoBack()).toBe(true);
|
|
88
|
+
const back = history.go(-1);
|
|
89
|
+
|
|
90
|
+
expect(back).toBeDefined();
|
|
91
|
+
expect(back.url).toBeInstanceOf(URL);
|
|
92
|
+
expect(back.url.href).toBe(A.href);
|
|
93
|
+
// The URL actually handed to the tab must be a real page, never "undefined".
|
|
94
|
+
expect(tab.navigated.at(-1)).toBe(A.href);
|
|
95
|
+
expect(tab.navigated.some((href) => href.includes("undefined"))).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("never reports canGoBack while sitting on the first entry", () => {
|
|
99
|
+
history.push(A);
|
|
100
|
+
history.push(B);
|
|
101
|
+
history.go(-1);
|
|
102
|
+
|
|
103
|
+
expect(history.index).toBe(0);
|
|
104
|
+
expect(history.canGoBack()).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("clamps overshooting travel to the ends of the trail", () => {
|
|
108
|
+
history.push(A);
|
|
109
|
+
history.push(B);
|
|
110
|
+
|
|
111
|
+
expect(history.go(-99).url.href).toBe(A.href);
|
|
112
|
+
expect(history.go(99).url.href).toBe(B.href);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("replaces the current entry without growing the trail", () => {
|
|
116
|
+
history.push(A);
|
|
117
|
+
history.push(B);
|
|
118
|
+
history.replace(C, null, null, false);
|
|
119
|
+
|
|
120
|
+
expect(history.states.map((s) => s.url.href)).toEqual([A.href, C.href]);
|
|
121
|
+
expect(history.index).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("survives a round trip through serialization", () => {
|
|
125
|
+
history.push(A);
|
|
126
|
+
history.push(B);
|
|
127
|
+
history.go(-1);
|
|
128
|
+
|
|
129
|
+
const restoredTab = fakeTab();
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
const restored = new History(restoredTab as any, history.serialize());
|
|
132
|
+
|
|
133
|
+
expect(restored.index).toBe(history.index);
|
|
134
|
+
expect(restored.current().url.href).toBe(A.href);
|
|
135
|
+
expect(restored.canGoForward()).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -94,8 +94,11 @@ export class History extends StatefulClass {
|
|
|
94
94
|
// disown states we're removing
|
|
95
95
|
this.states.slice(this.index + 1).forEach((state) => this.disown(state));
|
|
96
96
|
|
|
97
|
-
// "fork" history tree, creating a new timeline
|
|
98
|
-
|
|
97
|
+
// "fork" history tree, creating a new timeline. Only the entries *after*
|
|
98
|
+
// the current one are dropped: removing the current entry as well left
|
|
99
|
+
// index pointing past the end of the array, so states[index] was
|
|
100
|
+
// undefined and going back navigated into that hole.
|
|
101
|
+
this.states.splice(this.index + 1);
|
|
99
102
|
}
|
|
100
103
|
const hstate = new HistoryState({ url, state, title });
|
|
101
104
|
if (virtual) hstate.virtual = true;
|
|
@@ -129,7 +132,9 @@ export class History extends StatefulClass {
|
|
|
129
132
|
this.current().title = title;
|
|
130
133
|
this.current().favicon = null;
|
|
131
134
|
} else {
|
|
132
|
-
|
|
135
|
+
// push takes (url, title, state, navigate); passing state positionally
|
|
136
|
+
// as the title corrupted the new entry.
|
|
137
|
+
return this.push(url, title, state, navigate);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
if (navigate) {
|
|
@@ -204,7 +204,10 @@ export class Tab extends StatefulClass {
|
|
|
204
204
|
this.history.push(url, null, null, true, false);
|
|
205
205
|
}
|
|
206
206
|
replaceNavigate(url: URL) {
|
|
207
|
-
|
|
207
|
+
// replace takes (url, title, state, navigate); `true` was landing in the
|
|
208
|
+
// state slot, so the replacement carried a bogus state and never navigated
|
|
209
|
+
// explicitly.
|
|
210
|
+
this.history.replace(url, null, null, true);
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
back() {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type OmniboxResult,
|
|
10
10
|
} from "./suggestions";
|
|
11
11
|
import { trimUrl } from "./utils";
|
|
12
|
+
import { inlineCompletion } from "./autocomplete";
|
|
12
13
|
import { UrlInput } from "@components/Omnibar/UrlInput";
|
|
13
14
|
import { Suggestion } from "@components/Omnibar/Suggestion";
|
|
14
15
|
import { requestUnfocusFrames } from "@components/Shell";
|
|
@@ -97,32 +98,19 @@ export function Omnibox(
|
|
|
97
98
|
// if the user is actually trying to search something we can kill the trending suggestions
|
|
98
99
|
this.trendingSuggestions = [];
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
// Capture the flag for this run. It is cleared synchronously below, well
|
|
102
|
+
// before fetchSuggestions calls back, so the callback cannot read it.
|
|
103
|
+
const justDeleted = this.suggestionDenied;
|
|
104
|
+
|
|
105
|
+
fetchSuggestions(this.realvalue, justDeleted, (results) => {
|
|
101
106
|
this.searchSuggestions = results;
|
|
102
107
|
|
|
103
108
|
const firstResult = results[0];
|
|
104
109
|
if (!firstResult) return;
|
|
105
|
-
if (firstResult.kind === "search") {
|
|
106
|
-
if (!firstResult.title) return;
|
|
107
|
-
if (this.realvalue.length >= firstResult.title.length) return;
|
|
108
|
-
if (
|
|
109
|
-
!firstResult.title
|
|
110
|
-
.toLowerCase()
|
|
111
|
-
.startsWith(this.realvalue.toLowerCase())
|
|
112
|
-
)
|
|
113
|
-
return;
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
currentCursor,
|
|
119
|
-
currentCursor + firstResult.title.length
|
|
120
|
-
);
|
|
121
|
-
this.value = firstResult.title;
|
|
122
|
-
this.input.setSelectionRange(
|
|
123
|
-
currentCursor,
|
|
124
|
-
currentCursor + firstResult.title.length
|
|
125
|
-
);
|
|
111
|
+
let candidate: string | null | undefined;
|
|
112
|
+
if (firstResult.kind === "search") {
|
|
113
|
+
candidate = firstResult.title;
|
|
126
114
|
} else {
|
|
127
115
|
if (!firstResult.url) return;
|
|
128
116
|
|
|
@@ -136,24 +124,26 @@ export function Omnibox(
|
|
|
136
124
|
if (normalizedUrl.endsWith("/") && !this.realvalue.endsWith("/")) {
|
|
137
125
|
normalizedUrl = normalizedUrl.slice(0, -1);
|
|
138
126
|
}
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
!normalizedUrl.toLowerCase().startsWith(this.realvalue.toLowerCase())
|
|
142
|
-
)
|
|
143
|
-
return;
|
|
144
|
-
|
|
145
|
-
let currentCursor = this.input.selectionStart || 0;
|
|
146
|
-
|
|
147
|
-
this.input.setSelectionRange(
|
|
148
|
-
currentCursor,
|
|
149
|
-
currentCursor + normalizedUrl.length
|
|
150
|
-
);
|
|
151
|
-
this.value = normalizedUrl;
|
|
152
|
-
this.input.setSelectionRange(
|
|
153
|
-
currentCursor,
|
|
154
|
-
currentCursor + normalizedUrl.length
|
|
155
|
-
);
|
|
127
|
+
candidate = normalizedUrl;
|
|
156
128
|
}
|
|
129
|
+
|
|
130
|
+
const completion = inlineCompletion(
|
|
131
|
+
this.realvalue,
|
|
132
|
+
candidate,
|
|
133
|
+
justDeleted
|
|
134
|
+
);
|
|
135
|
+
if (completion === null) return;
|
|
136
|
+
|
|
137
|
+
const currentCursor = this.input.selectionStart || 0;
|
|
138
|
+
this.input.setSelectionRange(
|
|
139
|
+
currentCursor,
|
|
140
|
+
currentCursor + completion.length
|
|
141
|
+
);
|
|
142
|
+
this.value = completion;
|
|
143
|
+
this.input.setSelectionRange(
|
|
144
|
+
currentCursor,
|
|
145
|
+
currentCursor + completion.length
|
|
146
|
+
);
|
|
157
147
|
});
|
|
158
148
|
this.suggestionDenied = false;
|
|
159
149
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { inlineCompletion } from "./autocomplete";
|
|
4
|
+
|
|
5
|
+
describe("omnibox inline completion", () => {
|
|
6
|
+
it("completes a prefix while the user is typing", () => {
|
|
7
|
+
expect(inlineCompletion("ama", "amazon.com", false)).toBe("amazon.com");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("never re-adds text right after a deletion", () => {
|
|
11
|
+
// The completion is written back into the box with its tail selected, so
|
|
12
|
+
// re-adding it after a backspace means the next backspace only removes the
|
|
13
|
+
// completion again and the final character can never be deleted.
|
|
14
|
+
expect(inlineCompletion("a", "amazon.com", true)).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("leaves an emptied box empty", () => {
|
|
18
|
+
expect(inlineCompletion("", "amazon.com", true)).toBeNull();
|
|
19
|
+
expect(inlineCompletion("", "amazon.com", false)).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("ignores candidates that are not a continuation of the input", () => {
|
|
23
|
+
expect(inlineCompletion("xyz", "amazon.com", false)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("matches case-insensitively but preserves the candidate's casing", () => {
|
|
27
|
+
expect(inlineCompletion("AmA", "amazon.com", false)).toBe("amazon.com");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("adds nothing when the candidate is not longer than the input", () => {
|
|
31
|
+
expect(inlineCompletion("amazon.com", "amazon.com", false)).toBeNull();
|
|
32
|
+
expect(inlineCompletion("amazon.com/x", "amazon.com", false)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("tolerates a missing candidate", () => {
|
|
36
|
+
expect(inlineCompletion("ama", undefined, false)).toBeNull();
|
|
37
|
+
expect(inlineCompletion("ama", null, false)).toBeNull();
|
|
38
|
+
expect(inlineCompletion("ama", "", false)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("lets a full deletion sequence reach an empty box", () => {
|
|
42
|
+
// Walk "ama" down to "" the way repeated backspaces would, asserting the
|
|
43
|
+
// box is never refilled at any step.
|
|
44
|
+
let typed = "ama";
|
|
45
|
+
while (typed.length > 0) {
|
|
46
|
+
typed = typed.slice(0, -1);
|
|
47
|
+
expect(inlineCompletion(typed, "amazon.com", true)).toBeNull();
|
|
48
|
+
}
|
|
49
|
+
expect(typed).toBe("");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline omnibox autocompletion.
|
|
3
|
+
*
|
|
4
|
+
* The omnibox completes what you type by writing the candidate into the input
|
|
5
|
+
* and selecting the added tail, so the next keystroke replaces it. That is only
|
|
6
|
+
* correct while text is being *added*. Applying it after a deletion makes the
|
|
7
|
+
* next backspace consume the re-added completion instead of the character the
|
|
8
|
+
* user meant to remove, which strands the final character permanently.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The text to place in the omnibox, or null to leave what the user typed alone.
|
|
13
|
+
*
|
|
14
|
+
* @param typed what the user actually has in the box
|
|
15
|
+
* @param candidate the top suggestion, if any
|
|
16
|
+
* @param justDeleted whether the last edit removed characters
|
|
17
|
+
*/
|
|
18
|
+
export function inlineCompletion(
|
|
19
|
+
typed: string,
|
|
20
|
+
candidate: string | undefined | null,
|
|
21
|
+
justDeleted: boolean
|
|
22
|
+
): string | null {
|
|
23
|
+
// Never re-add text the user is in the middle of removing.
|
|
24
|
+
if (justDeleted) return null;
|
|
25
|
+
if (!typed || !candidate) return null;
|
|
26
|
+
// A candidate that is not strictly longer adds nothing to complete.
|
|
27
|
+
if (candidate.length <= typed.length) return null;
|
|
28
|
+
if (!candidate.toLowerCase().startsWith(typed.toLowerCase())) return null;
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { css, type FC } from "dreamland/core";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@components/Button";
|
|
4
|
+
import { roxxieTransport } from "../proxy/roxxie";
|
|
5
|
+
import { settingsService } from "..";
|
|
6
|
+
import {
|
|
7
|
+
AUTOMATIC_NODE,
|
|
8
|
+
buildNodeRows,
|
|
9
|
+
nodeSummary,
|
|
10
|
+
resolvePreferredNode,
|
|
11
|
+
type NodeRow,
|
|
12
|
+
} from "./nodes";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Exit-node picker. Lists the nodes the tracker is currently offering, what
|
|
16
|
+
* version of the node software each is running, and lets the user pin one
|
|
17
|
+
* instead of letting the browser choose by load and latency.
|
|
18
|
+
*/
|
|
19
|
+
export function AdvancedSettings(
|
|
20
|
+
this: FC<
|
|
21
|
+
Record<string, never>,
|
|
22
|
+
{
|
|
23
|
+
rows: NodeRow[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
error: string | null;
|
|
26
|
+
selected: string;
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
) {
|
|
30
|
+
this.rows = [];
|
|
31
|
+
this.loading = false;
|
|
32
|
+
this.error = null;
|
|
33
|
+
this.selected = settingsService.settings.preferredNodeId || AUTOMATIC_NODE;
|
|
34
|
+
|
|
35
|
+
const refresh = async () => {
|
|
36
|
+
if (!roxxieTransport) {
|
|
37
|
+
this.error = "The proxy transport is not running yet.";
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.loading = true;
|
|
41
|
+
this.error = null;
|
|
42
|
+
try {
|
|
43
|
+
const candidates = await roxxieTransport.listNodes();
|
|
44
|
+
this.rows = buildNodeRows(
|
|
45
|
+
candidates,
|
|
46
|
+
roxxieTransport.connectedNode?.node.nodeId
|
|
47
|
+
);
|
|
48
|
+
// A pin left over from a node that has since disappeared reads as
|
|
49
|
+
// automatic rather than showing a selection that cannot be honoured.
|
|
50
|
+
this.selected = resolvePreferredNode(
|
|
51
|
+
settingsService.settings.preferredNodeId,
|
|
52
|
+
this.rows
|
|
53
|
+
);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.error =
|
|
56
|
+
error instanceof Error ? error.message : "Could not reach the tracker.";
|
|
57
|
+
} finally {
|
|
58
|
+
this.loading = false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const choose = (nodeId: string) => {
|
|
63
|
+
this.selected = nodeId;
|
|
64
|
+
settingsService.settings.preferredNodeId = nodeId;
|
|
65
|
+
roxxieTransport?.setPreferredNode(nodeId || undefined);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
void refresh();
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<section class="setting-section">
|
|
72
|
+
<div class="section-header">
|
|
73
|
+
<h2>Exit Nodes</h2>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="section-content">
|
|
76
|
+
<div class="setting-group">
|
|
77
|
+
<h4>Node Selection</h4>
|
|
78
|
+
<p class="setting-description">
|
|
79
|
+
Traffic leaves through one of these community nodes. Automatic picks
|
|
80
|
+
the least loaded node with the best latency. Choosing a node applies
|
|
81
|
+
to the next connection.
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<div class="node-actions">
|
|
85
|
+
<Button
|
|
86
|
+
variant="secondary"
|
|
87
|
+
on:click={() => void refresh()}
|
|
88
|
+
disabled={use(this.loading)}
|
|
89
|
+
>
|
|
90
|
+
{use(this.loading).map((l) => (l ? "Refreshing…" : "Refresh"))}
|
|
91
|
+
</Button>
|
|
92
|
+
{use(this.selected).map(
|
|
93
|
+
(selected) =>
|
|
94
|
+
selected !== AUTOMATIC_NODE && (
|
|
95
|
+
<Button
|
|
96
|
+
variant="secondary"
|
|
97
|
+
on:click={() => choose(AUTOMATIC_NODE)}
|
|
98
|
+
>
|
|
99
|
+
Use automatic
|
|
100
|
+
</Button>
|
|
101
|
+
)
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{use(this.error).map(
|
|
106
|
+
(error) => error && <p class="node-error">{error}</p>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<div class="node-list">
|
|
110
|
+
<label class="node-option" class:selected={use(this.selected).map(
|
|
111
|
+
(s) => s === AUTOMATIC_NODE
|
|
112
|
+
)}>
|
|
113
|
+
<input
|
|
114
|
+
type="radio"
|
|
115
|
+
name="roxxie-node"
|
|
116
|
+
checked={use(this.selected).map((s) => s === AUTOMATIC_NODE)}
|
|
117
|
+
on:change={() => choose(AUTOMATIC_NODE)}
|
|
118
|
+
/>
|
|
119
|
+
<span class="node-text">
|
|
120
|
+
<span class="node-name">Automatic</span>
|
|
121
|
+
<span class="node-meta">
|
|
122
|
+
Pick the fastest available node for me
|
|
123
|
+
</span>
|
|
124
|
+
</span>
|
|
125
|
+
</label>
|
|
126
|
+
|
|
127
|
+
{use(this.rows).map((rows) =>
|
|
128
|
+
rows.length === 0 && !this.loading ? (
|
|
129
|
+
<p class="node-empty">No nodes are available right now.</p>
|
|
130
|
+
) : (
|
|
131
|
+
rows.map((row) => (
|
|
132
|
+
<label
|
|
133
|
+
class="node-option"
|
|
134
|
+
class:selected={use(this.selected).map(
|
|
135
|
+
(s) => s === row.nodeId
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
<input
|
|
139
|
+
type="radio"
|
|
140
|
+
name="roxxie-node"
|
|
141
|
+
checked={use(this.selected).map((s) => s === row.nodeId)}
|
|
142
|
+
on:change={() => choose(row.nodeId)}
|
|
143
|
+
/>
|
|
144
|
+
<span class="node-text">
|
|
145
|
+
<span class="node-name">
|
|
146
|
+
{row.name}
|
|
147
|
+
{row.connected && (
|
|
148
|
+
<span class="node-badge">connected</span>
|
|
149
|
+
)}
|
|
150
|
+
</span>
|
|
151
|
+
<span class="node-meta">{nodeSummary(row)}</span>
|
|
152
|
+
<span class="node-id">{row.nodeId}</span>
|
|
153
|
+
</span>
|
|
154
|
+
</label>
|
|
155
|
+
))
|
|
156
|
+
)
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
AdvancedSettings.style = css`
|
|
166
|
+
:scope .node-actions {
|
|
167
|
+
display: flex;
|
|
168
|
+
gap: var(--space-sm);
|
|
169
|
+
margin-block: var(--space-sm);
|
|
170
|
+
}
|
|
171
|
+
:scope .node-list {
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-direction: column;
|
|
174
|
+
gap: var(--space-xs);
|
|
175
|
+
}
|
|
176
|
+
:scope .node-option {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: flex-start;
|
|
179
|
+
gap: var(--space-sm);
|
|
180
|
+
padding: var(--space-sm);
|
|
181
|
+
border: 1px solid var(--popup_border);
|
|
182
|
+
border-radius: var(--radius-md, 8px);
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
}
|
|
185
|
+
:scope .node-option.selected {
|
|
186
|
+
border-color: var(--tab_line);
|
|
187
|
+
}
|
|
188
|
+
:scope .node-text {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
gap: 2px;
|
|
192
|
+
min-width: 0;
|
|
193
|
+
}
|
|
194
|
+
:scope .node-name {
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--space-xs);
|
|
199
|
+
}
|
|
200
|
+
:scope .node-badge {
|
|
201
|
+
font-size: 0.75em;
|
|
202
|
+
font-weight: 500;
|
|
203
|
+
padding: 1px 6px;
|
|
204
|
+
border-radius: 999px;
|
|
205
|
+
background: var(--tab_line);
|
|
206
|
+
color: var(--toolbar);
|
|
207
|
+
}
|
|
208
|
+
:scope .node-meta {
|
|
209
|
+
opacity: 0.8;
|
|
210
|
+
font-size: 0.9em;
|
|
211
|
+
}
|
|
212
|
+
:scope .node-id {
|
|
213
|
+
opacity: 0.5;
|
|
214
|
+
font-size: 0.8em;
|
|
215
|
+
font-family: monospace;
|
|
216
|
+
overflow-wrap: anywhere;
|
|
217
|
+
}
|
|
218
|
+
:scope .node-error {
|
|
219
|
+
color: var(--error, #e57373);
|
|
220
|
+
}
|
|
221
|
+
:scope .node-empty,
|
|
222
|
+
:scope .setting-description {
|
|
223
|
+
opacity: 0.8;
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
iconRefresh,
|
|
27
27
|
} from "../icons";
|
|
28
28
|
import { settingsService } from "..";
|
|
29
|
+
import { AdvancedSettings } from "./AdvancedSettings";
|
|
29
30
|
|
|
30
31
|
function ThemePreview(this: FC<{ theme: (typeof THEMES)[number] }>) {
|
|
31
32
|
const theme = this.theme;
|
|
@@ -701,6 +702,7 @@ export function SettingsPage(
|
|
|
701
702
|
{button("appearance", iconBrush, "Appearance")}
|
|
702
703
|
{button("search", iconSearch, "Search")}
|
|
703
704
|
{button("privacy", iconPrivacy, "Privacy & Security")}
|
|
705
|
+
{button("advanced", iconOptions, "Advanced Settings")}
|
|
704
706
|
{/* {button("extensions", iconExtension, "Extensions")} */}
|
|
705
707
|
{button("about", iconAbout, "About")}
|
|
706
708
|
</nav>
|
|
@@ -1303,6 +1305,15 @@ export function SettingsPage(
|
|
|
1303
1305
|
) : null
|
|
1304
1306
|
)}
|
|
1305
1307
|
|
|
1308
|
+
{/* Advanced Tab */}
|
|
1309
|
+
{use(this.selected).map((selected) =>
|
|
1310
|
+
selected === "advanced" ? (
|
|
1311
|
+
<div class="settings-tab">
|
|
1312
|
+
<AdvancedSettings />
|
|
1313
|
+
</div>
|
|
1314
|
+
) : null
|
|
1315
|
+
)}
|
|
1316
|
+
|
|
1306
1317
|
{/* About Tab */}
|
|
1307
1318
|
{use(this.selected).map((selected) =>
|
|
1308
1319
|
selected === "about" ? (
|