mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 04:59:53 +02:00
replace system boxes
This commit is contained in:
+27
-14
@@ -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.',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user