mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
custom accent color + port over desktop cover art color
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
/* eslint-disable react-hooks/refs -- HSV refs are read only by RNGH callbacks after render; keeping the gesture identity stable prevents an active color drag from being replaced mid-gesture. */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View,
|
||||
type LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
GestureHandlerRootView,
|
||||
} from 'react-native-gesture-handler';
|
||||
import {
|
||||
Canvas,
|
||||
LinearGradient,
|
||||
Rect,
|
||||
vec,
|
||||
} from '@shopify/react-native-skia';
|
||||
import { AppSheet, AppSheetTitle } from '@/components/sheets/AppSheet';
|
||||
import { Text } from '@/components/Text';
|
||||
import { fonts, radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import {
|
||||
hexToHsv,
|
||||
hsvToHex,
|
||||
normalizeHexColor,
|
||||
type HsvColor,
|
||||
} from '@/theme/colorUtils';
|
||||
|
||||
const SV_HEIGHT = 180;
|
||||
const HUE_HEIGHT = 28;
|
||||
const HANDLE_SIZE = 22;
|
||||
const HUE_COLORS = [
|
||||
'#ff0000',
|
||||
'#ffff00',
|
||||
'#00ff00',
|
||||
'#00ffff',
|
||||
'#0000ff',
|
||||
'#ff00ff',
|
||||
'#ff0000',
|
||||
];
|
||||
|
||||
interface AccentColorSheetProps {
|
||||
initialHex: string;
|
||||
onPreview: (hex: string | null) => void;
|
||||
onApply: (hex: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AccentColorSheet({
|
||||
initialHex,
|
||||
onPreview,
|
||||
onApply,
|
||||
onClose,
|
||||
}: AccentColorSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const normalizedInitial = normalizeHexColor(initialHex) ?? '#5b8aff';
|
||||
const [hsv, setHsv] = useState<HsvColor>(() => hexToHsv(normalizedInitial));
|
||||
const hsvRef = useRef(hsv);
|
||||
const [input, setInput] = useState(normalizedInitial.toUpperCase());
|
||||
const [pickerWidth, setPickerWidth] = useState(1);
|
||||
const validInput = normalizeHexColor(input);
|
||||
const previewHex = hsvToHex(hsv.h, hsv.s, hsv.v);
|
||||
const hueColor = hsvToHex(hsv.h, 1, 1);
|
||||
|
||||
useEffect(() => {
|
||||
if (validInput) onPreview(validInput);
|
||||
}, [onPreview, validInput]);
|
||||
|
||||
useEffect(
|
||||
() => () => onPreview(null),
|
||||
[onPreview],
|
||||
);
|
||||
|
||||
const commitHsv = useCallback((next: HsvColor) => {
|
||||
hsvRef.current = next;
|
||||
setHsv(next);
|
||||
setInput(hsvToHex(next.h, next.s, next.v).toUpperCase());
|
||||
}, []);
|
||||
|
||||
const updateSv = useCallback((x: number, y: number) => {
|
||||
const next = {
|
||||
...hsvRef.current,
|
||||
s: Math.min(1, Math.max(0, x / pickerWidth)),
|
||||
v: 1 - Math.min(1, Math.max(0, y / SV_HEIGHT)),
|
||||
};
|
||||
commitHsv(next);
|
||||
}, [commitHsv, pickerWidth]);
|
||||
|
||||
const updateHue = useCallback((x: number) => {
|
||||
const next = {
|
||||
...hsvRef.current,
|
||||
h: Math.min(359.999, Math.max(0, (x / pickerWidth) * 360)),
|
||||
};
|
||||
commitHsv(next);
|
||||
}, [commitHsv, pickerWidth]);
|
||||
|
||||
const svGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.minDistance(0)
|
||||
.runOnJS(true)
|
||||
.onBegin((event) => updateSv(event.x, event.y))
|
||||
.onUpdate((event) => updateSv(event.x, event.y)),
|
||||
[updateSv],
|
||||
);
|
||||
const hueGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.minDistance(0)
|
||||
.runOnJS(true)
|
||||
.onBegin((event) => updateHue(event.x))
|
||||
.onUpdate((event) => updateHue(event.x)),
|
||||
[updateHue],
|
||||
);
|
||||
|
||||
const onPickerLayout = (event: LayoutChangeEvent) => {
|
||||
setPickerWidth(Math.max(1, event.nativeEvent.layout.width));
|
||||
};
|
||||
|
||||
const changeInput = (next: string) => {
|
||||
setInput(next);
|
||||
const normalized = normalizeHexColor(next);
|
||||
if (normalized) {
|
||||
const nextHsv = hexToHsv(normalized);
|
||||
hsvRef.current = nextHsv;
|
||||
setHsv(nextHsv);
|
||||
}
|
||||
};
|
||||
|
||||
const apply = () => {
|
||||
if (!validInput) return;
|
||||
onApply(validInput);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible
|
||||
transparent
|
||||
statusBarTranslucent
|
||||
navigationBarTranslucent
|
||||
animationType="none"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<GestureHandlerRootView style={styles.modalRoot}>
|
||||
<AppSheet onClose={onClose}>
|
||||
<AppSheetTitle
|
||||
title="Custom accent"
|
||||
subtitle="Drag to choose a color or enter an exact hex value."
|
||||
/>
|
||||
|
||||
<GestureDetector gesture={svGesture}>
|
||||
<View
|
||||
style={styles.sv}
|
||||
onLayout={onPickerLayout}
|
||||
accessibilityRole="adjustable"
|
||||
accessibilityLabel="Accent saturation and brightness"
|
||||
>
|
||||
<Canvas pointerEvents="none" style={StyleSheet.absoluteFill}>
|
||||
<Rect x={0} y={0} width={pickerWidth} height={SV_HEIGHT} color={hueColor} />
|
||||
<Rect x={0} y={0} width={pickerWidth} height={SV_HEIGHT}>
|
||||
<LinearGradient
|
||||
start={vec(0, 0)}
|
||||
end={vec(pickerWidth, 0)}
|
||||
colors={['#ffffff', '#ffffff00']}
|
||||
/>
|
||||
</Rect>
|
||||
<Rect x={0} y={0} width={pickerWidth} height={SV_HEIGHT}>
|
||||
<LinearGradient
|
||||
start={vec(0, 0)}
|
||||
end={vec(0, SV_HEIGHT)}
|
||||
colors={['#00000000', '#000000']}
|
||||
/>
|
||||
</Rect>
|
||||
</Canvas>
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.handle,
|
||||
{
|
||||
backgroundColor: previewHex,
|
||||
left: hsv.s * pickerWidth - HANDLE_SIZE / 2,
|
||||
top: (1 - hsv.v) * SV_HEIGHT - HANDLE_SIZE / 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
|
||||
<GestureDetector gesture={hueGesture}>
|
||||
<View
|
||||
style={styles.hue}
|
||||
accessibilityRole="adjustable"
|
||||
accessibilityLabel="Accent hue"
|
||||
>
|
||||
<Canvas pointerEvents="none" style={StyleSheet.absoluteFill}>
|
||||
<Rect x={0} y={0} width={pickerWidth} height={HUE_HEIGHT}>
|
||||
<LinearGradient
|
||||
start={vec(0, 0)}
|
||||
end={vec(pickerWidth, 0)}
|
||||
colors={HUE_COLORS}
|
||||
/>
|
||||
</Rect>
|
||||
</Canvas>
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
styles.hueHandle,
|
||||
{ left: (hsv.h / 360) * pickerWidth - HANDLE_SIZE / 2 },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
|
||||
<View style={styles.inputRow}>
|
||||
<View style={[styles.preview, { backgroundColor: validInput ?? previewHex }]} />
|
||||
<BottomSheetTextInput
|
||||
value={input}
|
||||
onChangeText={changeInput}
|
||||
placeholder="#5B8AFF"
|
||||
placeholderTextColor={colors.textTertiary}
|
||||
style={[styles.input, input.length > 0 && !validInput && styles.inputInvalid]}
|
||||
autoCapitalize="characters"
|
||||
autoCorrect={false}
|
||||
maxLength={7}
|
||||
returnKeyType="done"
|
||||
selectionColor={colors.accent}
|
||||
onSubmitEditing={apply}
|
||||
accessibilityLabel="Accent hex color"
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={validInput ? colors.textTertiary : colors.warning}
|
||||
style={styles.validation}
|
||||
>
|
||||
{validInput ? 'Use three or six hexadecimal digits.' : 'Enter a valid hex color.'}
|
||||
</Text>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
style={[styles.button, styles.cancel]}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Text variant="label" color={colors.textSecondary}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={ripple.onAccent()}
|
||||
style={[styles.button, styles.apply, !validInput && styles.disabled]}
|
||||
disabled={!validInput}
|
||||
onPress={apply}
|
||||
>
|
||||
<Text variant="label" color={colors.bgPrimary}>Apply</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</AppSheet>
|
||||
</GestureHandlerRootView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
modalRoot: {
|
||||
flex: 1,
|
||||
},
|
||||
sv: {
|
||||
height: SV_HEIGHT,
|
||||
borderRadius: radius.md,
|
||||
overflow: 'hidden',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
hue: {
|
||||
height: HUE_HEIGHT,
|
||||
borderRadius: radius.pill,
|
||||
overflow: 'hidden',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
handle: {
|
||||
position: 'absolute',
|
||||
width: HANDLE_SIZE,
|
||||
height: HANDLE_SIZE,
|
||||
borderRadius: HANDLE_SIZE / 2,
|
||||
borderWidth: 3,
|
||||
borderColor: '#ffffff',
|
||||
shadowColor: '#000000',
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 2,
|
||||
elevation: 3,
|
||||
},
|
||||
hueHandle: {
|
||||
position: 'absolute',
|
||||
top: (HUE_HEIGHT - HANDLE_SIZE) / 2,
|
||||
width: HANDLE_SIZE,
|
||||
height: HANDLE_SIZE,
|
||||
borderRadius: HANDLE_SIZE / 2,
|
||||
borderWidth: 3,
|
||||
borderColor: '#ffffff',
|
||||
backgroundColor: 'transparent',
|
||||
elevation: 3,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
preview: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: fonts.mono.medium,
|
||||
fontSize: 17,
|
||||
letterSpacing: 1,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm + 2,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
backgroundColor: colors.bgTertiary,
|
||||
},
|
||||
inputInvalid: {
|
||||
borderColor: colors.warning,
|
||||
},
|
||||
validation: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: spacing.sm,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
button: {
|
||||
minWidth: 92,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.pill,
|
||||
alignItems: 'center',
|
||||
},
|
||||
cancel: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
},
|
||||
apply: {
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
}));
|
||||
|
||||
export default AccentColorSheet;
|
||||
@@ -1,36 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Canvas, LinearGradient, Rect, vec } from '@shopify/react-native-skia';
|
||||
import { Text } from '@/components/Text';
|
||||
import { spacing } from '@/theme';
|
||||
import { ACCENTS, ACCENT_IDS, type AccentId } from '@/theme/accents';
|
||||
import { ACCENTS, ACCENT_IDS, accentPreferenceBase } from '@/theme/accents';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { playHaptic } from '@/lib/haptics';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { AccentColorSheet } from '@/components/settings/AccentColorSheet';
|
||||
|
||||
const SWATCH_SIZE = 36;
|
||||
|
||||
interface AccentSwatchRowProps {
|
||||
value: AccentId;
|
||||
onChange: (id: AccentId) => void;
|
||||
}
|
||||
const RAINBOW = ['#ff5c5c', '#ffb454', '#2dd4a0', '#00b3ff', '#9d7bff', '#ff6b9d'];
|
||||
|
||||
/** Circular accent swatches; the selected one gets a ring + checkmark. */
|
||||
export function AccentSwatchRow({ value, onChange }: AccentSwatchRowProps) {
|
||||
export function AccentSwatchRow() {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const preference = useThemeStore((s) => s.accentPreference);
|
||||
const setAccent = useThemeStore((s) => s.setAccent);
|
||||
const setCustomAccent = useThemeStore((s) => s.setCustomAccent);
|
||||
const previewCustomAccent = useThemeStore((s) => s.previewCustomAccent);
|
||||
const initialHex = accentPreferenceBase(preference);
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
<View style={styles.row}>
|
||||
{ACCENT_IDS.map((id) => {
|
||||
const selected = id === value;
|
||||
const selected = preference.kind === 'preset' && id === preference.id;
|
||||
return (
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
key={id}
|
||||
onPress={() => {
|
||||
if (selected) return;
|
||||
playHaptic('selection');
|
||||
onChange(id);
|
||||
void setAccent(id);
|
||||
}}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected }}
|
||||
@@ -48,10 +54,56 @@ export function AccentSwatchRow({ value, onChange }: AccentSwatchRowProps) {
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
onPress={() => {
|
||||
playHaptic('selection');
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected: preference.kind === 'custom' }}
|
||||
accessibilityLabel="Custom accent"
|
||||
style={[
|
||||
styles.swatch,
|
||||
preference.kind === 'custom' && { backgroundColor: preference.hex },
|
||||
preference.kind === 'custom' && styles.swatchSelected,
|
||||
]}
|
||||
hitSlop={4}
|
||||
>
|
||||
{preference.kind !== 'custom' ? (
|
||||
<Canvas pointerEvents="none" style={StyleSheet.absoluteFill}>
|
||||
<Rect x={0} y={0} width={SWATCH_SIZE} height={SWATCH_SIZE}>
|
||||
<LinearGradient
|
||||
start={vec(0, SWATCH_SIZE)}
|
||||
end={vec(SWATCH_SIZE, 0)}
|
||||
colors={RAINBOW}
|
||||
/>
|
||||
</Rect>
|
||||
</Canvas>
|
||||
) : null}
|
||||
{preference.kind === 'custom' ? (
|
||||
<Ionicons name="checkmark" size={18} color={colors.bgPrimary} />
|
||||
) : (
|
||||
<Ionicons name="add" size={18} color="#ffffff" />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text variant="caption" color={colors.textSecondary}>
|
||||
Accent · {ACCENTS[value].label}
|
||||
Accent · {preference.kind === 'preset'
|
||||
? ACCENTS[preference.id].label
|
||||
: `Custom ${preference.hex.toUpperCase()}`}
|
||||
</Text>
|
||||
{pickerOpen ? (
|
||||
<AccentColorSheet
|
||||
initialHex={initialHex}
|
||||
onPreview={previewCustomAccent}
|
||||
onApply={(hex) => void setCustomAccent(hex)}
|
||||
onClose={() => {
|
||||
previewCustomAccent(null);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -73,6 +125,7 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
justifyContent: 'center',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
swatchSelected: {
|
||||
borderWidth: 2,
|
||||
|
||||
@@ -24,6 +24,10 @@ import { useAudioSettingsStore } from '@/stores/audioSettingsStore';
|
||||
import { useLibraryStore, type FolderWithCount } from '@/stores/libraryStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import type {
|
||||
CoverArtAccentMethod,
|
||||
NowPlayingAccentSource,
|
||||
} from '@/stores/themeStore';
|
||||
import type { LastFmStatus } from '@/types/lastFm';
|
||||
import { Text } from '@/components/Text';
|
||||
import { showAppDialog } from '@/components/dialogs/AppDialog';
|
||||
@@ -66,6 +70,17 @@ const HOME_GREETING_SEGMENTS = [
|
||||
{ key: 'off', label: 'Off' },
|
||||
];
|
||||
|
||||
const NOW_PLAYING_ACCENT_SEGMENTS = [
|
||||
{ key: 'app', label: 'App' },
|
||||
{ key: 'cover-art', label: 'Cover Art' },
|
||||
];
|
||||
|
||||
const COVER_ART_METHOD_SEGMENTS = [
|
||||
{ key: 'dominant', label: 'Dominant' },
|
||||
{ key: 'vibrant', label: 'Vibrant' },
|
||||
{ key: 'average', label: 'Average' },
|
||||
];
|
||||
|
||||
const ARTIST_GROUPING_OPTIONS: { mode: ArtistGroupingMode; title: string; description: string }[] = [
|
||||
{
|
||||
mode: 'astra',
|
||||
@@ -95,12 +110,14 @@ export function AppearanceSettingsPanel() {
|
||||
const colors = useColors();
|
||||
const baseTheme = useThemeStore((s) => s.baseTheme);
|
||||
const preferredDark = useThemeStore((s) => s.preferredDark);
|
||||
const accentId = useThemeStore((s) => s.accentId);
|
||||
const materialYouAvailable = useThemeStore((s) => s.materialYouAvailable);
|
||||
const resolvedId = useThemeStore((s) => s.theme.id);
|
||||
const nowPlayingAccentSource = useThemeStore((s) => s.nowPlayingAccentSource);
|
||||
const coverArtAccentMethod = useThemeStore((s) => s.coverArtAccentMethod);
|
||||
const setBaseTheme = useThemeStore((s) => s.setBaseTheme);
|
||||
const setPreferredDark = useThemeStore((s) => s.setPreferredDark);
|
||||
const setAccent = useThemeStore((s) => s.setAccent);
|
||||
const setNowPlayingAccentSource = useThemeStore((s) => s.setNowPlayingAccentSource);
|
||||
const setCoverArtAccentMethod = useThemeStore((s) => s.setCoverArtAccentMethod);
|
||||
|
||||
const options = THEME_OPTIONS.filter(
|
||||
(option) => option.id !== 'materialYou' || materialYouAvailable
|
||||
@@ -161,10 +178,41 @@ export function AppearanceSettingsPanel() {
|
||||
|
||||
{accentApplies ? (
|
||||
<View style={styles.appearanceBlock}>
|
||||
<AccentSwatchRow value={accentId} onChange={(id) => void setAccent(id)} />
|
||||
<AccentSwatchRow />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<SettingsSectionLabel spaced>NOW PLAYING ACCENT</SettingsSectionLabel>
|
||||
<SettingsCard>
|
||||
<Text variant="body" style={styles.settingTitle}>
|
||||
Accent source
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.settingNote}>
|
||||
Cover Art colors only the open player. The app accent is used when artwork is unavailable.
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
segments={NOW_PLAYING_ACCENT_SEGMENTS}
|
||||
value={nowPlayingAccentSource}
|
||||
onChange={(key) =>
|
||||
void setNowPlayingAccentSource(key as NowPlayingAccentSource)
|
||||
}
|
||||
/>
|
||||
{nowPlayingAccentSource === 'cover-art' ? (
|
||||
<View style={styles.coverArtMethod}>
|
||||
<Text variant="caption" color={colors.textSecondary}>
|
||||
Cover art method
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
segments={COVER_ART_METHOD_SEGMENTS}
|
||||
value={coverArtAccentMethod}
|
||||
onChange={(key) =>
|
||||
void setCoverArtAccentMethod(key as CoverArtAccentMethod)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsSectionLabel spaced>HOME</SettingsSectionLabel>
|
||||
<SettingsCard>
|
||||
<Text variant="body" style={styles.settingTitle}>
|
||||
@@ -496,6 +544,10 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
settingNote: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
coverArtMethod: {
|
||||
gap: spacing.sm,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
options: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user