feat: Add RTL (Right-to-Left) Language Support (#1717)
feat: RTL support
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -88,7 +88,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 15,
|
||||
marginLeft: 16,
|
||||
paddingRight: 8,
|
||||
marginStart: 16,
|
||||
paddingEnd: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -57,6 +57,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
radioButtonLabel: {
|
||||
fontSize: 16,
|
||||
marginLeft: 16,
|
||||
marginStart: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -72,6 +72,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
marginStart: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -136,7 +136,7 @@ export const useLibrary = (): UseLibraryReturnType => {
|
||||
getLibrary();
|
||||
}
|
||||
prevRestoreTasksCountRef.current = restoreTasksCount;
|
||||
}, [restoreTasksCount]);
|
||||
}, [getLibrary, restoreTasksCount]);
|
||||
|
||||
return {
|
||||
library,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}"`;
|
||||
},
|
||||
|
||||
@@ -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": "جاري تنزيل الفصل"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user