feat: Add RTL (Right-to-Left) Language Support (#1717)

feat: RTL support
This commit is contained in:
kingstar1111
2026-01-20 06:02:58 +03:00
committed by GitHub
parent f9c5ca303d
commit 198b7443ae
44 changed files with 772 additions and 700 deletions
+6
View File
@@ -1,5 +1,11 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { I18nManager } from 'react-native';
import { i18n } from './strings/translations';
const isRTL = i18n.locale.startsWith('ar') || i18n.locale.startsWith('he');
I18nManager.allowRTL(isRTL);
I18nManager.forceRTL(isRTL);
import App from './App';
+5 -5
View File
@@ -42,8 +42,8 @@ export const Checkbox: React.FC<CheckboxProps> = ({
status === 'indeterminate'
? 'indeterminate'
: status
? 'checked'
: 'unchecked'
? 'checked'
: 'unchecked'
}
onPress={onPress}
color={theme.primary}
@@ -85,11 +85,11 @@ export const SortItem = ({ label, status, onPress, theme }: SortItemProps) => (
const styles = StyleSheet.create({
defaultLabel: {
marginLeft: 12,
marginStart: 12,
},
icon: {
alignSelf: 'center',
left: 24,
start: 24,
position: 'absolute',
},
pressable: {
@@ -98,5 +98,5 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingVertical: 6,
},
sortItem: { paddingVertical: 16, paddingLeft: 64 },
sortItem: { paddingVertical: 16, paddingStart: 64 },
});
+1 -1
View File
@@ -44,7 +44,7 @@ const styles = StyleSheet.create({
chipContainer: {
borderRadius: 8,
height: 32,
marginRight: 8,
marginEnd: 8,
overflow: 'hidden',
},
label: {
@@ -37,7 +37,7 @@ const styles = StyleSheet.create({
colorPreview: {
borderRadius: 50,
height: 24,
marginRight: 16,
marginEnd: 16,
width: 24,
},
container: {
+2 -2
View File
@@ -171,7 +171,7 @@ const styles = StyleSheet.create({
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
marginEnd: 16,
},
description: {
fontSize: 12,
@@ -182,7 +182,7 @@ const styles = StyleSheet.create({
opacity: 0.5,
},
iconCtn: {
paddingLeft: 16,
paddingStart: 16,
},
infoCtn: {
paddingHorizontal: 16,
+2 -2
View File
@@ -88,7 +88,7 @@ const styles = StyleSheet.create({
flex: 1,
flexWrap: 'wrap',
fontSize: 15,
marginLeft: 16,
paddingRight: 8,
marginStart: 16,
paddingEnd: 8,
},
});
+7 -5
View File
@@ -107,11 +107,7 @@ const Menu: React.FC<MenuProps> & { Item: React.FC<MenuItemProps> } = ({
<Animated.View
style={[
styles.backdrop,
{
backgroundColor: theme.isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(0, 0, 0, 0.1)',
},
theme.isDark ? styles.backdropDark : styles.backdropLight,
]}
entering={backdropEntering}
exiting={backdropExiting}
@@ -187,6 +183,12 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
},
backdropDark: {
backgroundColor: 'rgba(0, 0, 0, 0.2)',
},
backdropLight: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
},
menuItem: {
paddingHorizontal: 16,
paddingVertical: 12,
+1 -1
View File
@@ -57,6 +57,6 @@ const styles = StyleSheet.create({
},
radioButtonLabel: {
fontSize: 16,
marginLeft: 16,
marginStart: 16,
},
});
+2 -2
View File
@@ -48,11 +48,11 @@ export const RadioButton: React.FC<Props> = ({
const styles = StyleSheet.create({
icon: {
alignSelf: 'center',
left: 24,
start: 24,
position: 'absolute',
},
label: {
marginLeft: 12,
marginStart: 12,
},
pressable: {
alignItems: 'center',
+2 -2
View File
@@ -22,8 +22,8 @@ const SafeAreaView: React.FC<SafeAreaViewProps> = ({
},
padding: {
paddingBottom: excludeBottom ? 0 : bottom,
paddingLeft: left,
paddingRight: right,
paddingStart: left,
paddingEnd: right,
paddingTop: excludeTop ? 0 : top,
},
});
@@ -49,8 +49,6 @@ const Searchbar: React.FC<SearcbarProps> = ({
const [extraMenu, showExtraMenu] = useState(false);
const marginTop = 8;
const marginRight = 16;
const marginLeft = 16;
return (
<View
@@ -58,8 +56,6 @@ const Searchbar: React.FC<SearcbarProps> = ({
styles.searchbarContainer,
{
marginTop,
marginRight,
marginLeft,
backgroundColor: theme.surface2,
},
]}
+1 -1
View File
@@ -72,6 +72,6 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
switch: {
marginLeft: 8,
marginStart: 8,
},
});
+4 -4
View File
@@ -151,7 +151,7 @@ const styles = StyleSheet.create({
checkIcon: {
position: 'absolute',
top: 5,
right: 5,
end: 5,
borderRadius: 50,
padding: 1.6,
zIndex: 1,
@@ -164,7 +164,7 @@ const styles = StyleSheet.create({
topBarAccent: {
width: 44,
height: 10,
marginLeft: 8,
marginStart: 8,
borderRadius: 50,
},
content: {
@@ -186,7 +186,7 @@ const styles = StyleSheet.create({
rowAccentSmall: {
height: 10,
width: 16,
marginLeft: 4,
marginStart: 4,
borderRadius: 50,
},
rowAccentShort: {
@@ -221,5 +221,5 @@ const styles = StyleSheet.create({
fontSize: 12,
paddingVertical: 4,
},
marginLeft: { marginLeft: 4 },
marginLeft: { marginStart: 4 },
});
+5 -5
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { version } from '../../../package.json';
import { newer } from '@utils/compareVersion';
import { MMKVStorage } from '@utils/mmkv/mmkv';
@@ -30,7 +30,7 @@ export const useGithubUpdateChecker = (): GithubUpdate => {
return timeSinceLastCheck >= ONE_DAY_MS;
};
const checkForRelease = async () => {
const checkForRelease = useCallback(async () => {
if (!shouldCheckForUpdate()) {
setChecking(false);
return;
@@ -65,11 +65,11 @@ export const useGithubUpdateChecker = (): GithubUpdate => {
// Silently fail in offline mode or on network errors
setChecking(false);
}
};
}, []);
const isNewVersion = (versionTag: string) => {
const currentVersion = `${version}`;
const regex = /[^\\d.]/;
const regex = /[^\d.]/;
const newVersion = versionTag.replace(regex, '');
@@ -78,7 +78,7 @@ export const useGithubUpdateChecker = (): GithubUpdate => {
useEffect(() => {
checkForRelease();
}, []);
}, [checkForRelease]);
if (!checking && latestRelease?.tag_name) {
return {
+13 -8
View File
@@ -59,6 +59,18 @@ const BottomNavigator = () => {
[],
);
const renderTabBar = useCallback(
(props: any) => (
<BottomTabBar
{...props}
theme={theme}
showLabelsInNav={showLabelsInNav}
renderIcon={renderIcon}
/>
),
[theme, showLabelsInNav, renderIcon],
);
return (
<Tab.Navigator
screenOptions={() => ({
@@ -70,14 +82,7 @@ const BottomNavigator = () => {
color: theme.onError,
},
})}
tabBar={props => (
<BottomTabBar
{...props}
theme={theme}
showLabelsInNav={showLabelsInNav}
renderIcon={renderIcon}
/>
)}
tabBar={renderTabBar}
>
<Tab.Screen
name="Library"
@@ -169,7 +169,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => {
{
backgroundColor: theme.primary,
marginBottom: bottom + 16,
marginRight: right + 16,
marginEnd: right + 16,
},
]}
label={getString('common.filter')}
@@ -196,6 +196,6 @@ const styles = StyleSheet.create({
bottom: 0,
margin: 16,
position: 'absolute',
right: 0,
end: 0,
},
});
@@ -175,24 +175,24 @@ const FilterItem: React.FC<FilterItemProps> = ({
</Pressable>
{isVisible
? filter.options.map(val => {
return (
<Checkbox
key={val.label}
label={val.label}
theme={theme}
status={value.includes(val.value)}
onPress={() =>
setSelectedFilters(prevFilters => ({
...prevFilters,
[filterKey]: {
type: FilterTypes.CheckboxGroup,
value: insertOrRemoveIntoArray(value, val.value),
},
}))
}
/>
);
})
return (
<Checkbox
key={val.label}
label={val.label}
theme={theme}
status={value.includes(val.value)}
onPress={() =>
setSelectedFilters(prevFilters => ({
...prevFilters,
[filterKey]: {
type: FilterTypes.CheckboxGroup,
value: insertOrRemoveIntoArray(value, val.value),
},
}))
}
/>
);
})
: null}
</View>
);
@@ -255,71 +255,71 @@ const FilterItem: React.FC<FilterItemProps> = ({
</Pressable>
{isVisible
? filter.options.map(val => {
return (
<Checkbox
key={val.label}
label={val.label}
theme={theme}
status={
value.include?.includes(val.value)
? true
: value.exclude?.includes(val.value)
return (
<Checkbox
key={val.label}
label={val.label}
theme={theme}
status={
value.include?.includes(val.value)
? true
: value.exclude?.includes(val.value)
? 'indeterminate'
: false
}
onPress={() => {
if (value.exclude?.includes(val.value)) {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [...(value.include || [])],
exclude: [
...(value.exclude?.filter(
f => f !== val.value,
) || []),
],
},
},
};
});
} else if (value.include?.includes(val.value)) {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [
...(value.include?.filter(
f => f !== val.value,
) || []),
],
exclude: [...(value.exclude || []), val.value],
},
},
};
});
} else {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [...(value.include || []), val.value],
exclude: value.exclude,
},
},
};
});
}
onPress={() => {
if (value.exclude?.includes(val.value)) {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [...(value.include || [])],
exclude: [
...(value.exclude?.filter(
f => f !== val.value,
) || []),
],
},
},
};
});
} else if (value.include?.includes(val.value)) {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [
...(value.include?.filter(
f => f !== val.value,
) || []),
],
exclude: [...(value.exclude || []), val.value],
},
},
};
});
} else {
setSelectedFilters(prev => {
return {
...prev,
[filterKey]: {
type: FilterTypes.ExcludableCheckboxGroup,
value: {
include: [...(value.include || []), val.value],
exclude: value.exclude,
},
},
};
});
}
}}
/>
);
})
}}
/>
);
})
: null}
</View>
);
@@ -350,45 +350,48 @@ const FilterBottomSheet: React.FC<BottomSheetProps> = ({
bottomSheetRef={filterSheetRef}
snapPoints={[400, 600]}
bottomInset={bottom}
backgroundStyle={styles.transparent}
style={[styles.container, { backgroundColor: overlay(2, theme.surface) }]}
handleComponent={() => (
<View
style={[styles.buttonContainer, { borderBottomColor: theme.outline }]}
>
<Button
title={getString('common.reset')}
onPress={() => {
setSelectedFilters(filters);
clearFilters(filters);
}}
/>
<Button
title={getString('common.filter')}
textColor={theme.onPrimary}
onPress={() => {
setFilters(selectedFilters);
filterSheetRef?.current?.close();
}}
mode="contained"
handleComponent={null}
children={
<View style={styles.flex}>
<View
style={[
styles.buttonContainer,
{ borderBottomColor: theme.outline },
]}
>
<Button
title={getString('common.reset')}
onPress={() => {
setSelectedFilters(filters);
clearFilters(filters);
}}
/>
<Button
title={getString('common.filter')}
textColor={theme.onPrimary}
onPress={() => {
setFilters(selectedFilters);
filterSheetRef?.current?.close();
}}
mode="contained"
/>
</View>
<BottomSheetFlatList
data={filters && (Object.entries(filters) as [string, Filters[string]][])}
keyExtractor={(item: [string, Filters[string]]) => 'filter' + item[0]}
renderItem={({ item }: { item: [string, Filters[string]] }) => (
<FilterItem
theme={theme}
filter={item[1]}
filterKey={item[0]}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
)}
/>
</View>
)}
>
<BottomSheetFlatList
data={filters && Object.entries(filters)}
keyExtractor={item => 'filter' + item[0]}
renderItem={({ item }) => (
<FilterItem
theme={theme}
filter={item[1]}
filterKey={item[0]}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
)}
/>
</BottomSheet>
}
/>
);
};
@@ -44,9 +44,8 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
styles.cardCtn,
{
backgroundColor: theme.secondaryContainer,
opacity: isActive ? 0.8 : 1,
elevation: isActive ? 8 : 2,
},
isActive && styles.activeCard,
]}
>
<View style={styles.buttonsCtn}>
@@ -77,17 +76,19 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
</Text>
{category.id === 2 && (
<Badge
style={{
backgroundColor: theme.primary,
paddingHorizontal: 8,
}}
style={[
styles.badge,
{
backgroundColor: theme.primary,
},
]}
>
System
</Badge>
)}
</View>
<View style={styles.flex} />
<View style={{ opacity: category.id === 2 ? 0.4 : 1 }}>
<View style={[category.id === 2 && styles.disabledOpacity]}>
<IconButton
name="pencil-outline"
color={category.id === 2 ? theme.outline : theme.onSurface}
@@ -97,7 +98,7 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
disabled={category.id === 2}
/>
</View>
<View style={{ opacity: category.id === 2 ? 0.4 : 1 }}>
<View style={[category.id === 2 && styles.disabledOpacity]}>
<IconButton
name="delete-outline"
color={category.id === 2 ? theme.outline : theme.onSurface}
@@ -143,24 +144,34 @@ const styles = StyleSheet.create({
paddingVertical: 8,
},
dragHandle: {
marginRight: 4,
marginEnd: 4,
},
flex: {
flex: 1,
},
manageBtn: {
marginLeft: 16,
marginStart: 16,
},
name: {
marginLeft: 16,
marginRight: 8,
marginStart: 16,
marginEnd: 8,
},
nameCtn: {
alignItems: 'center',
flexGrow: 1,
flexDirection: 'row',
marginLeft: 8,
paddingRight: 16,
marginStart: 8,
paddingEnd: 16,
paddingVertical: 4,
},
activeCard: {
opacity: 0.8,
elevation: 8,
},
badge: {
paddingHorizontal: 8,
},
disabledOpacity: {
opacity: 0.4,
},
});
@@ -175,6 +175,7 @@ const GlobalSearchSourceResults: React.FC<{ item: GlobalSearchResult }> = ({
item.plugin.site,
navigateToNovel,
navigation,
imageRequestInit,
noResultsColor,
novelInLibrary,
switchNovelToLibrary,
@@ -38,7 +38,7 @@ export const PluginListItem = memo(
const rightActionStyle = useMemo(
() => [styles.buttonGroup, { backgroundColor: theme.primary }],
[theme.error],
[theme.primary],
);
const containerStyle = useMemo(
() => [styles.container, { backgroundColor: theme.surface }],
@@ -155,6 +155,7 @@ export const PluginListItem = memo(
theme,
handlePinPress,
handleDeletePress,
handleWebviewPress,
isPluginPinned,
],
);
@@ -242,7 +243,7 @@ const styles = StyleSheet.create({
paddingVertical: 12,
},
details: {
marginLeft: 16,
marginStart: 16,
},
flex: { flex: 1 },
icon: {
@@ -254,7 +255,7 @@ const styles = StyleSheet.create({
lineHeight: 20,
},
pinnedIndicator: {
marginRight: -8,
marginEnd: -8,
},
rightActionsContainer: {
flexDirection: 'row',
@@ -129,7 +129,7 @@ const styles = StyleSheet.create({
paddingVertical: 12,
},
details: {
marginLeft: 16,
marginStart: 16,
},
flex: { flex: 1 },
icon: {
@@ -69,7 +69,7 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
marginLeft: 16,
marginStart: 16,
},
sourceIcon: {
borderRadius: 4,
@@ -72,13 +72,11 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
{history.novelName}
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>
{`${getString('historyScreen.chapter')} ${
history.chapterNumber
} • ${dayjs(history.readTime).format('LT').toUpperCase()}` +
`${
history.progress && history.progress > 0
? ' • ' + history.progress + '%'
: ''
{`${getString('historyScreen.chapter')} ${history.chapterNumber
} • ${dayjs(history.readTime).format('LT').toUpperCase()}` +
`${history.progress && history.progress > 0
? ' • ' + history.progress + '%'
: ''
}`}
</Text>
</View>
@@ -118,7 +116,7 @@ const styles = StyleSheet.create({
detailsContainer: {
flex: 1,
justifyContent: 'center',
marginLeft: 16,
marginStart: 16,
},
imageAndNameContainer: {
alignItems: 'center',
+20 -20
View File
@@ -317,18 +317,18 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => {
rightIcons={
selectedNovelIds.length
? [
{
iconName: 'select-all',
onPress: () =>
setSelectedNovelIds(currentNovels.map(novel => novel.id)),
},
]
{
iconName: 'select-all',
onPress: () =>
setSelectedNovelIds(currentNovels.map(novel => novel.id)),
},
]
: [
{
iconName: 'filter-variant',
onPress: () => bottomSheetRef.current?.present(),
},
]
{
iconName: 'filter-variant',
onPress: () => bottomSheetRef.current?.present(),
},
]
}
menuButtons={[
{
@@ -392,14 +392,14 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => {
/>
{useLibraryFAB &&
!isHistoryLoading &&
history &&
history.length !== 0 &&
!error ? (
!isHistoryLoading &&
history &&
history.length !== 0 &&
!error ? (
<FAB
style={[
styles.fab,
{ backgroundColor: theme.primary, marginRight: rightInset + 16 },
{ backgroundColor: theme.primary, marginEnd: rightInset + 16 },
]}
color={theme.onPrimary}
uppercase={false}
@@ -432,11 +432,11 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => {
/>
<LibraryBottomSheet
bottomSheetRef={bottomSheetRef}
style={{ marginLeft: leftInset, marginRight: rightInset }}
style={{ marginStart: leftInset, marginEnd: rightInset }}
/>
<Portal>
<Actionbar
viewStyle={{ paddingLeft: leftInset, paddingRight: rightInset }}
viewStyle={{ paddingStart: leftInset, paddingEnd: rightInset }}
active={selectedNovelIds.length > 0}
actions={[
{
@@ -488,7 +488,7 @@ function createStyles(theme: ThemeColors) {
return StyleSheet.create({
badgeCtn: {
borderRadius: 50,
marginLeft: 2,
marginStart: 2,
paddingHorizontal: 6,
paddingVertical: 2,
position: 'relative',
@@ -500,7 +500,7 @@ function createStyles(theme: ThemeColors) {
bottom: 0,
margin: 16,
position: 'absolute',
right: 0,
end: 0,
},
fontWeight500: {
fontWeight: 500,
+1 -1
View File
@@ -136,7 +136,7 @@ export const useLibrary = (): UseLibraryReturnType => {
getLibrary();
}
prevRestoreTasksCountRef.current = restoreTasksCount;
}, [restoreTasksCount]);
}, [getLibrary, restoreTasksCount]);
return {
library,
+8 -8
View File
@@ -109,8 +109,8 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
color: !unread
? theme.outline
: bookmark
? theme.primary
: theme.onSurface,
? theme.primary
: theme.onSurface,
flex: 1,
}}
numberOfLines={1}
@@ -119,8 +119,8 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
{showChapterTitles
? name
: getString('novelScreen.chapterChapnum', {
num: chapterNumber,
})}
num: chapterNumber,
})}
</Text>
</View>
<View
@@ -136,8 +136,8 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
color: !unread
? theme.outline
: bookmark
? theme.primary
: theme.onSurfaceVariant,
? theme.primary
: theme.onSurfaceVariant,
marginTop: 4,
},
styles.text,
@@ -151,7 +151,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
<Text
style={{
color: theme.outline,
marginLeft: chapter.releaseTime ? 5 : 0,
marginStart: chapter.releaseTime ? 5 : 0,
fontSize: 12,
marginTop: 4,
}}
@@ -200,6 +200,6 @@ const styles = StyleSheet.create({
fontSize: 12,
},
unreadIcon: {
marginRight: 4,
marginEnd: 4,
},
});
+1 -1
View File
@@ -225,7 +225,7 @@ const styles = StyleSheet.create({
row: {
flexDirection: 'row',
position: 'absolute',
right: 0,
end: 0,
},
});
@@ -34,12 +34,10 @@ export default function PageNavigationBottomSheet({
<View
style={[
styles.pageItemContainer,
{
backgroundColor: isSelected
? theme.isDark
? color(theme.primary).alpha(0.2).string()
: color(theme.primaryContainer).alpha(0.5).string()
: 'transparent',
isSelected && {
backgroundColor: theme.isDark
? color(theme.primary).alpha(0.2).string()
: color(theme.primaryContainer).alpha(0.5).string(),
},
]}
>
@@ -83,8 +81,8 @@ export default function PageNavigationBottomSheet({
styles.contentContainer,
{
backgroundColor: overlay(2, theme.surface),
marginLeft: left,
marginRight: right,
marginStart: left,
marginEnd: right,
paddingBottom: insets.bottom,
},
]}
@@ -134,9 +134,9 @@ const PagePaginationControl: React.FC<PagePaginationControlProps> = ({
style={[
styles.button,
{
borderColor: isActive ? 'transparent' : borderColor,
backgroundColor: isActive ? theme.primary : theme.surface,
},
isActive ? styles.activeButton : styles.inactiveButton,
]}
onPress={() => handlePagePress(pageIndex)}
android_ripple={{
@@ -150,8 +150,8 @@ const PagePaginationControl: React.FC<PagePaginationControlProps> = ({
styles.pageText,
{
color: isActive ? theme.onPrimary : theme.onSurface,
fontWeight: isActive ? '600' : '400',
},
isActive ? styles.activePageText : styles.inactivePageText,
]}
numberOfLines={1}
>
@@ -232,6 +232,18 @@ const styles = StyleSheet.create({
letterSpacing: 0.15,
maxWidth: 100,
},
activeButton: {
borderColor: 'transparent',
},
inactiveButton: {
borderColor: borderColor,
},
activePageText: {
fontWeight: '600',
},
inactivePageText: {
fontWeight: '400',
},
});
export default PagePaginationControl;
@@ -108,7 +108,7 @@ const TrackSearchDialog: React.FC<TrackSearchDialogProps> = ({
</TouchableRipple>
);
},
[selectedNovel, handleSelectNovel],
[selectedNovel, handleSelectNovel, theme.rippleColor, theme.primary, theme.onSurface],
);
return (
@@ -45,11 +45,14 @@ const TrackSheet: React.FC<TrackSheetProps> = ({ bottomSheetRef, novel }) => {
bottomSheetRef.current?.close();
}, [bottomSheetRef]);
const handleSetSearchTrackDialog = useCallback((tracker: TrackerMetadata) => {
closeBottomSheet();
setActiveTracker(tracker);
setTrackSearchDialog(true);
}, []);
const handleSetSearchTrackDialog = useCallback(
(tracker: TrackerMetadata) => {
closeBottomSheet();
setActiveTracker(tracker);
setTrackSearchDialog(true);
},
[closeBottomSheet],
);
const handleSetStatusDialog = useCallback(
(tracker: TrackerMetadata) => {
+1 -1
View File
@@ -122,7 +122,7 @@ export const ChapterContent = ({
}
return (
<View
style={[{ paddingLeft: left, paddingRight: right }, styles.container]}
style={[{ paddingStart: left, paddingEnd: right }, styles.container]}
>
{keepScreenOn ? <KeepScreenAwake /> : null}
{loading ? (
@@ -222,8 +222,7 @@ const createStylesheet = (theme: ThemeColors, insets: EdgeInsets) => {
drawerElementContainer: {
borderRadius: 50,
margin: 4,
marginLeft: 16,
marginRight: 16,
marginHorizontal: 16,
minHeight: 48,
overflow: 'hidden',
},
@@ -128,7 +128,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
bookmark: {
marginRight: 4,
marginEnd: 4,
},
container: {
flex: 1,
@@ -36,7 +36,7 @@ const ReaderFontPicker = () => {
/>
);
},
[isSelected, setChapterReaderSettings],
[isSelected, setChapterReaderSettings, theme],
);
return (
@@ -65,6 +65,6 @@ const styles = StyleSheet.create({
paddingVertical: 6,
},
title: {
marginRight: 16,
marginEnd: 16,
},
});
@@ -22,18 +22,18 @@ interface VoicePickerModalProps {
currentVoice?: Voice;
}
const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
visible,
onDismiss,
voices,
onSelect,
currentVoice
const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
visible,
onDismiss,
voices,
onSelect,
currentVoice
}) => {
const theme = useTheme();
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([]);
// Get system language safely using getLocales()
const systemLocale = getLocales()[0]?.languageCode || 'en';
// Get unique languages from voices
const availableLanguages = useMemo(() => {
const languages = new Set<string>();
@@ -61,7 +61,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
return lang === systemLocale;
});
}
return voices.filter(voice => {
if (voice.name === 'System') return true;
const lang = voice.language?.split('-')[0];
@@ -99,14 +99,14 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
Select Voice
</Text>
{/* Language Filter */}
<View style={styles.languageFilterContainer}>
<Text style={[styles.filterLabel, { color: theme.onSurfaceVariant }]}>
Filter by language:
</Text>
<ScrollView
horizontal
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.languageChipsScroll}
>
@@ -115,7 +115,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
const isSystemLang = lang === systemLocale;
const showingSystemOnly = selectedLanguages.length === 0;
const isActive = isSelected || (showingSystemOnly && isSystemLang);
return (
<Chip
key={lang}
@@ -125,10 +125,10 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
styles.languageChip,
isActive && { backgroundColor: theme.primary }
]}
textStyle={{
color: isActive ? theme.onPrimary : theme.onSurface,
fontSize: 12
}}
textStyle={[
styles.languageChipText,
{ color: isActive ? theme.onPrimary : theme.onSurface }
]}
>
{lang.toUpperCase()}
{isSystemLang && ' (System)'}
@@ -170,7 +170,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
)}
</View>
{currentVoice?.identifier === voice.identifier && (
<Text style={{ color: theme.primary, fontSize: 16 }}>✓</Text>
<Text style={[styles.checkIcon, { color: theme.primary }]}>✓</Text>
)}
</TouchableOpacity>
))
@@ -194,7 +194,7 @@ const TTSTab: React.FC = () => {
TTSEnable = true,
setChapterGeneralSettings,
} = useChapterGeneralSettings();
const { tts, setChapterReaderSettings } = useChapterReaderSettings();
const [voices, setVoices] = useState<Voice[]>([]);
const [voiceModalVisible, setVoiceModalVisible] = useState(false);
@@ -219,7 +219,7 @@ const TTSTab: React.FC = () => {
>
<View style={styles.section}>
<List.SubHeader theme={theme}>Text to Speech</List.SubHeader>
<ReaderSheetPreferenceItem
label="Enable TTS"
value={TTSEnable}
@@ -228,7 +228,7 @@ const TTSTab: React.FC = () => {
}
theme={theme}
/>
{TTSEnable && (
<>
<TouchableOpacity
@@ -407,7 +407,7 @@ const styles = StyleSheet.create({
flexGrow: 0,
},
languageChip: {
marginRight: 8,
marginEnd: 8,
marginBottom: 8,
},
voiceList: {
@@ -440,4 +440,10 @@ const styles = StyleSheet.create({
cancelButton: {
marginTop: 16,
},
languageChipText: {
fontSize: 12,
},
checkIcon: {
fontSize: 16,
},
});
+41 -43
View File
@@ -92,12 +92,12 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
useEffect(() => {
setReaderSettings(
getMMKVObject<ChapterReaderSettings>(CHAPTER_READER_SETTINGS) ||
initialChapterReaderSettings,
initialChapterReaderSettings,
);
}, [chapter.id]);
// Update battery level when chapter changes to ensure fresh value on navigation
const batteryLevel = useMemo(() => getBatteryLevelSync(), [chapter.id]);
const batteryLevel = useMemo(() => getBatteryLevelSync(), []);
const plugin = getPlugin(novel?.pluginId);
const pluginCustomJS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.js`;
const pluginCustomCSS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.css`;
@@ -286,6 +286,9 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
rate: readerSettingsRef.current.tts?.rate || 1,
});
};
const isRTL = plugin?.lang === 'Arabic' || plugin?.lang === 'Hebrew';
const readerDir = isRTL ? 'rtl' : 'ltr';
return (
<WebView
ref={webViewRef}
@@ -334,9 +337,9 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
| undefined;
const queue = Array.isArray(payload?.queue)
? payload?.queue.filter(
(item): item is string =>
typeof item === 'string' && item.trim().length > 0,
)
(item): item is string =>
typeof item === 'string' && item.trim().length > 0,
)
: [];
ttsQueueRef.current = queue;
if (typeof payload?.startIndex === 'number') {
@@ -416,7 +419,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
body: plugin?.imageRequestInit?.body,
html: `
<!DOCTYPE html>
<html>
<html dir="${readerDir}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" href="${assetsUriPrefix}/css/index.css">
@@ -441,8 +444,8 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
--theme-onSecondary: ${theme.onSecondary};
--theme-surface: ${theme.surface};
--theme-surface-0-9: ${color(theme.surface)
.alpha(0.9)
.toString()};
.alpha(0.9)
.toString()};
--theme-onSurface: ${theme.onSurface};
--theme-surfaceVariant: ${theme.surfaceVariant};
--theme-onSurfaceVariant: ${theme.onSurfaceVariant};
@@ -452,23 +455,20 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
@font-face {
font-family: ${readerSettings.fontFamily};
src: url("file:///android_asset/fonts/${
readerSettings.fontFamily
}.ttf");
src: url("file:///android_asset/fonts/${readerSettings.fontFamily
}.ttf");
}
</style>
<link rel="stylesheet" href="${pluginCustomCSS}">
<style>${readerSettings.customCSS}</style>
</head>
<body class="${
chapterGeneralSettings.pageReader ? 'page-reader' : ''
}">
<div class="transition-chapter" style="transform: ${
nextChapterScreenVisible.current
? 'translateX(-100%)'
: 'translateX(0%)'
};
<body class="${chapterGeneralSettings.pageReader ? 'page-reader' : ''
}">
<div class="transition-chapter" style="transform: ${nextChapterScreenVisible.current
? 'translateX(-100%)'
: 'translateX(0%)'
};
${chapterGeneralSettings.pageReader ? '' : 'display: none'}"
">${chapter.name}</div>
<div id="LNReader-chapter">
@@ -478,30 +478,28 @@ const WebViewReader: React.FC<WebViewReaderProps> = ({ onPress }) => {
</body>
<script>
var initialPageReaderConfig = ${JSON.stringify({
nextChapterScreenVisible: nextChapterScreenVisible.current,
})};
nextChapterScreenVisible: nextChapterScreenVisible.current,
})};
var initialReaderConfig = ${JSON.stringify({
readerSettings,
chapterGeneralSettings,
novel,
chapter,
nextChapter,
prevChapter,
batteryLevel,
autoSaveInterval: 2222,
DEBUG: __DEV__,
strings: {
finished: `${getString(
'readerScreen.finished',
)}: ${chapter.name.trim()}`,
nextChapter: getString('readerScreen.nextChapter', {
name: nextChapter?.name,
}),
noNextChapter: getString('readerScreen.noNextChapter'),
},
})}
readerSettings,
chapterGeneralSettings,
novel,
chapter,
nextChapter,
prevChapter,
batteryLevel,
autoSaveInterval: 2222,
DEBUG: __DEV__,
strings: {
finished: getString('readerScreen.finished') + ': ' + chapter.name.trim(),
nextChapter: getString('readerScreen.nextChapter', {
name: nextChapter?.name,
}),
noNextChapter: getString('readerScreen.noNextChapter'),
},
})}
</script>
<script src="${assetsUriPrefix}/js/polyfill-onscrollend.js"></script>
<script src="${assetsUriPrefix}/js/icons.js"></script>
@@ -130,9 +130,8 @@ const SettingsReaderScreen = () => {
@font-face {
font-family: ${readerSettings.fontFamily};
src: url("file:///android_asset/fonts/${
readerSettings.fontFamily
}.ttf");
src: url("file:///android_asset/fonts/${readerSettings.fontFamily
}.ttf");
}
</style>
@@ -171,7 +170,7 @@ const SettingsReaderScreen = () => {
return (
<SafeAreaView
excludeTop
style={{ flex: 1, backgroundColor: readerBackgroundColor }}
style={[styles.container, { backgroundColor: readerBackgroundColor }]}
>
<Appbar
mode="small"
@@ -181,7 +180,7 @@ const SettingsReaderScreen = () => {
/>
{/* Large Preview Area */}
<View style={{ flex: 1 }}>
<View style={styles.previewContainer}>
<WebView
ref={webViewRef}
originWhitelist={['*']}
@@ -189,7 +188,7 @@ const SettingsReaderScreen = () => {
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
javaScriptEnabled={true}
style={{ flex: 1, backgroundColor: readerBackgroundColor }}
style={[styles.webView, { backgroundColor: readerBackgroundColor }]}
nestedScrollEnabled={true}
onMessage={(ev: { nativeEvent: { data: string } }) => {
const event: WebViewPostEvent = JSON.parse(ev.nativeEvent.data);
@@ -232,8 +231,7 @@ const SettingsReaderScreen = () => {
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
${webViewCSS}
</head>
<body class="${
chapterGeneralSettings.pageReader ? 'page-reader' : ''
<body class="${chapterGeneralSettings.pageReader ? 'page-reader' : ''
}">
<div id="LNReader-chapter">
${dummyHTML}
@@ -242,24 +240,24 @@ const SettingsReaderScreen = () => {
</body>
<script>
var initialReaderConfig = ${JSON.stringify({
readerSettings,
chapterGeneralSettings,
novel,
chapter,
nextChapter: chapter,
batteryLevel,
autoSaveInterval: 2222,
DEBUG: __DEV__,
strings: {
finished: `${getString(
'readerScreen.finished',
)}: ${chapter.name.trim()}`,
nextChapter: getString('readerScreen.nextChapter', {
name: chapter.name,
}),
noNextChapter: getString('readerScreen.noNextChapter'),
},
})}
readerSettings,
chapterGeneralSettings,
novel,
chapter,
nextChapter: chapter,
batteryLevel,
autoSaveInterval: 2222,
DEBUG: __DEV__,
strings: {
finished: `${getString(
'readerScreen.finished',
)}: ${chapter.name.trim()}`,
nextChapter: getString('readerScreen.nextChapter', {
name: chapter.name,
}),
noNextChapter: getString('readerScreen.noNextChapter'),
},
})}
</script>
<script src="${assetsUriPrefix}/js/icons.js"></script>
<script src="${assetsUriPrefix}/js/van.js"></script>
@@ -351,4 +349,13 @@ const styles = StyleSheet.create({
tabContent: {
flex: 1,
},
container: {
flex: 1,
},
previewContainer: {
flex: 1,
},
webView: {
flex: 1,
},
});
@@ -23,7 +23,7 @@ const TabBar: React.FC<TabBarProps> = ({
theme,
}) => {
return (
<View style={[styles.container, { backgroundColor: theme.surface }]}>
<View style={styles.container}>
{tabs.map(tab => {
const isActive = activeTab === tab.id;
return (
@@ -31,9 +31,7 @@ const TabBar: React.FC<TabBarProps> = ({
key={tab.id}
style={[
styles.tab,
{
borderBottomColor: isActive ? theme.primary : 'transparent',
},
isActive && { borderBottomColor: theme.primary },
]}
onPress={() => onTabChange(tab.id)}
android_ripple={{
@@ -60,6 +58,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: 'rgba(0, 0, 0, 0.12)',
backgroundColor: 'transparent', // Will be set by theme if needed, but the lint was about static style
},
tab: {
flex: 1,
@@ -14,6 +14,7 @@ import { StorageAccessFramework } from 'expo-file-system/legacy';
import * as DocumentPicker from 'expo-document-picker';
import { useTheme, useChapterReaderSettings } from '@hooks/persisted';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
import { Button, ConfirmationDialog } from '@components/index';
import { showToast } from '@utils/showToast';
import { useBoolean } from '@hooks';
@@ -22,6 +23,7 @@ type CodeTab = 'css' | 'js';
const AdvancedTab: React.FC = () => {
const theme = useTheme();
const styles = createStyles(theme);
const { customCSS, customJS, setChapterReaderSettings } =
useChapterReaderSettings();
@@ -142,10 +144,7 @@ if (title) {
<Pressable
style={[
styles.tab,
activeCodeTab === 'css' && {
borderBottomColor: theme.primary,
borderBottomWidth: 2,
},
activeCodeTab === 'css' && styles.activeTab,
]}
onPress={() => setActiveCodeTab('css')}
android_ripple={{ color: theme.rippleColor }}
@@ -166,8 +165,8 @@ if (title) {
activeCodeTab === 'css'
? theme.primary
: theme.onSurfaceVariant,
fontWeight: activeCodeTab === 'css' ? '500' : '400',
},
activeCodeTab === 'css' && styles.activeTabLabel,
]}
>
CSS
@@ -177,10 +176,7 @@ if (title) {
<Pressable
style={[
styles.tab,
activeCodeTab === 'js' && {
borderBottomColor: theme.primary,
borderBottomWidth: 2,
},
activeCodeTab === 'js' && styles.activeTab,
]}
onPress={() => setActiveCodeTab('js')}
android_ripple={{ color: theme.rippleColor }}
@@ -201,8 +197,8 @@ if (title) {
activeCodeTab === 'js'
? theme.primary
: theme.onSurfaceVariant,
fontWeight: activeCodeTab === 'js' ? '500' : '400',
},
activeCodeTab === 'js' && styles.activeTabLabel,
]}
>
JS
@@ -304,80 +300,89 @@ if (title) {
export default AdvancedTab;
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollContainer: {
flex: 1,
},
contentContainer: {
paddingBottom: 24,
},
tabContainer: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: 'rgba(0, 0, 0, 0.12)',
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
minHeight: 48,
},
tabIcon: {
marginRight: 8,
},
tabLabel: {
fontSize: 14,
letterSpacing: 0.5,
},
editorContainer: {
marginHorizontal: 16,
marginTop: 16,
minHeight: 300,
},
codeEditor: {
minHeight: 300,
maxHeight: 400,
},
codeEditorContent: {
fontFamily: 'monospace',
fontSize: 13,
lineHeight: 20,
paddingTop: 12,
paddingBottom: 12,
},
hint: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: 12,
borderRadius: 8,
marginHorizontal: 16,
marginTop: 16,
gap: 8,
},
hintIcon: {
marginTop: 2,
},
hintText: {
flex: 1,
fontSize: 12,
lineHeight: 18,
},
actionButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginHorizontal: 16,
marginTop: 16,
gap: 8,
},
button: {
flex: 1,
},
bottomSpacing: {
height: 24,
},
});
const createStyles = (theme: ThemeColors) =>
StyleSheet.create({
container: {
flex: 1,
},
scrollContainer: {
flex: 1,
},
contentContainer: {
paddingBottom: 24,
},
tabContainer: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: 'rgba(0, 0, 0, 0.12)',
},
activeTab: {
borderBottomColor: theme.primary,
borderBottomWidth: 2,
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
minHeight: 48,
},
tabIcon: {
marginEnd: 8,
},
tabLabel: {
fontSize: 14,
letterSpacing: 0.5,
fontWeight: '400',
},
activeTabLabel: {
fontWeight: '500',
},
editorContainer: {
marginHorizontal: 16,
marginTop: 16,
minHeight: 300,
},
codeEditor: {
minHeight: 300,
maxHeight: 400,
},
codeEditorContent: {
fontFamily: 'monospace',
fontSize: 13,
lineHeight: 20,
paddingTop: 12,
paddingBottom: 12,
},
hint: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: 12,
borderRadius: 8,
marginHorizontal: 16,
marginTop: 16,
gap: 8,
},
hintIcon: {
marginTop: 2,
},
hintText: {
flex: 1,
fontSize: 12,
lineHeight: 18,
},
actionButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginHorizontal: 16,
marginTop: 16,
gap: 8,
},
button: {
flex: 1,
},
bottomSpacing: {
height: 24,
},
});
+58 -36
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { View, StyleSheet, Image } from 'react-native';
import {
Portal,
@@ -16,6 +16,25 @@ import MangaUpdatesLoginDialog from './components/MangaUpdatesLoginDialog';
import { authenticateWithCredentials } from '@services/Trackers/mangaUpdates';
import { showToast } from '@utils/showToast';
interface TrackerCheckIconProps {
theme: any;
checked: boolean;
}
const TrackerCheckIcon = ({ theme, checked, ...props }: TrackerCheckIconProps) => {
if (!checked) {
return null;
}
return (
<PaperList.Icon
{...props}
color={theme.primary}
icon="check"
style={styles.iconStyle}
/>
);
};
const AniListLogo = () => (
<View style={styles.logoContainer}>
<Image
@@ -83,6 +102,39 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
}
};
const renderAniListRight = useCallback(
(props: any) => (
<TrackerCheckIcon
{...props}
theme={theme}
checked={isTrackerAuthenticated('AniList')}
/>
),
[theme, isTrackerAuthenticated],
);
const renderMyAnimeListRight = useCallback(
(props: any) => (
<TrackerCheckIcon
{...props}
theme={theme}
checked={isTrackerAuthenticated('MyAnimeList')}
/>
),
[theme, isTrackerAuthenticated],
);
const renderMangaUpdatesRight = useCallback(
(props: any) => (
<TrackerCheckIcon
{...props}
theme={theme}
checked={isTrackerAuthenticated('MangaUpdates')}
/>
),
[theme, isTrackerAuthenticated],
);
return (
<SafeAreaView excludeTop>
<Provider>
@@ -106,17 +158,7 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
title="AniList"
titleStyle={{ color: theme.onSurface }}
left={AniListLogo}
right={
isTrackerAuthenticated('AniList')
? () => (
<PaperList.Icon
color={theme.primary}
icon="check"
style={styles.iconStyle}
/>
)
: undefined
}
right={renderAniListRight}
onPress={async () => {
if (isTrackerAuthenticated('AniList')) {
showModal('AniList');
@@ -134,17 +176,7 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
title="MyAnimeList"
titleStyle={{ color: theme.onSurface }}
left={MyAnimeListLogo}
right={
isTrackerAuthenticated('MyAnimeList')
? () => (
<PaperList.Icon
color={theme.primary}
icon="check"
style={styles.iconStyle}
/>
)
: undefined
}
right={renderMyAnimeListRight}
onPress={async () => {
if (isTrackerAuthenticated('MyAnimeList')) {
showModal('MyAnimeList');
@@ -162,17 +194,7 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
title="MangaUpdates"
titleStyle={{ color: theme.onSurface }}
left={MangaUpdatesLogo}
right={
isTrackerAuthenticated('MangaUpdates')
? () => (
<PaperList.Icon
color={theme.primary}
icon="check"
style={styles.iconStyle}
/>
)
: undefined
}
right={renderMangaUpdatesRight}
onPress={() => {
if (isTrackerAuthenticated('MangaUpdates')) {
showModal('MangaUpdates');
@@ -184,8 +206,8 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
style={styles.listItem}
/>
{isTrackerAuthenticated('MyAnimeList') &&
getTrackerAuth('MyAnimeList')?.auth?.expiresAt &&
getTrackerAuth('MyAnimeList')!.auth.expiresAt <
getTrackerAuth('MyAnimeList')?.auth?.expiresAt &&
getTrackerAuth('MyAnimeList')!.auth.expiresAt <
new Date(Date.now()) ? (
<>
<List.Divider theme={theme} />
@@ -22,12 +22,12 @@ type UpdateCardProps = {
descriptionText: string;
deleteChapter: (chapter: Update | DownloadedChapter) => void;
} & (
| { chapterList: Update[] | DownloadedChapter[]; chapterListInfo?: undefined }
| {
| { chapterList: Update[] | DownloadedChapter[]; chapterListInfo?: undefined }
| {
chapterListInfo: UpdateOverview;
chapterList?: undefined;
}
);
);
const UpdateNovelCard: React.FC<UpdateCardProps> = ({
onlyDownloadedChapters = false,
@@ -209,7 +209,7 @@ function createStyles(theme: ThemeColors) {
return StyleSheet.create({
alignSelf: { alignSelf: 'center' },
chapterList: {
marginLeft: -40,
marginStart: -40,
},
container: {
alignItems: 'center',
@@ -222,7 +222,7 @@ function createStyles(theme: ThemeColors) {
},
description: { fontSize: 12 },
novelCover: {
marginRight: 8,
marginEnd: 8,
},
padding: {
paddingHorizontal: 16,
+1 -1
View File
@@ -91,7 +91,7 @@ const insertLocalChapter = async (
}
const novelDir = NOVEL_STORAGE + '/local/' + novelId;
chapterText = chapterText.replace(
/=(?<= href=| src=)(["'])([^]*?)\1/g,
/[=](?<= href=| src=)(["'])([^]*?)\1/g,
(_, __, $2: string) => {
return `="file://${novelDir}/${$2.split(/[/\\]/).pop()}"`;
},
+236 -236
View File
@@ -28,21 +28,21 @@
"novelInsertFailed": "فشل إدراج الرواية",
"useFAB": "Use FAB instead of button",
"userAgent": "وكيل المستخدم",
"recreateDBIndexes": "",
"recreateDBIndexesToast": "Recreated DB indexes",
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices."
"recreateDBIndexes": "إعادة إنشاء فهارس قاعدة البيانات",
"recreateDBIndexesToast": "تمت إعادة إنشاء فهارس قاعدة البيانات",
"recreateDBIndexesDialogTitle": "سيتم إعادة إنشاء جميع فهارس قاعدة البيانات.\nقد يستغرق هذا بعض الوقت.",
"recreateDBIndexesDesc": "يعيد إنشاء فهارس قاعدة البيانات. قد يؤدي هذا إلى تحسين الأداء على الأجهزة الأبطأ."
},
"appearance": "مظهر",
"appearanceScreen": {
"accentColor": "لون مميز",
"alwaysShowNavLabels": "إظهار تسميات التنقل دائمًا",
"appLanguage": "",
"appLanguage": "لغة التطبيق",
"languagePickerModal": {
"title": "Select Language",
"restartNote": "You will need to restart the app for the language change to take full effect."
"title": "اختر اللغة",
"restartNote": "ستحتاج إلى إعادة تشغيل التطبيق ليتم تفعيل تغيير اللغة بالكامل."
},
"appLanguageDefault": "Default",
"appLanguageDefault": "الافتراضي",
"appTheme": "مظهر التطبيق",
"darkTheme": "المظهر الداكن",
"hideBackdrop": "إخفاء الخلفية",
@@ -52,22 +52,22 @@
"pureBlackDarkMode": "الأسود القاتم للوضع الداكن",
"showHistoryInTheNav": "إظهار التاريخ في شريط التنقل",
"showUpdatesInTheNav": "إظهار التحديثات في شريط التنقل",
"themeMode": "Theme mode",
"themeModeLight": "Light",
"themeModeDark": "Dark",
"themeModeSystem": "System",
"themeMode": "وضع السمة",
"themeModeLight": "فاتح",
"themeModeDark": "داكن",
"themeModeSystem": "النظام",
"theme": {
"default": "تلقائي",
"lavender": "",
"midnightDusk": "صخرة منتصف الليل",
"daybreakBloom": "Daybreak Bloom",
"strawberry": "Strawberry Daiquiri",
"tako": "Tako",
"teal": "Teal",
"turquoise": "Turquoise",
"yotsuba": "Yotsuba",
"catppuccin": "Catppuccin",
"yinyang": "Yin & Yang"
"lavender": "لافندر",
"midnightDusk": "منتصف الليل",
"daybreakBloom": "زهرة الفجر",
"strawberry": "فراولة دايكيري",
"tako": "تاكو",
"teal": "أزرق مخضر",
"turquoise": "فيروزي",
"yotsuba": "يوتسوبا",
"catppuccin": "كاتبوتشين",
"yinyang": "ين ويانغ"
}
},
"backupScreen": {
@@ -97,50 +97,50 @@
"restoringNovels": "Restoring Novels",
"restoringSettings": "Restoring Settings",
"settingsRestored": "Settings restored",
"settingsFileNotFound": "Settings file not found in backup",
"settingsRestoreFailed": "Failed to restore settings: %{error}",
"settingsFileWriteFailed": "Failed to write settings file: %{error}",
"versionFileWriteFailed": "Failed to write version file: %{error}",
"settingsFileNotFound": "لم يتم العثور على ملف الإعدادات في النسخة الاحتياطية",
"settingsRestoreFailed": "فشل استعادة الإعدادات: %{error}",
"settingsFileWriteFailed": "فشل كتابة ملف الإعدادات: %{error}",
"versionFileWriteFailed": "فشل كتابة ملف الإصدار: %{error}",
"drive": {
"backup": "Drive Backup",
"backupInterruped": "Drive Backup Interrupted",
"googleDriveBackup": "Google Drive Backup",
"restore": "Drive Restore",
"restoreInterruped": "Drive Restore Interrupted"
"backup": "نسخ احتياطي للدرايف",
"backupInterruped": "انقطع النسخ الاحتياطي للدرايف",
"googleDriveBackup": "نسخ احتياطي لجوجل درايف",
"restore": "استعادة من الدرايف",
"restoreInterruped": "انقطعت الاستعادة من الدرايف"
},
"googeDrive": "Googe Drive",
"googeDriveDesc": "Backup to your Google Drive",
"invalidBackupFolder": "Invalid backup folder",
"localBackup": "Local Backup",
"noBackupFound": "No backup found",
"preparingData": "Preparing Data",
"googeDrive": "جوجل درايف",
"googeDriveDesc": "نسخ احتياطي إلى جوجل درايف الخاص بك",
"invalidBackupFolder": "مجلد نسخة احتياطية غير صالح",
"localBackup": "نسخ احتياطي محلي",
"noBackupFound": "لم يتم العثور على نسخة احتياطية",
"preparingData": "تحضير البيانات",
"remote": {
"backup": "Self Host Backup",
"host": "Host",
"unknownHost": "Unknown host"
"backup": "نسخ احتياطي للاستضافة الذاتية",
"host": "المضيف",
"unknownHost": "مضيف مجهول"
},
"remoteBackup": "Remote Backup",
"restoreBackup": "Restore backup",
"restoreBackupDesc": "Restore library from backup file",
"restoreLargeBackupsWarning": "Restoring large backups may freeze the app until restoring is finished",
"restorinBackup": "Restoring backup",
"restoringData": "Restoring Data",
"selfHost": "Self Host",
"selfHostDesc": "Backup to your server",
"uploadingData": "Uploading Data",
"uploadingDownloadedFiles": "Uploading Downloaded Files"
"remoteBackup": "نسخ احتياطي عن بعد",
"restoreBackup": "استعادة النسخة الاحتياطية",
"restoreBackupDesc": "استعادة المكتبة من ملف النسخة الاحتياطية",
"restoreLargeBackupsWarning": "استعادة النسخ الاحتياطية الكبيرة قد تؤدي لتجميد التطبيق حتى انتهاء الاستعادة",
"restorinBackup": "استعادة النسخة الاحتياطية",
"restoringData": "استعادة البيانات",
"selfHost": "استضافة ذاتية",
"selfHostDesc": "نسخ احتياطي إلى خادمك الخاص",
"uploadingData": "رفع البيانات",
"uploadingDownloadedFiles": "رفع الملفات المحملة"
},
"browse": "تصفح",
"browseScreen": {
"addedToLibrary": "تمت الإضافة إلى المكتبة",
"available": "متاح",
"deletePluginMessage": "Are you sure you want to uninstall %{name}?",
"deletePluginMessage": "هل أنت متأكد أنك تريد إلغاء تثبيت %{name}؟",
"discover": "استكشف",
"globalSearch": "بحث عام",
"installFailed": "Installation failed: %{name}",
"installed": "Installed",
"installedPlugin": "Installed %{name}",
"installedPlugins": "Installed plugins",
"installFailed": "فشل التثبيت: %{name}",
"installed": "تم التثبيت",
"installedPlugin": "تم تثبيت %{name}",
"installedPlugins": "الإضافات المثبتة",
"lastUsed": "اخر استخدام",
"latest": "الاخير",
"listEmpty": "تفعيل اللغات من الاعدادات",
@@ -151,31 +151,31 @@
"selectSourceDesc": "Select a Source To Migrate From"
},
"noSource": "Your library does not have any novels from this source",
"pinnedPlugin": "Pinned %{name}",
"pinnedPlugins": "Pinned plugins",
"pinnedPlugin": "تم تثبيت %{name} في الأعلى",
"pinnedPlugins": "الإضافات المثبتة في الأعلى",
"removeFromLibrary": "تمت إزالته من المكتبة",
"searchbar": "البحث في المصادر",
"searchResults": "Search results",
"selectNovel": "Select Novel",
"uninstalledPlugin": "Uninstalled %{name}",
"unpinnedPlugin": "Unpinned %{name}",
"updateFailed": "",
"updatedTo": "Updated to %{version}",
"searchResults": "نتائج البحث",
"selectNovel": "اختر الرواية",
"uninstalledPlugin": "تم إلغاء تثبيت %{name}",
"unpinnedPlugin": "تم إلغاء التثبيت في الأعلى لـ %{name}",
"updateFailed": "فشل التحديث",
"updatedTo": "تم التحديث إلى %{version}",
"settings": {
"title": "Plugin Settings",
"description": "Fill in the plugin settings. Restart app to apply the settings."
"title": "إعدادات الإضافة",
"description": "املأ إعدادات الإضافة. أعد تشغيل التطبيق لتطبيق الإعدادات."
}
},
"browseSettings": "تصفح الإعدادات",
"browseSettings": "إعدادات التصفح",
"browseSettingsScreen": {
"concurrentSearches": "Concurrent Source Searches",
"multi": "Multi",
"concurrentSearches": "عمليات البحث المتوازنة في المصادر",
"multi": "متعدد",
"languages": "اللغات"
},
"categories": {
"addCategories": "إضافة تصنيف",
"cantDeleteDefault": "You cant delete default category",
"default": "Default",
"cantDeleteDefault": "لا يمكنك حذف التصنيف الافتراضي",
"default": "افتراضي",
"defaultCategory": "التصنيف الافتراضي",
"deleteModal": {
"desc": "هل ترغب في حذف التصنيف",
@@ -183,37 +183,37 @@
},
"duplicateError": "يوجد تصنيف بهذا الاسم بالفعل!",
"editCategories": "إعادة تسمية التصنيف",
"emptyMsg": "ليس لديك تصنيف. اضغط على زر علامة الزائد لإنشاء واحد لتنظيم مكتبتك",
"header": "تعديل التصنيف",
"local": "Local",
"setCategories": "حدد التصنيف",
"setModalEmptyMsg": "ليس لديك تصنيف. اضغط على زر تعديل لإنشاء واحد لتنظيم مكتبتك"
"emptyMsg": "ليس لديك تصنيفات. اضغط على زر علامة الزائد لإنشاء واحد لتنظيم مكتبتك",
"header": "تعديل التصنيفات",
"local": "محلي",
"setCategories": "حدد التصنيفات",
"setModalEmptyMsg": "ليس لديك تصنيفات. اضغط على زر تعديل لإنشاء واحد لتنظيم مكتبتك"
},
"repositories": {
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
"emptyMsg": "ليس لديك مستودعات. أضف مستودع الإضافات الأول لتبدأ."
},
"common": {
"about": "About",
"about": "حول",
"add": "إضافة",
"all": "الكل",
"backup": "Backup",
"cancel": "الغاء",
"backup": "نسخة احتياطية",
"cancel": "إلغاء",
"categories": "تصنيفات",
"chapters": "فصول",
"clear": "تنظيف",
"copiedToClipboard": "Copied to clipboard: %{name}",
"delete": "Delete",
"deleted": "Deleted %{name}",
"deprecated": "Deprecated",
"clear": "مسح",
"copiedToClipboard": "تم النسخ إلى الحافظة: %{name}",
"delete": "حذف",
"deleted": "تم حذف %{name}",
"deprecated": "مهجور",
"display": "عرض",
"done": "Done",
"downloads": "Downloads",
"done": "تم",
"downloads": "التنزيلات",
"edit": "تعديل",
"example": "Example",
"filter": "التصفية",
"globally": "عالمي",
"example": "مثال",
"filter": "تصفية",
"globally": "عالمياً",
"install": "تثبيت",
"logout": "Logout",
"logout": "تسجيل الخروج",
"name": "اسم",
"newUpdateAvailable": "تحديث جديد متاح",
"ok": "حسنآ",
@@ -224,27 +224,27 @@
"restore": "Restore",
"resume": "Resume",
"retry": "أعد المحاولة",
"save": "تثبيت",
"save": "حفظ",
"search": "بحث",
"searchFor": "بحث عن",
"searchResults": "نتائج البحث",
"settings": "الإعدادات",
"show": "Show",
"signIn": "Sign in",
"signOut": "Sign out",
"sort": "الفرز",
"show": "إظهار",
"signIn": "تسجيل الدخول",
"signOut": "تسجيل الخروج",
"sort": "فرز",
"submit": "إرسال",
"loading": "Loading ...",
"warning": "Warning"
"loading": "جارٍ التحميل...",
"warning": "تحذير"
},
"webview": {
"refresh": "Refresh",
"share": "Share",
"openInBrowser": "Open in browser",
"clearCookies": "Clear cookies",
"cookiesCleared": "Cookies cleared",
"clearData": "Clear WebView data",
"dataDeleted": "WebView data cleared"
"refresh": "تحديث",
"share": "مشاركة",
"openInBrowser": "فتح في المتصفح",
"clearCookies": "مسح ملفات تعريف الارتباط",
"cookiesCleared": "تم مسح ملفات تعريف الارتباط",
"clearData": "مسح بيانات WebView",
"dataDeleted": "تم مسح بيانات WebView"
},
"date": {
"calendar": {
@@ -255,18 +255,18 @@
}
},
"downloadScreen": {
"cancelDownloads": "Cancel downloads",
"cancelled": "Downloads cancelled.",
"chapterEmptyOrScrapeError": "Either chapter is empty or the app couldn't scrape it",
"chapterName": "Chapter: %{name}",
"completed": "Download completed",
"dbInfo": "يتم حفظ التنزيلات في قاعدة بيانات SQLite",
"downloading": "Downloading",
"downloadingNovel": "Downloading: %{name}",
"downloadsLower": "تم التنزيل",
"noDownloads": "لا توجد تحميلات",
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
"cancelDownloads": "إلغاء التنزيلات",
"cancelled": "تم إلغاء التنزيلات.",
"chapterEmptyOrScrapeError": "إما أن الفصل فارغ أو لم يتمكن التطبيق من كشطه",
"chapterName": "الفصل: %{name}",
"completed": "اكتمل التنزيل",
"dbInfo": "يتم حفظ التنزيلات في قاعدة بيانات SQLite.",
"downloading": "جارٍ التنزيل",
"downloadingNovel": "جارٍ تنزيل: %{name}",
"downloadsLower": "التنزيلات",
"noDownloads": "لا توجد تنزيلات",
"pluginNotFound": "لم يتم العثور على الإضافة!",
"removeDownloadsWarning": "هل أنت متأكد؟ سيتم حذف جميع الفصول المحملة."
},
"generalSettings": "العامة",
"generalSettingsScreen": {
@@ -275,32 +275,32 @@
"bySource": "حسب المصدر",
"chapterSort": "الفرز الافتراضي للفصول",
"desc": "(ترتيب تنازلي)",
"disableLoadingAnimations": "Disable loading animations",
"disableLoadingAnimationsDesc": "May improve performance on slower devices",
"disableLoadingAnimations": "تعطيل حركات التحميل",
"disableLoadingAnimationsDesc": "قد يحسن الأداء على الأجهزة الأبطأ",
"disableHapticFeedback": "تعطيل الاهتزاز",
"disableHapticFeedbackDescription": "Turn off vibrations for touch interactions.",
"disableHapticFeedbackDescription": "إيقاف الاهتزازات عند اللمس.",
"displayMode": "وضع العرض",
"downloadNewChapters": "تحميل فصول جديدة",
"downloadNewChapters": "تنزيل فصول جديدة",
"epub": "EPUB",
"epubLocation": "موقع EPUB",
"epubLocationDescription": "مكان لحفظ ملفات EPUB وتصديرها.",
"globalUpdate": "التحديث الشامل",
"gridSize": "Grid size",
"gridSizeDesc": "%{num} per row",
"epubLocationDescription": "المكان الذي تفتح فيه وتصدر إليه ملفات EPUB الخاصة بك.",
"globalUpdate": "تحديث شامل",
"gridSize": "حجم الشبكة",
"gridSizeDesc": "%{num} في كل صف",
"itemsPerRow": "العناصر في كل صف",
"itemsPerRowLibrary": "العناصر لكل صف في المكتبة",
"jumpToLastReadChapter": "Jump to last read chapter in list",
"jumpToLastReadChapter": "انتقل إلى آخر فصل مقروء في القائمة",
"novel": "رواية",
"novelBadges": "شارات الرواية",
"novelSort": "فرز الروايات",
"refreshMetadata": "تحديث البيانات الوصفية تلقائيًا",
"refreshMetadataDescription": "تحقق الغلاف الجديد و التفاصيل عند تحديث المكتبة",
"sortOrder": "Sort Order",
"updateLibrary": "تحديث المكتبة عند تشغيل التطبيق",
"updateLibraryDesc": "Not recommended for low devices",
"updateOngoing": "تحديث الروايات الجارية فقط",
"updateTime": "وقت آخر تحديث",
"useFAB": "استخدام FAB في المكتبة"
"refreshMetadata": "تحديث البيانات الوصفية تلقائياً",
"refreshMetadataDescription": "التحقق من الغلاف الجديد والتفاصيل عند تحديث المكتبة",
"sortOrder": "ترتيب الفرز",
"updateLibrary": "تحديث المكتبة عند التشغيل",
"updateLibraryDesc": "لا ينصح به للأجهزة الضعيفة",
"updateOngoing": "تحديث الروايات المستمرة فقط",
"updateTime": "إظهار وقت آخر تحديث",
"useFAB": "استخدام الزر العائم (FAB) في المكتبة"
},
"globalSearch": {
"allSources": "جميع المصادر",
@@ -308,10 +308,10 @@
},
"history": "السجل",
"historyScreen": {
"chapter": "Chapter",
"clearHistorWarning": "هل أنت متأكد؟ سيتم مسح كل التاريخ.",
"deleted": "History deleted.",
"nothingReadRecently": "Nothing read recently",
"chapter": "فصل",
"clearHistorWarning": "هل أنت متأكد؟ سيتم مسح كل السجل.",
"deleted": "تم حذف السجل.",
"nothingReadRecently": "لم يتم قراءة شيء مؤخراً",
"searchbar": "سجل البحث"
},
"library": "المكتبة",
@@ -328,57 +328,57 @@
"noTitle": "تغطية الشبكة فقط",
"numberOfItems": "Number of Items",
"showNoOfItems": "إظهار عدد العناصر",
"unread": "Unread",
"unreadBadges": "شارات الغير مقروء"
"unread": "غير مقروء",
"unreadBadges": "شارات غير مقروءة"
},
"filters": {
"completed": "مكتمل",
"downloaded": "التحميلات",
"downloaded": "منزل",
"started": "بدأت",
"unread": "غير مقروء"
},
"sortOrders": {
"alphabetically": "حسب الترتيب الأبجدي",
"alphabetically": "أبجدياً",
"dateAdded": "تاريخ الإضافة",
"download": "منزّل",
"download": "التنزيلات",
"lastRead": "آخر قراءة",
"lastUpdated": "آخر تحديث",
"totalChapters": "مجموع الفصول",
"totalChapters": "إجمالي الفصول",
"unread": "غير مقروء"
}
},
"empty": "مكتبتك فارغة، أضف رواية الى مكتبتك من خلال \"تصفح\".",
"empty": "مكتبتك فارغة. أضف سلاسل إلى مكتبتك من التصفح.",
"extraMenu": {
"importEpub": "Import Epub",
"openRandom": "Open Random Entry",
"updateCategory": "Update Category",
"updateLibrary": "Update Library"
"importEpub": "استيراد Epub",
"openRandom": "فتح مدخل عشوائي",
"updateCategory": "تحديث التصنيف",
"updateLibrary": "تحديث المكتبة"
},
"searchbar": "البحث في المكتبة"
},
"more": "المزيد",
"moreScreen": {
"downloadOnly": "التنزيلات فقط",
"downloadOnlyDesc": "Filters all novels in your library",
"downloadQueue": "Download queue",
"incognitoMode": "الوضع المخفي",
"incognitoModeDesc": "Pauses reading history"
"downloadOnlyDesc": "يصفي جميع الروايات في مكتبتك",
"downloadQueue": "قائمة التنزيلات",
"incognitoMode": "وضع التصفح الخفي",
"incognitoModeDesc": "يوقف سجل القراءة مؤقتاً"
},
"novelScreen": {
"addToLibaray": "إضافة إلى المكتبة",
"bottomSheet": {
"displays": {
"chapterNumber": "Chapter number",
"sourceTitle": "Source title"
"chapterNumber": "رقم الفصل",
"sourceTitle": "عنوان المصدر"
},
"filters": {
"bookmarked": "Bookmarked",
"downloaded": "Downloaded",
"unread": "Unread"
"bookmarked": "محفوظ",
"downloaded": "منزل",
"unread": "غير مقروء"
},
"order": {
"byChapterName": "By chapter name",
"bySource": "By source"
"byChapterName": "حسب اسم الفصل",
"bySource": "حسب المصدر"
}
},
"chapterChapnum": "Chapter %{num}",
@@ -399,10 +399,10 @@
"title": "Export Novel as EPUB"
},
"epub": {
"exportFailed": "Failed to export EPUB: %{error}",
"exportSuccess": "Successfully exported %{chapters} chapters as EPUB",
"noDownloadedChapters": "No downloaded chapters found. Please download chapters before exporting.",
"noNovelSelected": "No novel selected for export"
"exportFailed": "فشل تصدير EPUB: %{error}",
"exportSuccess": "تم تصدير %{chapters} فصلاً بنجاح كملف EPUB",
"noDownloadedChapters": "لم يتم العثور على فصول محملة. يرجى تحميل الفصول قبل التصدير.",
"noNovelSelected": "لم يتم اختيار رواية للتصدير"
},
"coverSaved": "Cover saved",
"coverNotSaved": "Cover not saved",
@@ -460,63 +460,63 @@
},
"readerScreen": {
"bottomSheet": {
"allowTextSelection": "Text selection",
"autoscroll": "Auto-scroll",
"bionicReading": "Bionic reading",
"tapToScroll": "Tap to scroll",
"allowTextSelection": "تحديد النص",
"autoscroll": "تمرير تلقائي",
"bionicReading": "قراءة بيونية",
"tapToScroll": "النقر للتمرير",
"color": "اللون",
"fontStyle": "نمط الخط",
"fullscreen": "تكبير الشاشة",
"lineHeight": "Line height",
"padding": "Padding",
"pageReader": "Paged reading (Experimental)",
"removeExtraSpacing": "Remove extra spacing",
"scrollAmount": "مقدار التمرير (افتراضيًا طول الشاشة)",
"showBatteryAndTime": "Battery & time",
"showProgressPercentage": "Reading progress",
"swipeGestures": "Swipe between chapters",
"textAlign": "Text alignment",
"fullscreen": "ملء الشاشة",
"lineHeight": "ارتفاع السطر",
"padding": "تباعد",
"pageReader": "قراءة بصفحات (تجريبي)",
"removeExtraSpacing": "إزالة المسافات الزائدة",
"scrollAmount": "مقدار التمرير (افتراضياً طول الشاشة)",
"showBatteryAndTime": "البطارية والوقت",
"showProgressPercentage": "تقدم القراءة",
"swipeGestures": "السحب بين الفصول",
"textAlign": "محاذاة النص",
"textSize": "حجم الخط",
"useChapterDrawerSwipeNavigation": "Swipe to open drawer",
"verticalSeekbar": "Vertical seekbar",
"keepScreenOn": "Keep screen on",
"volumeButtonsScroll": "Volume button scrolling"
"useChapterDrawerSwipeNavigation": "السحب لفتح الدرج",
"verticalSeekbar": "شريط تمرير رأسي",
"keepScreenOn": "إبقاء الشاشة مضاءة",
"volumeButtonsScroll": "التمرير بأزرار الصوت"
},
"drawer": {
"scrollToBottom": "التمرير للأسفل",
"scrollToCurrentChapter": "التمرير إلى الفصل الحالي",
"scrollToTop": "التمرير إلى الأعلى"
},
"emptyChapterMessage": "<p>Chapter is empty.</p><p>Report on <a href='https://github.com/LNReader/lnreader-sources/issues/new/choose'>GitHub</a> if it's available in WebView.</p><p>Plugin: %{pluginId}</p><p>Novel: %{novelName}</p><p>Chapter: %{chapterName}</p>",
"emptyChapterMessage": "<p>الفصل فارغ.</p><p>بلغ عنه على <a href='https://github.com/LNReader/lnreader-sources/issues/new/choose'>GitHub</a> إذا كان متاحاً في WebView.</p><p>الإضافة: %{pluginId}</p><p>الرواية: %{novelName}</p><p>الفصل: %{chapterName}</p>",
"finished": "انتهى",
"nextChapter": "Next: %{name}",
"noNextChapter": "لا يوجد فصل اخر",
"noPreviousChapter": "There's no previous chapter"
"nextChapter": "التالي: %{name}",
"noNextChapter": "لا يوجد فصل تالي",
"noPreviousChapter": "لا يوجد فصل سابق"
},
"readerSettings": {
"autoScrollInterval": "Scroll interval (seconds)",
"autoScrollOffset": "Scroll offset (screen heights)",
"autoScrollInterval": "فاصل التمرير (ثواني)",
"autoScrollOffset": "إزاحة التمرير (ارتفاع الشاشة)",
"backgroundColor": "لون الخلفية",
"backgroundColorModal": "Background color",
"clearCustomCSS": "Reset your custom CSS?",
"clearCustomJS": "Reset your custom JS?",
"cssHint": "Target specific sources using #sourceId-[SOURCEID] in your selectors",
"backgroundColorModal": "لون الخلفية",
"clearCustomCSS": "إعادة تعيين CSS المخصص؟",
"clearCustomJS": "إعادة تعيين JS المخصص؟",
"cssHint": "استهدف مصادر محددة باستخدام #sourceId-[SOURCEID] في محدداتك",
"customCSS": "تنسيقات CSS مخصصة",
"customJS": "مخصص JS",
"deleteCustomTheme": "Delete theme",
"jsHint": "Available variables: html, novelName, chapterName, sourceId, chapterId, novelId",
"navigationControls": "Navigation Controls",
"notSaved": "لم يُحفظ",
"openCSSFile": "Import CSS file",
"openJSFile": "Import JS file",
"preset": "الإعداد المسبق",
"readingMode": "Reading Mode",
"readerTheme": "Theme",
"saveCustomTheme": "Save theme",
"customJS": "JS مخصص",
"deleteCustomTheme": "حذف السمة",
"jsHint": "المتغيرات المتاحة: html, novelName, chapterName, sourceId, chapterId, novelId",
"navigationControls": "عناصر التحكم في التنقل",
"notSaved": "لم يتم الحفظ",
"openCSSFile": "استيراد ملف CSS",
"openJSFile": "استيراد ملف JS",
"preset": "إعداد مسبق",
"readingMode": "وضع القراءة",
"readerTheme": "السمة",
"saveCustomTheme": "حفظ السمة",
"textColor": "لون النص",
"textColorModal": "Text color",
"textColorModal": "لون النص",
"title": "القارئ",
"verticalSeekbarDesc": "Use vertical seekbar"
"verticalSeekbarDesc": "استخدم شريط تمرير رأسي"
},
"sourceScreen": {
"noResultsFound": "لم يتم العثور على أي نتائج"
@@ -524,51 +524,51 @@
"statsScreen": {
"downloadedChapters": "الفصول المحملة",
"genreDistribution": "توزيع الأنواع",
"readChapters": "الفصول المقروء",
"sources": "Sources",
"readChapters": "الفصول المقروءة",
"sources": "المصادر",
"statusDistribution": "توزيع الحالات",
"title": "الإحصائيات",
"titlesInLibrary": "العناوين في المكتبة",
"totalChapters": "مجموع الفصول",
"unreadChapters": "الفصول الغير مقروء"
"totalChapters": "إجمالي الفصول",
"unreadChapters": "الفصول غير المقروءة"
},
"tracking": "Tracking",
"tracking": "التتبع",
"trackingScreen": {
"logOutMessage": "Log out from %{name}?",
"revalidate": "Revalidate",
"services": "Services"
"logOutMessage": "تسجيل الخروج من %{name}؟",
"revalidate": "إعادة التحقق",
"services": "الخدمات"
},
"updates": "التحديثات",
"updatesScreen": {
"deletedChapters": "Deleted %{num} chapters",
"emptyView": "لا توجد تحديثات",
"deletedChapters": "تم حذف %{num} فصلاً",
"emptyView": "لا توجد تحديثات مؤخراً",
"lastUpdatedAt": "آخر تحديث للمكتبة:",
"libraryUpdated": "Library Updated",
"libraryUpdated": "تم تحديث المكتبة",
"newChapters": "فصول جديدة",
"novelsUpdated": "%{num} novels updated",
"novelsUpdated": "تم تحديث %{num} رواية",
"searchbar": "البحث في التحديثات",
"unableToGetNovel": "Unable to get novel",
"updatesLower": "التحديثات",
"updatingLibrary": "Updating library"
"unableToGetNovel": "تعذر جلب الرواية",
"updatesLower": "تحديثات",
"updatingLibrary": "تحديث المكتبة"
},
"onboardingScreen": {
"welcome": "Welcome",
"pickATheme": "Pick a theme",
"light": "Light",
"dark": "Dark",
"system": "System",
"complete": "Complete"
"welcome": "مرحباً",
"pickATheme": "اختر سمة",
"light": "فاتح",
"dark": "داكن",
"system": "النظام",
"complete": "إكمال"
},
"notifications": {
"IMPORT_EPUB": "Importing EPUB",
"UPDATE_LIBRARY": "Updating Library",
"DRIVE_BACKUP": "Google Drive Backup",
"DRIVE_RESTORE": "Google Drive Restore",
"SELF_HOST_BACKUP": "Self-Host Backup",
"SELF_HOST_RESTORE": "Self-Host Restore",
"LOCAL_BACKUP": "Local Backup",
"LOCAL_RESTORE": "Local Restore",
"MIGRATE_NOVEL": "Migrating Novel",
"DOWNLOAD_CHAPTER": "Downloading Chapter"
"IMPORT_EPUB": "جاري استيراد EPUB",
"UPDATE_LIBRARY": "جاري تحديث المكتبة",
"DRIVE_BACKUP": "نسخ احتياطي لجوجل درايف",
"DRIVE_RESTORE": "استعادة من جوجل درايف",
"SELF_HOST_BACKUP": "نسخ احتياطي للاستضافة الذاتية",
"SELF_HOST_RESTORE": "استعادة من الاستضافة الذاتية",
"LOCAL_BACKUP": "نسخ احتياطي محلي",
"LOCAL_RESTORE": "استعادة من نسخة احتياطية محلية",
"MIGRATE_NOVEL": "جاري نقل الرواية",
"DOWNLOAD_CHAPTER": "جاري تنزيل الفصل"
}
}
}