virtualize remotes list

This commit is contained in:
FTCHD
2026-08-20 18:49:50 +03:00
parent b5be41d25a
commit b22449dba9
3 changed files with 77 additions and 31 deletions
+1
View File
@@ -17,6 +17,7 @@
"@tanstack/query-persist-client-core": "^5.101.4", "@tanstack/query-persist-client-core": "^5.101.4",
"@tanstack/query-sync-storage-persister": "^5.101.4", "@tanstack/query-sync-storage-persister": "^5.101.4",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
"@tanstack/react-virtual": "^3.11.3",
"@tauri-apps/api": "~2.11.0", "@tauri-apps/api": "~2.11.0",
"@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2",
+1
View File
@@ -40,6 +40,7 @@
"@tanstack/query-persist-client-core": "^5.101.4", "@tanstack/query-persist-client-core": "^5.101.4",
"@tanstack/query-sync-storage-persister": "^5.101.4", "@tanstack/query-sync-storage-persister": "^5.101.4",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
"@tanstack/react-virtual": "^3.11.3",
"@tauri-apps/api": "~2.11.0", "@tauri-apps/api": "~2.11.0",
"@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2",
+75 -31
View File
@@ -10,6 +10,7 @@ import {
Spinner, Spinner,
} from '@heroui/react' } from '@heroui/react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useVirtualizer } from '@tanstack/react-virtual'
import { ask, message } from '@tauri-apps/plugin-dialog' import { ask, message } from '@tauri-apps/plugin-dialog'
import { platform } from '@tauri-apps/plugin-os' import { platform } from '@tauri-apps/plugin-os'
import { import {
@@ -21,7 +22,7 @@ import {
SettingsIcon, SettingsIcon,
Trash2Icon, Trash2Icon,
} from 'lucide-react' } from 'lucide-react'
import { type ReactNode, startTransition, useEffect, useMemo, useState } from 'react' import { type ReactNode, startTransition, useEffect, useMemo, useRef, useState } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { onErrorDialog } from '../../../lib/errors' import { onErrorDialog } from '../../../lib/errors'
import { formatBytes } from '../../../lib/format' import { formatBytes } from '../../../lib/format'
@@ -64,6 +65,18 @@ export default function RemotesSection() {
[sortedRemotes, searchQuery] [sortedRemotes, searchQuery]
) )
// Virtualize the remotes list: each RemoteCard is a fixed-height (h-20 = 80px) card with a
// gap-2.5 (10px) between rows, so a row slot is 90px. Each card also fires its own queries,
// so windowing keeps a long list from mounting every card (and its request fan-out) at once.
const scrollRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: filteredRemotes.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 90,
overscan: 6,
})
const [pickedRemote, setPickedRemote] = useState<string | null>(null) const [pickedRemote, setPickedRemote] = useState<string | null>(null)
const [editingDrawerOpen, setEditingDrawerOpen] = useState(false) const [editingDrawerOpen, setEditingDrawerOpen] = useState(false)
@@ -201,7 +214,7 @@ export default function RemotesSection() {
{Placeholder} {Placeholder}
{!Placeholder && ( {!Placeholder && (
<div className="flex flex-col gap-2.5 px-4 pb-10"> <div className="flex flex-col gap-2.5 px-4">
{sortedRemotes.length > 5 && ( {sortedRemotes.length > 5 && (
<Input <Input
placeholder="Search remotes..." placeholder="Search remotes..."
@@ -216,36 +229,67 @@ export default function RemotesSection() {
classNames={{ inputWrapper: 'bg-content2/60' }} classNames={{ inputWrapper: 'bg-content2/60' }}
/> />
)} )}
{filteredRemotes.map((remote) => ( <div
<RemoteCard ref={scrollRef}
key={remote} className="overflow-y-auto overscroll-none max-h-[calc(100dvh-14rem)] pb-10"
remote={remote} >
onAutoMountPress={() => { <div
startTransition(() => { style={{
setPickedRemote(remote) height: `${rowVirtualizer.getTotalSize()}px`,
setAutoMountDrawerOpen(true) position: 'relative',
}) width: '100%',
}} }}
onConfigPress={() => { >
startTransition(() => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {
setPickedRemote(remote) const remote = filteredRemotes[virtualRow.index]
setEditingDrawerOpen(true) return (
}) <div
}} key={remote}
onDeletePress={async () => { className="pb-2.5"
const confirmation = await ask( style={{
`Are you sure you want to remove ${remote}? This action cannot be reverted.`, position: 'absolute',
{ title: `Removing ${remote}`, kind: 'warning' } top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<RemoteCard
remote={remote}
onAutoMountPress={() => {
startTransition(() => {
setPickedRemote(remote)
setAutoMountDrawerOpen(true)
})
}}
onConfigPress={() => {
startTransition(() => {
setPickedRemote(remote)
setEditingDrawerOpen(true)
})
}}
onDeletePress={async () => {
const confirmation = await ask(
`Are you sure you want to remove ${remote}? This action cannot be reverted.`,
{
title: `Removing ${remote}`,
kind: 'warning',
}
)
if (!confirmation) {
return
}
deleteRemoteMutation.mutate(remote)
}}
/>
</div>
) )
})}
if (!confirmation) { </div>
return </div>
}
deleteRemoteMutation.mutate(remote)
}}
/>
))}
</div> </div>
)} )}
@@ -337,7 +381,7 @@ function RemoteCard({
key={remote} key={remote}
shadow="sm" shadow="sm"
isBlurred={true} isBlurred={true}
className="h-20 border-[0.5px] dark:border-none border-divider bg-content3/50 dark:bg-content2/90" className="w-full h-20 border-[0.5px] dark:border-none border-divider bg-content3/50 dark:bg-content2/90"
isPressable={true} isPressable={true}
onPress={onConfigPress} onPress={onConfigPress}
> >