mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 04:30:54 +02:00
port desktop's search engine
This commit is contained in:
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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 }),
|
||||
}));
|
||||
Reference in New Issue
Block a user