replace system boxes

This commit is contained in:
Boof2015
2026-07-25 17:00:04 -04:00
parent 0850a64a8f
commit 80c27b039f
16 changed files with 551 additions and 122 deletions
+27 -14
View File
@@ -1,6 +1,5 @@
import { useCallback, useState } from 'react';
import {
Alert,
InteractionManager,
Pressable,
StyleSheet,
@@ -20,6 +19,7 @@ import {
} from 'expo-file-system/legacy';
import { Screen } from '@/components/Screen';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import { EQGraph } from '@/components/eq/EQGraph';
import { BandStrip } from '@/components/eq/BandStrip';
import { BandDetailPanel, type EQEditableValue } from '@/components/eq/BandDetailPanel';
@@ -171,7 +171,7 @@ export default function EQScreen() {
};
const showPresetImportError = useCallback((message = 'That file is not an Astra EQ preset.') => {
Alert.alert('Could not import preset', message);
showAppDialog({ title: 'Could not import preset', message });
}, []);
const deletePreset = useCallback((preset: EQPreset) => {
@@ -188,14 +188,18 @@ export default function EQScreen() {
finishDelete();
return;
}
Alert.alert(
`Delete ${preset.name}?`,
`This will also clear ${assignmentCount} device assignment${assignmentCount === 1 ? '' : 's'}. The current sound will not change.`,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Delete', style: 'destructive', onPress: finishDelete },
]
);
showAppDialog({
title: `Delete ${preset.name}?`,
message: `This will also clear ${assignmentCount} device assignment${assignmentCount === 1 ? '' : 's'}. The current sound will not change.`,
actions: [
{ label: 'Cancel', role: 'cancel' },
{
label: 'Delete',
role: 'destructive',
onPress: finishDelete,
},
],
});
}, []);
const runCurrentPresetAction = useCallback(async (action: CurrentPresetAction, name: string) => {
@@ -211,18 +215,24 @@ export default function EQScreen() {
EQ_PRESET_MIME_TYPE
);
await writeAsStringAsync(fileUri, stringifyEQPresetFileContents(preset));
Alert.alert('Preset exported', `Saved ${fileName}.`);
showAppDialog({ title: 'Preset exported', message: `Saved ${fileName}.` });
return;
}
if (action === 'share') {
const available = await Sharing.isAvailableAsync();
if (!available) {
Alert.alert('Share unavailable', 'This device cannot open a share sheet right now.');
showAppDialog({
title: 'Share unavailable',
message: 'This device cannot open a share sheet right now.',
});
return;
}
if (!cacheDirectory) {
Alert.alert('Share unavailable', 'Astra could not create a temporary preset file.');
showAppDialog({
title: 'Share unavailable',
message: 'Astra could not create a temporary preset file.',
});
return;
}
const fileUri = `${cacheDirectory}${buildEQPresetFileName(preset.name)}`;
@@ -238,7 +248,10 @@ export default function EQScreen() {
setQrPreset({ name: preset.name, value: encodeEQPresetQr(preset) });
setSheet('qr');
} catch {
Alert.alert('Preset sharing failed', 'Astra could not finish that preset sharing action.');
showAppDialog({
title: 'Preset sharing failed',
message: 'Astra could not finish that preset sharing action.',
});
}
}, []);
+27 -23
View File
@@ -6,8 +6,7 @@ import {
import {
View,
Pressable,
StyleSheet,
Alert
StyleSheet
} from 'react-native';
import { Image } from 'expo-image';
import { Ionicons } from '@expo/vector-icons';
@@ -24,6 +23,7 @@ import {
AppSheetTitle
} from '@/components/sheets/AppSheet';
import { TextPromptModal } from '@/components/sheets/TextPromptModal';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
import { spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
@@ -199,34 +199,38 @@ export default function PlaylistScreen() {
try {
const result = await exportM3u(target);
if (result) {
Alert.alert(
'Playlist exported',
`Wrote ${result.entryCount} ${result.entryCount === 1 ? 'entry' : 'entries'} to "${fileDisplayName(result.fileUri)}".`
);
showAppDialog({
title: 'Playlist exported',
message: `Wrote ${result.entryCount} ${result.entryCount === 1 ? 'entry' : 'entries'} to "${fileDisplayName(result.fileUri)}".`,
});
}
} catch (err) {
Alert.alert('Export failed', errorMessage(err));
showAppDialog({ title: 'Export failed', message: errorMessage(err) });
}
};
const confirmDelete = (target: Playlist) => {
Alert.alert('Delete playlist?', `"${target.name}" will be deleted. Tracks are not touched.`, [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => {
void (async () => {
try {
await deletePlaylist(target.id);
goBack();
} catch (err) {
Alert.alert('Delete failed', errorMessage(err));
}
})();
showAppDialog({
title: 'Delete playlist?',
message: `"${target.name}" will be deleted. Tracks are not touched.`,
actions: [
{ label: 'Cancel', role: 'cancel' },
{
label: 'Delete',
role: 'destructive',
onPress: () => {
void (async () => {
try {
await deletePlaylist(target.id);
goBack();
} catch (err) {
showAppDialog({ title: 'Delete failed', message: errorMessage(err) });
}
})();
},
},
},
]);
],
});
};
// Move/remove only exist on real playlists; favorites rows use the standard
@@ -4,7 +4,6 @@ import {
useState,
} from 'react';
import {
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
@@ -19,6 +18,7 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
import { Screen } from '@/components/Screen';
import { SegmentedControl } from '@/components/SegmentedControl';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import {
AppSheet,
AppSheetItem,
@@ -784,7 +784,10 @@ export default function DynamicPlaylistEditorScreen() {
})
.catch((err) => {
if (!didCancel) {
Alert.alert('Rules unavailable', err instanceof Error ? err.message : String(err));
showAppDialog({
title: 'Rules unavailable',
message: err instanceof Error ? err.message : String(err),
});
router.back();
}
})
@@ -864,10 +867,14 @@ export default function DynamicPlaylistEditorScreen() {
apply();
return;
}
Alert.alert('Replace rules?', 'This preset will replace the current filters and result order.', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Replace', style: 'destructive', onPress: apply },
]);
showAppDialog({
title: 'Replace rules?',
message: 'This preset will replace the current filters and result order.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{ label: 'Replace', role: 'destructive', onPress: apply },
],
});
};
const openFieldPicker = (target: 'new' | ConditionEditorTarget) => {
@@ -961,7 +968,10 @@ export default function DynamicPlaylistEditorScreen() {
const createdPlaylist = await createDynamicPlaylist(trimmedName, normalizedRules);
router.replace(`/library/playlist/${createdPlaylist.id}`);
} catch (err) {
Alert.alert('Save failed', err instanceof Error ? err.message : String(err));
showAppDialog({
title: 'Save failed',
message: err instanceof Error ? err.message : String(err),
});
} finally {
setIsSaving(false);
}
+2
View File
@@ -50,6 +50,7 @@ import { SessionLifecycle } from '@/session/SessionLifecycle';
import { useLyricsSettingsStore } from '@/stores/lyricsSettingsStore';
import { useSleepTimerStore } from '@/stores/sleepTimerStore';
import { Text } from '@/components/Text';
import { AppDialogHost } from '@/components/dialogs/AppDialog';
// Anchor the root stack at the tabs so a deep link straight to a top-level route
// (the widget's `recently-played`, the notification-click redirect) builds
@@ -459,6 +460,7 @@ export default function RootLayout() {
/>
</View>
)}
<AppDialogHost />
</SafeAreaProvider>
</GestureHandlerRootView>
);
+9 -5
View File
@@ -5,7 +5,6 @@ import {
} from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
@@ -23,6 +22,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { AstraLogo } from '@/components/AstraLogo';
import { Screen } from '@/components/Screen';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import {
radius,
spacing,
@@ -339,10 +339,14 @@ export default function DesktopRemoteScreen() {
}, [connection, discoveryAvailable, discoveryRunning, message]);
const confirmForget = () => {
Alert.alert('Forget desktop?', 'This removes the saved desktop pairing from this phone.', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Forget', style: 'destructive', onPress: () => void forget() },
]);
showAppDialog({
title: 'Forget desktop?',
message: 'This removes the saved desktop pairing from this phone.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{ label: 'Forget', role: 'destructive', onPress: () => void forget() },
],
});
};
const pairDiscovered = (desktop: DesktopRemoteDiscoveredDesktop) => {
+10 -10
View File
@@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import {
ActivityIndicator,
Alert,
KeyboardAvoidingView,
Platform,
Pressable,
@@ -14,6 +13,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Screen } from '@/components/Screen';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import {
radius,
spacing,
@@ -176,21 +176,21 @@ export default function LastFmEditScreen() {
const onRemove = () => {
if (!editing) return;
Alert.alert(
`Remove ${editing.name}?`,
'This deletes the scrobble destination and its queued scrobbles from this device. Your history on the service is unaffected.',
[
{ text: 'Cancel', style: 'cancel' },
showAppDialog({
title: `Remove ${editing.name}?`,
message: 'This deletes the scrobble destination and its queued scrobbles from this device. Your history on the service is unaffected.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{
text: 'Remove',
style: 'destructive',
label: 'Remove',
role: 'destructive',
onPress: () => {
void deleteCustomProfile(editing.id);
router.back();
},
},
]
);
],
});
};
const backLabel = step === 'details' && !editing ? 'Service' : 'Scrobbling';
+17 -13
View File
@@ -1,6 +1,5 @@
import { useEffect } from 'react';
import {
Alert,
Pressable,
ScrollView,
StyleSheet,
@@ -11,6 +10,7 @@ import { useRouter } from 'expo-router';
import { Screen } from '@/components/Screen';
import { Text } from '@/components/Text';
import { HapticSwitch } from '@/components/HapticSwitch';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import {
radius,
spacing,
@@ -66,24 +66,28 @@ export default function LastFmScreen() {
const connectOfficial = (profile: LastFmProfileStatus) => {
if (status && !status.hasApiCredentials) {
Alert.alert(
'Last.fm not configured',
'This build has no Last.fm API key. Set EXPO_PUBLIC_LASTFM_API_KEY / _SHARED_SECRET, or add a custom Last.fm-compatible / ListenBrainz destination instead.'
);
showAppDialog({
title: 'Last.fm not configured',
message: 'This build has no Last.fm API key. Set EXPO_PUBLIC_LASTFM_API_KEY / _SHARED_SECRET, or add a custom Last.fm-compatible / ListenBrainz destination instead.',
});
return;
}
void beginAuth(profile.id);
};
const confirmDisconnect = (profile: LastFmProfileStatus) => {
Alert.alert(`Disconnect ${profile.name}?`, 'Astra will stop scrobbling to this destination.', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Disconnect',
style: 'destructive',
onPress: () => void disconnectProfile(profile.id),
},
]);
showAppDialog({
title: `Disconnect ${profile.name}?`,
message: 'Astra will stop scrobbling to this destination.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{
label: 'Disconnect',
role: 'destructive',
onPress: () => void disconnectProfile(profile.id),
},
],
});
};
const renderOfficial = (profile: LastFmProfileStatus) => {
+5 -2
View File
@@ -1,5 +1,4 @@
import {
Alert,
Linking,
View,
} from 'react-native';
@@ -11,6 +10,7 @@ import {
SettingsSectionScreen,
} from '@/components/settings/SettingsSectionScaffold';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import { createBuildInfo } from '@/release/buildInfo';
import { spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
@@ -26,7 +26,10 @@ async function openExternalLink(url: string, label: string) {
try {
await Linking.openURL(url);
} catch {
Alert.alert('Unable to open link', `Astra could not open ${label}.`);
showAppDialog({
title: 'Unable to open link',
message: `Astra could not open ${label}.`,
});
}
}
+18 -17
View File
@@ -1,8 +1,9 @@
import { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, Alert, Pressable, View } from 'react-native';
import { ActivityIndicator, Pressable, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import { ScanProgress } from '@/components/library/ScanProgress';
import {
SettingsNavRow,
@@ -80,35 +81,35 @@ export default function TroubleshootingSettingsScreen() {
};
const confirmRebuild = () => {
Alert.alert(
'Rebuild local library index?',
'A foreground scan will re-read every local track. Folders, playlists, favorites, history, remote sources, and settings are preserved.',
[
{ text: 'Cancel', style: 'cancel' },
showAppDialog({
title: 'Rebuild local library index?',
message: 'A foreground scan will re-read every local track. Folders, playlists, favorites, history, remote sources, and settings are preserved.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{
text: 'Rebuild',
label: 'Rebuild',
onPress: () => void run(
'rebuild',
() => useLibraryStore.getState().rebuildLocalIndex(),
'Local library index rebuilt.',
),
},
]
);
],
});
};
const confirmOnboarding = () => {
Alert.alert(
'Replay onboarding?',
'The first-run setup opens immediately. Your library and settings are kept.',
[
{ text: 'Cancel', style: 'cancel' },
showAppDialog({
title: 'Replay onboarding?',
message: 'The first-run setup opens immediately. Your library and settings are kept.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{
text: 'Replay',
label: 'Replay',
onPress: () => void run('onboarding', () => useOnboardingStore.getState().reset(), 'Opening onboarding…'),
},
]
);
],
});
};
return (
+10 -10
View File
@@ -1,6 +1,5 @@
import { useState } from 'react';
import {
Alert,
Pressable,
ScrollView,
StyleSheet,
@@ -10,6 +9,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { Screen } from '@/components/Screen';
import { Text } from '@/components/Text';
import { showAppDialog } from '@/components/dialogs/AppDialog';
import { ActionSheet, type ActionSheetItem } from '@/components/sheets/ActionSheet';
import {
radius,
@@ -52,18 +52,18 @@ export default function SourcesScreen() {
const [actionFor, setActionFor] = useState<RemoteSourceRow | null>(null);
const confirmRemove = (source: RemoteSourceRow) => {
Alert.alert(
`Remove ${source.name}?`,
'This removes the server and all of its tracks from your library. Favorites and playlist entries are kept but will show as missing.',
[
{ text: 'Cancel', style: 'cancel' },
showAppDialog({
title: `Remove ${source.name}?`,
message: 'This removes the server and all of its tracks from your library. Favorites and playlist entries are kept but will show as missing.',
actions: [
{ label: 'Cancel', role: 'cancel' },
{
text: 'Remove',
style: 'destructive',
label: 'Remove',
role: 'destructive',
onPress: () => void deleteSource(source.id, true),
},
]
);
],
});
};
const actionItems: ActionSheetItem[] = actionFor