feat: Standardize Dialogs with Material 3 Patterns
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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 +0,0 @@
|
||||
export { DialogTitle } from './DialogTitle';
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,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 },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
+12
-40
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface MD3ThemeType {
|
||||
|
||||
export interface ThemeColors extends MD3ThemeType {
|
||||
rippleColor?: string;
|
||||
surfaceContainerHigh?: string;
|
||||
surface2?: string;
|
||||
overlay3?: string;
|
||||
surfaceReader?: string;
|
||||
|
||||
Reference in New Issue
Block a user