@@ -15,46 +15,18 @@ import {
1515 RSwitch ,
1616 RTooltip ,
1717} from " @v2/lib" ;
18- import { useLocalStorage } from " @vueuse/core" ;
1918import type { Emitter } from " mitt" ;
20- import { storeToRefs } from " pinia" ;
2119import { computed , inject , onBeforeUnmount , ref , watch } from " vue" ;
2220import { useI18n } from " vue-i18n" ;
2321import socket from " @/services/socket" ;
24- import storeConfig from " @/stores/config" ;
25- import storeHeartbeat , { type MetadataOption } from " @/stores/heartbeat" ;
2622import { type SimpleRom } from " @/stores/roms" ;
2723import storeScanning from " @/stores/scanning" ;
2824import type { Events } from " @/types/emitter" ;
25+ import { useScanProviders } from " @/v2/composables/useScanProviders" ;
2926import { useSnackbar } from " @/v2/composables/useSnackbar" ;
3027
3128defineOptions ({ inheritAttrs: false });
3229
33- const LOCAL_STORAGE_METADATA_SOURCES_KEY = " scan.metadataSources" ;
34- const LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY =
35- " scan.launchboxRemoteEnabled" ;
36- const LOCAL_STORAGE_HASHEOUS_ENABLED_KEY = " scan.hasheousEnabled" ;
37- const LOCAL_STORAGE_PLAYMATCH_ENABLED_KEY = " scan.playmatchEnabled" ;
38-
39- // Hash-matcher providers — proxies that match files by hash and feed
40- // IDs into the primary catalogs. Filtered out of the main provider
41- // select and rendered as switch pills so users don't read them as
42- // standalone sources.
43- const HASH_MATCHER_KEYS = [" hasheous" , " playmatch" ] as const ;
44-
45- // Provider categorisation — mirrors the Scan view's split so this
46- // dialog reads as a sibling surface, not a parallel vocabulary.
47- const GENERAL_PROVIDER_KEYS = new Set ([
48- " igdb" ,
49- " ss" ,
50- " moby" ,
51- " launchbox" ,
52- " flashpoint" ,
53- " gamelist" ,
54- " libretro" ,
55- ]);
56- const SPECIFIC_PROVIDER_KEYS = new Set ([" ra" , " sgdb" , " hltb" ]);
57-
5830const { t } = useI18n ();
5931const emitter = inject <Emitter <Events >>(" emitter" );
6032const snackbar = useSnackbar ();
@@ -64,138 +36,24 @@ const show = ref(false);
6436// normalise to an array so the scan emit groups by platform without
6537// branching on the input shape.
6638const roms = ref <SimpleRom []>([]);
67- const heartbeat = storeHeartbeat ();
6839const scanningStore = storeScanning ();
69- const configStore = storeConfig ();
70- const { config } = storeToRefs (configStore );
71-
72- const calculateHashes = computed (() => ! config .value .SKIP_HASH_CALCULATION );
73-
74- const metadataOptions = computed (() =>
75- heartbeat
76- .getMetadataOptionsByPriority ()
77- .filter (
78- (option ) =>
79- ! (HASH_MATCHER_KEYS as readonly string []).includes (option .value ),
80- )
81- .map ((option ) => {
82- const requiresHashes = option .value === " ra" ;
83- const hashingDisabled = ! calculateHashes .value ;
84- let disabled = option .disabled ;
85- if (hashingDisabled && requiresHashes ) {
86- disabled = t (" scan.requires-hashes" , { source: option .name });
87- }
88- const name = option .value === " igdb" ? " IGDB" : option .name ;
89- return { ... option , name , disabled };
90- }),
91- );
9240
93- const generalProviders = computed <MetadataOption []>(() =>
94- metadataOptions .value .filter ((o ) => GENERAL_PROVIDER_KEYS .has (o .value )),
95- );
96- const specificProviders = computed <MetadataOption []>(() =>
97- metadataOptions .value .filter ((o ) => SPECIFIC_PROVIDER_KEYS .has (o .value )),
98- );
99-
100- const storedMetadataSources = useLocalStorage (
101- LOCAL_STORAGE_METADATA_SOURCES_KEY ,
102- [] as string [],
103- );
104- const launchboxRemoteEnabled = useLocalStorage (
105- LOCAL_STORAGE_LAUNCHBOX_REMOTE_ENABLED_KEY ,
106- true ,
107- );
108- const hasheousEnabled = useLocalStorage (
109- LOCAL_STORAGE_HASHEOUS_ENABLED_KEY ,
110- true ,
111- );
112- const playmatchEnabled = useLocalStorage (
113- LOCAL_STORAGE_PLAYMATCH_ENABLED_KEY ,
114- true ,
115- );
116-
117- const metadataSources = ref <MetadataOption []>([]);
118- const isLaunchboxSelected = computed (() =>
119- metadataSources .value .some ((s ) => s .value === " launchbox" ),
120- );
121-
122- watch (
123- [metadataOptions , storedMetadataSources ],
124- ([newOptions , newStoredMetadataSources ]) => {
125- const filteredMetadataSources = newOptions .filter (
126- (option ) =>
127- newStoredMetadataSources .includes (option .value ) && ! option .disabled ,
128- );
129- metadataSources .value =
130- filteredMetadataSources .length > 0
131- ? filteredMetadataSources
132- : heartbeat
133- .getEnabledMetadataOptions ()
134- .filter (
135- (o ) =>
136- ! (HASH_MATCHER_KEYS as readonly string []).includes (o .value ),
137- );
138- },
139- { immediate: true },
140- );
141-
142- interface HashMatcher {
143- value: " hasheous" | " playmatch" ;
144- name: string ;
145- logo: string ;
146- /** Reason the switch is forced off — surfaced in the hover tooltip.
147- * null when the switch is interactable. */
148- blockedReason: string | null ;
149- switchEnabled: boolean ;
150- }
151-
152- const hashMatchers = computed <HashMatcher []>(() => {
153- const sources = heartbeat .value .METADATA_SOURCES ;
154- const igdbSelected = metadataSources .value .some ((s ) => s .value === " igdb" );
155- const noHashes = ! calculateHashes .value ;
156-
157- const hasheousAdmin = Boolean (sources ?.HASHEOUS_API_ENABLED );
158- const playmatchAdmin = Boolean (sources ?.PLAYMATCH_API_ENABLED );
159-
160- return [
161- {
162- value: " hasheous" ,
163- name: " Hasheous" ,
164- logo: " /assets/scrappers/hasheous.png" ,
165- blockedReason: ! hasheousAdmin
166- ? t (" scan.disabled-by-admin" )
167- : noHashes
168- ? t (" scan.requires-hashes" , { source: " Hasheous" })
169- : null ,
170- switchEnabled: hasheousAdmin && ! noHashes ,
171- },
172- {
173- value: " playmatch" ,
174- name: " Playmatch" ,
175- logo: " /assets/scrappers/playmatch.png" ,
176- blockedReason: ! playmatchAdmin
177- ? t (" scan.disabled-by-admin" )
178- : noHashes
179- ? t (" scan.requires-hashes" , { source: " Playmatch" })
180- : ! igdbSelected
181- ? t (" scan.playmatch-requires-igdb" )
182- : null ,
183- switchEnabled: playmatchAdmin && ! noHashes && igdbSelected ,
184- },
185- ];
186- });
187-
188- function setHashMatcher(value : HashMatcher [" value" ], next : boolean ) {
189- if (value === " hasheous" ) hasheousEnabled .value = next ;
190- else playmatchEnabled .value = next ;
191- }
192-
193- function isHashMatcherOn(matcher : HashMatcher ): boolean {
194- if (! matcher .switchEnabled ) return false ;
195- return matcher .value === " hasheous"
196- ? hasheousEnabled .value
197- : playmatchEnabled .value ;
198- }
41+ const {
42+ calculateHashes,
43+ generalProviders,
44+ specificProviders,
45+ metadataSources,
46+ effectiveMetadataSources,
47+ generalAllSelected,
48+ specificAllSelected,
49+ isLaunchboxSelected,
50+ launchboxRemoteEnabled,
51+ hashMatchers,
52+ setHashMatcher,
53+ isHashMatcherOn,
54+ buildScanPayload,
55+ persistSelection,
56+ } = useScanProviders ();
19957
20058// Per-ROM scan types — the Scan view's "new platforms" / "quick"
20159// options don't apply to an already-ingested ROM. We keep `update`
@@ -280,7 +138,7 @@ function onScan() {
280138 if (roms .value .length === 0 ) return ;
281139
282140 scanningStore .setScanning (true );
283- storedMetadataSources . value = metadataSources . value . map (( s ) => s . value );
141+ persistSelection ( );
284142
285143 // Group rom ids by platform — the scan socket event accepts one
286144 // platform list + one rom-id list, so a selection that spans
@@ -305,31 +163,13 @@ function onScan() {
305163
306164 if (! socket .connected ) socket .connect ();
307165
308- // Build the apis payload — providers + hasheous (when its switch is
309- // on; the backend accepts it as a MetadataSource enum value).
310- // Playmatch has no enum entry; the backend gates it via the separate
311- // `playmatch_enabled` flag below.
312- const apis = metadataSources .value .map ((s ) => s .value );
313- const hasheousMatcher = hashMatchers .value .find (
314- (m ) => m .value === " hasheous" ,
315- );
316- if (hasheousMatcher && isHashMatcherOn (hasheousMatcher )) {
317- apis .push (" hasheous" );
318- }
319- const playmatchMatcher = hashMatchers .value .find (
320- (m ) => m .value === " playmatch" ,
321- );
322-
166+ const payload = buildScanPayload ();
323167 for (const [platformId, romIds] of byPlatform ) {
324168 socket .emit (" scan" , {
325169 platforms: [platformId ],
326170 roms_ids: romIds ,
327171 type: scanType .value ,
328- apis ,
329- launchbox_remote_enabled: launchboxRemoteEnabled .value ,
330- playmatch_enabled: playmatchMatcher
331- ? isHashMatcherOn (playmatchMatcher )
332- : false ,
172+ ... payload ,
333173 });
334174 }
335175
@@ -413,6 +253,7 @@ function closeDialog() {
413253 chips
414254 chip-tone="plain"
415255 show-all-option
256+ @update :all-selected =" generalAllSelected = $event "
416257 >
417258 <template #chip =" { item } " >
418259 <RTooltip :text =" item .raw .name " location="bottom">
@@ -503,6 +344,7 @@ function closeDialog() {
503344 chips
504345 chip-tone="plain"
505346 show-all-option
347+ @update :all-selected =" specificAllSelected = $event "
506348 >
507349 <template #chip =" { item } " >
508350 <RTooltip :text =" item .raw .name " location="bottom">
@@ -640,7 +482,7 @@ function closeDialog() {
640482 variant="translucent"
641483 color="primary"
642484 prepend-icon="mdi-magnify-scan"
643- :disabled =" metadataSources .length === 0 "
485+ :disabled =" effectiveMetadataSources .length === 0 "
644486 @click =" onScan "
645487 >
646488 {{ t("rom.refresh-metadata") }}
0 commit comments