mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
redesign dynamic playlists screen + revamp playlist UI/UX
This commit is contained in:
@@ -123,6 +123,7 @@ export function CollapsingHeader({
|
||||
heroExtra,
|
||||
disabled,
|
||||
onBack,
|
||||
onMore,
|
||||
onPlay,
|
||||
onShuffle,
|
||||
scrollY,
|
||||
@@ -141,6 +142,7 @@ export function CollapsingHeader({
|
||||
heroExtra?: ReactNode;
|
||||
disabled?: boolean;
|
||||
onBack: () => void;
|
||||
onMore?: () => void;
|
||||
onPlay: () => void;
|
||||
onShuffle: () => void;
|
||||
scrollY: SharedValue<number>;
|
||||
@@ -282,7 +284,7 @@ export function CollapsingHeader({
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
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,
|
||||
]}
|
||||
>
|
||||
@@ -290,7 +292,7 @@ export function CollapsingHeader({
|
||||
</Animated.Text>
|
||||
|
||||
<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'}
|
||||
>
|
||||
<Pressable onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||
@@ -301,6 +303,18 @@ export function CollapsingHeader({
|
||||
</Pressable>
|
||||
</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. */}
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -450,6 +464,13 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
moreButton: {
|
||||
position: 'absolute',
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
art: {
|
||||
position: 'absolute',
|
||||
borderRadius: radius.lg,
|
||||
|
||||
@@ -43,6 +43,7 @@ export function PlaylistRow({
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${name}, ${dynamic ? 'dynamic playlist, ' : ''}${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
|
||||
>
|
||||
<View style={styles.cover}>
|
||||
{coverHash ? (
|
||||
@@ -69,6 +70,7 @@ export function PlaylistRow({
|
||||
{dynamic ? <Ionicons name="sparkles" size={12} color={colors.accent} /> : null}
|
||||
</View>
|
||||
<Text variant="label" numberOfLines={1}>
|
||||
{dynamic ? 'Dynamic · ' : ''}
|
||||
{`${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
|
||||
{missingCount > 0 ? (
|
||||
<Text variant="label" color={colors.warning}>
|
||||
|
||||
@@ -57,6 +57,7 @@ export function PlaylistsView({
|
||||
|
||||
const [prompt, setPrompt] = useState<Prompt>(null);
|
||||
const [menuFor, setMenuFor] = useState<Playlist | 'favorites' | null>(null);
|
||||
const [addSheetOpen, setAddSheetOpen] = useState(false);
|
||||
|
||||
const handleExport = async (target: number | 'favorites') => {
|
||||
try {
|
||||
@@ -168,6 +169,7 @@ export function PlaylistsView({
|
||||
renderScrollComponent={PullSearchScrollView}
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={scrollEventThrottle}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListHeaderComponent={
|
||||
<PlaylistRow
|
||||
name="Favorites"
|
||||
@@ -194,46 +196,26 @@ export function PlaylistsView({
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="musical-notes-outline" size={28} color={colors.textTertiary} />
|
||||
<Text variant="body" color={colors.textSecondary} style={styles.emptyText}>
|
||||
No playlists yet. Create one or import an M3U below.
|
||||
No playlists yet.
|
||||
</Text>
|
||||
</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 ? (
|
||||
<AppSheet onClose={() => setMenuFor(null)}>
|
||||
<AppSheetTitle title={menuFor === 'favorites' ? 'Favorites' : menuFor.name} />
|
||||
@@ -242,6 +224,35 @@ export function PlaylistsView({
|
||||
))}
|
||||
</AppSheet>
|
||||
) : 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
|
||||
visible={prompt !== null}
|
||||
title={prompt?.kind === 'rename' ? 'Rename playlist' : 'New playlist'}
|
||||
@@ -266,11 +277,8 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md,
|
||||
marginTop: spacing.lg,
|
||||
listContent: {
|
||||
paddingBottom: 76,
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
@@ -281,14 +289,23 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
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',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
borderColor: colors.glassBorder,
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
borderColor: colors.accent,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.accentGlow,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user