mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
Merge pull request #1 from nekochanfood/feat/improve-duplicate-repo-handling
Feat/improve duplicate repo handling
This commit is contained in:
commit
a0ec779fab
6 changed files with 178 additions and 67 deletions
|
|
@ -8,7 +8,8 @@ The format is based on [Keep a Changelog].
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Add `reorder_user_repos` to reorder `userRepos` in `settings.json`
|
||||
- Add `reorder_user_repos_by_indices` to reorder `userRepos` in `settings.json`
|
||||
- Add `remove_repo_at_index` to remove a specific entry from `userRepos` in `settings.json`
|
||||
|
||||
### Changed
|
||||
- Improved saving interacting with setting files `#2485` `#2710`
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
DragOverlay,
|
||||
type DragStartEvent,
|
||||
defaultDropAnimation,
|
||||
defaultDropAnimationSideEffects,
|
||||
type Modifier,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
|
|
@ -69,6 +70,8 @@ export const Route = createFileRoute("/_main/packages/repositories/")({
|
|||
component: Page,
|
||||
});
|
||||
|
||||
type UserRepoWithListId = TauriUserRepository & { listId: string };
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Suspense>
|
||||
|
|
@ -84,6 +87,15 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({
|
|||
|
||||
const DRAG_OVERLAY_MODIFIERS = [restrictToVerticalAxis];
|
||||
|
||||
const customDropAnimation: typeof defaultDropAnimation = {
|
||||
...defaultDropAnimation,
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: { opacity: "0" },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const TABLE_HEAD = [
|
||||
"", // checkbox
|
||||
"general:name",
|
||||
|
|
@ -149,6 +161,18 @@ function useDragAutoScroll(
|
|||
}, [isActive, viewportRef]);
|
||||
}
|
||||
|
||||
function computeSlotKey(repo: TauriUserRepository, used: Set<string>): string {
|
||||
const base = `${repo.id} ${repo.url ?? ""}`;
|
||||
let key = base;
|
||||
let counter = 0;
|
||||
while (used.has(key)) {
|
||||
counter++;
|
||||
key = `${base} ${counter}`;
|
||||
}
|
||||
used.add(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function PageBody() {
|
||||
const result = useQuery(environmentRepositoriesInfo);
|
||||
|
||||
|
|
@ -198,19 +222,48 @@ function PageBody() {
|
|||
|
||||
const userRepos = result.data?.user_repositories;
|
||||
|
||||
const [orderedIds, setOrderedIds] = useState<string[]>(
|
||||
() => userRepos?.map((r) => r.id) ?? [],
|
||||
const listIdMapRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
const augmentedUserRepos = useMemo<UserRepoWithListId[]>(() => {
|
||||
if (!userRepos) {
|
||||
listIdMapRef.current = new Map();
|
||||
return [];
|
||||
}
|
||||
const prev = listIdMapRef.current;
|
||||
const next = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
const result: UserRepoWithListId[] = [];
|
||||
|
||||
for (const r of userRepos) {
|
||||
const key = computeSlotKey(r, usedKeys);
|
||||
const listId = prev.get(key) ?? crypto.randomUUID();
|
||||
next.set(key, listId);
|
||||
result.push({ ...r, listId });
|
||||
}
|
||||
|
||||
listIdMapRef.current = next;
|
||||
return result;
|
||||
}, [userRepos]);
|
||||
|
||||
const [orderedListIds, setOrderedListIds] = useState<string[]>(() =>
|
||||
augmentedUserRepos.map((r) => r.listId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOrderedIds(userRepos?.map((r) => r.id) ?? []);
|
||||
}, [userRepos]);
|
||||
setOrderedListIds(augmentedUserRepos.map((r) => r.listId));
|
||||
}, [augmentedUserRepos]);
|
||||
|
||||
const userRepoMap = useMemo(
|
||||
() => new Map((userRepos ?? []).map((r) => [r.id, r])),
|
||||
[userRepos],
|
||||
const userRepoByListId = useMemo(
|
||||
() => new Map(augmentedUserRepos.map((r) => [r.listId, r])),
|
||||
[augmentedUserRepos],
|
||||
);
|
||||
|
||||
const userRepoByListIdRef =
|
||||
useRef<Map<string, UserRepoWithListId>>(userRepoByListId);
|
||||
useEffect(() => {
|
||||
userRepoByListIdRef.current = userRepoByListId;
|
||||
}, [userRepoByListId]);
|
||||
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
const [columnWidths, setColumnWidths] = useState<number[]>([]);
|
||||
|
|
@ -219,24 +272,47 @@ function PageBody() {
|
|||
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
const orderedIdsSet = useMemo(() => new Set(orderedIds), [orderedIds]);
|
||||
const orderedListIdsSet = useMemo(
|
||||
() => new Set(orderedListIds),
|
||||
[orderedListIds],
|
||||
);
|
||||
|
||||
const collisionDetection = useCallback<CollisionDetection>(
|
||||
(args) =>
|
||||
closestCenter({
|
||||
...args,
|
||||
droppableContainers: args.droppableContainers.filter((c) =>
|
||||
orderedIdsSet.has(c.id as string),
|
||||
orderedListIdsSet.has(c.id as string),
|
||||
),
|
||||
}),
|
||||
[orderedIdsSet],
|
||||
[orderedListIdsSet],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const reorderMutation = useMutation({
|
||||
mutationFn: (ids: string[]) => commands.environmentReorderRepositories(ids),
|
||||
mutationFn: (listIds: string[]) => {
|
||||
const indices = listIds
|
||||
.map((lid) => userRepoByListId.get(lid)?.index)
|
||||
.filter((i): i is number => i !== undefined);
|
||||
return commands.environmentReorderRepositories(indices);
|
||||
},
|
||||
// Pin listIds to the new positions so duplicate-keyed rows don't swap their listIds on refetch.
|
||||
onMutate: (newListIds: string[]) => {
|
||||
const prevMap = new Map(listIdMapRef.current);
|
||||
const rebuilt = new Map<string, string>();
|
||||
const usedKeys = new Set<string>();
|
||||
for (const lid of newListIds) {
|
||||
const repo = userRepoByListIdRef.current.get(lid);
|
||||
if (!repo) continue;
|
||||
const key = computeSlotKey(repo, usedKeys);
|
||||
rebuilt.set(key, lid);
|
||||
}
|
||||
listIdMapRef.current = rebuilt;
|
||||
return { prevMap };
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
onError: (e) => {
|
||||
onError: (e, _newListIds, ctx) => {
|
||||
if (ctx?.prevMap) listIdMapRef.current = ctx.prevMap;
|
||||
toastThrownError(e);
|
||||
},
|
||||
});
|
||||
|
|
@ -288,8 +364,8 @@ function PageBody() {
|
|||
const activeVisualIndex = useMemo(() => {
|
||||
if (!activeId) return 0;
|
||||
const effectiveId = overId ?? activeId;
|
||||
return orderedIds.indexOf(effectiveId) + 2; // +2 for the 2 fixed rows
|
||||
}, [activeId, overId, orderedIds]);
|
||||
return orderedListIds.indexOf(effectiveId) + 2; // +2 for the 2 fixed rows
|
||||
}, [activeId, overId, orderedListIds]);
|
||||
|
||||
function handleDragStart(event: DragStartEvent) {
|
||||
setActiveId(event.active.id as string);
|
||||
|
|
@ -311,11 +387,11 @@ function PageBody() {
|
|||
setOverId(null);
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = orderedIds.indexOf(active.id as string);
|
||||
const newIndex = orderedIds.indexOf(over.id as string);
|
||||
const newIds = arrayMove(orderedIds, oldIndex, newIndex);
|
||||
setOrderedIds(newIds);
|
||||
reorderMutation.mutate(newIds);
|
||||
const oldIndex = orderedListIds.indexOf(active.id as string);
|
||||
const newIndex = orderedListIds.indexOf(over.id as string);
|
||||
const newListIds = arrayMove(orderedListIds, oldIndex, newIndex);
|
||||
setOrderedListIds(newListIds);
|
||||
reorderMutation.mutate(newListIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,8 +461,8 @@ function PageBody() {
|
|||
viewportRef={scrollViewportRef}
|
||||
>
|
||||
<RepositoryTableBody
|
||||
orderedIds={orderedIds}
|
||||
userRepoMap={userRepoMap}
|
||||
orderedListIds={orderedListIds}
|
||||
userRepoByListId={userRepoByListId}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
theadRowRef={theadRowRef}
|
||||
guiAnimation={guiAnimation}
|
||||
|
|
@ -400,12 +476,14 @@ function PageBody() {
|
|||
</VStack>
|
||||
<DragOverlay
|
||||
modifiers={DRAG_OVERLAY_MODIFIERS}
|
||||
dropAnimation={guiAnimation ? defaultDropAnimation : null}
|
||||
dropAnimation={guiAnimation ? customDropAnimation : null}
|
||||
>
|
||||
{activeId ? (
|
||||
<RepositoryDragOverlay
|
||||
repo={userRepoMap.get(activeId)}
|
||||
selected={!hiddenUserRepos.has(activeId)}
|
||||
repo={userRepoByListId.get(activeId)}
|
||||
selected={
|
||||
!hiddenUserRepos.has(userRepoByListId.get(activeId)?.id ?? "")
|
||||
}
|
||||
columnWidths={columnWidths}
|
||||
visualIndex={activeVisualIndex}
|
||||
guiAnimation={guiAnimation}
|
||||
|
|
@ -417,16 +495,16 @@ function PageBody() {
|
|||
}
|
||||
|
||||
function RepositoryTableBody({
|
||||
orderedIds,
|
||||
userRepoMap,
|
||||
orderedListIds,
|
||||
userRepoByListId,
|
||||
hiddenUserRepos,
|
||||
theadRowRef,
|
||||
guiAnimation,
|
||||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
orderedIds: string[];
|
||||
userRepoMap: Map<string, TauriUserRepository>;
|
||||
orderedListIds: string[];
|
||||
userRepoByListId: Map<string, UserRepoWithListId>;
|
||||
hiddenUserRepos: Set<string>;
|
||||
theadRowRef: React.RefObject<HTMLTableRowElement | null>;
|
||||
guiAnimation: boolean;
|
||||
|
|
@ -475,16 +553,18 @@ function RepositoryTableBody({
|
|||
isDragActive={isDragActive}
|
||||
/>
|
||||
<SortableContext
|
||||
items={orderedIds}
|
||||
items={orderedListIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{orderedIds.map((id, index) => {
|
||||
const repo = userRepoMap.get(id);
|
||||
{orderedListIds.map((listId, index) => {
|
||||
const repo = userRepoByListId.get(listId);
|
||||
if (!repo) return null;
|
||||
return (
|
||||
<RepositoryRow
|
||||
key={repo.id}
|
||||
key={listId}
|
||||
listId={listId}
|
||||
repoId={repo.id}
|
||||
repoIndex={repo.index}
|
||||
displayName={repo.display_name}
|
||||
url={repo.url}
|
||||
hiddenUserRepos={hiddenUserRepos}
|
||||
|
|
@ -602,7 +682,9 @@ function RepositoryRowCells({
|
|||
}
|
||||
|
||||
function RepositoryRow({
|
||||
listId,
|
||||
repoId,
|
||||
repoIndex,
|
||||
displayName,
|
||||
url,
|
||||
hiddenUserRepos,
|
||||
|
|
@ -613,7 +695,9 @@ function RepositoryRow({
|
|||
onToggleVisibility,
|
||||
isDragActive,
|
||||
}: {
|
||||
listId?: string;
|
||||
repoId: TauriUserRepository["id"];
|
||||
repoIndex?: number;
|
||||
displayName: TauriUserRepository["display_name"];
|
||||
url: TauriUserRepository["url"];
|
||||
hiddenUserRepos: Set<string>;
|
||||
|
|
@ -633,7 +717,7 @@ function RepositoryRow({
|
|||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: repoId, disabled: !canRemove });
|
||||
} = useSortable({ id: listId ?? repoId, disabled: !canRemove });
|
||||
|
||||
const visualIndex = useMemo(() => {
|
||||
if (isDragging) return rowIndex;
|
||||
|
|
@ -675,7 +759,7 @@ function RepositoryRow({
|
|||
onRemove={() =>
|
||||
void openSingleDialog(RemoveRepositoryDialog, {
|
||||
displayName,
|
||||
id: repoId,
|
||||
index: repoIndex ?? 0,
|
||||
})
|
||||
}
|
||||
dragListeners={listeners}
|
||||
|
|
@ -739,18 +823,18 @@ function RepositoryDragOverlay({
|
|||
function RemoveRepositoryDialog({
|
||||
dialog,
|
||||
displayName,
|
||||
id,
|
||||
index,
|
||||
}: {
|
||||
dialog: DialogContext<void>;
|
||||
displayName: string;
|
||||
id: string;
|
||||
index: number;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const removeRepository = useMutation({
|
||||
mutationFn: async (id: string) =>
|
||||
await commands.environmentRemoveRepository(id),
|
||||
onMutate: async (id) => {
|
||||
mutationFn: async (index: number) =>
|
||||
await commands.environmentRemoveRepository(index),
|
||||
onMutate: async (index) => {
|
||||
await queryClient.cancelQueries(environmentRepositoriesInfo);
|
||||
const data = queryClient.getQueryData(
|
||||
environmentRepositoriesInfo.queryKey,
|
||||
|
|
@ -758,10 +842,18 @@ function RemoveRepositoryDialog({
|
|||
if (data !== undefined) {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, {
|
||||
...data,
|
||||
user_repositories: data.user_repositories.filter((x) => x.id !== id),
|
||||
user_repositories: data.user_repositories.filter(
|
||||
(x) => x.index !== index,
|
||||
),
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
onError: (e, _index, ctx) => {
|
||||
queryClient.setQueryData(environmentRepositoriesInfo.queryKey, ctx);
|
||||
toastThrownError(e);
|
||||
},
|
||||
onSettled: () => queryClient.invalidateQueries(environmentRepositoriesInfo),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -781,7 +873,7 @@ function RemoveRepositoryDialog({
|
|||
<Button
|
||||
onClick={() => {
|
||||
dialog.close();
|
||||
removeRepository.mutate(id);
|
||||
removeRepository.mutate(index);
|
||||
}}
|
||||
className={"ml-2"}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export const commands = {
|
|||
environmentSetHideLocalUserPackages: (value: boolean) => __TAURI_INVOKE<null>("environment_set_hide_local_user_packages", { value }),
|
||||
environmentDownloadRepository: (url: string, headers: { [key in string]: string }) => __TAURI_INVOKE<TauriDownloadRepository>("environment_download_repository", { url, headers }),
|
||||
environmentAddRepository: (url: string, headers: { [key in string]: string }) => __TAURI_INVOKE<TauriAddRepositoryResult>("environment_add_repository", { url, headers }),
|
||||
environmentRemoveRepository: (id: string) => __TAURI_INVOKE<null>("environment_remove_repository", { id }),
|
||||
environmentReorderRepositories: (ids: string[]) => __TAURI_INVOKE<null>("environment_reorder_repositories", { ids }),
|
||||
environmentRemoveRepository: (index: number) => __TAURI_INVOKE<null>("environment_remove_repository", { index }),
|
||||
environmentReorderRepositories: (indices: number[]) => __TAURI_INVOKE<null>("environment_reorder_repositories", { indices }),
|
||||
environmentImportRepositoryPick: () => __TAURI_INVOKE<TauriImportRepositoryPickResult>("environment_import_repository_pick"),
|
||||
environmentImportDownloadRepositories: (channel: string, repositories: TauriRepositoryDescriptor[]) => __TAURI_INVOKE<AsyncCallResult<number, ([TauriRepositoryDescriptor, TauriDownloadRepository])[]>>("environment_import_download_repositories", { channel, repositories }),
|
||||
environmentImportAddRepositories: (repositories: TauriRepositoryDescriptor[]) => __TAURI_INVOKE<null>("environment_import_add_repositories", { repositories }),
|
||||
|
|
@ -405,6 +405,7 @@ export type TauriUserPackage = {
|
|||
};
|
||||
|
||||
export type TauriUserRepository = {
|
||||
index: number,
|
||||
id: string,
|
||||
url: string | null,
|
||||
display_name: string,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::commands::async_command::{AsyncCallResult, With, async_command};
|
||||
use crate::commands::prelude::*;
|
||||
use futures::future::{join_all, try_join_all};
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use log::info;
|
||||
|
|
@ -58,6 +58,7 @@ pub async fn environment_packages(
|
|||
|
||||
#[derive(Serialize, specta::Type)]
|
||||
struct TauriUserRepository {
|
||||
index: usize,
|
||||
id: String,
|
||||
url: Option<String>,
|
||||
display_name: String,
|
||||
|
|
@ -87,9 +88,11 @@ pub async fn environment_repositories_info(
|
|||
let user_repositories = settings
|
||||
.get_user_repos()
|
||||
.iter()
|
||||
.map(|x| {
|
||||
.enumerate()
|
||||
.map(|(index, x)| {
|
||||
let id = x.id().or(x.url().map(Url::as_str)).unwrap();
|
||||
TauriUserRepository {
|
||||
index,
|
||||
id: id.to_string(),
|
||||
url: x.url().map(|x| x.to_string()),
|
||||
display_name: x.name().unwrap_or(id).to_string(),
|
||||
|
|
@ -351,18 +354,15 @@ pub async fn environment_remove_repository(
|
|||
settings: State<'_, SettingsState>,
|
||||
packages: State<'_, PackagesState>,
|
||||
io: State<'_, DefaultEnvironmentIo>,
|
||||
id: String,
|
||||
index: usize,
|
||||
) -> Result<(), RustError> {
|
||||
let mut settings = settings.load_mut(io.inner()).await?;
|
||||
|
||||
let removed = settings.remove_repo(|r| r.id() == Some(id.as_str()));
|
||||
let removed = settings.remove_repo_at_index(index);
|
||||
|
||||
join_all(
|
||||
removed
|
||||
.iter()
|
||||
.map(|x| async { io.remove_file(x.local_path()).await.ok() }),
|
||||
)
|
||||
.await;
|
||||
if let Some(repo) = &removed {
|
||||
io.remove_file(repo.local_path()).await.ok();
|
||||
}
|
||||
|
||||
settings.save().await?;
|
||||
|
||||
|
|
@ -396,12 +396,11 @@ pub async fn environment_reorder_repositories(
|
|||
settings: State<'_, SettingsState>,
|
||||
packages: State<'_, PackagesState>,
|
||||
io: State<'_, DefaultEnvironmentIo>,
|
||||
ids: Vec<String>,
|
||||
indices: Vec<usize>,
|
||||
) -> Result<(), RustError> {
|
||||
let mut settings = settings.load_mut(io.inner()).await?;
|
||||
let ids_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
|
||||
log::info!("reorder user repositories: [{}]", ids.join(", "));
|
||||
settings.reorder_user_repos(&ids_refs);
|
||||
log::debug!("reorder user repositories by indices: {:?}", indices);
|
||||
settings.reorder_user_repos_by_indices(&indices);
|
||||
settings.save().await?;
|
||||
packages.clear_cache();
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -269,8 +269,12 @@ impl Settings {
|
|||
self.vpm.retain_user_repos(|x| !condition(x))
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos(&mut self, ids: &[&str]) {
|
||||
self.vpm.reorder_user_repos(ids);
|
||||
pub fn remove_repo_at_index(&mut self, index: usize) -> Option<UserRepoSetting> {
|
||||
self.vpm.remove_user_repo_at_index(index)
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos_by_indices(&mut self, indices: &[usize]) {
|
||||
self.vpm.reorder_user_repos_by_indices(indices);
|
||||
}
|
||||
|
||||
// auto configurations
|
||||
|
|
|
|||
|
|
@ -155,15 +155,29 @@ impl VpmSettings {
|
|||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos(&mut self, ids: &[&str]) {
|
||||
let mut repos = std::mem::take(&mut self.parsed.user_repos);
|
||||
let mut result = Vec::with_capacity(repos.len());
|
||||
for id in ids {
|
||||
if let Some(pos) = repos.iter().position(|r| r.id() == Some(id)) {
|
||||
result.push(repos.remove(pos));
|
||||
pub fn remove_user_repo_at_index(&mut self, index: usize) -> Option<UserRepoSetting> {
|
||||
let repos = &mut self.parsed.user_repos;
|
||||
if index < repos.len() {
|
||||
Some(repos.remove(index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reorder_user_repos_by_indices(&mut self, indices: &[usize]) {
|
||||
let mut pool: Vec<Option<UserRepoSetting>> = std::mem::take(&mut self.parsed.user_repos)
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.collect();
|
||||
let mut result = Vec::with_capacity(pool.len());
|
||||
for &idx in indices {
|
||||
if let Some(slot) = pool.get_mut(idx)
|
||||
&& let Some(repo) = slot.take()
|
||||
{
|
||||
result.push(repo);
|
||||
}
|
||||
}
|
||||
result.append(&mut repos);
|
||||
result.extend(pool.into_iter().flatten());
|
||||
self.parsed.user_repos = result;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue