redesign dynamic playlists screen + revamp playlist UI/UX

This commit is contained in:
Boof2015
2026-07-06 19:26:44 -04:00
parent 995e00e0e7
commit e1f99f6d61
5 changed files with 1229 additions and 463 deletions
+23 -2
View File
@@ -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,
+2
View File
@@ -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}>
+62 -45
View File
@@ -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,
},
});