feat: Standardize Dialogs with Material 3 Patterns

This commit is contained in:
Rajarshee Chatterjee
2026-07-23 23:05:53 +05:30
parent a5a6e8326f
commit dfd620aa29
51 changed files with 1606 additions and 1736 deletions
@@ -1,10 +1,9 @@
import React, { useState } from 'react';
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
import { Portal, TextInput } from 'react-native-paper';
import { Button, Modal } from '@components';
import { TextInput } from 'react-native-paper';
import { Dialog } from '@components';
import { ThemeColors } from '../../theme/types';
import { Row } from '@components/Common';
import { getString } from '@strings/translations';
interface ColorPickerModalProps {
@@ -78,14 +77,12 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
];
return (
<Portal>
<Modal visible={visible} onDismiss={onDismiss}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
{showAccentColors ? (
<Dialog.Root visible={visible} onDismiss={onDismiss}>
<Dialog.Title>{title}</Dialog.Title>
{showAccentColors ? (
<Dialog.ScrollArea>
<FlatList
contentContainerStyle={styles.marginBottom}
contentContainerStyle={styles.colorList}
data={accentColors}
numColumns={4}
keyExtractor={item => item}
@@ -104,7 +101,9 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
</View>
)}
/>
) : null}
</Dialog.ScrollArea>
) : null}
<Dialog.Content>
<TextInput
value={text}
defaultValue={typeof color === 'string' ? color : ''}
@@ -118,12 +117,15 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
error={Boolean(error)}
/>
<Text style={styles.errorText}>{error}</Text>
<Row style={styles.row}>
<Button title={getString('common.reset')} onPress={onReset} />
<Button title={getString('common.save')} onPress={onSubmitEditing} />
</Row>
</Modal>
</Portal>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action title={getString('common.reset')} onPress={onReset} />
<Dialog.Action
title={getString('common.save')}
onPress={onSubmitEditing}
/>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -134,10 +136,6 @@ const styles = StyleSheet.create({
color: '#FF0033',
paddingTop: 8,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
item: {
borderRadius: 4,
overflow: 'hidden',
@@ -148,8 +146,8 @@ const styles = StyleSheet.create({
marginVertical: 4,
},
flex: { flex: 1 },
marginBottom: { marginBottom: 8 },
row: {
justifyContent: 'flex-end',
colorList: {
paddingHorizontal: 20,
paddingVertical: 4,
},
});
@@ -1,71 +1,49 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { getString } from '@strings/translations';
import { Dialog, Portal } from 'react-native-paper';
import { ThemeColors } from '../../theme/types';
import Button from '../Button/Button';
import { Dialog, DialogActionTone } from '../Dialog';
interface ConfirmationDialogProps {
title?: string;
title: string;
message?: string;
visible: boolean;
theme: ThemeColors;
onSubmit: () => void;
confirmLabel: string;
cancelLabel?: string;
confirmTone?: DialogActionTone;
onConfirm: () => void | Promise<void>;
onDismiss: () => void;
}
const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
title = getString('common.warning'),
title,
message,
visible,
confirmLabel,
cancelLabel = getString('common.cancel'),
confirmTone = 'danger',
onDismiss,
theme,
onSubmit,
onConfirm,
}) => {
const handleOnSubmit = () => {
onSubmit();
const handleConfirm = () => {
void onConfirm();
onDismiss();
};
return (
<Portal>
<Dialog
visible={visible}
onDismiss={onDismiss}
style={[styles.container, { backgroundColor: theme.overlay3 }]}
>
<Dialog.Title style={{ color: theme.onSurface }}>{title}</Dialog.Title>
{message ? (
<Dialog.Content>
<Text style={[styles.content, { color: theme.onSurface }]}>
{message}
</Text>
</Dialog.Content>
) : null}
<View style={styles.buttonCtn}>
<Button onPress={handleOnSubmit} title={getString('common.ok')} />
<Button onPress={onDismiss} title={getString('common.cancel')} />
</View>
</Dialog>
</Portal>
<Dialog.Root visible={visible} onDismiss={onDismiss}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
{message ? <Dialog.Description>{message}</Dialog.Description> : null}
</Dialog.Header>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>{cancelLabel}</Dialog.Action>
<Dialog.Action tone={confirmTone} onPress={handleConfirm}>
{confirmLabel}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default ConfirmationDialog;
const styles = StyleSheet.create({
buttonCtn: {
flexDirection: 'row-reverse',
padding: 16,
},
container: {
borderRadius: 28,
shadowColor: 'transparent',
},
content: {
fontSize: 14,
letterSpacing: 0,
},
});
+282
View File
@@ -0,0 +1,282 @@
import React, { PropsWithChildren } from 'react';
import {
KeyboardAvoidingView,
Modal as NativeModal,
Platform,
Pressable,
StyleProp,
StyleSheet,
Text,
TextProps,
View,
ViewProps,
ViewStyle,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Color from 'color';
import { useTheme } from '@hooks/persisted';
import Button from '../Button/Button';
interface DialogRootProps extends PropsWithChildren {
visible: boolean;
onDismiss: () => void;
surfaceStyle?: StyleProp<ViewStyle>;
testID?: string;
}
interface DialogSectionProps extends ViewProps {
children: React.ReactNode;
}
interface DialogTextProps extends TextProps {
children: React.ReactNode;
}
type DialogActionTone = 'primary' | 'danger';
interface DialogActionProps extends React.ComponentProps<typeof Button> {
tone?: DialogActionTone;
}
const DialogRoot = ({
children,
visible,
onDismiss,
surfaceStyle,
testID = 'dialog',
}: DialogRootProps) => {
const theme = useTheme();
const backdropColor = Color(theme.scrim ?? '#000000')
.alpha(0.32)
.string();
return (
<NativeModal
animationType="fade"
hardwareAccelerated
navigationBarTranslucent
onRequestClose={onDismiss}
statusBarTranslucent
transparent
visible={visible}
>
<View style={[styles.modal, { backgroundColor: backdropColor }]}>
<Pressable
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
onPress={onDismiss}
style={StyleSheet.absoluteFill}
testID={`${testID}-backdrop`}
/>
<SafeAreaView pointerEvents="box-none" style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
pointerEvents="box-none"
style={styles.keyboardAvoidingView}
>
<View
pointerEvents="box-none"
style={styles.viewport}
testID={`${testID}-viewport`}
>
<View
accessibilityViewIsModal
style={[
styles.surface,
{
backgroundColor:
theme.surfaceContainerHigh ?? theme.surface,
},
surfaceStyle,
]}
testID={testID}
>
{children}
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
</NativeModal>
);
};
const DialogHeader = ({ children, style, ...props }: DialogSectionProps) => (
<View style={[styles.header, style]} {...props}>
{children}
</View>
);
const DialogTitle = ({ children, style, ...props }: DialogTextProps) => {
const theme = useTheme();
return (
<Text
accessibilityRole="header"
style={[styles.title, { color: theme.onSurface }, style]}
{...props}
>
{children}
</Text>
);
};
const DialogDescription = ({ children, style, ...props }: DialogTextProps) => {
const theme = useTheme();
return (
<Text
style={[styles.description, { color: theme.onSurfaceVariant }, style]}
{...props}
>
{children}
</Text>
);
};
const DialogContent = ({ children, style, ...props }: DialogSectionProps) => (
<View style={[styles.content, style]} {...props}>
{children}
</View>
);
const DialogList = ({ children, style, ...props }: DialogSectionProps) => (
<View style={[styles.list, style]} {...props}>
{children}
</View>
);
const DialogScrollArea = ({
children,
style,
testID = 'dialog-scroll-area',
...props
}: DialogSectionProps) => {
const theme = useTheme();
const dividerStyle = { backgroundColor: theme.outlineVariant };
return (
<View style={[styles.scrollArea, style]} testID={testID} {...props}>
<View
importantForAccessibility="no"
style={[styles.divider, dividerStyle]}
testID={`${testID}-top-divider`}
/>
{children}
<View
importantForAccessibility="no"
style={[styles.divider, dividerStyle]}
testID={`${testID}-bottom-divider`}
/>
</View>
);
};
const DialogActions = ({ children, style, ...props }: DialogSectionProps) => (
<View style={[styles.actions, style]} {...props}>
{children}
</View>
);
const DialogAction = ({
tone = 'primary',
textColor,
...props
}: DialogActionProps) => {
const theme = useTheme();
return (
<Button
textColor={textColor ?? (tone === 'danger' ? theme.error : theme.primary)}
{...props}
/>
);
};
export const Dialog = {
Root: DialogRoot,
Header: DialogHeader,
Title: DialogTitle,
Description: DialogDescription,
Content: DialogContent,
List: DialogList,
ScrollArea: DialogScrollArea,
Actions: DialogActions,
Action: DialogAction,
} as const;
export type {
DialogActionProps,
DialogActionTone,
DialogRootProps,
DialogSectionProps,
DialogTextProps,
};
const styles = StyleSheet.create({
modal: {
flex: 1,
},
safeArea: {
flex: 1,
},
keyboardAvoidingView: {
flex: 1,
},
viewport: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
padding: 24,
},
surface: {
borderCurve: 'continuous',
borderRadius: 28,
gap: 16,
maxHeight: '90%',
maxWidth: 560,
minWidth: 280,
padding: 24,
width: '100%',
},
header: {
gap: 16,
},
title: {
fontSize: 24,
fontWeight: '600',
letterSpacing: 0,
lineHeight: 32,
},
description: {
fontSize: 14,
letterSpacing: 0,
lineHeight: 20,
},
content: {
flexShrink: 1,
gap: 16,
},
list: {
flexShrink: 1,
marginHorizontal: -24,
},
scrollArea: {
flexShrink: 1,
marginHorizontal: -24,
},
divider: {
height: 1,
width: '100%',
},
actions: {
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
justifyContent: 'flex-end',
marginTop: 8,
},
});
+8
View File
@@ -0,0 +1,8 @@
export { Dialog } from './Dialog';
export type {
DialogActionProps,
DialogActionTone,
DialogRootProps,
DialogSectionProps,
DialogTextProps,
} from './Dialog';
@@ -1,25 +0,0 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { useTheme } from '@hooks/persisted';
interface DialogTitleProps {
title: string;
}
export const DialogTitle: React.FC<DialogTitleProps> = ({ title }) => {
const theme = useTheme();
return (
<Text style={[styles.dialogTitle, { color: theme.onSurface }]}>
{title}
</Text>
);
};
const styles = StyleSheet.create({
dialogTitle: {
fontSize: 24,
marginBottom: 16,
},
});
-1
View File
@@ -1 +0,0 @@
export { DialogTitle } from './DialogTitle';
-39
View File
@@ -1,39 +0,0 @@
import { useTheme } from '@hooks/persisted';
import React from 'react';
import { StyleSheet } from 'react-native';
import { ModalProps, overlay, Modal as PaperModal } from 'react-native-paper';
const Modal: React.FC<ModalProps> = ({
children,
visible,
onDismiss,
contentContainerStyle,
...props
}) => {
const theme = useTheme();
return (
<PaperModal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: overlay(2, theme.surface) },
contentContainerStyle,
]}
{...props}
>
{children}
</PaperModal>
);
};
const styles = StyleSheet.create({
modalContainer: {
borderRadius: 28,
margin: 30,
padding: 24,
shadowColor: 'transparent', // Modal weird shadow fix
},
});
export default Modal;
+29 -35
View File
@@ -1,13 +1,11 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text, Dimensions } from 'react-native';
import { Portal } from 'react-native-paper';
import { StyleSheet, Text, useWindowDimensions } from 'react-native';
import * as Linking from 'expo-linking';
import { ScrollView } from 'react-native-gesture-handler';
import Button from './Button/Button';
import { getString } from '@strings/translations';
import { useTheme } from '@hooks/persisted';
import { Modal } from '@components';
import { Dialog } from '@components';
interface NewUpdateDialogProps {
newVersion: {
@@ -22,34 +20,37 @@ const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
const theme = useTheme();
const modalHeight = Dimensions.get('window').height / 2;
const modalHeight = useWindowDimensions().height / 2;
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
</Text>
<ScrollView style={{ height: modalHeight }}>
<Dialog.Root
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
>
<Dialog.Title>
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
</Dialog.Title>
<Dialog.ScrollArea>
<ScrollView
contentContainerStyle={styles.scrollContent}
style={{ height: modalHeight }}
>
<Text style={[styles.body, { color: theme.onSurfaceVariant }]}>
{newVersion.body.split('\n').join('\n\n')}
</Text>
</ScrollView>
<View style={styles.buttonCtn}>
<Button
title={getString('common.cancel')}
onPress={() => showNewUpdateDialog(false)}
/>
<Button
title={getString('common.install')}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
/>
</View>
</Modal>
</Portal>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action
title={getString('common.cancel')}
onPress={() => showNewUpdateDialog(false)}
/>
<Dialog.Action
title={getString('common.install')}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
/>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -60,14 +61,7 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: '500',
},
buttonCtn: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 16,
},
modalHeader: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
scrollContent: {
paddingHorizontal: 24,
},
});
+126
View File
@@ -0,0 +1,126 @@
import './mocks';
import { fireEvent, render, screen } from '@testing-library/react-native';
import { Dialog } from '../Dialog';
import ConfirmationDialog from '../ConfirmationDialog/ConfirmationDialog';
const mockUseTheme = jest.fn();
jest.mock('@hooks/persisted', () => ({
useTheme: () => mockUseTheme(),
}));
describe('Dialog', () => {
beforeEach(() => {
mockUseTheme.mockReturnValue({
error: '#ba1a1a',
onSurface: '#1d1b20',
onSurfaceVariant: '#49454f',
outlineVariant: '#cac4d0',
primary: '#6750a4',
scrim: '#000000',
surface: '#fffbfe',
surfaceContainerHigh: '#ece6f0',
surface2: '#f7f2fa',
});
});
it('renders composed dialog sections', () => {
render(
<Dialog.Root visible onDismiss={() => {}}>
<Dialog.Header testID="dialog-header">
<Dialog.Title>Dialog title</Dialog.Title>
<Dialog.Description>Dialog description</Dialog.Description>
</Dialog.Header>
<Dialog.Content>
<></>
</Dialog.Content>
<Dialog.List testID="dialog-list">
<></>
</Dialog.List>
<Dialog.ScrollArea testID="scroll-area">
<></>
</Dialog.ScrollArea>
<Dialog.Actions testID="dialog-actions">
<Dialog.Action onPress={() => {}}>Save</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>,
);
expect(screen.getByRole('header', { name: 'Dialog title' })).toBeTruthy();
expect(screen.getByText('Dialog description')).toBeTruthy();
expect(screen.getByText('Save')).toBeTruthy();
expect(
screen.getByTestId('dialog', { includeHiddenElements: true }),
).toHaveStyle({
backgroundColor: '#ece6f0',
minWidth: 280,
});
expect(screen.getByTestId('dialog-header')).toHaveStyle({ gap: 16 });
expect(screen.getByTestId('dialog-actions')).toHaveStyle({
gap: 8,
marginTop: 8,
});
expect(screen.getByTestId('dialog-list')).toHaveStyle({
marginHorizontal: -24,
});
expect(screen.getByTestId('scroll-area-top-divider')).toHaveStyle({
backgroundColor: '#cac4d0',
height: 1,
});
expect(screen.getByTestId('scroll-area-bottom-divider')).toHaveStyle({
backgroundColor: '#cac4d0',
height: 1,
});
});
it('lets backdrop presses pass through the full-screen viewport', () => {
render(
<Dialog.Root visible onDismiss={() => {}}>
<Dialog.Title>Dialog title</Dialog.Title>
</Dialog.Root>,
);
expect(screen.getByTestId('dialog-viewport').props.pointerEvents).toBe(
'box-none',
);
});
it('dismisses when the backdrop is pressed', () => {
const onDismiss = jest.fn();
render(
<Dialog.Root visible onDismiss={onDismiss}>
<Dialog.Title>Dialog title</Dialog.Title>
</Dialog.Root>,
);
fireEvent.press(
screen.getByTestId('dialog-backdrop', {
includeHiddenElements: true,
}),
);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it('runs and dismisses a confirmation action', () => {
const onConfirm = jest.fn();
const onDismiss = jest.fn();
render(
<ConfirmationDialog
title="Delete item?"
confirmLabel="Delete"
visible
onConfirm={onConfirm}
onDismiss={onDismiss}
/>,
);
fireEvent.press(screen.getByText('Delete'));
expect(onConfirm).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});
+8 -2
View File
@@ -13,10 +13,16 @@ export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPrefe
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
export { Checkbox } from './Checkbox/Checkbox';
export { RadioButton } from './RadioButton/RadioButton';
export { Dialog } from './Dialog';
export type {
DialogActionProps,
DialogActionTone,
DialogRootProps,
DialogSectionProps,
DialogTextProps,
} from './Dialog';
export { default as ConfirmationDialog } from './ConfirmationDialog/ConfirmationDialog';
export { default as SafeAreaView } from './SafeAreaView/SafeAreaView';
export { default as Modal } from './Modal/Modal';
export { SegmentedControl } from './SegmentedControl';
export { DialogTitle } from './DialogTitle';
export { default as BottomTabBar } from './BottomTabBar';
export { default as Menu } from './Menu';
+7
View File
@@ -33,8 +33,15 @@ const getElevationColor = (colors: ThemeColors, elevation: number): string => {
.string();
};
const getSurfaceContainerHigh = (colors: ThemeColors): string =>
Color(colors.surface)
.mix(Color(colors.onSurface), colors.isDark ? 0.1 : 0.08)
.rgb()
.string();
const addComputedColors = (colors: ThemeColors): ThemeColors => ({
...colors,
surfaceContainerHigh: getSurfaceContainerHigh(colors),
surface2: getElevationColor(colors, 0.08),
overlay3: overlay(3, colors.surface),
rippleColor: Color(colors.primary).alpha(0.12).toString(),
@@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Portal, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { Button, Modal } from '@components/index';
import { Dialog } from '@components/index';
import { Category } from '../../../database/types';
import {
@@ -43,15 +42,13 @@ const AddCategoryModal: React.FC<AddCategoryModalProps> = ({
}
return (
<Portal>
<Modal visible={visible} onDismiss={close}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString(
isEditMode
? 'categories.editCategories'
: 'categories.addCategories',
)}
</Text>
<Dialog.Root visible={visible} onDismiss={close}>
<Dialog.Title>
{getString(
isEditMode ? 'categories.editCategories' : 'categories.addCategories',
)}
</Dialog.Title>
<Dialog.Content>
<TextInput
autoFocus
defaultValue={categoryName}
@@ -61,38 +58,30 @@ const AddCategoryModal: React.FC<AddCategoryModalProps> = ({
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
/>
<View style={styles.btnContainer}>
<Button
title={getString(isEditMode ? 'common.ok' : 'common.add')}
onPress={async () => {
if (isCategoryNameDuplicate(categoryName)) {
showToast(getString('categories.duplicateError'));
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action onPress={close}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action
onPress={async () => {
if (isCategoryNameDuplicate(categoryName)) {
showToast(getString('categories.duplicateError'));
} else {
if (isEditMode && category) {
updateCategory(category?.id, categoryName);
} else {
if (isEditMode && category) {
updateCategory(category?.id, categoryName);
} else {
await createCategory(categoryName);
}
finalize();
await createCategory(categoryName);
}
}}
/>
<Button title={getString('common.cancel')} onPress={close} />
</View>
</Modal>
</Portal>
finalize();
}
}}
>
{getString(isEditMode ? 'common.ok' : 'common.add')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default AddCategoryModal;
const styles = StyleSheet.create({
btnContainer: {
flexDirection: 'row-reverse',
marginTop: 24,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
});
@@ -1,13 +1,9 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Portal } from 'react-native-paper';
import { Button, Modal } from '@components/index';
import { ConfirmationDialog } from '@components/index';
import { Category } from '@database/types';
import { deleteCategoryById } from '@database/queries/CategoryQueries';
import { useTheme } from '@hooks/persisted';
import { getString } from '@strings/translations';
interface DeleteCategoryModalProps {
@@ -23,43 +19,21 @@ const DeleteCategoryModal: React.FC<DeleteCategoryModalProps> = ({
visible,
onSuccess,
}) => {
const theme = useTheme();
return (
<Portal>
<Modal visible={visible} onDismiss={closeModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('categories.deleteModal.header')}
</Text>
<Text style={[styles.modalDesc, { color: theme.onSurfaceVariant }]}>
{getString('categories.deleteModal.desc')}
{` "${category.name}"?`}
</Text>
<View style={styles.btnContainer}>
<Button
title={getString('common.ok')}
onPress={() => {
deleteCategoryById(category);
closeModal();
onSuccess();
}}
/>
<Button title={getString('common.cancel')} onPress={closeModal} />
</View>
</Modal>
</Portal>
<ConfirmationDialog
title={getString('categories.deleteModal.header')}
confirmLabel={getString('common.delete')}
message={`${getString('categories.deleteModal.desc')} "${
category.name
}"?`}
visible={visible}
onDismiss={closeModal}
onConfirm={() => {
deleteCategoryById(category);
void onSuccess();
}}
/>
);
};
export default DeleteCategoryModal;
const styles = StyleSheet.create({
btnContainer: {
flexDirection: 'row-reverse',
marginTop: 24,
},
modalDesc: {},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
});
+3 -2
View File
@@ -174,13 +174,14 @@ const PluginDetailsScreen = ({
) : null}
</ScrollView>
<ConfirmationDialog
title={getString('browseScreen.uninstall')}
confirmLabel={getString('browseScreen.uninstall')}
visible={showUninstallDialog}
message={getString('browseScreen.deletePluginMessage', {
name: plugin.name,
})}
onDismiss={() => setShowUninstallDialog(false)}
onSubmit={uninstall}
theme={theme}
onConfirm={uninstall}
/>
</SafeAreaView>
);
@@ -1,10 +1,9 @@
import React, { useState } from 'react';
import { StyleSheet, FlatList, Text, View, FlatListProps } from 'react-native';
import { Portal } from 'react-native-paper';
import { useState } from 'react';
import { StyleSheet, FlatList, Text, FlatListProps } from 'react-native';
import GlobalSearchNovelCover from '../globalsearch/GlobalSearchNovelCover';
import { showToast } from '@utils/showToast';
import { Button, Modal } from '@components';
import { Dialog } from '@components';
import { getString } from '@strings/translations';
import { MigrateNovelScreenProps } from '@navigators/types';
import { NovelInfo } from '@database/types';
@@ -89,42 +88,37 @@ const MigrationNovelList = ({
</Text>
}
/>
<Portal>
<Modal visible={migrateNovelDialog} onDismiss={hideMigrateNovelDialog}>
<Text
style={[
{
color: theme.onSurface,
},
styles.text,
]}
>
{getString('browseScreen.migration.dialogMessage', {
url: selectedNovel.name,
})}
</Text>
<View style={styles.row}>
<Button
onPress={hideMigrateNovelDialog}
title={getString('common.cancel')}
/>
<Button
onPress={() => {
hideMigrateNovelDialog();
backgroundTasks.enqueue({
name: 'MIGRATE_NOVEL',
data: {
pluginId,
fromNovel,
toNovelPath: selectedNovel.path,
},
});
}}
title={getString('novelScreen.migrate')}
/>
</View>
</Modal>
</Portal>
<Dialog.Root
visible={migrateNovelDialog}
onDismiss={hideMigrateNovelDialog}
>
<Dialog.Title>{getString('novelScreen.migrate')}</Dialog.Title>
<Dialog.Description>
{getString('browseScreen.migration.dialogMessage', {
url: selectedNovel.name,
})}
</Dialog.Description>
<Dialog.Actions>
<Dialog.Action
onPress={hideMigrateNovelDialog}
title={getString('common.cancel')}
/>
<Dialog.Action
onPress={() => {
hideMigrateNovelDialog();
backgroundTasks.enqueue({
name: 'MIGRATE_NOVEL',
data: {
pluginId,
fromNovel,
toNovelPath: selectedNovel.path,
},
});
}}
title={getString('novelScreen.migrate')}
/>
</Dialog.Actions>
</Dialog.Root>
</>
);
};
@@ -137,13 +131,5 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
paddingVertical: 8,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
text: {
fontSize: 18,
marginBottom: 16,
},
padding: { padding: 8, paddingVertical: 4 },
});
@@ -1,13 +1,10 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { Dialog, RadioButton } from '@components';
import { ThemeColors } from '@theme/types';
import { getString } from '@strings/translations';
import { useBrowseSettings } from '@hooks/persisted/index';
import { Modal } from '@components';
interface DisplayModeModalProps {
globalSearchConcurrency: number;
@@ -25,33 +22,33 @@ const ConcurrentSearchesModal: React.FC<DisplayModeModalProps> = ({
const { setBrowseSettings } = useBrowseSettings();
return (
<Portal>
<Modal visible={modalVisible} onDismiss={hideModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{getString('browseSettingsScreen.concurrentSearches')}
</Text>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(concurrency => (
<RadioButton
key={concurrency}
status={globalSearchConcurrency === concurrency}
onPress={() =>
setBrowseSettings({ globalSearchConcurrency: concurrency })
}
label={concurrency.toString()}
theme={theme}
/>
))}
</Modal>
</Portal>
<Dialog.Root visible={modalVisible} onDismiss={hideModal}>
<Dialog.Title>
{getString('browseSettingsScreen.concurrentSearches')}
</Dialog.Title>
<Dialog.ScrollArea>
<ScrollView style={styles.list}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(concurrency => (
<RadioButton
key={concurrency}
status={globalSearchConcurrency === concurrency}
onPress={() =>
setBrowseSettings({ globalSearchConcurrency: concurrency })
}
label={concurrency.toString()}
theme={theme}
/>
))}
</ScrollView>
</Dialog.ScrollArea>
</Dialog.Root>
);
};
export default ConcurrentSearchesModal;
const styles = StyleSheet.create({
modalHeader: {
fontSize: 24,
marginBottom: 10,
paddingHorizontal: 24,
list: {
maxHeight: 480,
},
});
+6 -10
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { StyleSheet, SectionList, Text } from 'react-native';
import dayjs from 'dayjs';
import { Portal } from 'react-native-paper';
import {
EmptyView,
@@ -143,14 +142,11 @@ const HistoryScreen = ({ navigation }: HistoryScreenProps) => {
/>
}
/>
<Portal>
<ClearHistoryDialog
visible={clearHistoryDialogVisible}
onSubmit={clearAllHistory}
onDismiss={closeClearHistoryDialog}
theme={theme}
/>
</Portal>
<ClearHistoryDialog
visible={clearHistoryDialogVisible}
onSubmit={clearAllHistory}
onDismiss={closeClearHistoryDialog}
/>
</>
)}
</SafeAreaView>
@@ -1,14 +1,10 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { Dialog, Portal } from 'react-native-paper';
import { Button } from '@components';
import { ConfirmationDialog } from '@components';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
interface ClearHistoryDialogProps {
visible: boolean;
theme: ThemeColors;
onSubmit: () => void;
onDismiss: () => void;
}
@@ -16,45 +12,16 @@ interface ClearHistoryDialogProps {
const ClearHistoryDialog: React.FC<ClearHistoryDialogProps> = ({
visible,
onDismiss,
theme,
onSubmit,
}) => {
const handleOnSubmit = () => {
onSubmit();
onDismiss();
};
return (
<Portal>
<Dialog
visible={visible}
onDismiss={onDismiss}
style={[styles.container, { backgroundColor: theme.overlay3 }]}
>
<Dialog.Title style={[styles.title, { color: theme.onSurface }]}>
{getString('historyScreen.clearHistorWarning')}
</Dialog.Title>
<Dialog.Actions>
<Button onPress={onDismiss}>{getString('common.cancel')}</Button>
<Button onPress={handleOnSubmit}>{getString('common.ok')}</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
}) => (
<ConfirmationDialog
title={getString('common.clear')}
message={getString('historyScreen.clearHistorWarning')}
confirmLabel={getString('common.clear')}
visible={visible}
onConfirm={onSubmit}
onDismiss={onDismiss}
/>
);
export default ClearHistoryDialog;
const styles = StyleSheet.create({
button: {
marginLeft: 4,
},
container: {
borderRadius: 28,
margin: 20,
},
title: {
fontSize: 16,
letterSpacing: 0,
},
});
+1 -2
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { Appbar as MaterialAppbar } from 'react-native-paper';
@@ -139,7 +139,6 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => {
setChapters([]);
hideDialog();
}}
theme={theme}
/>
</SafeAreaView>
);
@@ -1,61 +1,25 @@
import React from 'react';
import { Button } from '@components';
import { Dialog, overlay, Portal } from 'react-native-paper';
import { ThemeColors } from '@theme/types';
import { ConfirmationDialog } from '@components';
import { getString } from '@strings/translations';
import { StyleSheet } from 'react-native';
interface RemoveDownloadsDialogProps {
dialogVisible: boolean;
hideDialog: () => void;
theme: ThemeColors;
onSubmit: () => void;
}
const RemoveDownloadsDialog = ({
dialogVisible,
hideDialog,
theme,
onSubmit,
}: RemoveDownloadsDialogProps) => {
return (
<Portal>
<Dialog
visible={dialogVisible}
onDismiss={hideDialog}
style={[
{
backgroundColor: overlay(2, theme.surface),
},
styles.borderRadius,
]}
>
<Dialog.Title
style={[
{
color: theme.onSurface,
},
styles.fontSize,
]}
>
{getString('downloadScreen.removeDownloadsWarning')}
</Dialog.Title>
<Dialog.Actions>
<Button onPress={hideDialog}>{getString('common.cancel')}</Button>
<Button onPress={onSubmit}>{getString('common.ok')}</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
}: RemoveDownloadsDialogProps) => (
<ConfirmationDialog
title={`${getString('common.remove')} ${getString('common.downloads')}`}
message={getString('downloadScreen.removeDownloadsWarning')}
confirmLabel={getString('common.remove')}
visible={dialogVisible}
onConfirm={onSubmit}
onDismiss={hideDialog}
/>
);
export default RemoveDownloadsDialog;
const styles = StyleSheet.create({
fontSize: {
letterSpacing: 0,
fontSize: 16,
},
borderRadius: { borderRadius: 6 },
});
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View, TextInput } from 'react-native';
import { useState } from 'react';
import { StyleSheet, View, TextInput } from 'react-native';
import { Button, IconButton, Portal } from 'react-native-paper';
import { IconButton } from 'react-native-paper';
import { ThemeColors } from '@theme/types';
import { ChapterInfo, NovelInfo } from '@database/types';
import { getString } from '@strings/translations';
import { Modal } from '@components';
import { Dialog } from '@components';
interface DownloadCustomChapterModalProps {
theme: ThemeColors;
@@ -48,25 +48,33 @@ const DownloadCustomChapterModal = ({
};
return (
<Portal>
<Modal visible={modalVisible} onDismiss={onDismiss}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.download.customAmount')}
</Text>
<Dialog.Root visible={modalVisible} onDismiss={onDismiss}>
<Dialog.Title>
{getString('novelScreen.download.customAmount')}
</Dialog.Title>
<Dialog.Content>
<View style={styles.row}>
<IconButton
icon="chevron-double-left"
animated
size={24}
iconColor={theme.primary}
onPress={() => text > 9 && setText(prevState => prevState - 10)}
onPress={() => {
if (text > 9) {
setText(prevState => prevState - 10);
}
}}
/>
<IconButton
icon="chevron-left"
animated
size={24}
iconColor={theme.primary}
onPress={() => text > 0 && setText(prevState => prevState - 1)}
onPress={() => {
if (text > 0) {
setText(prevState => prevState - 1);
}
}}
/>
<TextInput
value={text.toString()}
@@ -90,29 +98,21 @@ const DownloadCustomChapterModal = ({
onPress={() => setText(prevState => prevState + 10)}
/>
</View>
<Button
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action title={getString('common.cancel')} onPress={onDismiss} />
<Dialog.Action
title={getString('libraryScreen.bottomSheet.display.download')}
onPress={onSubmit}
textColor={theme.onPrimary}
buttonColor={theme.primary}
>
{getString('libraryScreen.bottomSheet.display.download')}
</Button>
</Modal>
</Portal>
/>
</Dialog.Actions>
</Dialog.Root>
);
};
export default DownloadCustomChapterModal;
const styles = StyleSheet.create({
errorText: {
color: '#FF0033',
paddingTop: 8,
},
modalTitle: {
fontSize: 16,
marginBottom: 16,
},
row: { flexDirection: 'row', justifyContent: 'center' },
marginHorizontal: { marginHorizontal: 4 },
});
+156 -160
View File
@@ -9,11 +9,11 @@ import {
} from 'react-native';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import { Portal, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { updateNovelInfo } from '@database/queries/NovelQueries';
import { getString } from '@strings/translations';
import { Button, Modal } from '@components';
import { Dialog } from '@components';
import { ThemeColors } from '@theme/types';
import { NovelInfo } from '@database/types';
import { NovelStatus } from '@plugins/types';
@@ -28,7 +28,6 @@ interface EditInfoModalProps {
}
// --- Dynamic style helpers ---
const getModalTitleColor = (theme: ThemeColors) => ({ color: theme.onSurface });
const getStatusLabelColor = (theme: ThemeColors) => ({
color: theme.onSurfaceVariant,
});
@@ -41,8 +40,6 @@ const getStatusChipText = (selected: boolean, theme: ThemeColors) => ({
color: selected ? theme.primary : theme.onSurfaceVariant,
});
const getGenreListStyle = () => styles.genreList;
const getButtonRowStyle = () => styles.buttonRow;
const getFlex1 = () => styles.flex1;
// --- Main Component ---
const EditInfoModal = ({
@@ -70,156 +67,162 @@ const EditInfoModal = ({
const status = Object.values(NovelStatus);
return (
<Portal>
<Modal visible={modalVisible} onDismiss={hideModal}>
<Text style={[styles.modalTitle, getModalTitleColor(theme)]}>
{getString('novelScreen.edit.info')}
</Text>
<View style={styles.statusRow}>
<Text style={getStatusLabelColor(theme)}>
{getString('novelScreen.edit.status')}
</Text>
<ScrollView
style={getScrollViewStyle()}
horizontal
showsHorizontalScrollIndicator={false}
>
{status.map((item, index) => (
<View style={getStatusChipContainer()} key={'novelInfo' + index}>
<Pressable
style={[
styles.statusChipPressable,
getStatusChipPressable(novelInfo.status === item, theme),
]}
android_ripple={{
color: theme.rippleColor,
}}
onPress={() => setNovelInfo({ ...novel, status: item })}
<Dialog.Root visible={modalVisible} onDismiss={hideModal}>
<Dialog.Title>{getString('novelScreen.edit.info')}</Dialog.Title>
<Dialog.ScrollArea>
<ScrollView
contentContainerStyle={styles.formContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.statusRow}>
<Text style={getStatusLabelColor(theme)}>
{getString('novelScreen.edit.status')}
</Text>
<ScrollView
style={getScrollViewStyle()}
horizontal
showsHorizontalScrollIndicator={false}
>
{status.map((item, index) => (
<View
style={getStatusChipContainer()}
key={'novelInfo' + index}
>
<Text
style={getStatusChipText(novelInfo.status === item, theme)}
<Pressable
style={[
styles.statusChipPressable,
getStatusChipPressable(novelInfo.status === item, theme),
]}
android_ripple={{
color: theme.rippleColor,
}}
onPress={() => setNovelInfo({ ...novel, status: item })}
>
{translateNovelStatus(item)}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</View>
<TextInput
defaultValue={initialNovelInfo.name}
value={novelInfo.name}
placeholder={getString('novelScreen.edit.title', {
title: novel.name,
})}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, name: text })}
dense
style={styles.inputWrapper}
/>
<TextInput
defaultValue={initialNovelInfo.author ?? undefined}
value={novelInfo.author ?? undefined}
placeholder={getString('novelScreen.edit.author', {
author: novel.author,
})}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, author: text })}
dense
style={styles.inputWrapper}
/>
<TextInput
defaultValue={initialNovelInfo.artist ?? undefined}
value={novelInfo.artist ?? undefined}
placeholder={'Artist: ' + novel.artist}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, artist: text })}
dense
style={styles.inputWrapper}
/>
<TextInput
defaultValue={initialNovelInfo.summary ?? undefined}
value={novelInfo.summary ?? undefined}
placeholder={getString('novelScreen.edit.summary', {
summary: novel.summary?.substring(0, 16),
})}
numberOfLines={1}
mode="outlined"
onChangeText={text => setNovelInfo({ ...novel, summary: text })}
theme={{ colors: { ...theme } }}
dense
style={styles.inputWrapper}
/>
<TextInput
value={newGenre}
placeholder={getString('novelScreen.edit.addTag')}
numberOfLines={1}
mode="outlined"
onChangeText={text => setNewGenre(text)}
onSubmitEditing={() => {
const newGenreTrimmed = newGenre.trim();
if (newGenreTrimmed === '') {
return;
}
setNovelInfo(prevVal => ({
...prevVal,
genres: novelInfo.genres
? `${novelInfo.genres},` + newGenreTrimmed
: newGenreTrimmed,
}));
setNewGenre('');
}}
theme={{ colors: { ...theme } }}
dense
style={styles.inputWrapper}
/>
{novelInfo.genres !== undefined && novelInfo.genres !== '' ? (
<FlatList
style={getGenreListStyle()}
horizontal
data={novelInfo.genres?.split(',')}
keyExtractor={(_, index) => 'novelTag' + index}
renderItem={({ item }) => (
<GenreChip theme={theme} onPress={() => removeTag(item)}>
{item}
</GenreChip>
)}
showsHorizontalScrollIndicator={false}
<Text
style={getStatusChipText(
novelInfo.status === item,
theme,
)}
>
{translateNovelStatus(item)}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</View>
<TextInput
defaultValue={initialNovelInfo.name}
value={novelInfo.name}
placeholder={getString('novelScreen.edit.title', {
title: novel.name,
})}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, name: text })}
dense
style={styles.inputWrapper}
/>
) : null}
<View style={getButtonRowStyle()}>
<Button
onPress={() => {
setNovelInfo(initialNovelInfo);
updateNovelInfo(initialNovelInfo);
<TextInput
defaultValue={initialNovelInfo.author ?? undefined}
value={novelInfo.author ?? undefined}
placeholder={getString('novelScreen.edit.author', {
author: novel.author,
})}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, author: text })}
dense
style={styles.inputWrapper}
/>
<TextInput
defaultValue={initialNovelInfo.artist ?? undefined}
value={novelInfo.artist ?? undefined}
placeholder={'Artist: ' + novel.artist}
numberOfLines={1}
mode="outlined"
theme={{ colors: { ...theme } }}
onChangeText={text => setNovelInfo({ ...novel, artist: text })}
dense
style={styles.inputWrapper}
/>
<TextInput
defaultValue={initialNovelInfo.summary ?? undefined}
value={novelInfo.summary ?? undefined}
placeholder={getString('novelScreen.edit.summary', {
summary: novel.summary?.substring(0, 16),
})}
numberOfLines={1}
mode="outlined"
onChangeText={text => setNovelInfo({ ...novel, summary: text })}
theme={{ colors: { ...theme } }}
dense
style={styles.inputWrapper}
/>
<TextInput
value={newGenre}
placeholder={getString('novelScreen.edit.addTag')}
numberOfLines={1}
mode="outlined"
onChangeText={text => setNewGenre(text)}
onSubmitEditing={() => {
const newGenreTrimmed = newGenre.trim();
if (newGenreTrimmed === '') {
return;
}
setNovelInfo(prevVal => ({
...prevVal,
genres: novelInfo.genres
? `${novelInfo.genres},` + newGenreTrimmed
: newGenreTrimmed,
}));
setNewGenre('');
}}
>
{getString('common.reset')}
</Button>
<View style={getFlex1()} />
<Button
onPress={() => {
setNovel(novelInfo);
updateNovelInfo(novelInfo);
hideModal();
}}
>
{getString('common.save')}
</Button>
<Button onPress={hideModal}>{getString('common.cancel')}</Button>
</View>
</Modal>
</Portal>
theme={{ colors: { ...theme } }}
dense
style={styles.inputWrapper}
/>
{novelInfo.genres !== undefined && novelInfo.genres !== '' ? (
<FlatList
style={getGenreListStyle()}
horizontal
data={novelInfo.genres?.split(',')}
keyExtractor={(_, index) => 'novelTag' + index}
renderItem={({ item }) => (
<GenreChip theme={theme} onPress={() => removeTag(item)}>
{item}
</GenreChip>
)}
showsHorizontalScrollIndicator={false}
/>
) : null}
</ScrollView>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action
title={getString('common.reset')}
onPress={() => {
setNovelInfo(initialNovelInfo);
updateNovelInfo(initialNovelInfo);
}}
/>
<Dialog.Action title={getString('common.cancel')} onPress={hideModal} />
<Dialog.Action
title={getString('common.save')}
onPress={() => {
setNovel(novelInfo);
updateNovelInfo(novelInfo);
hideModal();
}}
/>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -268,9 +271,8 @@ const styles = StyleSheet.create({
fontSize: 14,
marginBottom: 12,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
formContent: {
paddingHorizontal: 24,
},
statusRow: {
marginVertical: 8,
@@ -291,12 +293,6 @@ const styles = StyleSheet.create({
genreList: {
marginVertical: 8,
},
buttonRow: {
flexDirection: 'row',
},
flex1: {
flex: 1,
},
genreChipContainer: {
flex: 1,
flexDirection: 'row',
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { TextInput, Text } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { openDocumentTree } from 'react-native-saf-x';
import { Button, List, Modal, SwitchItem } from '@components';
import { Dialog, List, SwitchItem } from '@components';
import { useBoolean } from '@hooks';
import { getString } from '@strings/translations';
@@ -93,11 +93,11 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
};
return (
<Modal visible={isVisible} onDismiss={onDismiss}>
<View>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.exportEpubModal.title')}
</Text>
<Dialog.Root visible={isVisible} onDismiss={onDismiss}>
<Dialog.Title>
{getString('novelScreen.exportEpubModal.title')}
</Dialog.Title>
<Dialog.Content>
<TextInput
onChangeText={setUri}
value={uri}
@@ -114,15 +114,15 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
/>
}
/>
</View>
<View style={styles.settings}>
</Dialog.Content>
<Dialog.List>
<SwitchItem
label={getString('novelScreen.exportEpubModal.exportAll')}
value={exportAll.value}
onPress={exportAll.toggle}
theme={theme}
/>
{!exportAll.value && (
{!exportAll.value ? (
<View style={styles.rangeInputs}>
<TextInput
label={getString('novelScreen.exportEpubModal.startChapter')}
@@ -147,7 +147,7 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
style={styles.rangeInput}
/>
</View>
)}
) : null}
<SwitchItem
label={getString('novelScreen.exportEpubModal.applyReaderTheme')}
value={useAppTheme.value}
@@ -167,17 +167,25 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
onPress={useCustomJS.toggle}
theme={theme}
/>
</View>
<List.InfoItem
style={styles.infoItem}
title={getString('novelScreen.exportEpubModal.downloadedChaptersOnly')}
theme={theme}
/>
<View style={styles.modalFooterCtn}>
<Button title={getString('common.submit')} onPress={onSubmit} />
<Button title={getString('common.cancel')} onPress={hideModal} />
</View>
</Modal>
</Dialog.List>
<Dialog.Content>
<List.InfoItem
style={styles.infoItem}
title={getString(
'novelScreen.exportEpubModal.downloadedChaptersOnly',
)}
theme={theme}
/>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action onPress={hideModal}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action onPress={onSubmit}>
{getString('common.submit')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -188,23 +196,11 @@ const styles = StyleSheet.create({
paddingHorizontal: 0,
},
modalFooterCtn: {
flexDirection: 'row-reverse',
paddingBottom: 20,
paddingTop: 8,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
settings: {
marginTop: 12,
},
rangeInputs: {
flexDirection: 'row',
gap: 12,
marginBottom: 12,
paddingHorizontal: 24,
},
rangeInput: {
flex: 1,
@@ -6,9 +6,9 @@ import {
TextInput as RNTextInput,
} from 'react-native';
import { getString } from '@strings/translations';
import { Button, Modal, SwitchItem } from '@components';
import { Dialog, SwitchItem } from '@components';
import { Portal, Text } from 'react-native-paper';
import { Text } from 'react-native-paper';
import { useTheme } from '@hooks/persisted';
import { ChapterInfo, NovelInfo } from '@database/types';
import { NovelScreenProps } from '@navigators/types';
@@ -187,55 +187,55 @@ const JumpToChapterModal = ({
const borderWidth = inputFocused || error ? 2 : 1;
const margin = inputFocused || error ? 0 : 1;
return (
<Portal>
<Modal visible={modalVisible} onDismiss={onDismiss}>
<View>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.jumpToChapterModal.jumpToChapter')}
</Text>
<RNTextInput
ref={inputRef}
placeholder={placeholder}
placeholderTextColor={'grey'}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
keyboardType={mode ? 'default' : 'numeric'}
onFocus={() => setInputFocused(true)}
onBlur={() => setInputFocused(false)}
style={[
{
color: theme.onBackground,
backgroundColor: theme.background,
borderColor: error
? theme.error
: inputFocused
? theme.primary
: theme.outline,
borderWidth: borderWidth,
margin: margin,
},
styles.textInput,
]}
/>
{!!error && (
<Text style={[styles.errorText, { color: errorColor }]}>
{error}
</Text>
)}
<SwitchItem
label={getString('novelScreen.jumpToChapterModal.openChapter')}
value={openChapter}
theme={theme}
onPress={() => setOpenChapter(!openChapter)}
/>
<SwitchItem
label={getString('novelScreen.jumpToChapterModal.chapterName')}
value={mode}
theme={theme}
onPress={() => setMode(!mode)}
/>
</View>
{result.length ? (
<Dialog.Root visible={modalVisible} onDismiss={onDismiss}>
<Dialog.Title>
{getString('novelScreen.jumpToChapterModal.jumpToChapter')}
</Dialog.Title>
<Dialog.Content>
<RNTextInput
ref={inputRef}
placeholder={placeholder}
placeholderTextColor={'grey'}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
keyboardType={mode ? 'default' : 'numeric'}
onFocus={() => setInputFocused(true)}
onBlur={() => setInputFocused(false)}
style={[
{
color: theme.onBackground,
backgroundColor: theme.background,
borderColor: error
? theme.error
: inputFocused
? theme.primary
: theme.outline,
borderWidth: borderWidth,
margin: margin,
},
styles.textInput,
]}
/>
{error ? (
<Text style={[styles.errorText, { color: errorColor }]}>{error}</Text>
) : null}
</Dialog.Content>
<Dialog.List>
<SwitchItem
label={getString('novelScreen.jumpToChapterModal.openChapter')}
value={openChapter}
theme={theme}
onPress={() => setOpenChapter(!openChapter)}
/>
<SwitchItem
label={getString('novelScreen.jumpToChapterModal.chapterName')}
value={mode}
theme={theme}
onPress={() => setMode(!mode)}
/>
</Dialog.List>
{result.length ? (
<Dialog.Content>
<View style={[styles.legendlist, { borderColor: theme.outline }]}>
<LegendList
recycleItems
@@ -247,13 +247,17 @@ const JumpToChapterModal = ({
contentContainerStyle={styles.listContentCtn}
/>
</View>
) : null}
<View style={styles.modalFooterCtn}>
<Button title={getString('common.submit')} onPress={onSubmit} />
<Button title={getString('common.cancel')} onPress={hideModal} />
</View>
</Modal>
</Portal>
</Dialog.Content>
) : null}
<Dialog.Actions>
<Dialog.Action onPress={hideModal}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action onPress={onSubmit}>
{getString('common.submit')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -279,14 +283,6 @@ const styles = StyleSheet.create({
listElementContainer: {
paddingVertical: 12,
},
modalFooterCtn: {
flexDirection: 'row-reverse',
paddingTop: 8,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
textInput: {
borderRadius: 4,
borderStyle: 'solid',
@@ -1,9 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native';
import { Divider, Portal } from 'react-native-paper';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { Button, Modal } from '@components/index';
import { Dialog } from '@components/index';
import { useTheme } from '@hooks/persisted';
@@ -48,17 +47,15 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
}, [getCategories, visible]);
return (
<Portal>
<Modal
visible={visible}
onDismiss={() => {
closeModal();
setSelectedCategories([]);
}}
>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('categories.setCategories')}
</Text>
<Dialog.Root
visible={visible}
onDismiss={() => {
closeModal();
setSelectedCategories([]);
}}
>
<Dialog.Title>{getString('categories.setCategories')}</Dialog.Title>
<Dialog.ScrollArea>
<FlatList
data={categories}
style={styles.categoryList}
@@ -72,56 +69,48 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
onPress={() =>
setSelectedCategories(xor(selectedCategories, [item]))
}
viewStyle={styles.checkboxView}
theme={theme}
/>
)}
ListEmptyComponent={
<Text style={{ color: theme.onSurfaceVariant }}>
<Text
style={[styles.emptyMessage, { color: theme.onSurfaceVariant }]}
>
{getString('categories.setModalEmptyMsg')}
</Text>
}
/>
<Divider
style={[
{
backgroundColor: theme.onSurfaceDisabled,
},
styles.divider,
]}
/>
<View style={styles.btnContainer}>
<Button
title={getString('common.edit')}
onPress={() => {
navigate('MoreStack', {
screen: 'Categories',
});
closeModal();
onEditCategories?.();
}}
/>
<View style={styles.flex} />
<Button
title={getString('common.cancel')}
onPress={() => {
closeModal();
}}
/>
<Button
title={getString('common.ok')}
onPress={async () => {
await updateNovelCategories(
novelIds,
selectedCategories.map(category => category.id),
);
closeModal();
onSuccess?.();
}}
/>
</View>
</Modal>
</Portal>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action
onPress={() => {
navigate('MoreStack', {
screen: 'Categories',
});
closeModal();
onEditCategories?.();
}}
>
{getString('common.edit')}
</Dialog.Action>
<View style={styles.flex} />
<Dialog.Action onPress={closeModal}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action
onPress={async () => {
await updateNovelCategories(
novelIds,
selectedCategories.map(category => category.id),
);
closeModal();
void onSuccess?.();
}}
>
{getString('common.ok')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -131,23 +120,11 @@ const styles = StyleSheet.create({
categoryList: {
maxHeight: Dimensions.get('window').height * 0.4,
},
divider: { height: 1, width: '90%', marginLeft: '5%' },
btnContainer: {
flexDirection: 'row',
marginTop: 20,
},
checkboxView: {
marginBottom: 5,
emptyMessage: {
paddingHorizontal: 24,
paddingVertical: 16,
},
flex: {
flex: 1,
},
modalTitle: {
fontSize: 24,
marginBottom: 20,
},
modelOption: {
fontSize: 15,
marginVertical: 10,
},
});
@@ -1,27 +1,22 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import React, { useState } from 'react';
import { TextInput } from 'react-native-paper';
import { Button, DialogTitle, Modal } from '@components';
import { Dialog } from '@components';
import { useTheme } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { TrackChaptersDialogProps } from './types';
const SetTrackChaptersDialog: React.FC<TrackChaptersDialogProps> = ({
trackItem,
visible,
onDismiss,
onUpdateChapters,
}) => {
type SetTrackChaptersDialogContentProps = Omit<
TrackChaptersDialogProps,
'visible'
>;
const SetTrackChaptersDialogContent: React.FC<
SetTrackChaptersDialogContentProps
> = ({ trackItem, onDismiss, onUpdateChapters }) => {
const theme = useTheme();
const [chapters, setChapters] = useState(String(trackItem.progress ?? 0));
useEffect(() => {
if (visible) {
setChapters(String(trackItem.progress ?? 0));
}
}, [visible, trackItem.progress]);
const handleSave = () => {
onUpdateChapters(chapters);
};
@@ -31,38 +26,40 @@ const SetTrackChaptersDialog: React.FC<TrackChaptersDialogProps> = ({
};
return (
<Modal visible={visible} onDismiss={onDismiss}>
<DialogTitle title="Chapters" />
<TextInput
value={chapters}
onChangeText={handleChangeText}
mode="outlined"
keyboardType="numeric"
theme={{
colors: {
primary: theme.primary,
placeholder: theme.outline,
text: theme.onSurface,
background: 'transparent',
},
}}
underlineColor={theme.outline}
/>
<View style={styles.buttonContainer}>
<Button onPress={onDismiss}>{getString('common.cancel')}</Button>
<Button onPress={handleSave}>{getString('common.save')}</Button>
</View>
</Modal>
<Dialog.Root visible onDismiss={onDismiss}>
<Dialog.Title>Chapters</Dialog.Title>
<Dialog.Content>
<TextInput
value={chapters}
onChangeText={handleChangeText}
mode="outlined"
keyboardType="numeric"
theme={{
colors: {
primary: theme.primary,
placeholder: theme.outline,
text: theme.onSurface,
background: 'transparent',
},
}}
underlineColor={theme.outline}
/>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action onPress={handleSave}>
{getString('common.save')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default SetTrackChaptersDialog;
const SetTrackChaptersDialog: React.FC<TrackChaptersDialogProps> = ({
visible,
...props
}) => (visible ? <SetTrackChaptersDialogContent {...props} /> : null);
const styles = StyleSheet.create({
buttonContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 8,
marginTop: 16,
},
});
export default SetTrackChaptersDialog;
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import React, { useMemo, useState } from 'react';
import { Button, DialogTitle, Modal } from '@components';
import { Dialog } from '@components';
import { getString } from '@strings/translations';
import {
AniListScoreSelector,
@@ -11,21 +10,16 @@ import {
} from './ScoreSelectors';
import { TrackScoreDialogProps } from './types';
const SetTrackScoreDialog: React.FC<TrackScoreDialogProps> = ({
type SetTrackScoreDialogContentProps = Omit<TrackScoreDialogProps, 'visible'>;
const SetTrackScoreDialogContent: React.FC<SetTrackScoreDialogContentProps> = ({
tracker,
trackItem,
visible,
onDismiss,
onUpdateScore,
}) => {
const [selectedScore, setSelectedScore] = useState(trackItem.score);
useEffect(() => {
if (visible) {
setSelectedScore(trackItem.score);
}
}, [visible, trackItem.score]);
const handleSave = () => {
onUpdateScore(selectedScore);
onDismiss();
@@ -67,24 +61,28 @@ const SetTrackScoreDialog: React.FC<TrackScoreDialogProps> = ({
}, [tracker, trackItem, selectedScore]);
return (
<Modal visible={visible} onDismiss={onDismiss}>
<DialogTitle title="Score" />
{ScoreSelector}
<View style={styles.buttonContainer}>
<Button onPress={onDismiss}>{getString('common.cancel')}</Button>
<Button onPress={handleSave}>{getString('common.save')}</Button>
</View>
</Modal>
<Dialog.Root visible onDismiss={onDismiss}>
<Dialog.Title>Score</Dialog.Title>
{tracker.name === 'Kitsu' || tracker.name === 'AniList' ? (
<Dialog.ScrollArea>{ScoreSelector}</Dialog.ScrollArea>
) : (
<Dialog.Content>{ScoreSelector}</Dialog.Content>
)}
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action onPress={handleSave}>
{getString('common.save')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default SetTrackScoreDialog;
const SetTrackScoreDialog: React.FC<TrackScoreDialogProps> = ({
visible,
...props
}) => (visible ? <SetTrackScoreDialogContent {...props} /> : null);
const styles = StyleSheet.create({
buttonContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 8,
marginTop: 16,
},
});
export default SetTrackScoreDialog;
@@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import React, { useState } from 'react';
import { Button, DialogTitle, Modal } from '@components';
import { Dialog } from '@components';
import { RadioButton, RadioButtonGroup } from '@components/RadioButton';
import { useTheme } from '@hooks/persisted';
import { getString } from '@strings/translations';
@@ -9,21 +8,14 @@ import { UserListStatus } from '@services/Trackers';
import { STATUS_LABELS } from './constants';
import { TrackStatusDialogProps } from './types';
const SetTrackStatusDialog: React.FC<TrackStatusDialogProps> = ({
trackItem,
visible,
onDismiss,
onUpdateStatus,
}) => {
type SetTrackStatusDialogContentProps = Omit<TrackStatusDialogProps, 'visible'>;
const SetTrackStatusDialogContent: React.FC<
SetTrackStatusDialogContentProps
> = ({ trackItem, onDismiss, onUpdateStatus }) => {
const theme = useTheme();
const [selectedStatus, setSelectedStatus] = useState(trackItem.status);
useEffect(() => {
if (visible) {
setSelectedStatus(trackItem.status);
}
}, [visible, trackItem.status]);
const handleSave = () => {
onUpdateStatus(selectedStatus);
onDismiss();
@@ -34,31 +26,33 @@ const SetTrackStatusDialog: React.FC<TrackStatusDialogProps> = ({
};
return (
<Modal visible={visible} onDismiss={onDismiss}>
<DialogTitle title="Status" />
<RadioButtonGroup
onValueChange={handleValueChange}
value={selectedStatus}
>
{Object.entries(STATUS_LABELS).map(([key, label]) => (
<RadioButton key={key} value={key} label={label} theme={theme} />
))}
</RadioButtonGroup>
<View style={styles.buttonContainer}>
<Button onPress={onDismiss}>{getString('common.cancel')}</Button>
<Button onPress={handleSave}>{getString('common.save')}</Button>
</View>
</Modal>
<Dialog.Root visible onDismiss={onDismiss}>
<Dialog.Title>Status</Dialog.Title>
<Dialog.List>
<RadioButtonGroup
onValueChange={handleValueChange}
value={selectedStatus}
>
{Object.entries(STATUS_LABELS).map(([key, label]) => (
<RadioButton key={key} value={key} label={label} theme={theme} />
))}
</RadioButtonGroup>
</Dialog.List>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action onPress={handleSave}>
{getString('common.save')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default SetTrackStatusDialog;
const SetTrackStatusDialog: React.FC<TrackStatusDialogProps> = ({
visible,
...props
}) => (visible ? <SetTrackStatusDialogContent {...props} /> : null);
const styles = StyleSheet.create({
buttonContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 8,
marginTop: 16,
},
});
export default SetTrackStatusDialog;
@@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, Image, StyleSheet, Text, View } from 'react-native';
import { ActivityIndicator, Image, StyleSheet, Text } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { TextInput, TouchableRipple } from 'react-native-paper';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import { Button, Modal } from '@components';
import { Dialog } from '@components';
import { getTracker, useTheme } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { SearchResult } from '@services/Trackers';
@@ -118,63 +118,59 @@ const TrackSearchDialog: React.FC<TrackSearchDialogProps> = ({
);
return (
<Modal visible={visible} onDismiss={onDismiss}>
<TextInput
value={searchText}
onChangeText={setSearchText}
onSubmitEditing={getSearchResults}
textColor={theme.onSurface}
theme={{
colors: {
primary: theme.primary,
text: theme.onSurface,
},
}}
style={styles.textInput}
underlineColor={theme.outline}
right={
<TextInput.Icon
color={theme.onSurfaceVariant}
icon="close"
onPress={handleClearSearch}
/>
}
/>
<ScrollView style={styles.scrollView}>
{loading ? (
<ActivityIndicator
color={theme.primary}
size={45}
style={styles.loader}
/>
) : (
searchResults.map(renderSearchResultCard)
)}
</ScrollView>
<View style={styles.buttonContainer}>
<Button onPress={handleRemoveSelection}>
{getString('common.remove')}
</Button>
<View style={styles.actionButtons}>
<Button onPress={onDismiss}>{getString('common.cancel')}</Button>
<Button onPress={handleConfirm}>OK</Button>
</View>
</View>
</Modal>
<Dialog.Root visible={visible} onDismiss={onDismiss}>
<Dialog.Title>{tracker.name}</Dialog.Title>
<Dialog.Content>
<TextInput
value={searchText}
onChangeText={setSearchText}
onSubmitEditing={getSearchResults}
textColor={theme.onSurface}
theme={{
colors: {
primary: theme.primary,
text: theme.onSurface,
},
}}
style={styles.textInput}
underlineColor={theme.outline}
right={
<TextInput.Icon
color={theme.onSurfaceVariant}
icon="close"
onPress={handleClearSearch}
/>
}
/>
</Dialog.Content>
<Dialog.ScrollArea>
<ScrollView style={styles.scrollView}>
{loading ? (
<ActivityIndicator
color={theme.primary}
size={45}
style={styles.loader}
/>
) : (
searchResults.map(renderSearchResultCard)
)}
</ScrollView>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action
title={getString('common.remove')}
onPress={handleRemoveSelection}
/>
<Dialog.Action title={getString('common.cancel')} onPress={onDismiss} />
<Dialog.Action title="OK" onPress={handleConfirm} />
</Dialog.Actions>
</Dialog.Root>
);
};
export default TrackSearchDialog;
const styles = StyleSheet.create({
actionButtons: {
flexDirection: 'row',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 30,
},
checkIcon: {
position: 'absolute',
right: 8,
@@ -1,11 +1,5 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
Text,
ScrollView,
TouchableOpacity,
} from 'react-native';
import { Pressable, View, StyleSheet, Text, ScrollView } from 'react-native';
import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
import Slider from '@react-native-community/slider';
import { getAvailableVoicesAsync, Voice } from 'expo-speech';
@@ -16,8 +10,8 @@ import {
useChapterReaderSettings,
} from '@hooks/persisted';
import { getString } from '@strings/translations';
import { List, Button } from '@components/index';
import { Portal, Modal, Chip } from 'react-native-paper';
import { Dialog, List, Button } from '@components/index';
import { Chip } from 'react-native-paper';
import ReaderSheetPreferenceItem from './ReaderSheetPreferenceItem';
interface VoicePickerModalProps {
@@ -85,28 +79,19 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
});
};
useEffect(() => {
// Reset to system language when modal opens
if (visible) {
setSelectedLanguages([]);
}
}, [visible]);
const handleDismiss = () => {
setSelectedLanguages([]);
onDismiss();
};
return (
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContent,
{ backgroundColor: theme.surface },
]}
>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
Select Voice
</Text>
{/* Language Filter */}
<Dialog.Root
visible={visible}
onDismiss={handleDismiss}
surfaceStyle={styles.modalContent}
>
<Dialog.Title>Select Voice</Dialog.Title>
<Dialog.Content>
<View style={styles.languageFilterContainer}>
<Text style={[styles.filterLabel, { color: theme.onSurfaceVariant }]}>
Filter by language:
@@ -144,8 +129,8 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
})}
</ScrollView>
</View>
{/* Voice List */}
</Dialog.Content>
<Dialog.ScrollArea>
<ScrollView style={styles.voiceList}>
{filteredVoices.length === 0 ? (
<Text
@@ -155,7 +140,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
</Text>
) : (
filteredVoices.map((voice: Voice, index: number) => (
<TouchableOpacity
<Pressable
key={index}
style={[
styles.voiceItem,
@@ -165,7 +150,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
]}
onPress={() => {
onSelect(voice);
onDismiss();
handleDismiss();
}}
>
<View style={styles.voiceItemContent}>
@@ -174,7 +159,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
>
{voice.name}
</Text>
{voice.language && (
{voice.language ? (
<Text
style={[
styles.voiceItemLanguage,
@@ -183,26 +168,22 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
>
{voice.language}
</Text>
)}
) : null}
</View>
{currentVoice?.identifier === voice.identifier && (
{currentVoice?.identifier === voice.identifier ? (
<Text style={[styles.checkIcon, { color: theme.primary }]}>
✓
</Text>
)}
</TouchableOpacity>
) : null}
</Pressable>
))
)}
</ScrollView>
<Button
title="Cancel"
mode="outlined"
onPress={onDismiss}
style={styles.cancelButton}
/>
</Modal>
</Portal>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action onPress={handleDismiss}>Cancel</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -246,9 +227,9 @@ const TTSTab: React.FC = () => {
theme={theme}
/>
{TTSEnable && (
{TTSEnable ? (
<>
<TouchableOpacity
<Pressable
style={styles.settingItem}
onPress={() => setVoiceModalVisible(true)}
>
@@ -258,7 +239,7 @@ const TTSTab: React.FC = () => {
<Text style={[styles.value, { color: theme.onSurfaceVariant }]}>
{tts?.voice?.name || 'System'}
</Text>
</TouchableOpacity>
</Pressable>
<View style={styles.sliderSection}>
<Text style={[styles.sliderLabel, { color: theme.onSurface }]}>
@@ -342,7 +323,7 @@ const TTSTab: React.FC = () => {
/>
</View>
</>
)}
) : null}
</View>
<View style={styles.bottomSpacing} />
@@ -406,16 +387,8 @@ const styles = StyleSheet.create({
height: 24,
},
modalContent: {
margin: 20,
borderRadius: 8,
padding: 20,
maxHeight: '80%',
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 12,
},
languageFilterContainer: {
marginBottom: 16,
},
@@ -435,11 +408,12 @@ const styles = StyleSheet.create({
marginTop: 8,
},
voiceItem: {
borderCurve: 'continuous',
borderRadius: 4,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 12,
borderRadius: 4,
marginBottom: 4,
},
voiceItemContent: {
@@ -457,9 +431,6 @@ const styles = StyleSheet.create({
padding: 20,
fontSize: 14,
},
cancelButton: {
marginTop: 16,
},
languageChipText: {
fontSize: 12,
},
+61 -72
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { Portal, Text, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { deleteCachedNovels, useTheme, useUserAgent } from '@hooks/persisted';
import { showToast } from '@utils/showToast';
@@ -13,9 +13,9 @@ import {
clearUpdates,
} from '@database/queries/ChapterQueries';
import { Appbar, Button, List, Modal, SafeAreaView } from '@components';
import { Appbar, Dialog, List, SafeAreaView } from '@components';
import { AdvancedSettingsScreenProps } from '@navigators/types';
import { ScrollView, StyleSheet, View } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import { getUserAgentSync } from 'react-native-device-info';
import CookieManager from '@preeternal/react-native-cookie-manager';
import { store } from '@plugins/helpers/storage';
@@ -99,40 +99,46 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
/>
</List.Section>
</ScrollView>
<Portal>
<ConfirmationDialog
message={getString(
'advancedSettingsScreen.deleteReadChaptersDialogTitle',
)}
visible={deleteReadChaptersDialog}
onSubmit={deleteReadChaptersFromDb}
onDismiss={hideDeleteReadChaptersDialog}
theme={theme}
/>
<ConfirmationDialog
message={getString('advancedSettingsScreen.clearDatabaseWarning')}
visible={clearDatabaseDialog}
onSubmit={deleteCachedNovels}
onDismiss={hideClearDatabaseDialog}
theme={theme}
/>
<ConfirmationDialog
message={getString('advancedSettingsScreen.clearUpdatesWarning')}
visible={clearUpdatesDialog}
onSubmit={() => {
clearUpdates();
showToast(getString('advancedSettingsScreen.clearUpdatesMessage'));
hideClearUpdatesDialog();
}}
onDismiss={hideClearUpdatesDialog}
theme={theme}
/>
<ConfirmationDialog
title={getString('advancedSettingsScreen.deleteReadChapters')}
confirmLabel={getString('common.delete')}
message={getString(
'advancedSettingsScreen.deleteReadChaptersDialogTitle',
)}
visible={deleteReadChaptersDialog}
onConfirm={deleteReadChaptersFromDb}
onDismiss={hideDeleteReadChaptersDialog}
/>
<ConfirmationDialog
title={getString('advancedSettingsScreen.clearCachedNovels')}
confirmLabel={getString('common.clear')}
message={getString('advancedSettingsScreen.clearDatabaseWarning')}
visible={clearDatabaseDialog}
onConfirm={deleteCachedNovels}
onDismiss={hideClearDatabaseDialog}
/>
<ConfirmationDialog
title={getString('advancedSettingsScreen.clearUpdatesTab')}
confirmLabel={getString('common.clear')}
message={getString('advancedSettingsScreen.clearUpdatesWarning')}
visible={clearUpdatesDialog}
onConfirm={() => {
clearUpdates();
showToast(getString('advancedSettingsScreen.clearUpdatesMessage'));
hideClearUpdatesDialog();
}}
onDismiss={hideClearUpdatesDialog}
/>
<Modal visible={userAgentModalVisible} onDismiss={hideUserAgentModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('advancedSettingsScreen.userAgent')}
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>{userAgent}</Text>
<Dialog.Root
visible={userAgentModalVisible}
onDismiss={hideUserAgentModal}
>
<Dialog.Title>
{getString('advancedSettingsScreen.userAgent')}
</Dialog.Title>
<Dialog.Description>{userAgent}</Dialog.Description>
<Dialog.Content>
<TextInput
multiline
mode="outlined"
@@ -143,27 +149,24 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
style={[{ color: theme.onSurface }, styles.textInput]}
theme={{ colors: { ...theme } }}
/>
<View style={styles.buttonGroup}>
<Button
onPress={() => {
setUserAgent(userAgentInput);
hideUserAgentModal();
}}
style={styles.button}
title={getString('common.save')}
mode="contained"
/>
<Button
style={styles.button}
onPress={() => {
setUserAgent(getUserAgentSync());
hideUserAgentModal();
}}
title={getString('common.reset')}
/>
</View>
</Modal>
</Portal>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action
onPress={() => {
setUserAgent(getUserAgentSync());
hideUserAgentModal();
}}
title={getString('common.reset')}
/>
<Dialog.Action
onPress={() => {
setUserAgent(userAgentInput);
hideUserAgentModal();
}}
title={getString('common.save')}
/>
</Dialog.Actions>
</Dialog.Root>
</SafeAreaView>
);
};
@@ -171,23 +174,9 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
export default AdvancedSettings;
const styles = StyleSheet.create({
button: {
flex: 1,
marginHorizontal: 8,
marginTop: 16,
},
buttonGroup: {
flexDirection: 'row-reverse',
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
textInput: {
borderRadius: 14,
fontSize: 12,
height: 120,
marginBottom: 8,
marginTop: 16,
},
});
@@ -1,10 +1,9 @@
import React from 'react';
import { Dialog, Portal } from 'react-native-paper';
import { StyleSheet, Text } from 'react-native';
import { StyleSheet } from 'react-native';
import { useTheme } from '@hooks/persisted';
import { Modal, RadioButton } from '@components';
import { Dialog, RadioButton } from '@components';
import { getString, setLocale } from '@strings/translations';
import { useMMKVString } from 'react-native-mmkv';
import { FlatList } from 'react-native-gesture-handler';
@@ -84,18 +83,18 @@ const LanguagePickerModal: React.FC<LanguagePickerModalProps> = ({
};
return (
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={styles.maxHeight}
>
<Dialog.Title theme={{ colors: theme }} style={styles.zeroPadding}>
{getString('appearanceScreen.appLanguage')}
</Dialog.Title>
<Text style={[styles.noteText, { color: theme.onSurfaceVariant }]}>
<Dialog.Root
visible={visible}
onDismiss={onDismiss}
surfaceStyle={styles.maxHeight}
>
<Dialog.Header>
<Dialog.Title>{getString('appearanceScreen.appLanguage')}</Dialog.Title>
<Dialog.Description>
{getString('appearanceScreen.languagePickerModal.restartNote')}
</Text>
</Dialog.Description>
</Dialog.Header>
<Dialog.ScrollArea>
<FlatList
data={languages}
keyExtractor={item => item.locale}
@@ -106,22 +105,16 @@ const LanguagePickerModal: React.FC<LanguagePickerModalProps> = ({
onPress={() => handleLanguageSelect(item.locale)}
label={item.nativeName}
theme={theme}
style={styles.zeroPadding}
/>
)}
/>
</Modal>
</Portal>
</Dialog.ScrollArea>
</Dialog.Root>
);
};
export default LanguagePickerModal;
const styles = StyleSheet.create({
noteText: {
lineHeight: 20,
marginBottom: 8,
},
zeroPadding: { paddingHorizontal: 0, marginHorizontal: 0, marginTop: 0 },
maxHeight: { maxHeight: '60%' },
});
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { ThemeColors } from '@theme/types';
import { StyleSheet, Text, View, Image } from 'react-native';
import { Portal, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { GoogleSignin, User } from '@react-native-google-signin/google-signin';
import { Button, EmptyView, Modal } from '@components';
import { Button, Dialog, EmptyView } from '@components';
import { FlatList, TouchableOpacity } from 'react-native-gesture-handler';
import * as Clipboard from 'expo-clipboard';
import { showToast } from '@utils/showToast';
@@ -177,32 +177,34 @@ function RestoreBackup({
return (
<>
<FlatList
contentContainerStyle={styles.backupList}
data={backupList}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<Button
mode="outlined"
style={styles.btnOutline}
onPress={() => {
closeModal();
backgroundTasks.enqueue({
name: 'DRIVE_RESTORE',
data: item,
});
}}
>
<Text style={{ color: theme.primary }}>
{item.name?.replace(/\.backup$/, ' ')}
</Text>
<Text style={[{ color: theme.secondary }, styles.fontSize]}>
{'(' + dayjs(item.createdTime).format('LL') + ')'}
</Text>
</Button>
)}
ListEmptyComponent={emptyComponent}
/>
<Dialog.ScrollArea>
<FlatList
contentContainerStyle={styles.backupList}
data={backupList}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<Button
mode="outlined"
style={styles.btnOutline}
onPress={() => {
closeModal();
backgroundTasks.enqueue({
name: 'DRIVE_RESTORE',
data: item,
});
}}
>
<Text style={{ color: theme.primary }}>
{item.name?.replace(/\.backup$/, ' ')}
</Text>
<Text style={[{ color: theme.secondary }, styles.fontSize]}>
{'(' + dayjs(item.createdTime).format('LL') + ')'}
</Text>
</Button>
)}
ListEmptyComponent={emptyComponent}
/>
</Dialog.ScrollArea>
<View style={styles.footerContainer}>
<Button
title={getString('common.cancel')}
@@ -280,40 +282,36 @@ export default function GoogleDriveModal({
};
return (
<Portal>
<Modal visible={visible} onDismiss={closeModal}>
<>
<View style={styles.titleContainer}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('backupScreen.drive.googleDriveBackup')}
</Text>
<TouchableOpacity
onLongPress={() => {
if (user?.user.email) {
Clipboard.setStringAsync(user.user.email).then(success => {
if (success) {
showToast(
getString('common.copiedToClipboard', {
name: user.user.email,
}),
);
}
});
<Dialog.Root visible={visible} onDismiss={closeModal}>
<Dialog.Header style={styles.titleContainer}>
<Dialog.Title>
{getString('backupScreen.drive.googleDriveBackup')}
</Dialog.Title>
<TouchableOpacity
onLongPress={() => {
if (user?.user.email) {
Clipboard.setStringAsync(user.user.email).then(success => {
if (success) {
showToast(
getString('common.copiedToClipboard', {
name: user.user.email,
}),
);
}
}}
>
{user ? (
<Image
source={{ uri: user?.user.photo || '' }}
style={styles.avatar}
/>
) : null}
</TouchableOpacity>
</View>
{renderModal()}
</>
</Modal>
</Portal>
});
}
}}
>
{user ? (
<Image
source={{ uri: user?.user.photo || '' }}
style={styles.avatar}
/>
) : null}
</TouchableOpacity>
</Dialog.Header>
<Dialog.Content>{renderModal()}</Dialog.Content>
</Dialog.Root>
);
}
@@ -344,14 +342,10 @@ const styles = StyleSheet.create({
borderRadius: 16,
width: '100%',
},
modalTitle: {
fontSize: 24,
},
titleContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
textAlignVertical: 'center',
},
fontSize: { fontSize: 12 },
@@ -1,14 +1,14 @@
import { list } from '@api/remote';
import { Button, EmptyView, Modal } from '@components';
import { Button, Dialog, EmptyView } from '@components';
import { useSelfHost } from '@hooks/persisted/useSelfHost';
import { backgroundTasks } from '@services/backgroundTasks';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
import { fetchTimeout } from '@utils/fetch/fetch';
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { Portal, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
enum BackupModal {
SET_HOST,
@@ -100,32 +100,34 @@ function RestoreBackup({
return (
<>
<FlatList
contentContainerStyle={styles.backupList}
data={backupList}
keyExtractor={(item, index) => item + '_' + index}
renderItem={({ item }) => (
<Button
mode="outlined"
style={styles.btnOutline}
onPress={() => {
closeModal();
backgroundTasks.enqueue({
name: 'SELF_HOST_RESTORE',
data: {
host,
backupFolder: item,
},
});
}}
>
<Text style={{ color: theme.primary }}>
{item.replace(/\.backup$/, ' ')}
</Text>
</Button>
)}
ListEmptyComponent={emptyComponent}
/>
<Dialog.ScrollArea>
<FlatList
contentContainerStyle={styles.backupList}
data={backupList}
keyExtractor={(item, index) => item + '_' + index}
renderItem={({ item }) => (
<Button
mode="outlined"
style={styles.btnOutline}
onPress={() => {
closeModal();
backgroundTasks.enqueue({
name: 'SELF_HOST_RESTORE',
data: {
host,
backupFolder: item,
},
});
}}
>
<Text style={{ color: theme.primary }}>
{item.replace(/\.backup$/, ' ')}
</Text>
</Button>
)}
ListEmptyComponent={emptyComponent}
/>
</Dialog.ScrollArea>
<View style={styles.footerContainer}>
<Button
title={getString('common.cancel')}
@@ -269,18 +271,10 @@ export default function SelfHostModal({
};
return (
<Portal>
<Modal visible={visible} onDismiss={closeModal}>
<>
<View style={styles.titleContainer}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('backupScreen.remote.backup')}
</Text>
</View>
{renderModal()}
</>
</Modal>
</Portal>
<Dialog.Root visible={visible} onDismiss={closeModal}>
<Dialog.Title>{getString('backupScreen.remote.backup')}</Dialog.Title>
<Dialog.Content>{renderModal()}</Dialog.Content>
</Dialog.Root>
);
}
@@ -311,14 +305,4 @@ const styles = StyleSheet.create({
borderRadius: 16,
width: '100%',
},
modalTitle: {
fontSize: 24,
},
titleContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
textAlignVertical: 'center',
},
});
@@ -3,15 +3,11 @@ import {
displayModesList,
} from '@screens/library/constants/constants';
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { Dialog, RadioButton } from '@components';
import { ThemeColors } from '@theme/types';
import { useLibrarySettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { Modal } from '@components';
interface DisplayModeModalProps {
displayMode: DisplayModes;
@@ -29,11 +25,11 @@ const DisplayModeModal: React.FC<DisplayModeModalProps> = ({
const { setLibrarySettings } = useLibrarySettings();
return (
<Portal>
<Modal visible={displayModalVisible} onDismiss={hideDisplayModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{getString('generalSettingsScreen.displayMode')}
</Text>
<Dialog.Root visible={displayModalVisible} onDismiss={hideDisplayModal}>
<Dialog.Title>
{getString('generalSettingsScreen.displayMode')}
</Dialog.Title>
<Dialog.List>
{displayModesList.map(mode => (
<RadioButton
key={mode.value}
@@ -43,16 +39,9 @@ const DisplayModeModal: React.FC<DisplayModeModalProps> = ({
theme={theme}
/>
))}
</Modal>
</Portal>
</Dialog.List>
</Dialog.Root>
);
};
export default DisplayModeModal;
const styles = StyleSheet.create({
modalHeader: {
fontSize: 24,
marginBottom: 10,
},
});
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, TextInput, View } from 'react-native';
import { Portal, TouchableRipple } from 'react-native-paper';
import { StyleSheet, Text, TextInput } from 'react-native';
import { Modal } from '@components';
import { Dialog } from '@components';
import { useAppSettings } from '@hooks/persisted';
import { DEFAULT_CHAPTER_DOWNLOAD_COOLDOWN_MS } from '@hooks/persisted/useSettings';
import { getString } from '@strings/translations';
@@ -75,14 +74,16 @@ const DownloadCooldownModal: React.FC<DownloadCooldownModalProps> = ({
};
return (
<Portal>
<Modal visible={visible} onDismiss={hideModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
<Dialog.Root visible={visible} onDismiss={hideModal}>
<Dialog.Header>
<Dialog.Title>
{getString('generalSettingsScreen.chapterDownloadCooldown')}
</Text>
<Text style={[styles.modalDesc, { color: theme.onSurfaceVariant }]}>
</Dialog.Title>
<Dialog.Description>
{getString('generalSettingsScreen.chapterDownloadCooldownDesc')}
</Text>
</Dialog.Description>
</Dialog.Header>
<Dialog.Content>
<TextInput
value={draft}
onChangeText={text => setDraft(sanitizeNumericInput(text))}
@@ -101,44 +102,18 @@ const DownloadCooldownModal: React.FC<DownloadCooldownModalProps> = ({
<Text style={[styles.warning, { color: theme.error }]}>
{getString('generalSettingsScreen.chapterDownloadCooldownWarning')}
</Text>
<View style={styles.actions}>
<TouchableRipple
onPress={reset}
borderless
style={styles.actionButton}
rippleColor={theme.rippleColor}
>
<Text style={[styles.actionLabel, { color: theme.primary }]}>
{getString('common.reset')}
</Text>
</TouchableRipple>
<TouchableRipple
onPress={save}
borderless
style={styles.actionButton}
rippleColor={theme.rippleColor}
>
<Text style={[styles.actionLabel, { color: theme.primary }]}>
{getString('common.ok')}
</Text>
</TouchableRipple>
</View>
</Modal>
</Portal>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action title={getString('common.reset')} onPress={reset} />
<Dialog.Action title={getString('common.ok')} onPress={save} />
</Dialog.Actions>
</Dialog.Root>
);
};
export default DownloadCooldownModal;
const styles = StyleSheet.create({
modalHeader: {
fontSize: 24,
marginBottom: 8,
},
modalDesc: {
fontSize: 14,
marginBottom: 16,
},
input: {
paddingVertical: 8,
paddingHorizontal: 12,
@@ -150,19 +125,4 @@ const styles = StyleSheet.create({
fontSize: 12,
marginTop: 12,
},
actions: {
flexDirection: 'row',
justifyContent: 'flex-end',
paddingTop: 16,
},
actionButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 8,
},
actionLabel: {
fontSize: 14,
fontWeight: '600',
textTransform: 'uppercase',
},
});
@@ -1,14 +1,10 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { Dialog, RadioButton } from '@components';
import { ThemeColors } from '@theme/types';
import { useLibrarySettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { Modal } from '@components';
interface GridSizeModalProps {
novelsPerRow: number;
@@ -34,18 +30,18 @@ const GridSizeModal: React.FC<GridSizeModalProps> = ({
};
return (
<Portal>
<Modal visible={gridSizeModalVisible} onDismiss={hideGridSizeModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
<Dialog.Root visible={gridSizeModalVisible} onDismiss={hideGridSizeModal}>
<Dialog.Header>
<Dialog.Title>
{getString('generalSettingsScreen.gridSize')}
</Text>
<Text
style={[styles.modalDescription, { color: theme.onSurfaceVariant }]}
>
</Dialog.Title>
<Dialog.Description>
{getString('generalSettingsScreen.gridSizeDesc', {
num: novelsPerRow,
})}
</Text>
</Dialog.Description>
</Dialog.Header>
<Dialog.List>
{Object.keys(gridSizes).map(item => {
const it = Number(item);
return (
@@ -59,24 +55,9 @@ const GridSizeModal: React.FC<GridSizeModalProps> = ({
/>
);
})}
</Modal>
</Portal>
</Dialog.List>
</Dialog.Root>
);
};
export default GridSizeModal;
const styles = StyleSheet.create({
modalDescription: {
fontSize: 16,
marginBottom: 16,
},
modalHeader: {
fontSize: 24,
marginBottom: 10,
},
slider: {
height: 40,
width: '100%',
},
});
@@ -1,13 +1,9 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { Dialog, RadioButton } from '@components';
import { ThemeColors } from '@theme/types';
import { useAppSettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { Modal } from '@components';
interface InactivityTimeoutModalProps {
inactivityTimeoutMs: number | undefined;
@@ -25,14 +21,16 @@ const InactivityTimeoutModal: React.FC<InactivityTimeoutModalProps> = ({
const { setAppSettings } = useAppSettings();
return (
<Portal>
<Modal visible={modalVisible} onDismiss={hideModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
<Dialog.Root visible={modalVisible} onDismiss={hideModal}>
<Dialog.Header>
<Dialog.Title>
{getString('generalSettingsScreen.inactivityTimeout')}
</Text>
<Text style={{ color: theme.onSurface }}>
</Dialog.Title>
<Dialog.Description>
{getString('generalSettingsScreen.inactivityTimeoutDesc')}
</Text>
</Dialog.Description>
</Dialog.Header>
<Dialog.List>
<RadioButton
key={0}
status={inactivityTimeoutMs === undefined}
@@ -44,21 +42,16 @@ const InactivityTimeoutModal: React.FC<InactivityTimeoutModalProps> = ({
<RadioButton
key={time}
status={inactivityTimeoutMs === time * 60 * 1000}
onPress={() => setAppSettings({ inactivityTimeoutMs: time * 60 * 1000 })}
onPress={() =>
setAppSettings({ inactivityTimeoutMs: time * 60 * 1000 })
}
label={`${getString('time.minutes', { count: time })}`}
theme={theme}
/>
))}
</Modal>
</Portal>
</Dialog.List>
</Dialog.Root>
);
};
export default InactivityTimeoutModal;
const styles = StyleSheet.create({
modalHeader: {
fontSize: 24,
marginBottom: 10,
},
});
@@ -1,9 +1,6 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import { Checkbox, Modal } from '@components';
import { Checkbox, Dialog } from '@components';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
import { useLibrarySettings } from '@hooks/persisted';
@@ -26,11 +23,14 @@ const NovelBadgesModal: React.FC<NovelBadgesModalProps> = ({
setLibrarySettings,
} = useLibrarySettings();
return (
<Portal>
<Modal visible={novelBadgesModalVisible} onDismiss={hideNovelBadgesModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{getString('libraryScreen.bottomSheet.display.badges')}
</Text>
<Dialog.Root
visible={novelBadgesModalVisible}
onDismiss={hideNovelBadgesModal}
>
<Dialog.Title>
{getString('libraryScreen.bottomSheet.display.badges')}
</Dialog.Title>
<Dialog.List>
<Checkbox
label={getString('libraryScreen.bottomSheet.display.downloadBadges')}
status={showDownloadBadges}
@@ -61,20 +61,9 @@ const NovelBadgesModal: React.FC<NovelBadgesModalProps> = ({
}
theme={theme}
/>
</Modal>
</Portal>
</Dialog.List>
</Dialog.Root>
);
};
export default NovelBadgesModal;
const styles = StyleSheet.create({
modalDescription: {
fontSize: 16,
marginBottom: 16,
},
modalHeader: {
fontSize: 24,
marginBottom: 10,
},
});
@@ -1,7 +1,4 @@
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { Portal } from 'react-native-paper';
import {
LibrarySortOrder,
@@ -11,7 +8,7 @@ import { ThemeColors } from '@theme/types';
import { SortItem } from '@components/Checkbox/Checkbox';
import { useLibrarySettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { Modal } from '@components';
import { Dialog } from '@components';
interface NovelSortModalProps {
novelSortModalVisible: boolean;
@@ -27,11 +24,11 @@ const NovelSortModal: React.FC<NovelSortModalProps> = ({
const { sortOrder = LibrarySortOrder.DateAdded_DESC, setLibrarySettings } =
useLibrarySettings();
return (
<Portal>
<Modal visible={novelSortModalVisible} onDismiss={hideNovelSortModal}>
<Text style={[styles.modalHeader, { color: theme.onSurface }]}>
{getString('generalSettingsScreen.sortOrder')}
</Text>
<Dialog.Root visible={novelSortModalVisible} onDismiss={hideNovelSortModal}>
<Dialog.Title>
{getString('generalSettingsScreen.sortOrder')}
</Dialog.Title>
<Dialog.List>
{librarySortOrderList.map(item => (
<SortItem
key={item.ASC}
@@ -51,26 +48,9 @@ const NovelSortModal: React.FC<NovelSortModalProps> = ({
}
/>
))}
</Modal>
</Portal>
</Dialog.List>
</Dialog.Root>
);
};
export default NovelSortModal;
const styles = StyleSheet.create({
modalDescription: {
fontSize: 16,
marginBottom: 16,
paddingHorizontal: 24,
},
modalHeader: {
fontSize: 24,
marginBottom: 10,
paddingHorizontal: 24,
},
slider: {
height: 40,
width: '100%',
},
});
@@ -1,8 +1,7 @@
import React from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { Button, Dialog } from 'react-native-paper';
import { RadioButton } from '@components';
import { Dialog, RadioButton } from '@components';
import { getString } from '@strings/translations';
import { useTheme } from '@hooks/persisted';
@@ -27,36 +26,29 @@ const DefaultCategoryDialog: React.FC<DefaultCategoryDialogProps> = ({
const theme = useTheme();
return (
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{ backgroundColor: theme.overlay3 }}
>
<Dialog.Title style={{ color: theme.onSurface }}>
{getString('categories.defaultCategory')}
</Dialog.Title>
<FlatList
style={styles.scrollArea}
initialNumToRender={10}
data={categories}
renderItem={({ item }) => (
<RadioButton
status={item.id === defaultCategoryId}
label={item.name}
onPress={() => setDefaultCategory(item.id)}
theme={theme}
/>
)}
/>
<Dialog.Root visible={visible} onDismiss={hideDialog}>
<Dialog.Title>{getString('categories.defaultCategory')}</Dialog.Title>
<Dialog.ScrollArea>
<FlatList
style={styles.scrollArea}
initialNumToRender={10}
data={categories}
renderItem={({ item }) => (
<RadioButton
status={item.id === defaultCategoryId}
label={item.name}
onPress={() => setDefaultCategory(item.id)}
theme={theme}
/>
)}
/>
</Dialog.ScrollArea>
<Dialog.Actions>
<Button
theme={{ colors: { primary: theme.primary } }}
onPress={hideDialog}
>
<Dialog.Action onPress={hideDialog}>
{getString('common.cancel')}
</Button>
</Dialog.Action>
</Dialog.Actions>
</Dialog>
</Dialog.Root>
);
};
@@ -1,12 +1,11 @@
import React from 'react';
import { Portal } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { Dialog, RadioButton } from '@components';
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
import { readerFonts } from '@utils/constants/readerConstants';
import { Modal } from '@components';
import { getString } from '@strings/translations';
interface FontPickerModalProps {
visible: boolean;
@@ -23,8 +22,11 @@ const FontPickerModal: React.FC<FontPickerModalProps> = ({
const { setChapterReaderSettings } = useChapterReaderSettings();
return (
<Portal>
<Modal visible={visible} onDismiss={onDismiss}>
<Dialog.Root visible={visible} onDismiss={onDismiss}>
<Dialog.Title>
{getString('readerScreen.bottomSheet.fontStyle')}
</Dialog.Title>
<Dialog.List>
{readerFonts.map(item => (
<RadioButton
key={item.fontFamily}
@@ -37,8 +39,13 @@ const FontPickerModal: React.FC<FontPickerModalProps> = ({
theme={theme}
/>
))}
</Modal>
</Portal>
</Dialog.List>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>
{getString('common.ok')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -1,13 +1,13 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Portal, TextInput, ActivityIndicator } from 'react-native-paper';
import { RadioButton } from '@components/RadioButton/RadioButton';
import { TextInput, ActivityIndicator } from 'react-native-paper';
import { Dialog, RadioButton } from '@components';
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
import { Voice } from 'expo-speech';
import { LegendList } from '@legendapp/list/react-native';
import { Modal } from '@components';
import { StyleSheet } from 'react-native';
import { getString } from '@strings/translations';
interface VoicePickerModalProps {
visible: boolean;
@@ -21,40 +21,38 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
voices,
}) => {
const theme = useTheme();
const [searchedVoices, setSearchedVoices] = useState<Voice[]>([]);
const [searchText, setSearchText] = useState('');
const { setChapterReaderSettings, tts } = useChapterReaderSettings();
const filteredVoices = useMemo(() => {
const normalizedSearch = searchText.toLocaleLowerCase();
return normalizedSearch
? voices.filter(voice =>
voice.name.toLocaleLowerCase().includes(normalizedSearch),
)
: voices;
}, [searchText, voices]);
return (
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[styles.containerStyle]}
>
<Dialog.Root
visible={visible}
onDismiss={onDismiss}
surfaceStyle={styles.containerStyle}
>
<Dialog.Title>Select voice</Dialog.Title>
<Dialog.Content>
<TextInput
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
onChangeText={setSearchText}
value={searchText}
placeholder="Search voice"
/>
</Dialog.Content>
<Dialog.ScrollArea style={styles.list}>
<LegendList
recycleItems
ListHeaderComponent={
<TextInput
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
onChangeText={text => {
setSearchText(text);
setSearchedVoices(
voices.filter(voice =>
voice.name
.toLocaleLowerCase()
.includes(text.toLocaleLowerCase()),
),
);
}}
value={searchText}
placeholder="Search voice"
/>
}
ListHeaderComponentStyle={styles.paddingHorizontal}
data={searchText ? searchedVoices : voices}
data={filteredVoices}
extraData={tts?.voice}
renderItem={({ item }) => (
<RadioButton
@@ -80,8 +78,13 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
/>
}
/>
</Modal>
</Portal>
</Dialog.ScrollArea>
<Dialog.Actions>
<Dialog.Action onPress={onDismiss}>
{getString('common.ok')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -91,6 +94,8 @@ const styles = StyleSheet.create({
containerStyle: {
flex: 1,
},
paddingHorizontal: { paddingHorizontal: 12 },
list: {
flex: 1,
},
marginTop: { marginTop: 16 },
});
@@ -8,10 +8,10 @@ import {
Platform,
} from 'react-native';
import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
import { TextInput, Portal } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import * as DocumentPicker from 'expo-document-picker';
import NativeFile from '@modules/native-file'
import NativeFile from '@modules/native-file';
import { useTheme, useChapterReaderSettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
@@ -275,23 +275,20 @@ if (title) {
<View style={styles.bottomSpacing} />
</BottomSheetScrollView>
{/* Confirmation Dialogs */}
<Portal>
<ConfirmationDialog
title={getString('readerSettings.clearCustomCSS')}
visible={clearCSSModal.value}
onSubmit={confirmResetCSS}
onDismiss={clearCSSModal.setFalse}
theme={theme}
/>
<ConfirmationDialog
title={getString('readerSettings.clearCustomJS')}
visible={clearJSModal.value}
onSubmit={confirmResetJS}
onDismiss={clearJSModal.setFalse}
theme={theme}
/>
</Portal>
<ConfirmationDialog
title={getString('readerSettings.clearCustomCSS')}
confirmLabel={getString('common.clear')}
visible={clearCSSModal.value}
onConfirm={confirmResetCSS}
onDismiss={clearCSSModal.setFalse}
/>
<ConfirmationDialog
title={getString('readerSettings.clearCustomJS')}
confirmLabel={getString('common.clear')}
visible={clearJSModal.value}
onConfirm={confirmResetJS}
onDismiss={clearJSModal.setFalse}
/>
</KeyboardAvoidingView>
);
};
@@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Portal, TextInput } from 'react-native-paper';
import { TextInput } from 'react-native-paper';
import { Button, Modal } from '@components/index';
import { Dialog } from '@components/index';
import { Repository } from '@database/types';
import { useTheme } from '@hooks/persisted';
@@ -26,11 +25,11 @@ const AddRepositoryModal: React.FC<AddRepositoryModalProps> = ({
const [repositoryUrl, setRepositoryUrl] = useState(repository?.url || '');
return (
<Portal>
<Modal visible={visible} onDismiss={closeModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{repository ? 'Edit repository' : 'Add repository'}
</Text>
<Dialog.Root visible={visible} onDismiss={closeModal}>
<Dialog.Title>
{repository ? 'Edit repository' : 'Add repository'}
</Dialog.Title>
<Dialog.Content>
<TextInput
autoFocus
defaultValue={repositoryUrl}
@@ -40,30 +39,22 @@ const AddRepositoryModal: React.FC<AddRepositoryModalProps> = ({
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
/>
<View style={styles.btnContainer}>
<Button
title={getString(repository ? 'common.ok' : 'common.add')}
onPress={() => {
upsertRepository(repositoryUrl, repository);
closeModal();
}}
/>
<Button title={getString('common.cancel')} onPress={closeModal} />
</View>
</Modal>
</Portal>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action onPress={closeModal}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action
onPress={() => {
upsertRepository(repositoryUrl, repository);
closeModal();
}}
>
{getString(repository ? 'common.ok' : 'common.add')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default AddRepositoryModal;
const styles = StyleSheet.create({
btnContainer: {
flexDirection: 'row-reverse',
marginTop: 24,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
});
@@ -1,15 +1,10 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Portal } from 'react-native-paper';
import { Button, Modal } from '@components/index';
import { ConfirmationDialog } from '@components/index';
import { Repository } from '@database/types';
import { deleteRepositoryById } from '@database/queries/RepositoryQueries';
import { useTheme } from '@hooks/persisted';
import { getString } from '@strings/translations';
interface DeleteRepositoryModalProps {
repository: Repository;
visible: boolean;
@@ -23,42 +18,19 @@ const DeleteRepositoryModal: React.FC<DeleteRepositoryModalProps> = ({
visible,
onSuccess,
}) => {
const theme = useTheme();
return (
<Portal>
<Modal visible={visible} onDismiss={closeModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{'Delete repository'}
</Text>
<Text style={[styles.modalDesc, { color: theme.onSurfaceVariant }]}>
{`Do you wish to delete repository "${repository.url}"?`}
</Text>
<View style={styles.btnContainer}>
<Button
title={getString('common.ok')}
onPress={async () => {
await deleteRepositoryById(repository.id);
closeModal();
onSuccess();
}}
/>
<Button title={getString('common.cancel')} onPress={closeModal} />
</View>
</Modal>
</Portal>
<ConfirmationDialog
title="Delete repository"
confirmLabel={getString('common.delete')}
message={`Do you wish to delete repository "${repository.url}"?`}
visible={visible}
onDismiss={closeModal}
onConfirm={async () => {
await deleteRepositoryById(repository.id);
onSuccess();
}}
/>
);
};
export default DeleteRepositoryModal;
const styles = StyleSheet.create({
btnContainer: {
flexDirection: 'row-reverse',
marginTop: 24,
},
modalDesc: {},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
});
+26 -66
View File
@@ -1,15 +1,9 @@
import React, { useCallback, useState } from 'react';
import { useCallback, useState } from 'react';
import { View, StyleSheet, Image } from 'react-native';
import {
Portal,
Text,
Button,
Provider,
List as PaperList,
} from 'react-native-paper';
import { Provider, List as PaperList } from 'react-native-paper';
import { getTracker, useTheme, useTracker } from '@hooks/persisted';
import { Appbar, List, Modal, SafeAreaView } from '@components';
import { Appbar, ConfirmationDialog, List, SafeAreaView } from '@components';
import { TrackerSettingsScreenProps } from '@navigators/types';
import { getString } from '@strings/translations';
import TrackerLoginDialog from './components/TrackerLoginDialog';
@@ -318,49 +312,29 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
) : null}
</List.Section>
<Portal>
<Modal visible={visible} onDismiss={hideModal}>
<Text style={[{ color: theme.onSurface }, styles.modalText]}>
{getString('trackingScreen.logOutMessage', {
name: logoutTrackerName,
})}
</Text>
<View style={styles.modalButtonRow}>
<Button
style={styles.modalButton}
labelStyle={[
{ color: theme.primary },
styles.modalButtonLabel,
]}
onPress={hideModal}
>
{getString('common.cancel')}
</Button>
<Button
style={styles.modalButton}
labelStyle={[
{ color: theme.primary },
styles.modalButtonLabel,
]}
onPress={() => {
removeTracker(logoutTrackerName as any);
hideModal();
}}
>
{getString('common.logout')}
</Button>
</View>
</Modal>
<TrackerLoginDialog
visible={credentialLoginTracker !== null}
trackerName={credentialLoginTracker || ''}
onDismiss={hideCredentialLogin}
onSubmit={handleCredentialLogin}
usernameLabel={
credentialLoginTracker === 'Kitsu' ? 'Email' : 'Username'
}
/>
</Portal>
<ConfirmationDialog
title={getString('common.logout')}
message={getString('trackingScreen.logOutMessage', {
name: logoutTrackerName,
})}
visible={visible}
confirmLabel={getString('common.logout')}
confirmTone="danger"
onConfirm={() => {
removeTracker(logoutTrackerName as any);
hideModal();
}}
onDismiss={hideModal}
/>
<TrackerLoginDialog
visible={credentialLoginTracker !== null}
trackerName={credentialLoginTracker || ''}
onDismiss={hideCredentialLogin}
onSubmit={handleCredentialLogin}
usernameLabel={
credentialLoginTracker === 'Kitsu' ? 'Email' : 'Username'
}
/>
</View>
</Provider>
</SafeAreaView>
@@ -376,20 +350,6 @@ const styles = StyleSheet.create({
screenPadding: {
paddingVertical: 8,
},
modalText: {
fontSize: 18,
},
modalButtonRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
modalButton: {
marginTop: 30,
},
modalButtonLabel: {
letterSpacing: 0,
textTransform: 'none',
},
logoContainer: {
paddingLeft: 16,
justifyContent: 'center',
@@ -1,8 +1,7 @@
import { Button, Modal } from '@components';
import { Dialog } from '@components';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TextInput } from 'react-native-paper';
interface ConnectionModalProps {
@@ -29,50 +28,42 @@ const ConnectionModal: React.FC<ConnectionModalProps> = ({
setPort,
}) => {
return (
<Modal visible={visible} onDismiss={closeModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
<TextInput
value={ipv4}
placeholder={'xxx.xxx.xxx.xxx'}
onChangeText={setIpv4}
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
placeholderTextColor={theme.onSurfaceDisabled}
/>
<TextInput
value={port}
onChangeText={setPort}
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
placeholderTextColor={theme.onSurfaceDisabled}
/>
<View style={styles.btnContainer}>
<Button
title={getString('common.ok')}
<Dialog.Root visible={visible} onDismiss={closeModal}>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Content>
<TextInput
value={ipv4}
placeholder={'xxx.xxx.xxx.xxx'}
onChangeText={setIpv4}
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
placeholderTextColor={theme.onSurfaceDisabled}
/>
<TextInput
value={port}
onChangeText={setPort}
mode="outlined"
underlineColor={theme.outline}
theme={{ colors: { ...theme } }}
placeholderTextColor={theme.onSurfaceDisabled}
/>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action onPress={closeModal}>
{getString('common.cancel')}
</Dialog.Action>
<Dialog.Action
onPress={() => {
closeModal();
handle(ipv4, port);
void handle(ipv4, port);
}}
/>
<Button title={getString('common.cancel')} onPress={closeModal} />
</View>
</Modal>
>
{getString('common.ok')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
export default ConnectionModal;
const styles = StyleSheet.create({
btnContainer: {
flexDirection: 'row-reverse',
marginTop: 24,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
});
@@ -1,12 +1,9 @@
import React from 'react';
import { Portal } from 'react-native-paper';
import { SortItem } from '@components/Checkbox/Checkbox';
import { ThemeColors } from '@theme/types';
import { AppSettings } from '@hooks/persisted/useSettings';
import { getString } from '@strings/translations';
import { Modal } from '@components';
import { Dialog } from '@components';
import { ChapterOrderKey } from '@database/constants';
interface DefaultChapterSortModalProps {
@@ -25,8 +22,11 @@ const DefaultChapterSortModal = ({
displayModalVisible,
}: DefaultChapterSortModalProps) => {
return (
<Portal>
<Modal visible={displayModalVisible} onDismiss={hideDisplayModal}>
<Dialog.Root visible={displayModalVisible} onDismiss={hideDisplayModal}>
<Dialog.Title>
{getString('generalSettingsScreen.chapterSort')}
</Dialog.Title>
<Dialog.List>
<SortItem
label={getString('generalSettingsScreen.bySource')}
theme={theme}
@@ -41,8 +41,13 @@ const DefaultChapterSortModal = ({
})
}
/>
</Modal>
</Portal>
</Dialog.List>
<Dialog.Actions>
<Dialog.Action onPress={hideDisplayModal}>
{getString('common.ok')}
</Dialog.Action>
</Dialog.Actions>
</Dialog.Root>
);
};
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { View, StyleSheet, TextInput } from 'react-native';
import { Text, Button } from 'react-native-paper';
import { Modal } from '@components';
import { StyleSheet, Text, TextInput } from 'react-native';
import { Dialog } from '@components';
import { useTheme } from '@hooks/persisted';
interface TrackerLoginDialogProps {
@@ -55,12 +54,9 @@ const TrackerLoginDialog: React.FC<TrackerLoginDialogProps> = ({
};
return (
<Modal visible={visible} onDismiss={handleCancel}>
<View style={styles.container}>
<Text style={[styles.title, { color: theme.onSurface }]}>
Login to {trackerName}
</Text>
<Dialog.Root visible={visible} onDismiss={handleCancel}>
<Dialog.Title>Login to {trackerName}</Dialog.Title>
<Dialog.Content>
<TextInput
style={[
styles.input,
@@ -103,42 +99,27 @@ const TrackerLoginDialog: React.FC<TrackerLoginDialogProps> = ({
{error}
</Text>
) : null}
<View style={styles.buttonRow}>
<Button
style={styles.button}
labelStyle={[{ color: theme.primary }, styles.buttonLabel]}
onPress={handleCancel}
disabled={isLoading}
>
Cancel
</Button>
<Button
style={styles.button}
labelStyle={[{ color: theme.primary }, styles.buttonLabel]}
onPress={handleSubmit}
disabled={isLoading}
loading={isLoading}
>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</View>
</View>
</Modal>
</Dialog.Content>
<Dialog.Actions>
<Dialog.Action
title="Cancel"
onPress={handleCancel}
disabled={isLoading}
/>
<Dialog.Action
title={isLoading ? 'Logging in...' : 'Login'}
onPress={handleSubmit}
disabled={isLoading}
loading={isLoading}
/>
</Dialog.Actions>
</Dialog.Root>
);
};
export default TrackerLoginDialog;
const styles = StyleSheet.create({
container: {
padding: 8,
},
title: {
fontSize: 24,
marginBottom: 24,
fontWeight: '500',
},
input: {
height: 48,
borderWidth: 1,
@@ -152,16 +133,4 @@ const styles = StyleSheet.create({
marginBottom: 16,
marginTop: -8,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 8,
},
button: {
marginLeft: 8,
},
buttonLabel: {
letterSpacing: 0,
textTransform: 'none',
},
});
+1
View File
@@ -38,6 +38,7 @@ export interface MD3ThemeType {
export interface ThemeColors extends MD3ThemeType {
rippleColor?: string;
surfaceContainerHigh?: string;
surface2?: string;
overlay3?: string;
surfaceReader?: string;