Compare commits
10 commits
cf49e46a6d
...
a617fbfadf
| Author | SHA1 | Date | |
|---|---|---|---|
| a617fbfadf | |||
| b2413e5e5a | |||
| 7ced3bc24f | |||
| 0276e5692e | |||
| f22e0cb701 | |||
| c32c3a2e2d | |||
| 88260d7ea2 | |||
| 4a4adb1ae5 | |||
| 96a4598e47 | |||
| e20447dbc4 |
12 changed files with 236 additions and 91 deletions
|
|
@ -1,2 +1 @@
|
||||||
[build]
|
|
||||||
jobs = 6
|
|
||||||
|
|
|
||||||
29
.github/workflows/rust.yml
vendored
29
.github/workflows/rust.yml
vendored
|
|
@ -3,6 +3,7 @@ name: Rust
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
tags: [ "v*" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
|
@ -58,6 +59,10 @@ jobs:
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Build (release)
|
- name: Build (release)
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wherry-x86_64
|
||||||
|
path: target/release/wherry
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -81,3 +86,27 @@ jobs:
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test
|
run: cargo test
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build, test]
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wherry-x86_64
|
||||||
|
- name: Package binary
|
||||||
|
run: |
|
||||||
|
mkdir -p pkg/usr/bin pkg/usr/share/licenses/wherry pkg/usr/share/pixmaps pkg/usr/share/applications
|
||||||
|
install -Dm755 wherry pkg/usr/bin/wherry
|
||||||
|
install -Dm644 LICENSE pkg/usr/share/licenses/wherry/LICENSE
|
||||||
|
install -Dm644 src/icons/128x128.png pkg/usr/share/pixmaps/wherry.png
|
||||||
|
install -Dm644 dist/wherry.desktop pkg/usr/share/applications/wherry.desktop
|
||||||
|
tar czf wherry-${{ github.ref_name }}-x86_64-linux.tar.gz -C pkg .
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: wherry-${{ github.ref_name }}-x86_64-linux.tar.gz
|
||||||
|
generate_release_notes: true
|
||||||
|
|
|
||||||
22
LICENSE
22
LICENSE
|
|
@ -392,13 +392,8 @@ governed by this License along with a term that is a further
|
||||||
restriction, you may remove that term. If a license document contains
|
restriction, you may remove that term. If a license document contains
|
||||||
a further restriction but permits relicensing or conveying under this
|
a further restriction but permits relicensing or conveying under this
|
||||||
License, you may add to a covered work material governed by the terms
|
License, you may add to a covered work material governed by the terms
|
||||||
<<<<<<< HEAD
|
|
||||||
of that license document, provided that the further restriction does
|
of that license document, provided that the further restriction does
|
||||||
not survive such relicensing or conveying.
|
not survive such relicensing or conveying.
|
||||||
=======
|
|
||||||
of that license document, provided that the further restriction does not
|
|
||||||
survive such relicensing or conveying.
|
|
||||||
>>>>>>> 049e4f1 (Initial commit: LoFlum FTP/SFTP client)
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
If you add terms to a covered work in accord with this section, you
|
||||||
must place, in the relevant source files, a statement of the
|
must place, in the relevant source files, a statement of the
|
||||||
|
|
@ -516,20 +511,12 @@ in a country, would infringe one or more identifiable patents in that
|
||||||
country that you have reason to believe are valid.
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
If, pursuant to or in connection with a single transaction or
|
||||||
<<<<<<< HEAD
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
covered work, and grant a patent license to some of the parties
|
covered work, and grant a patent license to some of the parties
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
or convey a specific copy of the covered work, then the patent license
|
or convey a specific copy of the covered work, then the patent license
|
||||||
you grant is automatically extended to all recipients of the covered
|
you grant is automatically extended to all recipients of the covered
|
||||||
work and works based on it.
|
work and works based on it.
|
||||||
=======
|
|
||||||
arrangement, you convey, or procure by conveying, a covered work, and
|
|
||||||
grant a patent license to some of the parties receiving the covered work
|
|
||||||
authorizing them to use, propagate, modify or convey a specific copy of
|
|
||||||
the covered work, then the patent license you grant is automatically
|
|
||||||
extended to all recipients of the covered work and works based on it.
|
|
||||||
>>>>>>> 049e4f1 (Initial commit: LoFlum FTP/SFTP client)
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
A patent license is "discriminatory" if it does not include within
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
|
@ -547,13 +534,8 @@ contain the covered work, unless you entered into that arrangement,
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
<<<<<<< HEAD
|
|
||||||
any implied license or other defenses to infringement that may
|
any implied license or other defenses to infringement that may
|
||||||
otherwise be available to you under applicable patent law.
|
otherwise be available to you under applicable patent law.
|
||||||
=======
|
|
||||||
any implied license or other defenses to infringement that may be
|
|
||||||
otherwise available to you under applicable patent law.
|
|
||||||
>>>>>>> 049e4f1 (Initial commit: LoFlum FTP/SFTP client)
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
|
@ -594,11 +576,7 @@ Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
=======
|
|
||||||
If the Program specifies that a proxy can decide whether future
|
|
||||||
>>>>>>> 049e4f1 (Initial commit: LoFlum FTP/SFTP client)
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
|
||||||
7
dist/.SRCINFO
vendored
7
dist/.SRCINFO
vendored
|
|
@ -1,13 +1,12 @@
|
||||||
pkgbase = wherry
|
pkgbase = wherry
|
||||||
pkgdesc = A modern dual-pane file manager with SFTP/FTP/FTPS support
|
pkgdesc = A modern dual-pane file manager with SFTP/FTP/FTPS support
|
||||||
pkgver = 0.1.0
|
pkgver = 0.1.0
|
||||||
pkgrel = 1
|
pkgrel = 2
|
||||||
url = https://github.com/loki5512344/Wherry
|
url = https://github.com/loki5512344/Wherry
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
license = GPL3
|
license = GPL3
|
||||||
makedepends = cargo
|
makedepends = cargo
|
||||||
makedepends = pkg-config
|
makedepends = pkg-config
|
||||||
makedepends = patchelf
|
|
||||||
depends = gcc-libs
|
depends = gcc-libs
|
||||||
depends = gtk3
|
depends = gtk3
|
||||||
depends = webkit2gtk-4.1
|
depends = webkit2gtk-4.1
|
||||||
|
|
@ -18,9 +17,9 @@ pkgbase = wherry
|
||||||
depends = librsvg
|
depends = librsvg
|
||||||
depends = openssl
|
depends = openssl
|
||||||
depends = libssh2
|
depends = libssh2
|
||||||
depends = libappindicator-gtk3
|
|
||||||
depends = dbus
|
depends = dbus
|
||||||
|
optdepends = mold: faster linking
|
||||||
source = https://github.com/loki5512344/Wherry/archive/refs/tags/v0.1.0.tar.gz
|
source = https://github.com/loki5512344/Wherry/archive/refs/tags/v0.1.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = 3cebca09e29629f4edb56a42074cad9b1f2539fc64cf3e96c3e7208dbacff1ec
|
||||||
|
|
||||||
pkgname = wherry
|
pkgname = wherry
|
||||||
|
|
|
||||||
11
dist/PKGBUILD
vendored
11
dist/PKGBUILD
vendored
|
|
@ -1,8 +1,8 @@
|
||||||
# Maintainer: loki5512344 <dev14dbq@gmail.com>
|
# Maintainer: loki5512344 <mauhhhh7@gmail.com>
|
||||||
|
|
||||||
pkgname=wherry
|
pkgname=wherry
|
||||||
pkgver=0.1.0
|
pkgver=0.1.0
|
||||||
pkgrel=1
|
pkgrel=2
|
||||||
pkgdesc="A modern dual-pane file manager with SFTP/FTP/FTPS support"
|
pkgdesc="A modern dual-pane file manager with SFTP/FTP/FTPS support"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
url='https://github.com/loki5512344/Wherry'
|
url='https://github.com/loki5512344/Wherry'
|
||||||
|
|
@ -18,12 +18,12 @@ depends=(
|
||||||
'librsvg'
|
'librsvg'
|
||||||
'openssl'
|
'openssl'
|
||||||
'libssh2'
|
'libssh2'
|
||||||
'libappindicator-gtk3'
|
|
||||||
'dbus'
|
'dbus'
|
||||||
)
|
)
|
||||||
makedepends=('cargo' 'pkg-config' 'patchelf')
|
makedepends=('cargo' 'pkg-config')
|
||||||
|
optdepends=('mold: faster linking')
|
||||||
source=("https://github.com/loki5512344/Wherry/archive/refs/tags/v$pkgver.tar.gz")
|
source=("https://github.com/loki5512344/Wherry/archive/refs/tags/v$pkgver.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('3cebca09e29629f4edb56a42074cad9b1f2539fc64cf3e96c3e7208dbacff1ec')
|
||||||
|
|
||||||
_repodir="Wherry-$pkgver"
|
_repodir="Wherry-$pkgver"
|
||||||
|
|
||||||
|
|
@ -36,7 +36,6 @@ prepare() {
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$_repodir"
|
cd "$srcdir/$_repodir"
|
||||||
export CARGO_TARGET_DIR="$srcdir/target"
|
export CARGO_TARGET_DIR="$srcdir/target"
|
||||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="/usr/bin/cc"
|
|
||||||
cargo build --release --frozen
|
cargo build --release --frozen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
22
dist/wherry-bin/.SRCINFO
vendored
Normal file
22
dist/wherry-bin/.SRCINFO
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
pkgbase = wherry-bin
|
||||||
|
pkgdesc = A modern dual-pane file manager with SFTP/FTP/FTPS support (pre-compiled)
|
||||||
|
pkgver = 0.1.0
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/loki5512344/Wherry
|
||||||
|
arch = x86_64
|
||||||
|
license = GPL3
|
||||||
|
depends = gcc-libs
|
||||||
|
depends = gtk3
|
||||||
|
depends = webkit2gtk-4.1
|
||||||
|
depends = libsoup3
|
||||||
|
depends = cairo
|
||||||
|
depends = gdk-pixbuf2
|
||||||
|
depends = glib2
|
||||||
|
depends = librsvg
|
||||||
|
depends = openssl
|
||||||
|
depends = libssh2
|
||||||
|
depends = dbus
|
||||||
|
source = https://github.com/loki5512344/Wherry/releases/download/v0.1.0/wherry-v0.1.0-x86_64-linux.tar.gz
|
||||||
|
sha256sums = d00bedf0ea2890a18d5c506880a33f4bd87eeec2228309279a3d06b1657a7b5e
|
||||||
|
|
||||||
|
pkgname = wherry-bin
|
||||||
28
dist/wherry-bin/PKGBUILD
vendored
Normal file
28
dist/wherry-bin/PKGBUILD
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Maintainer: loki5512344 <mauhhhh7@gmail.com>
|
||||||
|
|
||||||
|
pkgname=wherry-bin
|
||||||
|
pkgver=0.1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="A modern dual-pane file manager with SFTP/FTP/FTPS support (pre-compiled)"
|
||||||
|
arch=('x86_64')
|
||||||
|
url='https://github.com/loki5512344/Wherry'
|
||||||
|
license=('GPL3')
|
||||||
|
depends=(
|
||||||
|
'gcc-libs'
|
||||||
|
'gtk3'
|
||||||
|
'webkit2gtk-4.1'
|
||||||
|
'libsoup3'
|
||||||
|
'cairo'
|
||||||
|
'gdk-pixbuf2'
|
||||||
|
'glib2'
|
||||||
|
'librsvg'
|
||||||
|
'openssl'
|
||||||
|
'libssh2'
|
||||||
|
'dbus'
|
||||||
|
)
|
||||||
|
source=("https://github.com/loki5512344/Wherry/releases/download/v$pkgver/wherry-v$pkgver-x86_64-linux.tar.gz")
|
||||||
|
sha256sums=('d00bedf0ea2890a18d5c506880a33f4bd87eeec2228309279a3d06b1657a7b5e')
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cp -r "$srcdir"/* "$pkgdir"/
|
||||||
|
}
|
||||||
|
|
@ -649,8 +649,42 @@ html[data-platform="linux"] ::-webkit-scrollbar {
|
||||||
height: 7px;
|
height: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Pane resize handle ──────────────────────────────────────────────── */
|
||||||
|
.pane-resize-handle {
|
||||||
|
width: 5px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 -2px;
|
||||||
|
transition: background 0.12s var(--ease-out);
|
||||||
|
}
|
||||||
|
.pane-resize-handle:hover,
|
||||||
|
.pane-resize-handle:active {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
.pane-resize-handle::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 1px;
|
||||||
|
height: 36px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 1px;
|
||||||
|
transition: background 0.12s var(--ease-out), height 0.12s var(--ease-out);
|
||||||
|
}
|
||||||
|
.pane-resize-handle:hover::after,
|
||||||
|
.pane-resize-handle:active::after {
|
||||||
|
background: var(--accent);
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Split-pane grid ─────────────────────────────────────────────────── */
|
/* ── Split-pane grid ─────────────────────────────────────────────────── */
|
||||||
.pane-grid {
|
.pane-grid {
|
||||||
|
display: grid;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -663,9 +697,6 @@ html[data-platform="linux"] ::-webkit-scrollbar {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.pane-cell + .pane-cell {
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.pane-cell.drop-zone {
|
.pane-cell.drop-zone {
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ const paneRefs = new Map();
|
||||||
function paneKind(p) { return p.tabId ?? "local"; }
|
function paneKind(p) { return p.tabId ?? "local"; }
|
||||||
function paneLabel(p) { return p.tabId === null ? "Local" : (state.tabs.find((t) => t.id === p.tabId)?.label ?? "?"); }
|
function paneLabel(p) { return p.tabId === null ? "Local" : (state.tabs.find((t) => t.id === p.tabId)?.label ?? "?"); }
|
||||||
|
|
||||||
|
function ensureFlex(p) { if (p.flex == null) p.flex = 1; return p; }
|
||||||
|
|
||||||
/* Render the full pane grid */
|
/* Render the full pane grid */
|
||||||
export function renderPanes(container) {
|
export function renderPanes(container) {
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
@ -19,10 +21,9 @@ export function renderPanes(container) {
|
||||||
|
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "pane-grid";
|
wrap.className = "pane-grid";
|
||||||
wrap.style.cssText = `display:grid;grid-template-columns:${buildGrid()};flex:1;min-height:0;`;
|
applyGrid(wrap);
|
||||||
wrap.addEventListener("dragover", (e) => { e.preventDefault(); });
|
wrap.addEventListener("dragover", (e) => { e.preventDefault(); });
|
||||||
wrap.addEventListener("drop", (e) => {
|
wrap.addEventListener("drop", (e) => {
|
||||||
// Drop on empty area (after last pane) → append
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const tabId = e.dataTransfer.getData("tab-id");
|
const tabId = e.dataTransfer.getData("tab-id");
|
||||||
if (!tabId) return;
|
if (!tabId) return;
|
||||||
|
|
@ -31,13 +32,91 @@ export function renderPanes(container) {
|
||||||
});
|
});
|
||||||
container.appendChild(wrap);
|
container.appendChild(wrap);
|
||||||
|
|
||||||
for (const pane of state.layout.panes) {
|
for (let i = 0; i < state.layout.panes.length; i++) {
|
||||||
const cell = createPaneCell(pane);
|
if (i > 0) {
|
||||||
|
const handle = createResizeHandle(i - 1);
|
||||||
|
wrap.appendChild(handle);
|
||||||
|
}
|
||||||
|
const cell = createPaneCell(state.layout.panes[i]);
|
||||||
wrap.appendChild(cell);
|
wrap.appendChild(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyGrid(wrap) {
|
||||||
|
const n = state.layout.panes.length;
|
||||||
|
const totalFlex = state.layout.panes.reduce((s, p) => s + (p.flex || 1), 0);
|
||||||
|
const cols = state.layout.panes.map((p) => `${((p.flex || 1) / totalFlex * 100).toFixed(2)}fr`).join(" ");
|
||||||
|
wrap.style.cssText = `display:grid;grid-template-columns:${n > 1 ? cols : "1fr"};flex:1;min-height:0;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResizeHandle(index) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "pane-resize-handle";
|
||||||
|
div.dataset.index = index;
|
||||||
|
div.addEventListener("mousedown", onResizeStart);
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizing = null;
|
||||||
|
|
||||||
|
function onResizeStart(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const handle = e.currentTarget;
|
||||||
|
const index = parseInt(handle.dataset.index);
|
||||||
|
const wrap = handle.closest(".pane-grid");
|
||||||
|
const rect = wrap.getBoundingClientRect();
|
||||||
|
const startX = e.clientX;
|
||||||
|
|
||||||
|
const panes = state.layout.panes;
|
||||||
|
const totalFlex = panes.reduce((s, p) => s + (p.flex || 1), 0);
|
||||||
|
const currentPct = panes.map((p) => (p.flex || 1) / totalFlex);
|
||||||
|
|
||||||
|
resizing = { index, startX, wrap, rect, panes, currentPct, totalFlex };
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onResizeMove);
|
||||||
|
document.addEventListener("mouseup", onResizeEnd);
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResizeMove(e) {
|
||||||
|
if (!resizing) return;
|
||||||
|
const { index, startX, rect, panes, currentPct } = resizing;
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const totalWidth = rect.width;
|
||||||
|
if (totalWidth <= 0) return;
|
||||||
|
|
||||||
|
const leftPct = currentPct[index];
|
||||||
|
const rightPct = currentPct[index + 1];
|
||||||
|
const pctChange = dx / totalWidth;
|
||||||
|
|
||||||
|
const newLeft = Math.max(0.1, Math.min(0.9, leftPct + pctChange));
|
||||||
|
const newRight = Math.max(0.1, Math.min(0.9, rightPct - pctChange));
|
||||||
|
if (newLeft + newRight < 0.2) return;
|
||||||
|
|
||||||
|
const newTotal = newLeft + newRight + currentPct.slice(0, index).reduce((s, v) => s + v, 0) + currentPct.slice(index + 2).reduce((s, v) => s + v, 0);
|
||||||
|
|
||||||
|
const scale = currentPct.reduce((a, b) => a + b, 0) / newTotal;
|
||||||
|
for (let i = 0; i < panes.length; i++) {
|
||||||
|
if (i === index) panes[i].flex = newLeft * scale;
|
||||||
|
else if (i === index + 1) panes[i].flex = newRight * scale;
|
||||||
|
else panes[i].flex = currentPct[i] * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrap = resizing.wrap;
|
||||||
|
applyGrid(wrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResizeEnd() {
|
||||||
|
resizing = null;
|
||||||
|
document.removeEventListener("mousemove", onResizeMove);
|
||||||
|
document.removeEventListener("mouseup", onResizeEnd);
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
}
|
||||||
|
|
||||||
function createPaneCell(pane) {
|
function createPaneCell(pane) {
|
||||||
|
ensureFlex(pane);
|
||||||
const cell = document.createElement("div");
|
const cell = document.createElement("div");
|
||||||
cell.className = "pane-cell";
|
cell.className = "pane-cell";
|
||||||
cell.dataset.paneId = pane.id;
|
cell.dataset.paneId = pane.id;
|
||||||
|
|
@ -60,14 +139,12 @@ function createPaneCell(pane) {
|
||||||
header.addEventListener("dragend", onHeaderDragEnd);
|
header.addEventListener("dragend", onHeaderDragEnd);
|
||||||
|
|
||||||
// Close btn
|
// Close btn
|
||||||
if (state.layout.panes.length > 1) {
|
const closeBtn = document.createElement("button");
|
||||||
const closeBtn = document.createElement("button");
|
closeBtn.className = "pane-close-btn";
|
||||||
closeBtn.className = "pane-close-btn";
|
closeBtn.title = "Close pane";
|
||||||
closeBtn.title = "Close";
|
closeBtn.innerHTML = iconMarkup("close", 13);
|
||||||
closeBtn.innerHTML = iconMarkup("close", 13);
|
closeBtn.addEventListener("click", (e) => { e.stopPropagation(); closePane(pane.id); });
|
||||||
closeBtn.addEventListener("click", (e) => { e.stopPropagation(); closePane(pane.id); });
|
header.appendChild(closeBtn);
|
||||||
header.appendChild(closeBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
const body = document.createElement("div");
|
const body = document.createElement("div");
|
||||||
|
|
@ -94,12 +171,6 @@ function createPaneCell(pane) {
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildGrid() {
|
|
||||||
const n = state.layout.panes.length;
|
|
||||||
if (n <= 1) return "1fr";
|
|
||||||
return state.layout.panes.map(() => "1fr").join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* In-place update (avoids full rebuild on every tick) */
|
/* In-place update (avoids full rebuild on every tick) */
|
||||||
export function updatePanes(container) {
|
export function updatePanes(container) {
|
||||||
const wrap = container.querySelector(".pane-grid");
|
const wrap = container.querySelector(".pane-grid");
|
||||||
|
|
@ -110,7 +181,7 @@ export function updatePanes(container) {
|
||||||
const expected = state.layout.panes.map((p) => p.id);
|
const expected = state.layout.panes.map((p) => p.id);
|
||||||
if (ids.join(",") !== expected.join(",")) return renderPanes(container);
|
if (ids.join(",") !== expected.join(",")) return renderPanes(container);
|
||||||
|
|
||||||
wrap.style.gridTemplateColumns = buildGrid();
|
applyGrid(wrap);
|
||||||
|
|
||||||
for (const pane of state.layout.panes) {
|
for (const pane of state.layout.panes) {
|
||||||
const ref = paneRefs.get(pane.id);
|
const ref = paneRefs.get(pane.id);
|
||||||
|
|
@ -127,20 +198,10 @@ export function updatePanes(container) {
|
||||||
|
|
||||||
// ── Drag & Drop: tab bar → pane ─────────────────────────────────────────
|
// ── Drag & Drop: tab bar → pane ─────────────────────────────────────────
|
||||||
|
|
||||||
/* Tab bar tabs become draggable — set data "tab-id" on dragstart. Pane cells
|
let activeZone = null;
|
||||||
get visual drop zones (left/right/top/bottom/center), but two rules keep
|
let holdTimer = null;
|
||||||
it from feeling random:
|
let holdPaneId = null;
|
||||||
1. The pane already showing the dragged tab never offers itself as a
|
let openPaneId = null;
|
||||||
target -- there's nowhere sensible to put a tab inside its own pane.
|
|
||||||
2. Every other pane needs a beat of sustained hover (HOVER_OPEN_DELAY)
|
|
||||||
before it "opens" as a drop target. Zones appearing the instant the
|
|
||||||
pointer crosses into a pane read as jumpy; the pause makes hovering
|
|
||||||
read as an intentional choice instead. */
|
|
||||||
|
|
||||||
let activeZone = null; // "<paneId>-<zone>" of the currently shown zone overlay
|
|
||||||
let holdTimer = null; // pending "open" timer for the pane below
|
|
||||||
let holdPaneId = null; // pane the hold timer is counting for
|
|
||||||
let openPaneId = null; // pane that passed the hold and is now a live drop target
|
|
||||||
|
|
||||||
function computeZone(cell, clientX, clientY) {
|
function computeZone(cell, clientX, clientY) {
|
||||||
const rect = cell.getBoundingClientRect();
|
const rect = cell.getBoundingClientRect();
|
||||||
|
|
@ -193,10 +254,6 @@ function onPaneDragOver(e) {
|
||||||
const paneId = cell.dataset.paneId;
|
const paneId = cell.dataset.paneId;
|
||||||
const pane = state.layout.panes.find((p) => p.id === paneId);
|
const pane = state.layout.panes.find((p) => p.id === paneId);
|
||||||
|
|
||||||
// Dragging a tab over the pane that already shows it -- never a valid
|
|
||||||
// target. Stop the event here so it doesn't bubble to the grid wrap's
|
|
||||||
// unconditional preventDefault(), which would otherwise still allow a
|
|
||||||
// drop (routed to the "append at end" handler) at this pointer position.
|
|
||||||
if (pane && pane.tabId === getDraggedTabId()) {
|
if (pane && pane.tabId === getDraggedTabId()) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
|
@ -246,7 +303,7 @@ function onPaneDrop(e) {
|
||||||
const wasOpen = openPaneId === paneId;
|
const wasOpen = openPaneId === paneId;
|
||||||
const zone = cell.dataset.dropZone || "center";
|
const zone = cell.dataset.dropZone || "center";
|
||||||
resetPaneDrag();
|
resetPaneDrag();
|
||||||
if (!tabId || !wasOpen) return; // released before this pane "opened" -- ignore
|
if (!tabId || !wasOpen) return;
|
||||||
|
|
||||||
const idx = state.layout.panes.findIndex((p) => p.id === paneId);
|
const idx = state.layout.panes.findIndex((p) => p.id === paneId);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
|
|
@ -255,9 +312,9 @@ function onPaneDrop(e) {
|
||||||
switch (zone) {
|
switch (zone) {
|
||||||
case "left": addPane(tid, idx); break;
|
case "left": addPane(tid, idx); break;
|
||||||
case "right": addPane(tid, idx + 1); break;
|
case "right": addPane(tid, idx + 1); break;
|
||||||
case "top": addPane(tid, idx + 1); break; // in flat layout, left/right suffices
|
case "top": addPane(tid, idx + 1); break;
|
||||||
case "bottom": addPane(tid, idx + 1); break; // top/bottom = same as right
|
case "bottom": addPane(tid, idx + 1); break;
|
||||||
case "center": // replace this pane (but don't add duplicate)
|
case "center":
|
||||||
if (!state.layout.panes.some((p) => p.tabId === tid && p.id !== paneId)) {
|
if (!state.layout.panes.some((p) => p.tabId === tid && p.id !== paneId)) {
|
||||||
state.layout.panes[idx].tabId = tid;
|
state.layout.panes[idx].tabId = tid;
|
||||||
}
|
}
|
||||||
|
|
@ -266,8 +323,6 @@ function onPaneDrop(e) {
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A drag can end (dropped, cancelled, Escaped) outside every pane cell --
|
|
||||||
// clean up hold/zone state unconditionally so it never gets stuck.
|
|
||||||
window.addEventListener("dragend", resetPaneDrag);
|
window.addEventListener("dragend", resetPaneDrag);
|
||||||
|
|
||||||
// ── Header drag (reorder) ───────────────────────────────────────────────
|
// ── Header drag (reorder) ───────────────────────────────────────────────
|
||||||
|
|
@ -292,28 +347,28 @@ function parsedTabId(raw) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPane(tabId, index) {
|
function addPane(tabId, index) {
|
||||||
// If this tab is already in another pane, remove it
|
|
||||||
const existing = state.layout.panes.findIndex((p) => p.tabId === tabId);
|
const existing = state.layout.panes.findIndex((p) => p.tabId === tabId);
|
||||||
if (existing !== -1) {
|
if (existing !== -1) {
|
||||||
state.layout.panes.splice(existing, 1);
|
state.layout.panes.splice(existing, 1);
|
||||||
if (existing < index) index--;
|
if (existing < index) index--;
|
||||||
}
|
}
|
||||||
state.layout.panes.splice(index, 0, { id: `pane-${Date.now()}`, tabId });
|
state.layout.panes.splice(index, 0, { id: `pane-${Date.now()}`, tabId, flex: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePane(paneId) {
|
function closePane(paneId) {
|
||||||
if (state.layout.panes.length <= 1) return;
|
const pane = state.layout.panes.find((p) => p.id === paneId);
|
||||||
|
if (!pane) return;
|
||||||
|
const isLastRemote = pane.tabId !== null && state.layout.panes.filter((p) => p.tabId !== null).length === 1;
|
||||||
state.layout.panes = state.layout.panes.filter((p) => p.id !== paneId);
|
state.layout.panes = state.layout.panes.filter((p) => p.id !== paneId);
|
||||||
|
if (state.layout.panes.length === 0) {
|
||||||
|
state.layout.panes.push({ id: `pane-${Date.now()}`, tabId: null, flex: 1 });
|
||||||
|
}
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API: open a tab in a pane from outside ───────────────────────
|
// ── Public API: open a tab in a pane from outside ───────────────────────
|
||||||
export function openInPane(tabId) {
|
export function openInPane(tabId) {
|
||||||
// If already visible, do nothing (it's already in some pane)
|
|
||||||
if (state.layout.panes.some((p) => p.tabId === tabId)) return;
|
if (state.layout.panes.some((p) => p.tabId === tabId)) return;
|
||||||
|
|
||||||
// Replace the first pane (single-pane UX — click = switch, not split)
|
|
||||||
// Splitting only happens via drag-and-drop into edge zones.
|
|
||||||
state.layout.panes[0].tabId = tabId;
|
state.layout.panes[0].tabId = tabId;
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,13 @@ export async function spawnConnect(params) {
|
||||||
state.activeTabId = connectionId;
|
state.activeTabId = connectionId;
|
||||||
state.lastRemoteTabId = connectionId;
|
state.lastRemoteTabId = connectionId;
|
||||||
state.activePane = "remote";
|
state.activePane = "remote";
|
||||||
// Replace first pane with new connection (click = switch, not split)
|
// Open dual-pane: local (left) + remote (right)
|
||||||
state.layout.panes[0].tabId = connectionId;
|
if (state.layout.panes.length === 1 && state.layout.panes[0].tabId === null) {
|
||||||
|
state.layout.panes.push({ id: `pane-${Date.now()}`, tabId: connectionId, flex: 1 });
|
||||||
|
state.layout.panes[0].flex = 1;
|
||||||
|
} else {
|
||||||
|
state.layout.panes[0].tabId = connectionId;
|
||||||
|
}
|
||||||
state.connectLoading = false;
|
state.connectLoading = false;
|
||||||
state.showConnectDialog = false;
|
state.showConnectDialog = false;
|
||||||
state.statusMessage = `Connected to ${params.host}`;
|
state.statusMessage = `Connected to ${params.host}`;
|
||||||
|
|
|
||||||
|
|
@ -501,7 +501,7 @@ pub async fn new_window(
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
let label_clone = label.clone();
|
let label_clone = label.clone();
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api: _, .. } = event
|
if let tauri::WindowEvent::CloseRequested { .. } = event
|
||||||
&& let Some(win) = app_handle.get_webview_window(&label_clone)
|
&& let Some(win) = app_handle.get_webview_window(&label_clone)
|
||||||
{
|
{
|
||||||
save_window_state_internal(&win, &db);
|
save_window_state_internal(&win, &db);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ pub fn run() {
|
||||||
let handle2 = handle.clone();
|
let handle2 = handle.clone();
|
||||||
let label2 = label.clone();
|
let label2 = label.clone();
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api: _, .. } = event
|
if let tauri::WindowEvent::CloseRequested { .. } = event
|
||||||
&& let Some(win) = handle2.get_webview_window(&label2)
|
&& let Some(win) = handle2.get_webview_window(&label2)
|
||||||
{
|
{
|
||||||
commands::save_window_state_internal(&win, &db2);
|
commands::save_window_state_internal(&win, &db2);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue