mirror of
https://github.com/FluxaMedia/fluxa-desktop.git
synced 2026-08-19 21:51:32 +00:00
feat: show error popups with a reason for failed avatar pack/addon adds
Avatar pack discovery used to return an empty array on every failure path (bad URL, repo not found, no packs, no valid images), so a failed add looked identical to nothing happening. It now throws a typed AvatarPackDiscoveryError and the settings screen shows it in a Toast. Also wires up the new profileAvatarPackManifestPlan core route so a direct pack.json link (GitHub or any other host) can be added without needing the whole-repository discovery flow. Addon installation already surfaced its error inline; it now also shows a Toast popup with the failure reason.
This commit is contained in:
parent
15ca2b3e02
commit
a1a338ffa2
6 changed files with 97 additions and 6 deletions
|
|
@ -282,6 +282,7 @@ export const CORE_METHODS = [
|
|||
'profileAvatarDefault',
|
||||
'profileAvatarPackCatalog',
|
||||
'profileAvatarPackDiscoveryPlan',
|
||||
'profileAvatarPackManifestPlan',
|
||||
'profileAvatarPackParse',
|
||||
'profileAvatarPackRepositoryPlan',
|
||||
'profileConnectionState',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ export interface ProfilePickerSettings {
|
|||
avatarPacks: ProfileAvatarPack[];
|
||||
}
|
||||
|
||||
export type AvatarPackDiscoveryReason = 'invalid_url' | 'repository_not_found' | 'no_packs_found' | 'no_valid_avatars';
|
||||
|
||||
export class AvatarPackDiscoveryError extends Error {
|
||||
constructor(public reason: AvatarPackDiscoveryReason) {
|
||||
super(reason);
|
||||
this.name = 'AvatarPackDiscoveryError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadProfilePickerSettings(): Promise<ProfilePickerSettings> {
|
||||
const stored = await storageRead<Partial<ProfilePickerSettings>>(STORAGE_KEY);
|
||||
return {
|
||||
|
|
@ -33,24 +42,42 @@ export async function saveProfilePickerSettings(settings: ProfilePickerSettings)
|
|||
await storageWrite(STORAGE_KEY, settings);
|
||||
}
|
||||
|
||||
async function discoverDirectManifestPack(repositoryUrl: string): Promise<ProfileAvatarPack | null> {
|
||||
const manifestPlan = await coreInvoke<{ manifestUrl: string } | null>(
|
||||
'profileAvatarPackManifestPlan',
|
||||
JSON.stringify({ repositoryUrl }),
|
||||
);
|
||||
if (!manifestPlan) return null;
|
||||
const pack = await fetchJson(manifestPlan.manifestUrl);
|
||||
const parsed = await coreInvoke<{ title: string; manifestUrl: string; avatars: ProfileAvatar[] } | null>(
|
||||
'profileAvatarPackParse',
|
||||
JSON.stringify({ manifestUrl: manifestPlan.manifestUrl, pack }),
|
||||
);
|
||||
if (!parsed || !parsed.avatars.length) throw new AvatarPackDiscoveryError('no_valid_avatars');
|
||||
return { ...parsed, id: parsed.manifestUrl, repositoryUrl };
|
||||
}
|
||||
|
||||
export async function discoverProfileAvatarPacks(repositoryUrl: string): Promise<ProfileAvatarPack[]> {
|
||||
const directPack = await discoverDirectManifestPack(repositoryUrl);
|
||||
if (directPack) return [directPack];
|
||||
|
||||
const repositoryPlan = await coreInvoke<{ repositoryApiUrl: string } | null>(
|
||||
'profileAvatarPackRepositoryPlan',
|
||||
JSON.stringify({ repositoryUrl }),
|
||||
);
|
||||
if (!repositoryPlan) return [];
|
||||
if (!repositoryPlan) throw new AvatarPackDiscoveryError('invalid_url');
|
||||
const repository = await fetchJson(repositoryPlan.repositoryApiUrl);
|
||||
const discoveryPlan = await coreInvoke<{ reference: string; treeApiUrl: string } | null>(
|
||||
'profileAvatarPackDiscoveryPlan',
|
||||
JSON.stringify({ repositoryUrl, repository }),
|
||||
);
|
||||
if (!discoveryPlan) return [];
|
||||
if (!discoveryPlan) throw new AvatarPackDiscoveryError('repository_not_found');
|
||||
const tree = await fetchJson(discoveryPlan.treeApiUrl);
|
||||
const catalog = await coreInvoke<{ categories: Array<{ manifestUrl: string }> } | null>(
|
||||
'profileAvatarPackCatalog',
|
||||
JSON.stringify({ repositoryUrl, reference: discoveryPlan.reference, tree }),
|
||||
);
|
||||
if (!catalog?.categories.length) return [];
|
||||
if (!catalog?.categories.length) throw new AvatarPackDiscoveryError('no_packs_found');
|
||||
const packs = await Promise.all(catalog.categories.map(async ({ manifestUrl }) => {
|
||||
const pack = await fetchJson(manifestUrl);
|
||||
const parsed = await coreInvoke<{ title: string; manifestUrl: string; avatars: ProfileAvatar[] } | null>(
|
||||
|
|
@ -61,7 +88,9 @@ export async function discoverProfileAvatarPacks(repositoryUrl: string): Promise
|
|||
? { ...parsed, id: parsed.manifestUrl, repositoryUrl }
|
||||
: null;
|
||||
}));
|
||||
return packs.filter((pack): pack is ProfileAvatarPack => pack !== null);
|
||||
const found = packs.filter((pack): pack is ProfileAvatarPack => pack !== null);
|
||||
if (!found.length) throw new AvatarPackDiscoveryError('no_valid_avatars');
|
||||
return found;
|
||||
}
|
||||
|
||||
export async function refreshAvatarPackRepository(repositoryUrl: string): Promise<ProfilePickerSettings> {
|
||||
|
|
|
|||
|
|
@ -866,6 +866,8 @@
|
|||
"addons.uptime_24h": "24h uptime",
|
||||
"addons.installed_dialog_title": "Addon Added",
|
||||
"addons.installed_dialog_body": "\"%s\" has been added to your addons.",
|
||||
"addons.install_failed_title": "Couldn't install addon",
|
||||
"addons.install_failed_message": "The manifest URL couldn't be loaded or parsed.",
|
||||
"plugins.title": "Plugins",
|
||||
"plugins.subtitle": "Plugin repositories and scrapers",
|
||||
"plugins.install": "Install Plugin Repository",
|
||||
|
|
@ -1279,13 +1281,19 @@
|
|||
"profiles.picker_background_placeholder": "Background image URL",
|
||||
"profiles.clear_background": "Clear background",
|
||||
"profiles.avatar_packs": "Profile image packs",
|
||||
"profiles.avatar_packs_desc": "Add a GitHub repository containing profile image packs.",
|
||||
"profiles.avatar_packs_desc": "Add a repository or pack link to import profile images.",
|
||||
"profiles.avatar_pack_repository_placeholder": "https://github.com/owner/repository",
|
||||
"profiles.add_pack": "Add pack",
|
||||
"profiles.remove_pack": "Remove pack",
|
||||
"profiles.refresh_pack": "Refresh pack",
|
||||
"profiles.no_avatar_packs": "No profile image packs have been added.",
|
||||
"profiles.avatar_pack_count": "%s profile images",
|
||||
"profiles.avatar_pack_error_title": "Couldn't add profile image pack",
|
||||
"profiles.avatar_pack_error_invalid_url": "That doesn't look like a GitHub repository URL.",
|
||||
"profiles.avatar_pack_error_repository_not_found": "Couldn't find that repository on GitHub.",
|
||||
"profiles.avatar_pack_error_no_packs_found": "No pack.json files were found in that repository.",
|
||||
"profiles.avatar_pack_error_no_valid_avatars": "The pack.json files found didn't contain any usable images.",
|
||||
"profiles.avatar_pack_error_generic": "Something went wrong while adding this pack.",
|
||||
"detail.similar_source": "Recommendations from",
|
||||
"detail.similar_source_auto": "Automatic",
|
||||
"detail.similar_source_trakt": "Trakt Recommendations",
|
||||
|
|
|
|||
|
|
@ -866,6 +866,8 @@
|
|||
"addons.uptime_24h": "24 sa çalışma",
|
||||
"addons.installed_dialog_title": "Addon Eklendi",
|
||||
"addons.installed_dialog_body": "\"%s\" addonlarınıza eklendi.",
|
||||
"addons.install_failed_title": "Addon kurulamadı",
|
||||
"addons.install_failed_message": "Manifest URL'si yüklenemedi veya ayrıştırılamadı.",
|
||||
"plugins.title": "Pluginler",
|
||||
"plugins.subtitle": "Plugin depoları ve scraperlar",
|
||||
"plugins.install": "Plugin Deposu Kur",
|
||||
|
|
@ -1279,13 +1281,19 @@
|
|||
"profiles.picker_background_placeholder": "Arka plan görseli URL'si",
|
||||
"profiles.clear_background": "Arka planı temizle",
|
||||
"profiles.avatar_packs": "Profil görseli paketleri",
|
||||
"profiles.avatar_packs_desc": "Profil görseli paketleri içeren bir GitHub deposu ekleyin.",
|
||||
"profiles.avatar_packs_desc": "Profil görseli almak için bir depo veya paket linki ekleyin.",
|
||||
"profiles.avatar_pack_repository_placeholder": "https://github.com/sahip/depo",
|
||||
"profiles.add_pack": "Paket ekle",
|
||||
"profiles.remove_pack": "Paketi kaldır",
|
||||
"profiles.refresh_pack": "Paketi yenile",
|
||||
"profiles.no_avatar_packs": "Henüz profil görseli paketi eklenmedi.",
|
||||
"profiles.avatar_pack_count": "%s profil görseli",
|
||||
"profiles.avatar_pack_error_title": "Profil görseli paketi eklenemedi",
|
||||
"profiles.avatar_pack_error_invalid_url": "Bu bir GitHub deposu URL'sine benzemiyor.",
|
||||
"profiles.avatar_pack_error_repository_not_found": "Bu depo GitHub'da bulunamadı.",
|
||||
"profiles.avatar_pack_error_no_packs_found": "Bu depoda pack.json dosyası bulunamadı.",
|
||||
"profiles.avatar_pack_error_no_valid_avatars": "Bulunan pack.json dosyalarında kullanılabilir görsel yoktu.",
|
||||
"profiles.avatar_pack_error_generic": "Bu paket eklenirken bir sorun oluştu.",
|
||||
"detail.similar_source": "Öneri kaynağı",
|
||||
"detail.similar_source_auto": "Otomatik",
|
||||
"detail.similar_source_trakt": "Trakt Önerileri",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,29 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { ImagePlus, Plus, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
AvatarPackDiscoveryError,
|
||||
discoverProfileAvatarPacks,
|
||||
refreshAvatarPackRepository,
|
||||
type ProfilePickerSettings,
|
||||
saveProfilePickerSettings,
|
||||
} from '../core/profileAvatarPacks';
|
||||
import { Toast } from '../components/Toast';
|
||||
import { t } from '../i18n';
|
||||
|
||||
const AVATAR_PACK_ERROR_MESSAGE_KEYS: Record<string, string> = {
|
||||
invalid_url: 'profiles.avatar_pack_error_invalid_url',
|
||||
repository_not_found: 'profiles.avatar_pack_error_repository_not_found',
|
||||
no_packs_found: 'profiles.avatar_pack_error_no_packs_found',
|
||||
no_valid_avatars: 'profiles.avatar_pack_error_no_valid_avatars',
|
||||
};
|
||||
|
||||
function avatarPackErrorMessage(error: unknown): string {
|
||||
if (error instanceof AvatarPackDiscoveryError) {
|
||||
return t(AVATAR_PACK_ERROR_MESSAGE_KEYS[error.reason] ?? 'profiles.avatar_pack_error_generic');
|
||||
}
|
||||
return t('profiles.avatar_pack_error_generic');
|
||||
}
|
||||
|
||||
export function ProfilePickerSettings({
|
||||
settings,
|
||||
onSaved,
|
||||
|
|
@ -22,6 +38,7 @@ export function ProfilePickerSettings({
|
|||
const [packs, setPacks] = useState(settings.avatarPacks);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [refreshingRepo, setRefreshingRepo] = useState<string | null>(null);
|
||||
const [avatarPackError, setAvatarPackError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const save = async (next: ProfilePickerSettings) => {
|
||||
|
|
@ -39,6 +56,8 @@ export function ProfilePickerSettings({
|
|||
setPacks(nextPacks);
|
||||
setRepositoryUrl('');
|
||||
await save({ backgroundUrl: backgroundUrl || undefined, avatarPacks: nextPacks });
|
||||
} catch (error) {
|
||||
setAvatarPackError(avatarPackErrorMessage(error));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
|
@ -51,6 +70,8 @@ export function ProfilePickerSettings({
|
|||
const next = await refreshAvatarPackRepository(repositoryUrl);
|
||||
setPacks(next.avatarPacks);
|
||||
onSaved(next);
|
||||
} catch (error) {
|
||||
setAvatarPackError(avatarPackErrorMessage(error));
|
||||
} finally {
|
||||
setRefreshingRepo(null);
|
||||
}
|
||||
|
|
@ -107,6 +128,16 @@ export function ProfilePickerSettings({
|
|||
</div>
|
||||
<button onClick={onBack} style={S.backButton}>{t('common.back')}</button>
|
||||
<input ref={fileInputRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={uploadBackground} />
|
||||
{avatarPackError && (
|
||||
<div style={{ position: 'fixed', top: '1rem', right: '1rem', zIndex: 100 }}>
|
||||
<Toast
|
||||
variant="error"
|
||||
title={t('profiles.avatar_pack_error_title')}
|
||||
message={avatarPackError}
|
||||
onClose={() => setAvatarPackError(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { AddonsSection } from '../components/settings/AddonsSection';
|
|||
import { PluginsSection } from '../components/settings/PluginsSection';
|
||||
import { DownloadsSection } from '../components/settings/DownloadsSection';
|
||||
import { AddonAddedDialog } from '../components/AddonAddedDialog';
|
||||
import { Toast } from '../components/Toast';
|
||||
|
||||
async function settingsFetchJson(url: string): Promise<unknown> {
|
||||
const response = await httpFetchText(url);
|
||||
|
|
@ -487,6 +488,19 @@ export function SettingsScreen({ state, onDispatch, activeProfile, onProfileUpda
|
|||
{addedAddonName && (
|
||||
<AddonAddedDialog addonName={addedAddonName} onConfirm={() => setAddedAddonName(null)} />
|
||||
)}
|
||||
{addonInstallStatus.error && (
|
||||
<div style={{ position: 'fixed', top: '1rem', right: '1rem', zIndex: 100 }}>
|
||||
<Toast
|
||||
variant="error"
|
||||
title={t('addons.install_failed_title')}
|
||||
message={t('addons.install_failed_message')}
|
||||
details={addonInstallStatus.error}
|
||||
detailsLabel={t('player.error_show_details')}
|
||||
detailsHideLabel={t('player.error_hide_details')}
|
||||
onClose={() => setAddonInstallStatus((prev) => ({ ...prev, error: null }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue