mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 04:30:54 +02:00
redesign dynamic playlists screen + revamp playlist UI/UX
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet
|
StyleSheet,
|
||||||
|
Alert
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
AppSheetItem,
|
AppSheetItem,
|
||||||
AppSheetTitle
|
AppSheetTitle
|
||||||
} from '@/components/sheets/AppSheet';
|
} from '@/components/sheets/AppSheet';
|
||||||
|
import { TextPromptModal } from '@/components/sheets/TextPromptModal';
|
||||||
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
|
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
|
||||||
import { colors, spacing } from '@/theme';
|
import { colors, spacing } from '@/theme';
|
||||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||||
@@ -32,7 +34,17 @@ import { artworkThumbUri, artworkUri } from '@/library/artwork';
|
|||||||
import { formatDuration } from '@/lib/format';
|
import { formatDuration } from '@/lib/format';
|
||||||
import { useLibraryDetailBack } from '@/navigation/useLibraryDetailBack';
|
import { useLibraryDetailBack } from '@/navigation/useLibraryDetailBack';
|
||||||
import type { DbTrack } from '@/types/library';
|
import type { DbTrack } from '@/types/library';
|
||||||
import type { PlaylistTrackEntry } from '@/types/playlist';
|
import type { Playlist, PlaylistTrackEntry } from '@/types/playlist';
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "content://…/Test.m3u8" -> "Test.m3u8" for the export confirmation. */
|
||||||
|
function fileDisplayName(fileUri: string): string {
|
||||||
|
const decoded = decodeURIComponent(fileUri.split('/').pop() ?? fileUri);
|
||||||
|
return decoded.split(/[/:]/).pop() || fileUri;
|
||||||
|
}
|
||||||
|
|
||||||
function basename(path: string): string {
|
function basename(path: string): string {
|
||||||
const decoded = decodeURIComponent(path.split('/').pop() ?? path);
|
const decoded = decodeURIComponent(path.split('/').pop() ?? path);
|
||||||
@@ -55,6 +67,8 @@ function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Prompt = { kind: 'rename'; playlist: Playlist } | null;
|
||||||
|
|
||||||
export default function PlaylistScreen() {
|
export default function PlaylistScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id, from } = useLocalSearchParams<{ id: string; from?: string }>();
|
const { id, from } = useLocalSearchParams<{ id: string; from?: string }>();
|
||||||
@@ -69,6 +83,9 @@ export default function PlaylistScreen() {
|
|||||||
const closePlaylist = usePlaylistStore((s) => s.closePlaylist);
|
const closePlaylist = usePlaylistStore((s) => s.closePlaylist);
|
||||||
const moveTrack = usePlaylistStore((s) => s.moveTrack);
|
const moveTrack = usePlaylistStore((s) => s.moveTrack);
|
||||||
const removeFromPlaylist = usePlaylistStore((s) => s.removeFromPlaylist);
|
const removeFromPlaylist = usePlaylistStore((s) => s.removeFromPlaylist);
|
||||||
|
const renamePlaylist = usePlaylistStore((s) => s.renamePlaylist);
|
||||||
|
const deletePlaylist = usePlaylistStore((s) => s.deletePlaylist);
|
||||||
|
const exportM3u = usePlaylistStore((s) => s.exportM3u);
|
||||||
const markPlayed = usePlaylistStore((s) => s.markPlayed);
|
const markPlayed = usePlaylistStore((s) => s.markPlayed);
|
||||||
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
|
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -77,6 +94,8 @@ export default function PlaylistScreen() {
|
|||||||
|
|
||||||
const [actionEntry, setActionEntry] = useState<PlaylistTrackEntry | null>(null);
|
const [actionEntry, setActionEntry] = useState<PlaylistTrackEntry | null>(null);
|
||||||
const [missingEntry, setMissingEntry] = useState<PlaylistTrackEntry | null>(null);
|
const [missingEntry, setMissingEntry] = useState<PlaylistTrackEntry | null>(null);
|
||||||
|
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||||
|
const [prompt, setPrompt] = useState<Prompt>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playlistId == null || Number.isNaN(playlistId)) return;
|
if (playlistId == null || Number.isNaN(playlistId)) return;
|
||||||
@@ -137,6 +156,40 @@ export default function PlaylistScreen() {
|
|||||||
if (playlistId != null && !Number.isNaN(playlistId)) void markPlayed(playlistId);
|
if (playlistId != null && !Number.isNaN(playlistId)) void markPlayed(playlistId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = async (target: number | 'favorites') => {
|
||||||
|
try {
|
||||||
|
const result = await exportM3u(target);
|
||||||
|
if (result) {
|
||||||
|
Alert.alert(
|
||||||
|
'Playlist exported',
|
||||||
|
`Wrote ${result.entryCount} ${result.entryCount === 1 ? 'entry' : 'entries'} to "${fileDisplayName(result.fileUri)}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert('Export failed', 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);
|
||||||
|
router.back();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert('Delete failed', errorMessage(err));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
// Move/remove only exist on real playlists; favorites rows use the standard
|
// Move/remove only exist on real playlists; favorites rows use the standard
|
||||||
// sheet (its favorite toggle is the "remove" affordance there).
|
// sheet (its favorite toggle is the "remove" affordance there).
|
||||||
const extraItems: TrackActionSheetItem[] =
|
const extraItems: TrackActionSheetItem[] =
|
||||||
@@ -234,16 +287,18 @@ export default function PlaylistScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Edit dynamic playlist rules"
|
||||||
>
|
>
|
||||||
<Ionicons name="sparkles" size={14} color={colors.accent} />
|
<Ionicons name="sparkles" size={14} color={colors.accent} />
|
||||||
<Text variant="label" color={colors.accent}>
|
<Text variant="label" color={colors.accent}>
|
||||||
Edit rules
|
Rules
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
disabled={playable.length === 0}
|
disabled={playable.length === 0}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
|
onMore={() => setOptionsOpen(true)}
|
||||||
onPlay={() => startPlayback(0)}
|
onPlay={() => startPlayback(0)}
|
||||||
onShuffle={startShuffle}
|
onShuffle={startShuffle}
|
||||||
scrollY={scrollY}
|
scrollY={scrollY}
|
||||||
@@ -277,6 +332,74 @@ export default function PlaylistScreen() {
|
|||||||
) : null}
|
) : null}
|
||||||
</AppSheet>
|
</AppSheet>
|
||||||
) : null}
|
) : null}
|
||||||
|
{optionsOpen ? (
|
||||||
|
<AppSheet onClose={() => setOptionsOpen(false)}>
|
||||||
|
<AppSheetTitle title={name} />
|
||||||
|
{isFavorites ? (
|
||||||
|
<AppSheetItem
|
||||||
|
label="Export M3U"
|
||||||
|
icon="download-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setOptionsOpen(false);
|
||||||
|
void handleExport('favorites');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : playlist && playlistId != null ? (
|
||||||
|
<>
|
||||||
|
{isDynamic ? (
|
||||||
|
<AppSheetItem
|
||||||
|
label="Edit rules"
|
||||||
|
icon="options-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setOptionsOpen(false);
|
||||||
|
router.push({
|
||||||
|
pathname: '/library/playlist/edit-dynamic' as never,
|
||||||
|
params: { id: String(playlistId) },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<AppSheetItem
|
||||||
|
label="Rename…"
|
||||||
|
icon="pencil-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setOptionsOpen(false);
|
||||||
|
setPrompt({ kind: 'rename', playlist });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppSheetItem
|
||||||
|
label="Export M3U"
|
||||||
|
icon="download-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setOptionsOpen(false);
|
||||||
|
void handleExport(playlistId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppSheetItem
|
||||||
|
label="Delete…"
|
||||||
|
icon="trash-outline"
|
||||||
|
destructive
|
||||||
|
onPress={() => {
|
||||||
|
setOptionsOpen(false);
|
||||||
|
confirmDelete(playlist);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</AppSheet>
|
||||||
|
) : null}
|
||||||
|
<TextPromptModal
|
||||||
|
visible={prompt !== null}
|
||||||
|
title="Rename playlist"
|
||||||
|
placeholder="Playlist name"
|
||||||
|
initialValue={prompt?.playlist.name ?? ''}
|
||||||
|
submitLabel="Rename"
|
||||||
|
onSubmit={(nextName) => {
|
||||||
|
if (prompt) void renamePlaylist(prompt.playlist.id, nextName);
|
||||||
|
setPrompt(null);
|
||||||
|
}}
|
||||||
|
onClose={() => setPrompt(null)}
|
||||||
|
/>
|
||||||
</Screen>
|
</Screen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -123,6 +123,7 @@ export function CollapsingHeader({
|
|||||||
heroExtra,
|
heroExtra,
|
||||||
disabled,
|
disabled,
|
||||||
onBack,
|
onBack,
|
||||||
|
onMore,
|
||||||
onPlay,
|
onPlay,
|
||||||
onShuffle,
|
onShuffle,
|
||||||
scrollY,
|
scrollY,
|
||||||
@@ -141,6 +142,7 @@ export function CollapsingHeader({
|
|||||||
heroExtra?: ReactNode;
|
heroExtra?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
onMore?: () => void;
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
onShuffle: () => void;
|
onShuffle: () => void;
|
||||||
scrollY: SharedValue<number>;
|
scrollY: SharedValue<number>;
|
||||||
@@ -282,7 +284,7 @@ export function CollapsingHeader({
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[
|
style={[
|
||||||
styles.barTitle,
|
styles.barTitle,
|
||||||
{ top: barCenterY - 12, left: thumbCenterX + ART_COLLAPSED / 2 + spacing.sm, right: 84 },
|
{ top: barCenterY - 12, left: thumbCenterX + ART_COLLAPSED / 2 + spacing.sm, right: onMore ? 124 : 84 },
|
||||||
barTitleStyle,
|
barTitleStyle,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -290,7 +292,7 @@ export function CollapsingHeader({
|
|||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.barIcons, { top: barCenterY - 16, right: spacing.md }, barIconsStyle]}
|
style={[styles.barIcons, { top: barCenterY - 16, right: onMore ? spacing.md + 40 : spacing.md }, barIconsStyle]}
|
||||||
pointerEvents={collapsed ? 'auto' : 'none'}
|
pointerEvents={collapsed ? 'auto' : 'none'}
|
||||||
>
|
>
|
||||||
<Pressable onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
<Pressable onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||||
@@ -301,6 +303,18 @@ export function CollapsingHeader({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
{onMore ? (
|
||||||
|
<Pressable
|
||||||
|
onPress={onMore}
|
||||||
|
hitSlop={8}
|
||||||
|
style={[styles.moreButton, { top: barCenterY - 16, right: spacing.md }]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Playlist options"
|
||||||
|
>
|
||||||
|
<Ionicons name="ellipsis-horizontal" size={22} color={colors.textPrimary} />
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Rendered last so the large art sits on top of the header text until it tucks away. */}
|
{/* Rendered last so the large art sits on top of the header text until it tucks away. */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
@@ -450,6 +464,13 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
moreButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
art: {
|
art: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function PlaylistRow({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`${name}, ${dynamic ? 'dynamic playlist, ' : ''}${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
|
||||||
>
|
>
|
||||||
<View style={styles.cover}>
|
<View style={styles.cover}>
|
||||||
{coverHash ? (
|
{coverHash ? (
|
||||||
@@ -69,6 +70,7 @@ export function PlaylistRow({
|
|||||||
{dynamic ? <Ionicons name="sparkles" size={12} color={colors.accent} /> : null}
|
{dynamic ? <Ionicons name="sparkles" size={12} color={colors.accent} /> : null}
|
||||||
</View>
|
</View>
|
||||||
<Text variant="label" numberOfLines={1}>
|
<Text variant="label" numberOfLines={1}>
|
||||||
|
{dynamic ? 'Dynamic · ' : ''}
|
||||||
{`${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
|
{`${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
|
||||||
{missingCount > 0 ? (
|
{missingCount > 0 ? (
|
||||||
<Text variant="label" color={colors.warning}>
|
<Text variant="label" color={colors.warning}>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function PlaylistsView({
|
|||||||
|
|
||||||
const [prompt, setPrompt] = useState<Prompt>(null);
|
const [prompt, setPrompt] = useState<Prompt>(null);
|
||||||
const [menuFor, setMenuFor] = useState<Playlist | 'favorites' | null>(null);
|
const [menuFor, setMenuFor] = useState<Playlist | 'favorites' | null>(null);
|
||||||
|
const [addSheetOpen, setAddSheetOpen] = useState(false);
|
||||||
|
|
||||||
const handleExport = async (target: number | 'favorites') => {
|
const handleExport = async (target: number | 'favorites') => {
|
||||||
try {
|
try {
|
||||||
@@ -168,6 +169,7 @@ export function PlaylistsView({
|
|||||||
renderScrollComponent={PullSearchScrollView}
|
renderScrollComponent={PullSearchScrollView}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
scrollEventThrottle={scrollEventThrottle}
|
scrollEventThrottle={scrollEventThrottle}
|
||||||
|
contentContainerStyle={styles.listContent}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<PlaylistRow
|
<PlaylistRow
|
||||||
name="Favorites"
|
name="Favorites"
|
||||||
@@ -194,46 +196,26 @@ export function PlaylistsView({
|
|||||||
<View style={styles.empty}>
|
<View style={styles.empty}>
|
||||||
<Ionicons name="musical-notes-outline" size={28} color={colors.textTertiary} />
|
<Ionicons name="musical-notes-outline" size={28} color={colors.textTertiary} />
|
||||||
<Text variant="body" color={colors.textSecondary} style={styles.emptyText}>
|
<Text variant="body" color={colors.textSecondary} style={styles.emptyText}>
|
||||||
No playlists yet. Create one or import an M3U below.
|
No playlists yet.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
ListFooterComponent={
|
|
||||||
<View style={styles.actions}>
|
|
||||||
<Pressable
|
|
||||||
style={styles.action}
|
|
||||||
onPress={() => setPrompt({ kind: 'create' })}
|
|
||||||
accessibilityRole="button"
|
|
||||||
>
|
|
||||||
<Ionicons name="add" size={18} color={colors.accent} />
|
|
||||||
<Text variant="body" color={colors.accent}>
|
|
||||||
New playlist
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
style={styles.action}
|
|
||||||
onPress={() => router.push('/library/playlist/edit-dynamic' as never)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
>
|
|
||||||
<Ionicons name="sparkles-outline" size={16} color={colors.accent} />
|
|
||||||
<Text variant="body" color={colors.accent}>
|
|
||||||
New dynamic
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
style={styles.action}
|
|
||||||
onPress={() => void handleImport()}
|
|
||||||
accessibilityRole="button"
|
|
||||||
>
|
|
||||||
<Ionicons name="document-text-outline" size={16} color={colors.textSecondary} />
|
|
||||||
<Text variant="body" color={colors.textSecondary}>
|
|
||||||
Import M3U
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<View style={styles.addBar}>
|
||||||
|
<Pressable
|
||||||
|
style={styles.addButton}
|
||||||
|
onPress={() => setAddSheetOpen(true)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Add playlist"
|
||||||
|
>
|
||||||
|
<Ionicons name="add" size={20} color={colors.accentTextStrong} />
|
||||||
|
<Text variant="body" color={colors.accentTextStrong}>
|
||||||
|
Add
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
{menuFor !== null ? (
|
{menuFor !== null ? (
|
||||||
<AppSheet onClose={() => setMenuFor(null)}>
|
<AppSheet onClose={() => setMenuFor(null)}>
|
||||||
<AppSheetTitle title={menuFor === 'favorites' ? 'Favorites' : menuFor.name} />
|
<AppSheetTitle title={menuFor === 'favorites' ? 'Favorites' : menuFor.name} />
|
||||||
@@ -242,6 +224,35 @@ export function PlaylistsView({
|
|||||||
))}
|
))}
|
||||||
</AppSheet>
|
</AppSheet>
|
||||||
) : null}
|
) : null}
|
||||||
|
{addSheetOpen ? (
|
||||||
|
<AppSheet onClose={() => setAddSheetOpen(false)}>
|
||||||
|
<AppSheetTitle title="Add playlist" />
|
||||||
|
<AppSheetItem
|
||||||
|
label="Standard playlist"
|
||||||
|
icon="list-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setAddSheetOpen(false);
|
||||||
|
setPrompt({ kind: 'create' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppSheetItem
|
||||||
|
label="Dynamic playlist"
|
||||||
|
icon="sparkles-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setAddSheetOpen(false);
|
||||||
|
router.push('/library/playlist/edit-dynamic' as never);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppSheetItem
|
||||||
|
label="Import M3U"
|
||||||
|
icon="document-text-outline"
|
||||||
|
onPress={() => {
|
||||||
|
setAddSheetOpen(false);
|
||||||
|
void handleImport();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AppSheet>
|
||||||
|
) : null}
|
||||||
<TextPromptModal
|
<TextPromptModal
|
||||||
visible={prompt !== null}
|
visible={prompt !== null}
|
||||||
title={prompt?.kind === 'rename' ? 'Rename playlist' : 'New playlist'}
|
title={prompt?.kind === 'rename' ? 'Rename playlist' : 'New playlist'}
|
||||||
@@ -266,11 +277,8 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
actions: {
|
listContent: {
|
||||||
flexDirection: 'row',
|
paddingBottom: 76,
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: spacing.md,
|
|
||||||
marginTop: spacing.lg,
|
|
||||||
},
|
},
|
||||||
empty: {
|
empty: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -281,14 +289,23 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
maxWidth: 260,
|
maxWidth: 260,
|
||||||
},
|
},
|
||||||
action: {
|
addBar: {
|
||||||
|
borderTopColor: colors.glassBorder,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
backgroundColor: colors.bgPrimary,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingTop: spacing.sm,
|
||||||
|
paddingBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
minHeight: 46,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: spacing.xs,
|
justifyContent: 'center',
|
||||||
borderColor: colors.glassBorder,
|
gap: spacing.sm,
|
||||||
|
borderColor: colors.accent,
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
borderRadius: radius.pill,
|
borderRadius: radius.pill,
|
||||||
paddingHorizontal: spacing.lg,
|
backgroundColor: colors.accentGlow,
|
||||||
paddingVertical: spacing.sm,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user