Theseus: the left panel resizes, and steps aside like Opera's
Web apps in the left panel were a fixed 380 px, too narrow for WhatsApp, and add-on panels a fixed 400. A grip on the panel's right edge now resizes it, each app keeps its own width (settings.leftPanelWidths), a double-click resets it, and the page always keeps at least 320 px. The grip is a small view of Theseus's own laid over the edge, since the panel may be a third-party page; while dragging it covers the window so the drag keeps its events. Like Opera's sidebar, the left panel now hides when a new tab is opened or the right sidebar opens. It is only hidden, so reopening lands where the user left off. Add-on panels on the left used to get the right sidebar's grip on their left edge, where dragging resized the right sidebar; that grip is hidden there and its drags are ignored.
This commit is contained in:
parent
00aa37ab2b
commit
29b346250f
4 changed files with 153 additions and 5 deletions
107
main.js
107
main.js
|
|
@ -2275,6 +2275,15 @@ let leftPanelId = null; // panelId shown, or null when closed
|
|||
let leftPanelLoadedId = null; // what leftPanel has loaded (kept while closed, so reopening keeps state)
|
||||
let leftPanelMax = false; // widened by the panel (silentmode.sidebar.toggleMax)
|
||||
const LEFT_PANEL_W = 400;
|
||||
// The left panel (a quick-link web app or an add-on panel) is resizable from
|
||||
// a grip on its right edge; each app keeps its own width in
|
||||
// settings.leftPanelWidths, keyed "q:<quick-link id>" / "p:<panelId>", so
|
||||
// WhatsApp can stay wide while a small tool stays narrow.
|
||||
const LEFT_PANEL_W_MIN = 280;
|
||||
const TAB_AREA_MIN = 320; // the panel never squeezes the page below this
|
||||
let panelGrip; // panel-grip.html, laid over the panel's right edge
|
||||
let panelGripDragging = false;
|
||||
const PANEL_GRIP_W = 6;
|
||||
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
||||
// The value below is the default; settings.sidebarWidth overrides it once
|
||||
// loadSettings() runs and persists any drag adjustment made by the user.
|
||||
|
|
@ -3261,7 +3270,7 @@ function layout() {
|
|||
}
|
||||
// Quick-link panel: a narrow mini-view just right of the strip, showing the
|
||||
// active quick-link's web app. Only takes space when something is open.
|
||||
const quickW = (quickPanel && activeQuickLinkId && !fsTab) ? QUICK_PANEL_W : 0;
|
||||
const quickW = (quickPanel && activeQuickLinkId && !fsTab) ? leftPanelWidth(QUICK_PANEL_W, width - leftW) : 0;
|
||||
if (quickPanel) {
|
||||
quickPanel.setBounds({ x: leftW, y: chromeH, width: quickW, height: bodyH });
|
||||
try { quickPanel.setVisible(quickW > 0); } catch {}
|
||||
|
|
@ -3272,13 +3281,21 @@ function layout() {
|
|||
// An add-on's left panel. Widened, it covers the tab area like a page of
|
||||
// its own, up to the right sidebar, which keeps its width: the tabs keep
|
||||
// their narrow-panel bounds underneath instead of being squeezed aside.
|
||||
const addonLeftW = (leftPanel && leftPanelId && !fsTab) ? LEFT_PANEL_W : 0;
|
||||
const addonLeftShownW = addonLeftW && leftPanelMax ? Math.max(LEFT_PANEL_W, width - leftW - sideW) : addonLeftW;
|
||||
const addonLeftW = (leftPanel && leftPanelId && !fsTab) ? leftPanelWidth(LEFT_PANEL_W, width - leftW) : 0;
|
||||
const addonLeftShownW = addonLeftW && leftPanelMax ? Math.max(addonLeftW, width - leftW - sideW) : addonLeftW;
|
||||
if (leftPanel) {
|
||||
leftPanel.setBounds({ x: leftW, y: chromeH, width: addonLeftShownW, height: bodyH });
|
||||
try { leftPanel.setVisible(addonLeftShownW > 0); } catch {}
|
||||
}
|
||||
const panelW = quickW || addonLeftW;
|
||||
if (panelGrip) {
|
||||
// At rest a thin strip on the boundary; while dragging, the whole body,
|
||||
// so the drag keeps its events wherever the pointer goes.
|
||||
const showGrip = panelW > 0 && !(leftPanelId && leftPanelMax);
|
||||
if (panelGripDragging) panelGrip.setBounds({ x: 0, y: chromeH, width, height: bodyH });
|
||||
else panelGrip.setBounds({ x: leftW + panelW - PANEL_GRIP_W / 2, y: chromeH, width: PANEL_GRIP_W, height: bodyH });
|
||||
try { panelGrip.setVisible(showGrip || panelGripDragging); } catch {}
|
||||
}
|
||||
const tabX = leftW + panelW;
|
||||
const tabW = Math.max(0, width - tabX - sideW);
|
||||
for (const t of tabs) t.view.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
||||
|
|
@ -3515,6 +3532,7 @@ function setSidebar(show, panelId) {
|
|||
sidebarActivePanelId = panel.panelId;
|
||||
try { sidebar.webContents.loadFile(panel.pageFile); } catch (e) { console.warn("sidebar loadFile failed:", e?.message); }
|
||||
}
|
||||
hideLeftPanels();
|
||||
sidebarVisible = true;
|
||||
sidebar.setVisible(true);
|
||||
try { win.contentView.removeChildView(sidebar); win.contentView.addChildView(sidebar); } catch {}
|
||||
|
|
@ -4725,6 +4743,17 @@ function createWindow() {
|
|||
win.contentView.addChildView(leftPanel);
|
||||
styleScrollbars(leftPanel.webContents);
|
||||
leftPanel.setVisible(false);
|
||||
// The add-on panel page gets the RIGHT sidebar's grip from sidebar-preload,
|
||||
// on its left edge, where it would resize the wrong sidebar. Hide it; the
|
||||
// left panel is resized from panelGrip on its right edge.
|
||||
leftPanel.webContents.on("dom-ready", () => {
|
||||
leftPanel?.webContents.insertCSS('div[aria-label="Resize sidebar"]{display:none!important}').catch(() => {});
|
||||
});
|
||||
panelGrip = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "panel-grip-preload.js") } });
|
||||
try { panelGrip.setBackgroundColor("#00000000"); } catch {}
|
||||
win.contentView.addChildView(panelGrip);
|
||||
panelGrip.webContents.loadFile("panel-grip.html");
|
||||
panelGrip.setVisible(false);
|
||||
// Add-on approval overlay (approval-modal capability). Transparent view
|
||||
// over the tab area, loaded once, shown per request.
|
||||
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
||||
|
|
@ -4781,7 +4810,7 @@ function createWindow() {
|
|||
// pumpApproval (and every later dapp request) until a full restart.
|
||||
if (approvalCurrent) { const c = approvalCurrent; approvalCurrent = null; try { c.resolve("cancel"); } catch {} }
|
||||
for (const q of approvalQueue.splice(0)) { try { q.resolve("cancel"); } catch {} }
|
||||
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; leftPanel = null; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
||||
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; leftPanel = null; panelGrip = null; panelGripDragging = false; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
||||
linkStatusVisible = false;
|
||||
});
|
||||
layout();
|
||||
|
|
@ -4984,7 +5013,7 @@ ipcMain.handle("navigate", (e, input) => {
|
|||
return navigateTab(t ? t.id : activeId, input);
|
||||
});
|
||||
ipcMain.handle("search", (_e, q) => { if (leftPanelMax) setLeftPanelMax(false); return navigateTab(activeId, SEARCH(q)); });
|
||||
ipcMain.handle("new-tab", () => createTab());
|
||||
ipcMain.handle("new-tab", () => { hideLeftPanels(); return createTab(); });
|
||||
ipcMain.handle("close-tab", (_e, id) => closeTab(id));
|
||||
ipcMain.handle("switch-tab", (_e, id) => setActive(id));
|
||||
// Tab context menu backing IPCs. All scoped to a specific tab id so the
|
||||
|
|
@ -5519,6 +5548,7 @@ ipcMain.handle("sidebar-toggle", () => { toggleSidebar(); return sidebarVisible;
|
|||
// grip. Delta is px per mousemove; we clamp, layout, and debounce the save.
|
||||
let _sidebarSaveTimer = null;
|
||||
ipcMain.handle("sidebar-drag", (_e, deltaPx) => {
|
||||
if (isLeftSender(_e)) return sidebarW; // left panel has its own grip
|
||||
const d = Number(deltaPx) || 0;
|
||||
// Drag-to-resize exits maximize mode — the user is asking for a specific
|
||||
// width. We snap out of maximize first so the delta lands on the pre-max
|
||||
|
|
@ -7927,6 +7957,71 @@ function quickLinksPayload() {
|
|||
function emitQuickLinksState() {
|
||||
try { quicklinks?.webContents.send("quicklinks-state", quickLinksPayload()); } catch {}
|
||||
}
|
||||
function leftPanelKey() {
|
||||
if (activeQuickLinkId) return "q:" + activeQuickLinkId;
|
||||
if (leftPanelId) return "p:" + leftPanelId;
|
||||
return null;
|
||||
}
|
||||
// The open app's width, clamped so the page keeps at least TAB_AREA_MIN.
|
||||
function leftPanelWidth(def, room) {
|
||||
const k = leftPanelKey();
|
||||
const saved = k && settings.leftPanelWidths && Number(settings.leftPanelWidths[k]);
|
||||
const max = Math.max(LEFT_PANEL_W_MIN, (room || 0) - TAB_AREA_MIN);
|
||||
return Math.max(LEFT_PANEL_W_MIN, Math.min(max, saved || def));
|
||||
}
|
||||
// The grip has to stay above whichever panel view was raised last.
|
||||
function raisePanelGrip() {
|
||||
if (!panelGrip) return;
|
||||
try { win.contentView.removeChildView(panelGrip); win.contentView.addChildView(panelGrip); } catch {}
|
||||
}
|
||||
// Opera-style: the left panel steps aside when the user opens something else
|
||||
// (a new tab, the right sidebar). It is only hidden — the app keeps its
|
||||
// state, so reopening it is instant and lands where the user left off.
|
||||
function hideLeftPanels() {
|
||||
if (activeQuickLinkId) closeQuickPanel();
|
||||
if (leftPanelId) closeLeftPanel();
|
||||
}
|
||||
const isGripSender = (e) => !!panelGrip && e.sender === panelGrip.webContents;
|
||||
let _leftPanelSaveTimer = null;
|
||||
ipcMain.handle("panel-grip-start", (e) => {
|
||||
if (!isGripSender(e) || !leftPanelKey()) return false;
|
||||
panelGripDragging = true;
|
||||
layout();
|
||||
return true;
|
||||
});
|
||||
// windowX: the pointer's x in the window. While dragging, the grip view
|
||||
// covers the window from x = 0, so its clientX is exactly that.
|
||||
ipcMain.handle("panel-grip-move", (e, windowX) => {
|
||||
if (!isGripSender(e) || !panelGripDragging || !winAlive()) return;
|
||||
const k = leftPanelKey();
|
||||
if (!k) return;
|
||||
const stripW = (quicklinks && settings.quickLinksShow) ? QUICKLINKS_W : 0;
|
||||
const want = Math.round(Number(windowX) - stripW);
|
||||
if (!Number.isFinite(want)) return;
|
||||
settings.leftPanelWidths = { ...(settings.leftPanelWidths || {}), [k]: Math.max(LEFT_PANEL_W_MIN, want) };
|
||||
layout();
|
||||
});
|
||||
ipcMain.handle("panel-grip-end", (e) => {
|
||||
if (!isGripSender(e)) return;
|
||||
panelGripDragging = false;
|
||||
// Store what layout actually allowed, not the raw pointer position.
|
||||
const k = leftPanelKey();
|
||||
if (k && settings.leftPanelWidths && settings.leftPanelWidths[k]) {
|
||||
const room = win.getContentBounds().width - ((quicklinks && settings.quickLinksShow) ? QUICKLINKS_W : 0);
|
||||
settings.leftPanelWidths[k] = leftPanelWidth(activeQuickLinkId ? QUICK_PANEL_W : LEFT_PANEL_W, room);
|
||||
}
|
||||
layout();
|
||||
clearTimeout(_leftPanelSaveTimer);
|
||||
_leftPanelSaveTimer = setTimeout(saveSettings, 300);
|
||||
});
|
||||
ipcMain.handle("panel-grip-reset", (e) => {
|
||||
if (!isGripSender(e)) return;
|
||||
const k = leftPanelKey();
|
||||
if (!k || !settings.leftPanelWidths) return;
|
||||
delete settings.leftPanelWidths[k];
|
||||
layout();
|
||||
saveSettings();
|
||||
});
|
||||
function openLeftPanel(panelId) {
|
||||
const p = leftPanels().find((x) => x.panelId === panelId);
|
||||
if (!leftPanel || !p) return false;
|
||||
|
|
@ -7938,6 +8033,7 @@ function openLeftPanel(panelId) {
|
|||
try { leftPanel.webContents.loadFile(p.pageFile); } catch (e) { console.warn("left panel loadFile failed:", e?.message); }
|
||||
}
|
||||
try { win.contentView.removeChildView(leftPanel); win.contentView.addChildView(leftPanel); } catch {}
|
||||
raisePanelGrip();
|
||||
layout();
|
||||
emitQuickLinksState();
|
||||
try { leftPanel.webContents.send("sidebar-visibility", true); } catch {}
|
||||
|
|
@ -7963,6 +8059,7 @@ function openQuickPanel(id) {
|
|||
const sameHost = (() => { try { return cur && new URL(cur).host === new URL(L.url).host; } catch { return false; } })();
|
||||
if (!sameHost) quickPanel.webContents.loadURL(L.url);
|
||||
} catch (e) { console.warn("[quicklinks] loadURL failed:", e?.message); }
|
||||
raisePanelGrip();
|
||||
layout();
|
||||
emitQuickLinksState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@
|
|||
"js-dialog-preload.js",
|
||||
"quicklinks.html",
|
||||
"quicklinks-preload.js",
|
||||
"panel-grip-preload.js",
|
||||
"panel-grip.html",
|
||||
"engine-icons/**/*",
|
||||
"lib/**/*",
|
||||
"package.json",
|
||||
|
|
|
|||
12
panel-grip-preload.js
Normal file
12
panel-grip-preload.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Resize grip for the left panel (quick-link web apps and add-on panels).
|
||||
// The panel itself may be a third-party page (WhatsApp, Telegram…), so the
|
||||
// grip is a separate view of Theseus's own laid over the panel's right edge.
|
||||
// While dragging, main stretches this view over the whole window so every
|
||||
// mousemove and the final mouseup land here, wherever the pointer goes.
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
contextBridge.exposeInMainWorld("grip", {
|
||||
start: () => ipcRenderer.invoke("panel-grip-start"),
|
||||
move: (windowX) => ipcRenderer.invoke("panel-grip-move", windowX),
|
||||
end: () => ipcRenderer.invoke("panel-grip-end"),
|
||||
reset: () => ipcRenderer.invoke("panel-grip-reset"),
|
||||
});
|
||||
37
panel-grip.html
Normal file
37
panel-grip.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<style>
|
||||
/* Transparent everywhere except the thin line at the panel edge. At rest
|
||||
the view is a few pixels wide and sits on the boundary; while dragging
|
||||
main widens it to the whole window and the line follows the pointer. */
|
||||
html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; cursor: col-resize; user-select: none; }
|
||||
#line { position: fixed; top: 0; bottom: 0; width: 2px; left: 50%; transform: translateX(-1px);
|
||||
background: rgba(140,150,170,.45); transition: background .12s; }
|
||||
body:hover #line { background: rgba(214,255,61,.45); }
|
||||
body.drag #line { display: none; }
|
||||
</style></head>
|
||||
<body title="Drag to resize · double-click to reset">
|
||||
<div id="line"></div>
|
||||
<script>
|
||||
let dragging = false;
|
||||
document.addEventListener("mousedown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
dragging = true;
|
||||
document.body.classList.add("drag");
|
||||
window.grip.start();
|
||||
});
|
||||
// While dragging, this view covers the window from its left edge, so clientX
|
||||
// is the pointer's x in the window.
|
||||
document.addEventListener("mousemove", (e) => { if (dragging) window.grip.move(e.clientX); });
|
||||
const stop = () => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
document.body.classList.remove("drag");
|
||||
window.grip.end();
|
||||
};
|
||||
document.addEventListener("mouseup", stop);
|
||||
window.addEventListener("blur", stop);
|
||||
document.addEventListener("dblclick", () => window.grip.reset());
|
||||
</script>
|
||||
</body></html>
|
||||
Loading…
Add table
Reference in a new issue