diff --git a/src/lib/components/party/Party.svelte b/src/lib/components/party/Party.svelte
index 1b90f745..957dcab0 100644
--- a/src/lib/components/party/Party.svelte
+++ b/src/lib/components/party/Party.svelte
@@ -20,7 +20,8 @@
useUpdateSummonUncap,
useSwapWeapons,
useSwapCharacters,
- useSwapSummons
+ useSwapSummons,
+ useSyncAllPartyItems
} from '$lib/api/mutations/grid.mutations'
// TanStack Query mutations - Party
@@ -132,6 +133,7 @@
const swapWeapons = useSwapWeapons()
const swapCharacters = useSwapCharacters()
const swapSummons = useSwapSummons()
+ const syncAllItems = useSyncAllPartyItems()
// TanStack Query mutations - Party
const updatePartyMutation = useUpdateParty()
@@ -269,6 +271,17 @@
const mainWeaponElement = $derived(mainWeapon?.element ?? mainWeapon?.weapon?.element)
const partyElement = $derived((party as any)?.element)
+ // Check if any items in the party are linked to collection (for sync menu option)
+ const hasCollectionLinks = $derived.by(() => {
+ const hasLinkedWeapons = (party?.weapons ?? []).some((w) => w?.collectionWeaponId)
+ const hasLinkedCharacters = (party?.characters ?? []).some((c) => c?.collectionCharacterId)
+ const hasLinkedSummons = (party?.summons ?? []).some((s) => s?.collectionSummonId)
+ return hasLinkedWeapons || hasLinkedCharacters || hasLinkedSummons
+ })
+
+ // Check if syncing is in progress
+ const isSyncingAll = $derived(syncAllItems.isPending)
+
function handleTabChange(tab: GridType) {
activeTab = tab // Instant UI update
@@ -368,6 +381,25 @@
}
}
+ async function syncFromCollection() {
+ if (!canEdit() || !hasCollectionLinks) return
+
+ loading = true
+ error = null
+
+ try {
+ await syncAllItems.mutateAsync({
+ partyId: party.id,
+ partyShortcode: party.shortcode
+ })
+ // Party will be updated via cache invalidation
+ } catch (err: any) {
+ error = err.message || 'Failed to sync from collection'
+ } finally {
+ loading = false
+ }
+ }
+
let deleteDialogOpen = $state(false)
let deleting = $state(false)
@@ -846,6 +878,14 @@
+ {#if hasCollectionLinks}
+
+
+
+ {/if}
+
{/if}
{#if authUserId}
diff --git a/src/lib/components/sidebar/SearchContent.svelte b/src/lib/components/sidebar/SearchContent.svelte
index 73ed22b5..8303aec1 100644
--- a/src/lib/components/sidebar/SearchContent.svelte
+++ b/src/lib/components/sidebar/SearchContent.svelte
@@ -151,7 +151,8 @@
}
})
- // Collection query - only enabled when in collection mode and authUserId is provided
+ // Collection query - enabled when authUserId is provided
+ // Used both for collection mode AND for highlighting owned items in "all" mode
// Type assertion needed because different types have different query result types
// but they all share the same structure with different content types
const collectionQueryResult = createInfiniteQuery(() => {
@@ -163,26 +164,27 @@
} as ReturnType
}
- const currentFilters = {
+ // For collection mode, apply filters; for "all" mode, fetch all to build owned set
+ const currentFilters = searchMode === 'collection' ? {
element: elementFilters.length > 0 ? elementFilters : undefined,
rarity: rarityFilters.length > 0 ? rarityFilters : undefined
- }
+ } : {}
switch (type) {
case 'weapon':
return {
...collectionQueries.weapons(authUserId, currentFilters),
- enabled: searchMode === 'collection'
+ enabled: true // Always enabled when authUserId exists
} as unknown as ReturnType
case 'character':
return {
...collectionQueries.characters(authUserId, currentFilters),
- enabled: searchMode === 'collection'
+ enabled: true
}
case 'summon':
return {
...collectionQueries.summons(authUserId, currentFilters),
- enabled: searchMode === 'collection'
+ enabled: true
} as unknown as ReturnType
}
})
@@ -199,6 +201,25 @@
return filterCollectionByQuery(allItems, debouncedSearchQuery)
})
+ // Set of owned item granblue IDs for fast lookup (used in "all" mode to highlight owned items)
+ const ownedItemIds = $derived.by(() => {
+ const pages = collectionQueryResult.data?.pages ?? []
+ const allItems = pages.flatMap((page) => page.results)
+ const ids = new Set()
+ for (const item of allItems) {
+ const entity = 'character' in item ? item.character : 'weapon' in item ? item.weapon : item.summon
+ if (entity.granblueId) {
+ ids.add(String(entity.granblueId))
+ }
+ }
+ return ids
+ })
+
+ // Helper to check if an item is owned (in user's collection)
+ function isOwned(item: AddItemResult): boolean {
+ return ownedItemIds.has(String(item.granblueId))
+ }
+
// Deduplicate by id - needed because the API may return the same item across pages
// (e.g., due to items being added/removed between page fetches)
const searchResults = $derived.by(() => {
@@ -404,11 +425,13 @@
{:else if searchResults.length > 0}
{#each searchResults as item (item.id)}
+ {@const owned = searchMode === 'all' && authUserId && isOwned(item)}
-