gui: the header tabs lose their grey track, a Plugins card's move is one word beside the name, and an agent row's grip shows only under the pointer (the user: 这个 tab 栏的背景灰色条有点丑 · 这里的紫色按钮也有点丑,而且还有的变成了两行 · 排序 icon 只在 hover 到这个条目的时候显示,logo hover 的排序 icon 可以删掉)
The header's .seg drew a grey track behind its tabs; now only the open tab's white thumb marks it (the account-mode .am-seg keeps its own). A Plugins card offering a built-in's accounts had a solid purple bar of its own at the card's foot, "Move my 2 Command Code Plan accounts", which broke onto two lines; it is now the tinted pill beside the name that Install is, reading "Move" / "迁移", its title saying whose accounts and how many and what moving does. In the Agents list the grip in a row's left margin (#479) was drawn on every row all the time, and hovering the logo turned it into a second grip; the margin grip now shows only for the row under the pointer, the one being dragged, or a handle with its menu open or the keyboard on it (kept in a rule of its own, for Safari 15), and the logo stays the logo. Dragging or clicking the grip still does what the logo does. agent-grip now checks no grip is drawn with the pointer away, the hovered row's alone is, and the logo has no grip of its own; plugin-move-overflow checks the move button sits in the card's top row on one line. agent-grip, account-arrange, agent-layout, the agent-models tests, brand, header-centre, header-zoom-fit, phone-web, strip-scrollbar, old-webkit and the plugin tests pass in Chromium and WebKit, except plugin-move's known Chromium flake and click-scroll, which fail the same way on unchanged main on this loaded machine. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and go test -tags nogui ./internal/gui/... pass.
This commit is contained in:
1 parent
ee5f974f98
commit
ec10d3bd98
8 files changed
+71
-56
No files matched your search
+13
-16
@@ -201,7 +201,7 @@ body.window:not(.mac):not(.linux):not(.web) .brand { display: none; } /* Windows
|
||||
.seg {
|
||||
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
||||
display: inline-flex; padding: 2px; border-radius: 8px;
|
||||
background: var(--seg-track);
|
||||
/* no grey track behind the tabs: only the open one's thumb */
|
||||
}
|
||||
.seg > .thumb { border-radius: 6px; background: var(--seg-thumb); box-shadow: var(--seg-edge); }
|
||||
body.panel .seg { display: none; }
|
||||
@@ -329,28 +329,25 @@ body.window .row .ic { width: 22px; height: 22px; }
|
||||
.agent-fold .row { opacity: 0; transform: translateY(-10px) scale(.985); transform-origin: 50% 0; transition: opacity .22s ease, transform .32s cubic-bezier(.4, 0, .2, 1); transition-delay: calc((var(--n) - 1 - var(--i)) * 22ms); }
|
||||
.agent-fold.open .row { opacity: 1; transform: none; transition: opacity .42s ease, transform .56s cubic-bezier(.22, 1, .36, 1); transition-delay: calc(70ms + var(--i) * 48ms); }
|
||||
|
||||
/* the logo is the row's handle: hovered, it turns into a grip to drag the row
|
||||
by, and a click opens the row's menu (Move up, Move down, Hide). It takes
|
||||
no more room than the logo did. */
|
||||
/* the logo is the row's handle: drag it to move the row, and a click opens
|
||||
the row's menu (Move up, Move down, Hide). It stays the logo under the
|
||||
pointer; the grip that says the row can be taken hold of is in its margin. */
|
||||
.ag-handle { display: inline-grid; place-items: center; flex: none; width: 24px; height: 24px; margin: -3px; padding: 0; border: 0; border-radius: 6px; background: none; color: inherit; cursor: grab; touch-action: none; transition: background-color .15s; }
|
||||
body.window .ag-handle { width: 28px; height: 28px; }
|
||||
.ag-handle > .ic, .ag-handle > .grip { grid-area: 1 / 1; transition: opacity .15s, transform .2s cubic-bezier(.22, 1, .36, 1); }
|
||||
.ag-handle > .grip { display: grid; place-items: center; color: var(--muted); opacity: 0; transform: scale(.8); }
|
||||
.ag-handle > .ic { grid-area: 1 / 1; transition: opacity .15s; }
|
||||
.ag-handle:hover, .ag-handle.open { background: var(--pill); }
|
||||
.ag-handle:focus-visible { background: var(--pill); outline: 1.5px solid var(--fg-2); outline-offset: 0; }
|
||||
.agent-fold .ag-handle { cursor: default; }
|
||||
#agents > .row .ag-handle:is(:hover, .open) > .ic, #agents > .row.dragging .ag-handle > .ic { opacity: 0; transform: scale(.8); }
|
||||
#agents > .row .ag-handle:focus-visible > .ic { opacity: 0; transform: scale(.8); }
|
||||
#agents > .row .ag-handle:is(:hover, .open) > .grip, #agents > .row.dragging .ag-handle > .grip { opacity: 1; transform: none; }
|
||||
#agents > .row .ag-handle:focus-visible > .grip { opacity: 1; transform: none; }
|
||||
/* the logo's grip comes up only on hover, so a row in the list also carries a
|
||||
faint one in its left margin, always there, that says the row can be taken
|
||||
hold of (#479): it is the handle's own, so a drag or a click on it does what
|
||||
the logo does, and it takes no room from the row */
|
||||
/* a row in the list carries a grip in its left margin (#479), drawn only
|
||||
while the row is under the pointer, dragged, or its handle has the
|
||||
keyboard or its menu open: always there, it was noise down every row. It
|
||||
is the handle's own, so a drag or a click on it does what the logo does,
|
||||
and it takes no room from the row */
|
||||
#agents > .row .ag-handle { position: relative; }
|
||||
#agents > .row .ag-handle::before { content: ""; position: absolute; left: -6px; top: 50%; width: 5px; height: 11px; margin-top: -5.5px; background: radial-gradient(circle, var(--grip-dot) .9px, transparent 1.2px) 0 0 / 3.5px 3.75px; --grip-dot: var(--faint); }
|
||||
#agents > .row .ag-handle::before { content: ""; position: absolute; left: -6px; top: 50%; width: 5px; height: 11px; margin-top: -5.5px; background: radial-gradient(circle, var(--muted) .9px, transparent 1.2px) 0 0 / 3.5px 3.75px; opacity: 0; transition: opacity .15s; }
|
||||
body.panel #agents > .row .ag-handle::before { left: -9px; }
|
||||
#agents > .row:is(:hover, .dragging) .ag-handle::before { --grip-dot: var(--muted); }
|
||||
#agents > .row:is(:hover, .dragging) .ag-handle::before, #agents > .row .ag-handle.open::before { opacity: 1; }
|
||||
#agents > .row .ag-handle:focus-visible::before { opacity: 1; }
|
||||
/* one hidden by hand reads as put away, with its way back beside its name */
|
||||
.agent-fold .row.put-away .ag-handle > .ic, .agent-fold .row.put-away .name { opacity: .6; }
|
||||
.agent-fold .row.put-away .who { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
@@ -901,22 +901,20 @@ function setAgentHidden(a, hide) {
|
||||
}
|
||||
|
||||
const ALT = /^Mac/.test(navigator.platform) ? "⌥" : "Alt+";
|
||||
const GRIP = "M6 4h.01M10 4h.01M6 8h.01M10 8h.01M6 12h.01M10 12h.01";
|
||||
const EYE_OFF = "M6.6 3.7A6.9 6.9 0 0 1 8 3.5c3.75 0 6.25 4.5 6.25 4.5a11 11 0 0 1-1.5 2M4.4 4.4C2.7 5.55 1.75 8 1.75 8S4.25 12.5 8 12.5c1.2 0 2.25-.45 3.1-1.05M6.75 6.75a1.75 1.75 0 0 0 2.5 2.5M2 2l12 12";
|
||||
const EYE = "M1.75 8S4.25 3.5 8 3.5 14.25 8 14.25 8 11.75 12.5 8 12.5 1.75 8 1.75 8ZM8 9.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z";
|
||||
|
||||
// agentHandle is the row's logo, which is also its handle: drag it to move
|
||||
// the row, click it (or right-click the row) for Move up, Move down and
|
||||
// Hide; Alt+↑/↓ moves it from the keyboard.
|
||||
// Hide; Alt+↑/↓ moves it from the keyboard. The grip that says so is drawn
|
||||
// in the row's margin on hover (app.css), not over the logo.
|
||||
function agentHandle(a, row, inFold) {
|
||||
const b = el("button", "ag-handle");
|
||||
b.type = "button";
|
||||
b.setAttribute("aria-label", t("Arrange {agent}", { agent: a.name }));
|
||||
b.setAttribute("aria-haspopup", "menu");
|
||||
b.title = inFold ? t(isHidden(a) ? "Show {agent}" : "Hide {agent}", { agent: a.name }) : t("Drag to reorder · click to move or hide");
|
||||
const grip = el("span", "grip");
|
||||
grip.append(svg(GRIP, 14, 2.4));
|
||||
b.append(icon(a.icon), grip);
|
||||
b.append(icon(a.icon));
|
||||
b.onkeydown = (e) => {
|
||||
if (inFold || !e.altKey || (e.key !== "ArrowUp" && e.key !== "ArrowDown")) return;
|
||||
e.preventDefault();
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
.pm-act.done:hover:not(:disabled) { background: var(--green-soft); color: var(--green); }
|
||||
.pm-act.busy { color: var(--muted); font-weight: 500; }
|
||||
.pm-act:disabled:not(.busy):not(.done) { opacity: .55; }
|
||||
.pm-card > .pm-act.move { height: auto; min-height: 26px; padding: 4px 12px; white-space: normal; text-align: center; }
|
||||
.pm-act .spin { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid currentColor; border-right-color: transparent; animation: spin .7s linear infinite; }
|
||||
|
||||
/* loading: cards drawn as grey bars */
|
||||
|
||||
@@ -201,9 +201,13 @@
|
||||
}
|
||||
}
|
||||
const moveButton = (b, c, here) => {
|
||||
b.classList.add("go", "move");
|
||||
b.textContent = here ? t("Move {name} here", { name: c.name }) : t(c.accounts === 1 ? "Move my {n} {name} account" : "Move my {n} {name} accounts", { name: c.name, n: c.accounts });
|
||||
b.title = t("Runs {name} on this plugin in place of the built-in, with the same accounts; nothing changes if one doesn't work through it", { name: c.name });
|
||||
// one word beside the name, as Install is: the card already says whose
|
||||
// accounts, the title says how many and what it does (a whole sentence
|
||||
// was a solid bar under the card that broke onto two lines)
|
||||
b.classList.add("get", "move");
|
||||
b.textContent = t("Move");
|
||||
b.title = (here ? t("Move {name} here", { name: c.name }) : t(c.accounts === 1 ? "Move my {n} {name} account" : "Move my {n} {name} accounts", { name: c.name, n: c.accounts }))
|
||||
+ " — " + t("Runs {name} on this plugin in place of the built-in, with the same accounts; nothing changes if one doesn't work through it", { name: c.name });
|
||||
b.disabled = busy.size > 0;
|
||||
b.onclick = (ev) => { ev.stopPropagation(); move(c); };
|
||||
return b;
|
||||
@@ -299,11 +303,7 @@
|
||||
by.append(v);
|
||||
} else by.append(el("span", "", l.npm?.publisher || l.package));
|
||||
who.append(by);
|
||||
const act = actionFor(l.package, l.name, l);
|
||||
// a move says whose accounts: too long for the top row, it has one of its own
|
||||
const own = act.classList.contains("move");
|
||||
top.append(logo(l.icon), who);
|
||||
if (!own) top.append(act);
|
||||
top.append(logo(l.icon), who, actionFor(l.package, l.name, l));
|
||||
const sum = el("p", "pm-sum", summary(l));
|
||||
const meta = el("div", "pm-meta");
|
||||
if (l.npm?.weekly) {
|
||||
@@ -322,7 +322,6 @@
|
||||
meta.append(b);
|
||||
}
|
||||
c.append(top, sum, meta);
|
||||
if (own) c.append(act);
|
||||
c.onclick = () => detail(l);
|
||||
c.onkeydown = (ev) => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); detail(l); } };
|
||||
return c;
|
||||
|
||||
@@ -104,10 +104,10 @@ code the browser page shows; an API-key way opens the account. English and
|
||||
Chinese, Chromium and WebKit, with the API faked.
|
||||
|
||||
`plugin-move-overflow.test.cjs` checks a Plugins card that offers a
|
||||
built-in's accounts ("Move my 1 Grok (SuperGrok) account"): the button has a
|
||||
row of its own at the card's foot, so it stays inside the card and the
|
||||
plugin's name and "magpie community" aren't cut, in a three-, two- and
|
||||
one-column window. English and Chinese, Chromium and WebKit, with the API
|
||||
built-in's accounts: the button is one word beside the name, as Install is
|
||||
("Move", its title saying whose accounts and how many), on one line, and it
|
||||
stays inside the card and the plugin's name and "magpie community" aren't
|
||||
cut, in a three-, two- and one-column window. English and Chinese, Chromium and WebKit, with the API
|
||||
faked.
|
||||
|
||||
`plugin-key-hint.test.cjs` checks a plugin's API-key way: its label titles
|
||||
@@ -1291,9 +1291,10 @@ node --test internal/gui/tests/seg-contrast.test.cjs
|
||||
```
|
||||
|
||||
`agent-grip.test.cjs` checks that hiding an agent can be found without a
|
||||
right-click (#479): with the pointer away, every row in the Agents list draws
|
||||
a faint grip in its left margin, beside its logo; dragging the grip moves the
|
||||
row, and clicking it opens the row's menu, whose Hide puts the row in the fold
|
||||
right-click (#479): a row in the Agents list under the pointer draws a grip
|
||||
in its left margin, beside its logo, and no row does with the pointer away;
|
||||
the logo stays the logo on hover (no second grip over it); dragging the grip
|
||||
moves the row, and clicking it opens the row's menu, whose Hide puts the row in the fold
|
||||
at the foot of the list. Opened, the fold's Show button is there without the
|
||||
pointer on its row, and brings the row back; no click moves the page. It runs
|
||||
in the window and the tray panel, in Chromium and WebKit, in English and
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// #479: an agent row's handle (its logo, which turns into a grip on hover;
|
||||
// a click opens Move up, Move down, Hide) showed nothing until the pointer
|
||||
// was on it, so hiding an agent was found only by right-clicking, and a
|
||||
// hidden one's way back, the Show button in the fold at the foot of the
|
||||
// list, came up only with the pointer on its row. Now every row in the list
|
||||
// carries a faint grip in its left margin, there with the pointer away: it is
|
||||
// the handle's own, so dragging it moves the row and clicking it opens the
|
||||
// menu. Hidden from that menu, the row goes into the fold, whose Show button
|
||||
// #479: an agent row's handle (its logo; a click opens Move up, Move down,
|
||||
// Hide) showed nothing until the pointer was on it, so hiding an agent was
|
||||
// found only by right-clicking, and a hidden one's way back, the Show button
|
||||
// in the fold at the foot of the list, came up only with the pointer on its
|
||||
// row. Every row in the list then carried a faint grip in its left margin,
|
||||
// always there; down every row that was noise, and the logo also turned into
|
||||
// a second grip on hover. Now the margin grip is drawn only with the pointer
|
||||
// on its row, and the logo stays the logo: the grip is the handle's own, so
|
||||
// dragging it moves the row and clicking it opens the menu. Hidden from that menu, the row goes into the fold, whose Show button
|
||||
// is there without the pointer on it and brings the row back. No click moves
|
||||
// the page. In the window and the tray panel, Chromium and WebKit, English
|
||||
// and Chinese; no backend, the API is faked here.
|
||||
@@ -46,7 +47,7 @@ function fixture(lang) {
|
||||
}
|
||||
|
||||
const order = (page) => page.locator("#agents > .row.agent").evaluateAll((rs) => rs.map((r) => r.dataset.id));
|
||||
// the faint grip of each row in the list, as drawn with the pointer away
|
||||
// the margin grip of each row in the list, where it is drawn
|
||||
const grips = (page) => page.locator("#agents > .row.agent").evaluateAll((rs) => rs.map((r) => {
|
||||
const h = r.querySelector(".ag-handle"), s = getComputedStyle(h, "::before"), hb = h.getBoundingClientRect(), rb = r.getBoundingClientRect();
|
||||
const left = hb.left + parseFloat(s.left), w = parseFloat(s.width), ht = parseFloat(s.height);
|
||||
@@ -76,14 +77,28 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
await page.locator("#agents > .row.agent").nth(2).waitFor();
|
||||
await page.mouse.move(1, 1);
|
||||
|
||||
// with the pointer away, every row's grip is drawn, in its margin
|
||||
// with the pointer away no row's grip is drawn; it is there, in the
|
||||
// row's margin, for the row under the pointer, and the logo stays as
|
||||
// it was
|
||||
const shown = (page) => page.locator("#agents > .row.agent").evaluateAll((rs) => rs.map((r) => getComputedStyle(r.querySelector(".ag-handle"), "::before").opacity));
|
||||
await page.waitForFunction(() => [...document.querySelectorAll("#agents > .row.agent .ag-handle")].every((h) => getComputedStyle(h, "::before").opacity === "0"));
|
||||
let g = await grips(page);
|
||||
for (const x of g) {
|
||||
assert.notEqual(x.content, "none", `${x.id} has no grip with the pointer away`);
|
||||
assert.notEqual(x.content, "none", `${x.id} has no grip`);
|
||||
assert.match(x.image, /gradient/, `${x.id}'s grip draws nothing`);
|
||||
assert(x.opacity > 0 && x.visibility === "visible" && x.w >= 4 && x.ht >= 8, `${x.id}'s grip can't be seen: ${JSON.stringify(x)}`);
|
||||
assert(x.visibility === "visible" && x.w >= 4 && x.ht >= 8, `${x.id}'s grip can't be seen: ${JSON.stringify(x)}`);
|
||||
assert(x.inRow, `${x.id}'s grip is off its row or over its logo: ${JSON.stringify(x)}`);
|
||||
}
|
||||
assert.equal(await page.locator("#agents .ag-handle .grip").count(), 0, "the logo still has a grip of its own");
|
||||
const row1 = page.locator("#agents > .row.agent").nth(1);
|
||||
await row1.locator(".name").hover();
|
||||
await page.waitForFunction(() => {
|
||||
const rs = [...document.querySelectorAll("#agents > .row.agent .ag-handle")].map((h) => getComputedStyle(h, "::before").opacity);
|
||||
return rs[1] === "1" && rs.every((o, i) => i === 1 || o === "0");
|
||||
});
|
||||
await row1.locator(".ag-handle").hover();
|
||||
assert.equal(await row1.locator(".ag-handle > .ic").evaluate((i) => getComputedStyle(i).opacity), "1", "the logo changes on hover");
|
||||
assert.equal((await shown(page))[1], "1");
|
||||
|
||||
// dragging by the grip moves the row
|
||||
const before = await order(page);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// A Plugins card that offers a built-in's accounts keeps its button inside
|
||||
// the card: "Move my Grok (SuperGrok) accounts (1)" ran past the card's right
|
||||
// edge, and Kiro's shorter one squeezed its name to "K…" and cut "magpie
|
||||
// community". The button now has a row of its own at the card's foot and
|
||||
// says how many accounts it moves ("Move my 1 Grok (SuperGrok) account").
|
||||
// community". It then had a solid row of its own at the card's foot, which
|
||||
// broke onto two lines ("Move my 2 Command Code Plan accounts"); it is now
|
||||
// one word beside the name, as Install is ("Move"), its title saying whose
|
||||
// accounts and how many, and stays on one line.
|
||||
// Every card stays whole, its name and byline uncut, in a three-, two- and
|
||||
// one-column window. In English and Chinese; the API is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
@@ -46,7 +48,7 @@ function serve(lang) {
|
||||
};
|
||||
}
|
||||
|
||||
const card = { en: "Move my 1 Grok (SuperGrok) account", zh: "迁移我的 1 个 Grok (SuperGrok) 账号" };
|
||||
const card = { en: "Move", zh: "迁移" };
|
||||
const launch = (engine) => engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" });
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
@@ -61,7 +63,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang));
|
||||
await page.goto("http://magpie.test/?view=plugins");
|
||||
await page.locator("#view-plugins button", { hasText: card[lang] }).waitFor();
|
||||
await page.locator("#view-plugins button", { hasText: card[lang] }).first().waitFor();
|
||||
if (process.env.ARTIFACT_DIR) await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `plugin-move-overflow-${engine}-${lang}-${width}.png`) });
|
||||
|
||||
const cards = await page.locator("#view-plugins .pm-card").evaluateAll((cs) => cs.map((c) => {
|
||||
@@ -74,6 +76,10 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
return { name: c.querySelector(".pm-name b")?.textContent, out, cut };
|
||||
}));
|
||||
assert.ok(cards.length >= listings.length, "the market's cards aren't there");
|
||||
// the move is one line, in the card's top row
|
||||
const moves = await page.locator("#view-plugins .pm-card .pm-act.move").evaluateAll((bs) => bs.map((b) => [b.parentElement.className, Math.round(b.getBoundingClientRect().height)]));
|
||||
assert.ok(moves.length >= 1);
|
||||
for (const [row, h] of moves) { assert.equal(row, "pm-top"); assert.equal(h, 26, "one line"); }
|
||||
const lefts = await page.locator("#view-plugins .pm-card").evaluateAll((cs) => new Set(cs.map((c) => Math.round(c.getBoundingClientRect().left))).size);
|
||||
assert.equal(lefts, cols, `not ${cols} columns at ${width}px`);
|
||||
const wrong = cards.flatMap((c) => [...(c.out.length ? [`${c.name}: runs past its card`] : []), ...c.cut.map((x) => `${c.name}: "${x}" is cut`)]);
|
||||
|
||||
@@ -75,12 +75,12 @@ const L = {
|
||||
move: "Move to the plugin", busy: "Installing the plugin and checking each account…", again: "Try again", back: "Use the built-in again",
|
||||
failed: "It stays built-in: magpie couldn't reach npm to install the plugin. Check the network or proxy, then try again.",
|
||||
onPlugin: "The community Zed plugin", builtin: "magpie's built-in · or the community Zed plugin", done: "Zed now runs on its plugin — 2 accounts, 1 model.",
|
||||
subs: "Subscriptions", card: "Move my 2 Zed accounts", own: "Zed itself stays signed in as it is." },
|
||||
subs: "Subscriptions", card: "Move", own: "Zed itself stays signed in as it is." },
|
||||
zh: { runs: "运行方式", line: "Zed 可以改由社区插件运行,账号不变。", look: "查看",
|
||||
move: "迁移到插件", busy: "正在安装插件并逐个检查账号…", again: "重试", back: "改回内置",
|
||||
failed: "仍使用内置:无法连接 npm 安装插件,请检查网络或代理后重试。",
|
||||
onPlugin: "社区 Zed 插件", builtin: "magpie 内置 · 也可改用社区 Zed 插件", done: "Zed 现在由插件运行——2 个账号,1 个模型。",
|
||||
subs: "订阅", card: "迁移我的 2 个 Zed 账号", own: "Zed 本身的登录保持不变。" },
|
||||
subs: "订阅", card: "迁移", own: "Zed 本身的登录保持不变。" },
|
||||
};
|
||||
|
||||
const launch = (engine) => engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" });
|
||||
|
||||
Reference in new issue
Block a user