diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 21980fd8..324cc50f 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -1016,16 +1016,6 @@ jobs: security find-identity -v -p codesigning $KEYCHAIN_PATH - - name: Build macOS App Store Package - if: steps.check_certs.outputs.has_certs == 'true' - env: - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_OPTIONS: --max-old-space-size=4096 - run: | - BUILD_VERSION="${{ github.run_number }}" - npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" - - name: Check for App Store Connect API credentials id: check_asc_creds run: | @@ -1044,24 +1034,16 @@ jobs: if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' run: gem install fastlane -N - - name: Upload and submit to Mac App Store + - name: Write App Store Connect API key if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' env: APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} run: | - PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1) - if [ -z "$PKG_FILE" ]; then - echo "PKG file not found in release/; aborting." - exit 1 - fi - - VERSION=$(node -p "require('./package.json').version") - - # Write API key JSON that Fastlane deliver expects; the PEM's - # newlines must be preserved as literal \n escapes, not stripped, - # or spaceship fails to parse the key (invalid curve name). + # Write API key JSON that Fastlane expects; the PEM's newlines + # must be preserved as literal \n escapes, not stripped, or + # spaceship fails to parse the key (invalid curve name). mkdir -p /tmp/asc_keys KEY_P8_PATH="/tmp/asc_keys/AuthKey_${APPLE_KEY_ID}.p8" API_KEY_JSON="/tmp/asc_keys/api_key.json" @@ -1080,6 +1062,65 @@ jobs: }, null, 2) + "\n"); ' > "$API_KEY_JSON" + - name: Resolve next build number from App Store Connect + id: build_number + if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + APP_VERSION=$(node -p "require('./package.json').version") + OUT_FILE="$RUNNER_TEMP/latest_build_number.txt" + LANE_DIR="$RUNNER_TEMP/asc_lane/fastlane" + mkdir -p "$LANE_DIR" + + cat > "$LANE_DIR/Fastfile" <&1 || true + + LATEST="" + if [ -f "$OUT_FILE" ]; then + LATEST=$(cat "$OUT_FILE" | tr -d '[:space:]') + fi + + if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then + echo "Could not resolve latest build number from App Store Connect; falling back to run number." + LATEST="${{ github.run_number }}" + fi + echo "build_version=$((LATEST + 1))" >> "$GITHUB_OUTPUT" + + - name: Build macOS App Store Package + if: steps.check_certs.outputs.has_certs == 'true' + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 + run: | + BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}" + npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + + - name: Upload and submit to Mac App Store + if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1) + if [ -z "$PKG_FILE" ]; then + echo "PKG file not found in release/; aborting." + exit 1 + fi + + VERSION=$(node -p "require('./package.json').version") + API_KEY_JSON="/tmp/asc_keys/api_key.json" + fastlane deliver \ --pkg "$PKG_FILE" \ --api_key_path "$API_KEY_JSON" \ diff --git a/src/ui/sidebar/SidebarTree.tsx b/src/ui/sidebar/SidebarTree.tsx index 41db3d23..c2ae9c5e 100644 --- a/src/ui/sidebar/SidebarTree.tsx +++ b/src/ui/sidebar/SidebarTree.tsx @@ -1813,6 +1813,34 @@ export function SidebarTree({ mode: "create" | "edit"; folder?: HostFolder; } | null>(null); + const [compactHostView, setCompactHostView] = useState( + () => localStorage.getItem("compactHostView") === "true", + ); + const [trayOnClick, setTrayOnClick] = useState( + () => localStorage.getItem("hostTrayOnClick") === "true", + ); + + useEffect(() => { + const handler = () => + setCompactHostView(localStorage.getItem("compactHostView") === "true"); + window.addEventListener("storage", handler); + window.addEventListener("compactHostViewChanged", handler); + return () => { + window.removeEventListener("storage", handler); + window.removeEventListener("compactHostViewChanged", handler); + }; + }, []); + + useEffect(() => { + const handler = () => + setTrayOnClick(localStorage.getItem("hostTrayOnClick") === "true"); + window.addEventListener("storage", handler); + window.addEventListener("hostTrayOnClickChanged", handler); + return () => { + window.removeEventListener("storage", handler); + window.removeEventListener("hostTrayOnClickChanged", handler); + }; + }, []); function handleDragHostStart(hostId: string) { // When the dragged host is part of an active selection, move the whole set. @@ -2051,6 +2079,10 @@ export function SidebarTree({ const visibleRows = collectVisibleRows(children, query, openFolders); const parentRef = useRef(null); + const isTouchOnly = + typeof window !== "undefined" && window.matchMedia("(hover: none)").matches; + const clickTrayActive = trayOnClick || isTouchOnly; + const virtualizer = useVirtualizer({ count: visibleRows.length, getScrollElement: () => parentRef.current, @@ -2058,9 +2090,15 @@ export function SidebarTree({ const row = visibleRows[index]; if (!row) return 36; if (isFolder(row.item)) return 36; - // Expanded action tray is taller than a single host row. - if (openTrayHostId === row.item.id) return 88; - return 40; + const isOpen = openTrayHostId === row.item.id; + if (compactHostView) { + // Compact rows are a single line; the tray adds a second wrapped row. + return isOpen ? 88 : 32; + } + // Default rows show name + address (+ optional tags), taller to start with. + // Click-tray mode also keeps the connection buttons visible even when closed. + const base = clickTrayActive ? 76 : 56; + return isOpen ? base + 96 : base; }, overscan: 12, getItemKey: (index) => { @@ -2072,10 +2110,22 @@ export function SidebarTree({ }, }); - // Remeasure when tray open state or tree shape changes (variable row heights). + // Remeasure when the tree shape changes (rows added/removed/reordered), so + // stale cached sizes from before don't leak onto different rows. Tray + // open/close is intentionally excluded — `measureElement`'s ResizeObserver + // already tracks that live via the CSS transition, and force-resetting the + // cache here would snap rows back to the rough estimate mid-animation and + // cause visible jitter. useLayoutEffect(() => { virtualizer.measure(); - }, [virtualizer, openTrayHostId, openFolders, query, visibleRows.length]); + }, [ + virtualizer, + openFolders, + query, + visibleRows.length, + compactHostView, + trayOnClick, + ]); if (loading) { return (