port desktop's search engine

This commit is contained in:
Boof2015
2026-06-28 09:46:08 -04:00
parent 3ec3f5ecf2
commit dcdddb350d
19 changed files with 2944 additions and 223 deletions
+335
View File
@@ -0,0 +1,335 @@
// Last.fm settings store — ported from desktop
// `src/renderer/stores/lastFmSettingsStore.ts`. The browser-auth polling loop is
// kept verbatim; the only change is that `window.electronAPI.lastFm.*` IPC calls
// become direct calls into the in-process scrobble service singleton.
import { create } from 'zustand';
import type {
LastFmAuthFinishResult,
LastFmAuthStartResult,
LastFmCustomProfileInput,
LastFmStatus,
} from '@/types/lastFm';
import {
getLastFmService,
initLastFmService,
setLastFmStatusListener,
} from '@/services/lastfm';
const LASTFM_AUTH_POLL_INTERVAL_MS = 2_000;
const LASTFM_AUTH_POLL_TIMEOUT_MS = 2 * 60 * 1_000;
interface LastFmSettingsStore {
status: LastFmStatus | null;
isLoading: boolean;
isInitialized: boolean;
isAuthorizing: boolean;
errorMessage: string;
authHint: string;
init: () => Promise<void>;
refresh: () => Promise<void>;
setEnabled: (enabled: boolean) => Promise<LastFmStatus | null>;
createCustomProfile: (input: LastFmCustomProfileInput) => Promise<LastFmStatus | null>;
updateCustomProfile: (
profileId: string,
input: LastFmCustomProfileInput
) => Promise<LastFmStatus | null>;
deleteCustomProfile: (profileId: string) => Promise<LastFmStatus | null>;
setProfileEnabled: (profileId: string, enabled: boolean) => Promise<LastFmStatus | null>;
beginAuth: (profileId: string) => Promise<LastFmAuthStartResult | null>;
finishAuth: () => Promise<LastFmAuthFinishResult | null>;
disconnectProfile: (profileId: string) => Promise<LastFmStatus | null>;
resetToDefaults: () => Promise<LastFmStatus | null>;
}
let statusSubscribed = false;
let authPollTimer: ReturnType<typeof setTimeout> | null = null;
let authPollInFlight = false;
let authPollDeadlineMs = 0;
function toErrorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return 'Failed to update Last.fm settings.';
}
function buildDefaultAuthHint(status: LastFmStatus): string {
if (!status.authPending) return '';
return 'Approve Astra in your browser tab. Connection will complete automatically.';
}
export const useLastFmSettingsStore = create<LastFmSettingsStore>((set, get) => {
const stopAuthPolling = (): void => {
if (authPollTimer) {
clearTimeout(authPollTimer);
authPollTimer = null;
}
authPollInFlight = false;
authPollDeadlineMs = 0;
if (get().isAuthorizing) {
set({ isAuthorizing: false });
}
};
const scheduleAuthPoll = (delayMs: number): void => {
if (authPollTimer) {
clearTimeout(authPollTimer);
}
authPollTimer = setTimeout(() => {
authPollTimer = null;
void pollAuthCompletion();
}, Math.max(0, delayMs));
};
const applyStatus = (status: LastFmStatus): LastFmStatus => {
if (!status.authPending) {
stopAuthPolling();
}
set({
status,
errorMessage: '',
authHint: buildDefaultAuthHint(status),
});
return status;
};
const ensureSubscription = (): void => {
if (statusSubscribed) return;
statusSubscribed = true;
setLastFmStatusListener((status) => {
applyStatus(status);
});
};
const fetchStatus = async (): Promise<LastFmStatus> => {
const service = await initLastFmService();
ensureSubscription();
return applyStatus(service.getStatus());
};
const pollAuthCompletion = async (): Promise<void> => {
if (authPollInFlight) return;
authPollInFlight = true;
try {
const result = await getLastFmService().finishAuth();
const status = await fetchStatus().catch(() => null);
if (result.ok) {
stopAuthPolling();
set({ authHint: '', errorMessage: '' });
return;
}
const stillPending = status?.authPending ?? false;
if (stillPending) {
if (Date.now() >= authPollDeadlineMs) {
stopAuthPolling();
set({
authHint: 'Authorization still pending. Approve Astra on Last.fm, then press Connect again.',
errorMessage: '',
});
return;
}
set({
authHint: 'Waiting for Last.fm approval in your browser...',
errorMessage: '',
});
scheduleAuthPoll(LASTFM_AUTH_POLL_INTERVAL_MS);
return;
}
stopAuthPolling();
set({
authHint: '',
errorMessage: result.message,
});
} catch (error) {
const status = await fetchStatus().catch(() => null);
const stillPending = status?.authPending ?? false;
if (stillPending && Date.now() < authPollDeadlineMs) {
set({
authHint: 'Waiting for Last.fm approval in your browser...',
errorMessage: '',
});
scheduleAuthPoll(LASTFM_AUTH_POLL_INTERVAL_MS);
return;
}
stopAuthPolling();
set({ errorMessage: toErrorMessage(error), authHint: '' });
} finally {
authPollInFlight = false;
}
};
const startAuthPolling = (): void => {
stopAuthPolling();
authPollDeadlineMs = Date.now() + LASTFM_AUTH_POLL_TIMEOUT_MS;
set({
isAuthorizing: true,
authHint: 'Waiting for Last.fm approval in your browser...',
errorMessage: '',
});
scheduleAuthPoll(1_000);
};
return {
status: null,
isLoading: false,
isInitialized: false,
isAuthorizing: false,
errorMessage: '',
authHint: '',
init: async () => {
if (get().isInitialized) return;
set({ isLoading: true });
try {
await fetchStatus();
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
} finally {
set({ isLoading: false, isInitialized: true });
}
},
refresh: async () => {
set({ isLoading: true });
try {
await fetchStatus();
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
} finally {
set({ isLoading: false });
}
},
setEnabled: async (enabled: boolean) => {
try {
const status = await getLastFmService().setEnabled(enabled);
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
createCustomProfile: async (input: LastFmCustomProfileInput) => {
try {
const status = await getLastFmService().createCustomProfile(input);
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
updateCustomProfile: async (profileId: string, input: LastFmCustomProfileInput) => {
try {
const status = await getLastFmService().updateCustomProfile(profileId, input);
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
deleteCustomProfile: async (profileId: string) => {
try {
const status = await getLastFmService().deleteCustomProfile(profileId);
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
setProfileEnabled: async (profileId: string, enabled: boolean) => {
try {
const status = await getLastFmService().setProfileEnabled(profileId, enabled);
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
beginAuth: async (profileId: string) => {
try {
const result = await getLastFmService().beginAuth(profileId);
const status = await fetchStatus().catch(() => null);
if (result.ok && (status?.authPending ?? result.authPending)) {
startAuthPolling();
} else if (result.ok) {
stopAuthPolling();
set({ errorMessage: '', authHint: status ? buildDefaultAuthHint(status) : '' });
} else {
stopAuthPolling();
set({
errorMessage: result.message,
authHint: status ? buildDefaultAuthHint(status) : '',
});
}
return result;
} catch (error) {
stopAuthPolling();
set({ errorMessage: toErrorMessage(error), authHint: '' });
return null;
}
},
finishAuth: async () => {
try {
const result = await getLastFmService().finishAuth();
const status = await fetchStatus().catch(() => null);
if (result.ok) {
stopAuthPolling();
set({ authHint: '', errorMessage: '' });
} else if (status?.authPending) {
set({
authHint: 'Waiting for Last.fm approval in your browser...',
errorMessage: '',
});
} else {
stopAuthPolling();
set({
errorMessage: result.message,
authHint: status ? buildDefaultAuthHint(status) : '',
});
}
return result;
} catch (error) {
stopAuthPolling();
set({ errorMessage: toErrorMessage(error), authHint: '' });
return null;
}
},
disconnectProfile: async (profileId: string) => {
try {
stopAuthPolling();
const status = await getLastFmService().disconnectProfile(profileId);
set({ authHint: '' });
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
resetToDefaults: async () => {
try {
stopAuthPolling();
const status = await getLastFmService().resetToDefaults();
set({ authHint: '' });
return applyStatus(status);
} catch (error) {
set({ errorMessage: toErrorMessage(error) });
return null;
}
},
};
});
+22
View File
@@ -0,0 +1,22 @@
import { create } from 'zustand';
interface SearchStore {
isQuickSearchOpen: boolean;
initialQuery: string;
openVersion: number;
openQuickSearch: (initialQuery?: string) => void;
closeQuickSearch: () => void;
}
export const useSearchStore = create<SearchStore>((set) => ({
isQuickSearchOpen: false,
initialQuery: '',
openVersion: 0,
openQuickSearch: (initialQuery = '') =>
set((state) => ({
isQuickSearchOpen: true,
initialQuery,
openVersion: state.openVersion + 1,
})),
closeQuickSearch: () => set({ isQuickSearchOpen: false }),
}));