Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae8d055689 | |||
| 1607f80a0a | |||
| a0c20e209c | |||
| 648477931b | |||
| e42e8f66f2 | |||
| de5358727a | |||
| 706459783f | |||
| 1ca9a679cf | |||
| 920f88aee4 | |||
| cef3ae5bdf | |||
| 39c788e576 | |||
| d40713ac99 | |||
| f53bed7190 | |||
| 0b9c415681 | |||
| 38003c59e0 | |||
| f1119d8c30 | |||
| a5e2b4d4f0 | |||
| a12cba030d | |||
| 56d1140aad | |||
| 0379141304 | |||
| b6e9f7157d | |||
| edb3df719f | |||
| 9bde3df74d | |||
| 2426e6fe3e | |||
| 93209abebd | |||
| 158786ccd5 | |||
| 77be5624f2 | |||
| 4b580c90ef |
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
name: Issue report
|
||||
description: Report an issue in LNReader
|
||||
labels: [Bug]
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "2.1.3"
|
||||
Example: "2.1.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -88,7 +88,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve LNReader
|
||||
labels: [Feature Request]
|
||||
labels: [feature]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
@@ -34,7 +34,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import AppErrorBoundary, {
|
||||
ErrorFallback,
|
||||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import CrashRecoveryGate from '@components/AppErrorBoundary/CrashRecoveryGate';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
@@ -64,7 +65,7 @@ const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
|
||||
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const Application = () => {
|
||||
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
|
||||
const { success: databaseReady, error: databaseError } = useInitDatabase();
|
||||
const { ready: servicesReady, error: servicesError } =
|
||||
@@ -80,9 +81,7 @@ const App = () => {
|
||||
|
||||
if (initializationError) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||||
</ThemeProvider>
|
||||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,30 +90,33 @@ const App = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider>
|
||||
<ThemedRootView>
|
||||
<KeyboardProvider>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<ThemedPaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar
|
||||
translucent={true}
|
||||
backgroundColor="transparent"
|
||||
/>
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</ThemedPaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</KeyboardProvider>
|
||||
</ThemedRootView>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
<ThemedRootView>
|
||||
<KeyboardProvider>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<ThemedPaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</ThemedPaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</KeyboardProvider>
|
||||
</ThemedRootView>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider>
|
||||
<CrashRecoveryGate>
|
||||
<Application />
|
||||
</CrashRecoveryGate>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"expo": {
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
@@ -56,7 +56,7 @@
|
||||
]
|
||||
],
|
||||
"android": {
|
||||
"versionCode": 4196355,
|
||||
"versionCode": 4196356,
|
||||
"package": "com.rajarsheechatterjee.LNReader",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/native/ic_launcher_foreground.png",
|
||||
@@ -70,6 +70,13 @@
|
||||
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
],
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "SEND",
|
||||
"category": ["DEFAULT"],
|
||||
"data": [{ "mimeType": "text/plain" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
@@ -14,6 +14,41 @@ html {
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
#text-selection-ui {
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--theme-outlineVariant);
|
||||
border-radius: 24px;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#text-selection-ui button {
|
||||
min-width: 88px;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--theme-onSurface);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#text-selection-ui button + button {
|
||||
border-inline-start: 1px solid var(--theme-outlineVariant);
|
||||
}
|
||||
|
||||
#text-selection-ui button:active {
|
||||
background-color: var(--theme-surfaceVariant);
|
||||
}
|
||||
|
||||
body {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
|
||||
@@ -15,6 +15,12 @@ body.page-reader > #LNReader-chapter {
|
||||
column-fill: auto;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
body.page-reader.no-animation > #LNReader-chapter {
|
||||
transition: none !important;
|
||||
}
|
||||
body.page-reader.no-animation .transition-chapter {
|
||||
transition: none !important;
|
||||
}
|
||||
.transition-chapter {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
+55
-14
@@ -211,10 +211,22 @@ window.tts = new (function () {
|
||||
|
||||
this.normalizeText = text => {
|
||||
if (!text) return '';
|
||||
return text
|
||||
const normalized = text
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/^["'“”‘’]+|["'“”‘’]+$/g, '')
|
||||
.replace(/\s*([.,!?;:])\s*/g, '$1 ')
|
||||
.trim();
|
||||
|
||||
const dashOnlyText = normalized.replace(/\s/g, '');
|
||||
if (
|
||||
dashOnlyText.length >= 3 &&
|
||||
/^[\-‐‑‒–—―−⁓⸺⸻﹘﹣-]+$/u.test(dashOnlyText)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// if can find a readable node, else stop tts
|
||||
@@ -512,7 +524,8 @@ van.derive(() => {
|
||||
}
|
||||
});
|
||||
|
||||
window.pageReader = new (function () {
|
||||
var pageReader;
|
||||
window.pageReader = pageReader = new (function () {
|
||||
const config =
|
||||
typeof initialPageReaderConfig === 'undefined'
|
||||
? {}
|
||||
@@ -532,18 +545,21 @@ window.pageReader = new (function () {
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
if (!this.chapterEnding) return;
|
||||
}
|
||||
const noAnim = Boolean(
|
||||
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||
);
|
||||
this.chapterEnding.style.transition = 'unset';
|
||||
if (bool) {
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
requestAnimationFrame(() => {
|
||||
if (!instant) {
|
||||
if (!instant && !noAnim) {
|
||||
this.chapterEnding.style.transition = 'transform 200ms';
|
||||
}
|
||||
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
||||
});
|
||||
this.chapterEndingVisible.val = true;
|
||||
} else {
|
||||
if (!instant) {
|
||||
if (!instant && !noAnim) {
|
||||
this.chapterEnding.style.transition = 'transform 200ms';
|
||||
}
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
@@ -567,26 +583,35 @@ window.pageReader = new (function () {
|
||||
return;
|
||||
}
|
||||
destPage = parseInt(destPage, 10);
|
||||
const noAnim = Boolean(
|
||||
reader.generalSettings?.val?.pageReaderDisableAnimation,
|
||||
);
|
||||
if (destPage < 0) {
|
||||
if (!reader.prevChapter) return;
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.prevChapter.name;
|
||||
this.showChapterEnding(true, false, true);
|
||||
this.showChapterEnding(true, noAnim, true);
|
||||
this.chapterNavigationPending = true;
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'prev' });
|
||||
}, 200);
|
||||
setTimeout(
|
||||
() => {
|
||||
reader.post({ type: 'prev' });
|
||||
},
|
||||
noAnim ? 0 : 200,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
if (!reader.nextChapter) return;
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.nextChapter.name;
|
||||
this.showChapterEnding(true);
|
||||
this.showChapterEnding(true, noAnim);
|
||||
this.chapterNavigationPending = true;
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'next' });
|
||||
}, 200);
|
||||
setTimeout(
|
||||
() => {
|
||||
reader.post({ type: 'next' });
|
||||
},
|
||||
noAnim ? 0 : 200,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.page.val = destPage;
|
||||
@@ -594,7 +619,7 @@ window.pageReader = new (function () {
|
||||
'translateX(-' + destPage * 100 + '%)';
|
||||
|
||||
const newProgress = parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
((this.page.val + 1) / this.totalPages.val) * 100,
|
||||
10,
|
||||
);
|
||||
|
||||
@@ -675,6 +700,18 @@ window.pageReader = new (function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
van.derive(() => {
|
||||
document.body.classList.toggle(
|
||||
'no-animation',
|
||||
Boolean(reader.generalSettings.val.pageReaderDisableAnimation),
|
||||
);
|
||||
reader.chapterElement?.style.removeProperty('transition');
|
||||
const chapterEnding =
|
||||
this.chapterEnding ||
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
chapterEnding?.style.removeProperty('transition');
|
||||
});
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -893,7 +930,11 @@ window.addEventListener('load', () => {
|
||||
if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) {
|
||||
pageReader.ignoreClickUntil = Date.now() + 400;
|
||||
}
|
||||
reader.chapterElement.style.transition = 'transform 200ms';
|
||||
reader.chapterElement.style.transition = reader.generalSettings.val
|
||||
.pageReaderDisableAnimation
|
||||
? 'none'
|
||||
: 'transform 200ms';
|
||||
reader.chapterElement.style.removeProperty('transition');
|
||||
const diffXPercentage = diffX / reader.layoutWidth;
|
||||
if (diffXPercentage < -0.3) {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
|
||||
Vendored
+2
@@ -29,6 +29,8 @@ export interface Reader {
|
||||
finished: string;
|
||||
nextChapter: string;
|
||||
noNextChapter: string;
|
||||
removeText: string;
|
||||
replaceText: string;
|
||||
};
|
||||
|
||||
setAdjacentChapters: (adjacent: {
|
||||
|
||||
@@ -10,28 +10,11 @@ window.textRemover = new (function () {
|
||||
selectionUI = div(
|
||||
{
|
||||
id: 'text-selection-ui',
|
||||
style: `
|
||||
position: fixed;
|
||||
background: color-mix(in srgb, var(--theme-surface), transparent 10%);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
z-index: 100000;
|
||||
opacity: 0;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
|
||||
transition: opacity 150ms
|
||||
`,
|
||||
role: 'group',
|
||||
},
|
||||
button(
|
||||
{
|
||||
style: `
|
||||
background: var(--theme-secondary);
|
||||
color: var(--theme-onSecondary);
|
||||
padding: 6px 12px;
|
||||
margin: 2px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
`,
|
||||
type: 'button',
|
||||
onclick: e => {
|
||||
if (reader.hidden.val) {
|
||||
e.stopPropagation();
|
||||
@@ -39,19 +22,11 @@ window.textRemover = new (function () {
|
||||
removeSelectedText();
|
||||
},
|
||||
},
|
||||
'Remove',
|
||||
reader.strings.removeText,
|
||||
),
|
||||
button(
|
||||
{
|
||||
style: `
|
||||
background: var(--theme-secondary);
|
||||
color: var(--theme-onSecondary);
|
||||
padding: 6px 12px;
|
||||
margin: 2px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
`,
|
||||
type: 'button',
|
||||
onclick: e => {
|
||||
if (reader.hidden.val) {
|
||||
e.stopPropagation();
|
||||
@@ -59,7 +34,7 @@ window.textRemover = new (function () {
|
||||
replaceSelectedText();
|
||||
},
|
||||
},
|
||||
'Replace',
|
||||
reader.strings.replaceText,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -138,12 +113,14 @@ window.textRemover = new (function () {
|
||||
}
|
||||
|
||||
ui.style.opacity = '1';
|
||||
ui.style.pointerEvents = 'auto';
|
||||
isUIActive = true;
|
||||
}
|
||||
|
||||
function hideSelectionUI() {
|
||||
if (selectionUI) {
|
||||
selectionUI.style.opacity = '0';
|
||||
selectionUI.style.pointerEvents = 'none';
|
||||
}
|
||||
isUIActive = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import { registerRootComponent } from 'expo';
|
||||
import { AppRegistry, I18nManager } from 'react-native';
|
||||
import { i18n } from './src/i18n/translations';
|
||||
import { runHeadlessBackgroundTask } from './src/services/backgroundTasks';
|
||||
import { installJsCrashHandler } from './src/services/crashLogs/installJsCrashHandler';
|
||||
|
||||
installJsCrashHandler();
|
||||
|
||||
AppRegistry.registerHeadlessTask(
|
||||
'LNReaderBackgroundTask',
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ const baseTransform = {
|
||||
|
||||
const baseTransformIgnorePatterns = [
|
||||
// 'node_modules/(?!(.pnpm/|@op-engineering|drizzle-orm|lodash-es|@babel/runtime))',
|
||||
'node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|color|@op-engineering|drizzle-orm|lodash-es))',
|
||||
'node_modules/(?!(?:.pnpm/)?((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|@material/material-color-utilities|@pchmn/expo-material3-theme|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|color|@op-engineering|drizzle-orm|lodash-es))',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
+68
-9
@@ -5,13 +5,41 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
data class BackgroundTaskSummary(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val state: String,
|
||||
val progress: Double?,
|
||||
val progressText: String?,
|
||||
val attempt: Int,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface BackgroundTaskDao {
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
suspend fun insert(task: BackgroundTaskEntity)
|
||||
|
||||
@Query("SELECT * FROM background_tasks ORDER BY createdAt ASC")
|
||||
suspend fun getAll(): List<BackgroundTaskEntity>
|
||||
@Query(
|
||||
"SELECT id, type, title, description, state, progress, progressText, " +
|
||||
"attempt, createdAt, updatedAt " +
|
||||
"FROM background_tasks " +
|
||||
"WHERE state IN ('queued', 'running', 'paused') ORDER BY createdAt ASC",
|
||||
)
|
||||
suspend fun getActive(): List<BackgroundTaskSummary>
|
||||
|
||||
@Query(
|
||||
"DELETE FROM background_tasks " +
|
||||
"WHERE state IN ('succeeded', 'failed', 'cancelled') " +
|
||||
"AND updatedAt < :cutoff",
|
||||
)
|
||||
suspend fun deleteOldTerminal(cutoff: Long)
|
||||
|
||||
@Query("DELETE FROM background_tasks WHERE id = :id AND state = :state")
|
||||
suspend fun deleteIfState(id: String, state: String): Int
|
||||
|
||||
@Query("SELECT * FROM background_tasks WHERE id = :id LIMIT 1")
|
||||
suspend fun get(id: String): BackgroundTaskEntity?
|
||||
@@ -19,11 +47,42 @@ interface BackgroundTaskDao {
|
||||
@Query("SELECT * FROM background_tasks WHERE type = :type AND state IN ('queued', 'running', 'paused') LIMIT 1")
|
||||
suspend fun getActiveByType(type: String): BackgroundTaskEntity?
|
||||
|
||||
@Query("UPDATE background_tasks SET state = :state, updatedAt = :updatedAt WHERE id = :id")
|
||||
suspend fun updateState(id: String, state: String, updatedAt: Long)
|
||||
@Query("SELECT * FROM background_tasks WHERE queueName = :queueName AND state = 'queued' ORDER BY createdAt ASC")
|
||||
suspend fun getQueuedByQueueName(queueName: String): List<BackgroundTaskEntity>
|
||||
|
||||
@Query(
|
||||
"UPDATE background_tasks SET state = 'paused', updatedAt = :updatedAt " +
|
||||
"WHERE id = :id AND state IN ('queued', 'running')",
|
||||
)
|
||||
suspend fun markPaused(id: String, updatedAt: Long): Int
|
||||
|
||||
@Query(
|
||||
"UPDATE background_tasks SET state = 'queued', updatedAt = :updatedAt " +
|
||||
"WHERE id = :id AND state = 'paused'",
|
||||
)
|
||||
suspend fun markQueued(id: String, updatedAt: Long): Int
|
||||
|
||||
@Query(
|
||||
"UPDATE background_tasks SET state = 'cancelled', updatedAt = :updatedAt " +
|
||||
"WHERE id = :id AND state IN ('queued', 'running', 'paused')",
|
||||
)
|
||||
suspend fun markCancelled(id: String, updatedAt: Long): Int
|
||||
|
||||
@Query("UPDATE background_tasks SET state = :state, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
|
||||
suspend fun finishRunning(id: String, state: String, updatedAt: Long)
|
||||
suspend fun finishRunning(id: String, state: String, updatedAt: Long): Int
|
||||
|
||||
@Query(
|
||||
"UPDATE background_tasks " +
|
||||
"SET state = :state, progress = :progress, progressText = :progressText, updatedAt = :updatedAt " +
|
||||
"WHERE id = :id AND state = 'running'",
|
||||
)
|
||||
suspend fun finishRunningWithProgress(
|
||||
id: String,
|
||||
state: String,
|
||||
progress: Double?,
|
||||
progressText: String?,
|
||||
updatedAt: Long,
|
||||
): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
@@ -48,11 +107,11 @@ interface BackgroundTaskDao {
|
||||
maxConcurrentDownloads: Int,
|
||||
): Int
|
||||
|
||||
@Query("UPDATE background_tasks SET progress = :progress, progressText = :progressText, updatedAt = :updatedAt WHERE id = :id")
|
||||
suspend fun updateProgress(id: String, progress: Double?, progressText: String?, updatedAt: Long)
|
||||
@Query("UPDATE background_tasks SET progress = :progress, progressText = :progressText, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
|
||||
suspend fun updateProgress(id: String, progress: Double?, progressText: String?, updatedAt: Long): Int
|
||||
|
||||
@Query("UPDATE background_tasks SET checkpoint = :checkpoint, updatedAt = :updatedAt WHERE id = :id")
|
||||
suspend fun updateCheckpoint(id: String, checkpoint: String?, updatedAt: Long)
|
||||
@Query("UPDATE background_tasks SET checkpoint = :checkpoint, updatedAt = :updatedAt WHERE id = :id AND state = 'running'")
|
||||
suspend fun updateCheckpoint(id: String, checkpoint: String?, updatedAt: Long): Int
|
||||
|
||||
@Query("UPDATE background_tasks SET workId = :workId, updatedAt = :updatedAt WHERE id = :id")
|
||||
suspend fun assignWork(id: String, workId: String, updatedAt: Long)
|
||||
|
||||
+12
-1
@@ -37,8 +37,19 @@ object BackgroundTaskScheduler {
|
||||
return request.id
|
||||
}
|
||||
|
||||
suspend fun cancel(context: Context, taskId: String) {
|
||||
suspend fun cancel(context: Context, taskId: String, isRunning: Boolean) {
|
||||
// A queued worker must finish as a no-op so WorkManager does not cancel
|
||||
// every task chained after it.
|
||||
if (!isRunning) return
|
||||
|
||||
val dao = BackgroundTaskDatabase.get(context).tasks()
|
||||
// The running worker may delete the row as it consumes cancellation.
|
||||
// Capture the queue before cancelling that worker.
|
||||
val queueName = dao.get(taskId)?.queueName ?: return
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(taskId).await()
|
||||
// Cancelling running work also cancels its dependents, so rebuild the
|
||||
// remaining queue after the worker has stopped.
|
||||
dao.getQueuedByQueueName(queueName).forEach { enqueue(context, it.id) }
|
||||
}
|
||||
|
||||
suspend fun enqueueLibraryUpdate(
|
||||
|
||||
+102
-41
@@ -8,6 +8,8 @@ import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LNReaderTaskWorker(
|
||||
appContext: Context,
|
||||
@@ -16,8 +18,12 @@ class LNReaderTaskWorker(
|
||||
override suspend fun doWork(): Result {
|
||||
val taskId = inputData.getString(BackgroundTaskScheduler.TASK_ID) ?: return Result.failure()
|
||||
val dao = BackgroundTaskDatabase.get(applicationContext).tasks()
|
||||
val task = dao.get(taskId) ?: return Result.failure()
|
||||
if (task.state == BackgroundTaskState.CANCELLED || task.state == BackgroundTaskState.PAUSED) {
|
||||
val task = dao.get(taskId) ?: return Result.success()
|
||||
if (task.state == BackgroundTaskState.CANCELLED) {
|
||||
consumeCancellation(dao, taskId)
|
||||
return Result.success()
|
||||
}
|
||||
if (task.state == BackgroundTaskState.PAUSED) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@@ -28,13 +34,23 @@ class LNReaderTaskWorker(
|
||||
MAX_CONCURRENT_DOWNLOADS,
|
||||
)
|
||||
if (claimed == 0) {
|
||||
return if (dao.get(taskId)?.state == BackgroundTaskState.QUEUED) {
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.success()
|
||||
return when (dao.get(taskId)?.state) {
|
||||
BackgroundTaskState.QUEUED -> Result.retry()
|
||||
BackgroundTaskState.CANCELLED -> {
|
||||
consumeCancellation(dao, taskId)
|
||||
Result.success()
|
||||
}
|
||||
else -> Result.success()
|
||||
}
|
||||
}
|
||||
val runningTask = dao.get(taskId) ?: return Result.failure()
|
||||
val runningTask = dao.get(taskId) ?: return Result.success()
|
||||
if (runningTask.state != BackgroundTaskState.RUNNING) {
|
||||
if (runningTask.state == BackgroundTaskState.CANCELLED) {
|
||||
consumeCancellation(dao, taskId)
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
setForeground(createForegroundInfo(runningTask))
|
||||
val execution = TaskExecutionRegistry.register(taskId)
|
||||
|
||||
@@ -47,61 +63,106 @@ class LNReaderTaskWorker(
|
||||
|
||||
when (val executionResult = execution.await()) {
|
||||
TaskExecutionResult.Success -> {
|
||||
val currentState = dao.get(taskId)?.state
|
||||
if (currentState == BackgroundTaskState.CANCELLED) {
|
||||
TaskNotificationFactory.dismiss(applicationContext, taskId)
|
||||
return Result.success()
|
||||
}
|
||||
if (currentState == BackgroundTaskState.PAUSED) {
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(applicationContext, it) }
|
||||
return Result.success()
|
||||
}
|
||||
dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, System.currentTimeMillis())
|
||||
dao.get(taskId)?.let {
|
||||
TaskNotificationFactory.postTerminal(applicationContext, it)
|
||||
if (dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, System.currentTimeMillis()) > 0) {
|
||||
postOwnedTerminal(taskId, dao, BackgroundTaskState.SUCCEEDED)
|
||||
} else {
|
||||
resultAfterLostOwnership(taskId, dao)
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
is TaskExecutionResult.Failure -> {
|
||||
val currentState = dao.get(taskId)?.state
|
||||
if (currentState == BackgroundTaskState.PAUSED || currentState == BackgroundTaskState.CANCELLED) {
|
||||
return Result.success()
|
||||
}
|
||||
if (executionResult.shouldRetry) {
|
||||
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
|
||||
Result.retry()
|
||||
} else {
|
||||
dao.updateState(taskId, BackgroundTaskState.FAILED, System.currentTimeMillis())
|
||||
dao.get(taskId)?.let {
|
||||
TaskNotificationFactory.postTerminal(applicationContext, it)
|
||||
if (dao.finishRunning(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis()) > 0) {
|
||||
Result.retry()
|
||||
} else {
|
||||
resultAfterLostOwnership(taskId, dao)
|
||||
}
|
||||
} else if (
|
||||
dao.finishRunning(taskId, BackgroundTaskState.FAILED, System.currentTimeMillis()) > 0
|
||||
) {
|
||||
postOwnedTerminal(taskId, dao, BackgroundTaskState.FAILED)
|
||||
Result.success()
|
||||
} else {
|
||||
resultAfterLostOwnership(taskId, dao)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: CancellationException) {
|
||||
val latestState = dao.get(taskId)?.state
|
||||
if (latestState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
|
||||
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
|
||||
withContext(NonCancellable) {
|
||||
when (dao.get(taskId)?.state) {
|
||||
BackgroundTaskState.CANCELLED -> consumeCancellation(dao, taskId)
|
||||
BackgroundTaskState.RUNNING -> dao.finishRunning(
|
||||
taskId,
|
||||
BackgroundTaskState.QUEUED,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
val latestState = dao.get(taskId)?.state
|
||||
if (latestState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
|
||||
val now = System.currentTimeMillis()
|
||||
val message = error.message ?: error.javaClass.simpleName
|
||||
dao.updateProgress(taskId, null, message, now)
|
||||
dao.updateState(taskId, BackgroundTaskState.FAILED, now)
|
||||
dao.get(taskId)?.let {
|
||||
TaskNotificationFactory.postTerminal(applicationContext, it)
|
||||
val now = System.currentTimeMillis()
|
||||
val message = error.message ?: error.javaClass.simpleName
|
||||
if (
|
||||
dao.finishRunningWithProgress(
|
||||
taskId,
|
||||
BackgroundTaskState.FAILED,
|
||||
null,
|
||||
message,
|
||||
now,
|
||||
) > 0
|
||||
) {
|
||||
postOwnedTerminal(taskId, dao, BackgroundTaskState.FAILED)
|
||||
Result.failure()
|
||||
} else {
|
||||
when (dao.get(taskId)?.state) {
|
||||
BackgroundTaskState.CANCELLED -> {
|
||||
consumeCancellation(dao, taskId)
|
||||
Result.success()
|
||||
}
|
||||
BackgroundTaskState.PAUSED -> Result.success()
|
||||
else -> Result.failure()
|
||||
}
|
||||
}
|
||||
Result.failure()
|
||||
} finally {
|
||||
TaskExecutionRegistry.cancel(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resultAfterLostOwnership(
|
||||
taskId: String,
|
||||
dao: BackgroundTaskDao,
|
||||
): Result {
|
||||
return when (dao.get(taskId)?.state) {
|
||||
BackgroundTaskState.CANCELLED -> {
|
||||
consumeCancellation(dao, taskId)
|
||||
Result.success()
|
||||
}
|
||||
BackgroundTaskState.PAUSED -> {
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(applicationContext, it) }
|
||||
Result.success()
|
||||
}
|
||||
else -> Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun postOwnedTerminal(
|
||||
taskId: String,
|
||||
dao: BackgroundTaskDao,
|
||||
state: String,
|
||||
) {
|
||||
val terminalTask = dao.get(taskId) ?: return
|
||||
if (terminalTask.state != state) return
|
||||
TaskNotificationFactory.postTerminal(applicationContext, terminalTask)
|
||||
dao.deleteIfState(taskId, state)
|
||||
}
|
||||
|
||||
private suspend fun consumeCancellation(dao: BackgroundTaskDao, taskId: String) {
|
||||
if (dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED) > 0) {
|
||||
TaskNotificationFactory.dismiss(applicationContext, taskId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createForegroundInfo(task: BackgroundTaskEntity): ForegroundInfo {
|
||||
val id = TaskNotificationFactory.notificationId(task.id)
|
||||
val notification = TaskNotificationFactory.build(applicationContext, task)
|
||||
|
||||
+42
-25
@@ -57,7 +57,10 @@ class NativeBackgroundTasksModule : Module() {
|
||||
|
||||
AsyncFunction("getTasks") {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
dao.getAll().map(::toRecord)
|
||||
dao.deleteOldTerminal(
|
||||
System.currentTimeMillis() - TERMINAL_TASK_RETENTION_MS,
|
||||
)
|
||||
dao.getActive().map(::toSummary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +73,12 @@ class NativeBackgroundTasksModule : Module() {
|
||||
AsyncFunction("pause") { taskId: String ->
|
||||
runBlocking(Dispatchers.IO) {
|
||||
requireTask(taskId)
|
||||
dao.updateState(taskId, BackgroundTaskState.PAUSED, System.currentTimeMillis())
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
emitInterruption(taskId, "pause")
|
||||
if (dao.markPaused(taskId, System.currentTimeMillis()) > 0) {
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
emitInterruption(taskId, "pause")
|
||||
}
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(appContext.reactContext!!, it) }
|
||||
}
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(appContext.reactContext!!, it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,21 +88,29 @@ class NativeBackgroundTasksModule : Module() {
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
throw IllegalStateException("Task is still pausing; try resuming again shortly")
|
||||
}
|
||||
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
|
||||
BackgroundTaskScheduler.enqueue(appContext.reactContext!!, taskId)
|
||||
if (dao.markQueued(taskId, System.currentTimeMillis()) > 0) {
|
||||
BackgroundTaskScheduler.enqueue(appContext.reactContext!!, taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("cancel") { taskId: String ->
|
||||
runBlocking(Dispatchers.IO) {
|
||||
requireTask(taskId)
|
||||
dao.updateState(taskId, BackgroundTaskState.CANCELLED, System.currentTimeMillis())
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
val task = requireTask(taskId)
|
||||
val isRunning = task.state == BackgroundTaskState.RUNNING ||
|
||||
TaskExecutionRegistry.isActive(taskId)
|
||||
if (dao.markCancelled(taskId, System.currentTimeMillis()) == 0) {
|
||||
TaskNotificationFactory.dismiss(appContext.reactContext!!, taskId)
|
||||
return@runBlocking
|
||||
}
|
||||
if (isRunning) {
|
||||
emitInterruption(taskId, "cancel")
|
||||
}
|
||||
BackgroundTaskScheduler.cancel(appContext.reactContext!!, taskId)
|
||||
dao.updateCheckpoint(taskId, null, System.currentTimeMillis())
|
||||
BackgroundTaskScheduler.cancel(appContext.reactContext!!, taskId, isRunning)
|
||||
TaskNotificationFactory.dismiss(appContext.reactContext!!, taskId)
|
||||
if (!isRunning && task.state != BackgroundTaskState.QUEUED) {
|
||||
dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,22 +138,13 @@ class NativeBackgroundTasksModule : Module() {
|
||||
val now = System.currentTimeMillis()
|
||||
dao.updateCheckpoint(taskId, null, now)
|
||||
dao.updateProgress(taskId, null, completionText, now)
|
||||
dao.finishRunning(taskId, BackgroundTaskState.SUCCEEDED, now)
|
||||
TaskExecutionRegistry.complete(taskId, TaskExecutionResult.Success)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("fail") { taskId: String, error: String, shouldRetry: Boolean ->
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val currentState = dao.get(taskId)?.state
|
||||
if (currentState !in listOf(BackgroundTaskState.PAUSED, BackgroundTaskState.CANCELLED)) {
|
||||
dao.updateProgress(taskId, null, error, System.currentTimeMillis())
|
||||
dao.finishRunning(
|
||||
taskId,
|
||||
if (shouldRetry) BackgroundTaskState.QUEUED else BackgroundTaskState.FAILED,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
dao.updateProgress(taskId, null, error, System.currentTimeMillis())
|
||||
TaskExecutionRegistry.complete(taskId, TaskExecutionResult.Failure(error, shouldRetry))
|
||||
}
|
||||
}
|
||||
@@ -177,22 +180,36 @@ class NativeBackgroundTasksModule : Module() {
|
||||
private suspend fun requireTask(taskId: String): BackgroundTaskEntity =
|
||||
dao.get(taskId) ?: throw IllegalArgumentException("Unknown background task: $taskId")
|
||||
|
||||
private fun toRecord(task: BackgroundTaskEntity): Map<String, Any?> = mapOf(
|
||||
private fun toSummary(task: BackgroundTaskSummary): Map<String, Any?> = mapOf(
|
||||
"id" to task.id,
|
||||
"type" to task.type,
|
||||
"payload" to task.payload,
|
||||
"title" to task.title,
|
||||
"description" to task.description,
|
||||
"state" to task.state,
|
||||
"progress" to task.progress,
|
||||
"progressText" to task.progressText,
|
||||
"checkpoint" to task.checkpoint,
|
||||
"attempt" to task.attempt,
|
||||
"createdAt" to task.createdAt.toDouble(),
|
||||
"updatedAt" to task.updatedAt.toDouble(),
|
||||
)
|
||||
|
||||
private fun toRecord(task: BackgroundTaskEntity): Map<String, Any?> = mapOf(
|
||||
"id" to task.id,
|
||||
"type" to task.type,
|
||||
"title" to task.title,
|
||||
"description" to task.description,
|
||||
"state" to task.state,
|
||||
"progress" to task.progress,
|
||||
"progressText" to task.progressText,
|
||||
"attempt" to task.attempt,
|
||||
"createdAt" to task.createdAt.toDouble(),
|
||||
"updatedAt" to task.updatedAt.toDouble(),
|
||||
"payload" to task.payload,
|
||||
"checkpoint" to task.checkpoint,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val TERMINAL_TASK_RETENTION_MS = 24 * 60 * 60 * 1000L
|
||||
@Volatile
|
||||
private var reactContextRef: WeakReference<ReactApplicationContext>? = null
|
||||
|
||||
|
||||
+20
-10
@@ -14,27 +14,37 @@ class TaskActionReceiver : BroadcastReceiver() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val dao = BackgroundTaskDatabase.get(context).tasks()
|
||||
dao.get(taskId) ?: return@launch
|
||||
val task = dao.get(taskId) ?: return@launch
|
||||
when (intent.action) {
|
||||
TaskNotificationFactory.ACTION_PAUSE -> {
|
||||
dao.updateState(taskId, BackgroundTaskState.PAUSED, System.currentTimeMillis())
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
NativeBackgroundTasksModule.emitInterruption(taskId, "pause")
|
||||
if (dao.markPaused(taskId, System.currentTimeMillis()) > 0) {
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
NativeBackgroundTasksModule.emitInterruption(taskId, "pause")
|
||||
}
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(context, it) }
|
||||
}
|
||||
dao.get(taskId)?.let { TaskNotificationFactory.update(context, it) }
|
||||
}
|
||||
TaskNotificationFactory.ACTION_RESUME -> {
|
||||
if (TaskExecutionRegistry.isActive(taskId)) return@launch
|
||||
dao.updateState(taskId, BackgroundTaskState.QUEUED, System.currentTimeMillis())
|
||||
BackgroundTaskScheduler.enqueue(context, taskId)
|
||||
if (dao.markQueued(taskId, System.currentTimeMillis()) > 0) {
|
||||
BackgroundTaskScheduler.enqueue(context, taskId)
|
||||
}
|
||||
}
|
||||
TaskNotificationFactory.ACTION_CANCEL -> {
|
||||
dao.updateState(taskId, BackgroundTaskState.CANCELLED, System.currentTimeMillis())
|
||||
if (TaskExecutionRegistry.isActive(taskId)) {
|
||||
val isRunning = task.state == BackgroundTaskState.RUNNING ||
|
||||
TaskExecutionRegistry.isActive(taskId)
|
||||
if (dao.markCancelled(taskId, System.currentTimeMillis()) == 0) {
|
||||
TaskNotificationFactory.dismiss(context, taskId)
|
||||
return@launch
|
||||
}
|
||||
if (isRunning) {
|
||||
NativeBackgroundTasksModule.emitInterruption(taskId, "cancel")
|
||||
}
|
||||
BackgroundTaskScheduler.cancel(context, taskId)
|
||||
BackgroundTaskScheduler.cancel(context, taskId, isRunning)
|
||||
TaskNotificationFactory.dismiss(context, taskId)
|
||||
if (!isRunning && task.state != BackgroundTaskState.QUEUED) {
|
||||
dao.deleteIfState(taskId, BackgroundTaskState.CANCELLED)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import NativeBackgroundTasks, {
|
||||
NativeBackgroundTaskRecord,
|
||||
NativeBackgroundTaskSummary,
|
||||
} from './src/NativeBackgroundTasksModule';
|
||||
export default NativeBackgroundTasks;
|
||||
export { NativeBackgroundTaskRecord };
|
||||
export { NativeBackgroundTaskRecord, NativeBackgroundTaskSummary };
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { requireNativeModule } from 'expo-modules-core';
|
||||
|
||||
export type NativeBackgroundTaskRecord = {
|
||||
export type NativeBackgroundTaskSummary = {
|
||||
id: string;
|
||||
type: string;
|
||||
payload: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
state: string;
|
||||
progress?: number;
|
||||
progressText?: string;
|
||||
checkpoint?: string;
|
||||
attempt: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
export type NativeBackgroundTaskRecord = NativeBackgroundTaskSummary & {
|
||||
payload: string;
|
||||
checkpoint?: string;
|
||||
};
|
||||
|
||||
type NativeBackgroundTasksModule = {
|
||||
getTasks(): Promise<NativeBackgroundTaskSummary[]>;
|
||||
getTask(taskId: string): Promise<NativeBackgroundTaskRecord | null>;
|
||||
|
||||
enqueue(
|
||||
type: string,
|
||||
payload: string,
|
||||
@@ -24,8 +30,6 @@ type NativeBackgroundTasksModule = {
|
||||
allowsDuplicates: boolean,
|
||||
queueName: string,
|
||||
): Promise<string>;
|
||||
getTasks(): Promise<NativeBackgroundTaskRecord[]>;
|
||||
getTask(taskId: string): Promise<NativeBackgroundTaskRecord | null>;
|
||||
pause(taskId: string): Promise<void>;
|
||||
resume(taskId: string): Promise<void>;
|
||||
cancel(taskId: string): Promise<void>;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'expo.modules.nativecrashlogs'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.nativecrashlogs"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.17.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<provider
|
||||
android:name="expo.modules.nativecrashlogs.CrashHandlerInitializationProvider"
|
||||
android:authorities="${applicationId}.crash-handler-initializer"
|
||||
android:exported="false"
|
||||
android:initOrder="110" />
|
||||
|
||||
<provider
|
||||
android:name="expo.modules.nativecrashlogs.CrashLogFileProvider"
|
||||
android:authorities="${applicationId}.crash-logs"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/crash_log_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Process
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
internal class CrashExceptionHandler private constructor(
|
||||
private val applicationContext: Context,
|
||||
private val defaultHandler: Thread.UncaughtExceptionHandler?,
|
||||
) : Thread.UncaughtExceptionHandler {
|
||||
override fun uncaughtException(thread: Thread, exception: Throwable) {
|
||||
try {
|
||||
val recordedJsCrash = CrashLogStore.readPendingCrash(applicationContext)
|
||||
CrashLogStore.writePendingCrash(
|
||||
applicationContext,
|
||||
buildString {
|
||||
appendLine("Native crash captured: ${timestamp()}")
|
||||
appendLine("Thread: ${thread.name}")
|
||||
appendLine()
|
||||
append(exception.stackTraceToString())
|
||||
if (recordedJsCrash != null) {
|
||||
appendLine()
|
||||
appendLine()
|
||||
appendLine("Previously recorded JavaScript crash:")
|
||||
append(recordedJsCrash)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
// The platform handler must still terminate the process normally.
|
||||
} finally {
|
||||
val handler = defaultHandler
|
||||
if (handler != null) {
|
||||
handler.uncaughtException(thread, exception)
|
||||
} else {
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun install(context: Context) {
|
||||
val currentHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
if (currentHandler is CrashExceptionHandler) return
|
||||
Thread.setDefaultUncaughtExceptionHandler(
|
||||
CrashExceptionHandler(context.applicationContext, currentHandler),
|
||||
)
|
||||
}
|
||||
|
||||
private fun timestamp(): String =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT).format(Date())
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
|
||||
class CrashHandlerInitializationProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
context?.let(CrashExceptionHandler::install)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?,
|
||||
): Cursor? = null
|
||||
|
||||
override fun getType(uri: Uri): String? = null
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int = 0
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import androidx.core.content.FileProvider
|
||||
|
||||
class CrashLogFileProvider : FileProvider(R.xml.crash_log_paths)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
internal object CrashLogRedactor {
|
||||
private val headerPattern = Regex(
|
||||
pattern = "(?i)(authorization|cookie|set-cookie)(\\s*[:=]\\s*).*$",
|
||||
options = setOf(RegexOption.MULTILINE),
|
||||
)
|
||||
private val urlQueryPattern = Regex(
|
||||
pattern = "(?i)(https?://[^\\s?#]+)\\?[^\\s#\"')\\]]+",
|
||||
)
|
||||
|
||||
fun redact(contents: String): String = contents
|
||||
.replace(headerPattern, "$1$2[REDACTED]")
|
||||
.replace(urlQueryPattern, "$1?[REDACTED]")
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
internal object CrashLogStore {
|
||||
private const val CRASH_DIRECTORY = "crash-reports"
|
||||
private const val PENDING_CRASH_FILE = "last_crash.txt"
|
||||
private const val SHARE_DIRECTORY = "crash-logs"
|
||||
private const val SHARE_FILE = "lnreader_crash_logs.txt"
|
||||
|
||||
fun createShareFile(context: Context): File {
|
||||
val directory = File(context.cacheDir, SHARE_DIRECTORY)
|
||||
check(directory.exists() || directory.mkdirs()) {
|
||||
"Failed to create crash log cache directory"
|
||||
}
|
||||
return File(directory, SHARE_FILE)
|
||||
}
|
||||
|
||||
fun readPendingCrash(context: Context): String? {
|
||||
val file = pendingCrashFile(context)
|
||||
if (!file.isFile) return null
|
||||
return file.readText().takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun writePendingCrash(context: Context, contents: String) {
|
||||
val file = pendingCrashFile(context)
|
||||
val directory = file.parentFile
|
||||
check(directory != null && (directory.exists() || directory.mkdirs())) {
|
||||
"Failed to create crash report directory"
|
||||
}
|
||||
|
||||
val temporaryFile = File(directory, "$PENDING_CRASH_FILE.tmp")
|
||||
temporaryFile.writeText(contents)
|
||||
if (!temporaryFile.renameTo(file)) {
|
||||
file.writeText(contents)
|
||||
temporaryFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPendingCrash(context: Context) {
|
||||
val file = pendingCrashFile(context)
|
||||
check(!file.exists() || file.delete()) { "Failed to clear pending crash report" }
|
||||
}
|
||||
|
||||
private fun pendingCrashFile(context: Context): File =
|
||||
File(File(context.filesDir, CRASH_DIRECTORY), PENDING_CRASH_FILE)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal object LogcatCollector {
|
||||
private const val MAX_LOG_BYTES = 512 * 1024
|
||||
private const val MAX_LOG_LINES = 2_000
|
||||
private const val LOGCAT_TIMEOUT_SECONDS = 2L
|
||||
|
||||
fun collect(context: Context, requestedLevel: String): String {
|
||||
val level = if (requestedLevel == "V") "V" else "E"
|
||||
val output = File.createTempFile("lnreader-logcat-", ".tmp", context.cacheDir)
|
||||
return try {
|
||||
val process = ProcessBuilder(
|
||||
"logcat",
|
||||
"-d",
|
||||
"-t",
|
||||
MAX_LOG_LINES.toString(),
|
||||
"-v",
|
||||
"year",
|
||||
"-v",
|
||||
"zone",
|
||||
"*:$level",
|
||||
)
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(output)
|
||||
.start()
|
||||
|
||||
if (!process.waitFor(LOGCAT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
process.destroyForcibly()
|
||||
}
|
||||
readBounded(output)
|
||||
} catch (error: Exception) {
|
||||
"Logcat unavailable: ${error.message ?: error.javaClass.simpleName}"
|
||||
} finally {
|
||||
output.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun readBounded(file: File): String {
|
||||
if (!file.isFile || file.length() == 0L) return "No accessible logcat entries."
|
||||
|
||||
val wasTruncated = file.length() > MAX_LOG_BYTES
|
||||
val bytes = file.inputStream().use { input ->
|
||||
var bytesToSkip = if (wasTruncated) file.length() - MAX_LOG_BYTES else 0L
|
||||
while (bytesToSkip > 0) {
|
||||
val skipped = input.skip(bytesToSkip)
|
||||
if (skipped <= 0) break
|
||||
bytesToSkip -= skipped
|
||||
}
|
||||
val buffer = ByteArray(MAX_LOG_BYTES)
|
||||
var totalRead = 0
|
||||
while (totalRead < buffer.size) {
|
||||
val count = input.read(buffer, totalRead, buffer.size - totalRead)
|
||||
if (count == -1) break
|
||||
totalRead += count
|
||||
}
|
||||
buffer.copyOf(totalRead)
|
||||
}
|
||||
val sanitized = bytes.toString(Charsets.UTF_8)
|
||||
.lineSequence()
|
||||
.joinToString("\n", transform = CrashLogRedactor::redact)
|
||||
|
||||
return if (wasTruncated) {
|
||||
"[Older logcat entries truncated]\n$sanitized"
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.FileProvider
|
||||
import expo.modules.kotlin.Promise
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class NativeCrashLogsModule : Module() {
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("NativeCrashLogs")
|
||||
|
||||
OnCreate {
|
||||
appContext.reactContext?.let(CrashExceptionHandler::install)
|
||||
}
|
||||
|
||||
OnDestroy {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
AsyncFunction("shareCrashLogs") { report: String, logLevel: String, promise: Promise ->
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
val file = CrashLogStore.createShareFile(context)
|
||||
file.writeText(
|
||||
CrashLogRedactor.redact(
|
||||
report.take(MAX_REPORT_CHARACTERS).trimEnd(),
|
||||
),
|
||||
)
|
||||
file.appendText("\n\n[App-accessible logcat]\n")
|
||||
file.appendText(LogcatCollector.collect(context, logLevel))
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
shareFile(context, file)
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (error: Exception) {
|
||||
promise.reject("ECRASHLOG", "Failed to share crash logs", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function("recordJsCrash") { stackTrace: String ->
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
CrashLogStore.writePendingCrash(context, stackTrace.take(MAX_REPORT_CHARACTERS))
|
||||
}
|
||||
|
||||
AsyncFunction("getPendingCrash") { promise: Promise ->
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
promise.resolve(CrashLogStore.readPendingCrash(context))
|
||||
} catch (error: Exception) {
|
||||
promise.reject("ECRASHREAD", "Failed to read pending crash", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("clearPendingCrash") { promise: Promise ->
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
CrashLogStore.clearPendingCrash(context)
|
||||
promise.resolve(null)
|
||||
} catch (error: Exception) {
|
||||
promise.reject("ECRASHCLEAR", "Failed to clear pending crash", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("getDebugInfo") { promise: Promise ->
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
val webViewVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
WebView.getCurrentWebViewPackage()?.versionName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
promise.resolve(mapOf("webViewVersion" to webViewVersion))
|
||||
} catch (error: Exception) {
|
||||
promise.reject("EDEBUGINFO", "Failed to read native debug info", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function("restartApp") {
|
||||
val context = requireNotNull(appContext.reactContext) {
|
||||
"React context is unavailable"
|
||||
}
|
||||
val launchIntent = requireNotNull(
|
||||
context.packageManager.getLaunchIntentForPackage(context.packageName),
|
||||
) { "Application launch intent is unavailable" }
|
||||
try {
|
||||
CrashLogStore.clearPendingCrash(context)
|
||||
} catch (_: Exception) {
|
||||
// Restart remains useful even if stale crash cleanup fails.
|
||||
}
|
||||
launchIntent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP,
|
||||
)
|
||||
val activity = appContext.currentActivity
|
||||
if (activity == null) {
|
||||
context.startActivity(launchIntent)
|
||||
} else {
|
||||
activity.runOnUiThread {
|
||||
activity.finishAffinity()
|
||||
activity.startActivity(launchIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareFile(context: Context, file: java.io.File) {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.crash-logs",
|
||||
file,
|
||||
)
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
clipData = ClipData.newRawUri("LNReader crash logs", uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val chooser = Intent.createChooser(shareIntent, "Share crash logs")
|
||||
if (appContext.currentActivity == null) {
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(chooser)
|
||||
} else {
|
||||
appContext.currentActivity?.startActivity(chooser)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_REPORT_CHARACTERS = 256 * 1024
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="crash_logs" path="crash-logs/" />
|
||||
</paths>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package expo.modules.nativecrashlogs
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CrashLogRedactorTest {
|
||||
@Test
|
||||
fun redactsSensitiveHeaders() {
|
||||
val report = "Authorization: Bearer secret\nCookie=session-secret\nmessage"
|
||||
|
||||
assertEquals(
|
||||
"Authorization: [REDACTED]\nCookie=[REDACTED]\nmessage",
|
||||
CrashLogRedactor.redact(report),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun redactsUrlQueries() {
|
||||
val report = "Request failed: https://example.com/chapter?id=12&token=secret"
|
||||
|
||||
assertEquals(
|
||||
"Request failed: https://example.com/chapter?[REDACTED]",
|
||||
CrashLogRedactor.redact(report),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["expo.modules.nativecrashlogs.NativeCrashLogsModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import NativeCrashLogs from './src/NativeCrashLogsModule';
|
||||
|
||||
export default NativeCrashLogs;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "native-crash-logs"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { requireOptionalNativeModule } from 'expo-modules-core';
|
||||
|
||||
export type NativeCrashDebugInfo = {
|
||||
webViewVersion: string | null;
|
||||
};
|
||||
|
||||
export type NativeCrashLogsModule = {
|
||||
shareCrashLogs(report: string, logLevel: 'E' | 'V'): Promise<void>;
|
||||
recordJsCrash(stackTrace: string): void;
|
||||
getPendingCrash(): Promise<string | null>;
|
||||
clearPendingCrash(): Promise<void>;
|
||||
getDebugInfo(): Promise<NativeCrashDebugInfo>;
|
||||
restartApp(): void;
|
||||
};
|
||||
|
||||
export default requireOptionalNativeModule<NativeCrashLogsModule>(
|
||||
'NativeCrashLogs',
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'expo.modules.nativesharereceiver'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.nativesharereceiver"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package expo.modules.nativesharereceiver
|
||||
|
||||
import android.content.Intent
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class NativeShareReceiverModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("NativeShareReceiver")
|
||||
|
||||
Events("SharedText")
|
||||
|
||||
Function("getInitialSharedText") {
|
||||
val intent = appContext.currentActivity?.intent
|
||||
val text = extractSharedText(intent)
|
||||
if (text != null) {
|
||||
// Consume the share so JS reloads (Fast Refresh, OTA) that re-read
|
||||
// the activity intent don't re-deliver the same text.
|
||||
intent?.removeExtra(Intent.EXTRA_TEXT)
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
OnNewIntent { intent ->
|
||||
extractSharedText(intent)?.let { text ->
|
||||
sendEvent("SharedText", mapOf("text" to text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractSharedText(intent: Intent?): String? {
|
||||
if (intent?.action != Intent.ACTION_SEND) {
|
||||
return null
|
||||
}
|
||||
return intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["expo.modules.nativesharereceiver.NativeShareReceiverModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import NativeShareReceiver from './src/NativeShareReceiverModule';
|
||||
export default NativeShareReceiver;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "native-share-receiver"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NativeModule, requireNativeModule } from 'expo-modules-core';
|
||||
|
||||
export type NativeShareReceiverEvents = {
|
||||
SharedText: (payload: { text: string }) => void;
|
||||
};
|
||||
|
||||
declare class NativeShareReceiverModule extends NativeModule<NativeShareReceiverEvents> {
|
||||
getInitialSharedText(): string | null;
|
||||
}
|
||||
|
||||
export default requireNativeModule<NativeShareReceiverModule>(
|
||||
'NativeShareReceiver',
|
||||
);
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:android:release": "pnpm run generate:env:release && npx expo run:android --variant release",
|
||||
@@ -72,6 +72,7 @@
|
||||
"htmlparser2": "^12.0.0",
|
||||
"i18n-js": "^4.5.3",
|
||||
"lodash-es": "^4.18.1",
|
||||
"prettier": "2.8.8",
|
||||
"protobufjs": "^8.7.1",
|
||||
"react": "19.2.3",
|
||||
"react-native": "^0.86.0",
|
||||
@@ -133,7 +134,6 @@
|
||||
"jest-expo": "^57.0.2",
|
||||
"lint-staged": "^12.5.0",
|
||||
"nitrogen": "^0.36.1",
|
||||
"prettier": "2.8.8",
|
||||
"react-native-nitro-modules": "^0.36.0",
|
||||
"react-test-renderer": "19.2.3",
|
||||
"typescript": "~6.0.3"
|
||||
@@ -148,6 +148,7 @@
|
||||
"reactNativeDirectoryCheck": {
|
||||
"exclude": [
|
||||
"native-file",
|
||||
"native-crash-logs",
|
||||
"nitro-tts",
|
||||
"native-volume-button-listener",
|
||||
"native-zip-archive",
|
||||
|
||||
Generated
+3
-3
@@ -119,6 +119,9 @@ importers:
|
||||
lodash-es:
|
||||
specifier: ^4.18.1
|
||||
version: 4.18.1
|
||||
prettier:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8
|
||||
protobufjs:
|
||||
specifier: ^8.7.1
|
||||
version: 8.7.1
|
||||
@@ -297,9 +300,6 @@ importers:
|
||||
nitrogen:
|
||||
specifier: ^0.36.1
|
||||
version: 0.36.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3)(supports-color@9.4.0))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(supports-color@9.4.0))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
|
||||
prettier:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8
|
||||
react-native-nitro-modules:
|
||||
specifier: ^0.36.0
|
||||
version: 0.36.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3)(supports-color@9.4.0))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(supports-color@9.4.0))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, View, Text, StatusBar } from 'react-native';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { StyleSheet, View, Text, StatusBar, ScrollView } from 'react-native';
|
||||
import ErrorBoundary from 'react-native-error-boundary';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { BUILD_TYPE, GIT_HASH } from '@env';
|
||||
import { version } from '../../../package.json';
|
||||
import { getString } from '@i18n/translations';
|
||||
import { getErrorChainMessages } from '@utils/error';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { Button, List } from '@components';
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { restartApplication, shareCrashLogs } from '@services/crashLogs';
|
||||
|
||||
interface ErrorFallbackProps {
|
||||
error: Error;
|
||||
@@ -19,6 +23,7 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
|
||||
resetError,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
|
||||
const fallbackGetString = (
|
||||
key: Parameters<typeof getString>[0],
|
||||
@@ -33,23 +38,53 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
|
||||
};
|
||||
|
||||
const chainMessages = useMemo(() => getErrorChainMessages(error), [error]);
|
||||
const versionDetails = [
|
||||
`${fallbackGetString(
|
||||
'aboutScreen.version',
|
||||
'Version',
|
||||
)}: ${version} (${DeviceInfo.getBuildNumber()})`,
|
||||
BUILD_TYPE || 'Custom build',
|
||||
GIT_HASH,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
|
||||
const handleCopyStackTrace = async () => {
|
||||
const copyStackTrace = async () => {
|
||||
try {
|
||||
const message = chainMessages.join('\n\nCaused by: ');
|
||||
await Clipboard.setStringAsync(`${message}\n\n${error.stack}`);
|
||||
showToast(
|
||||
fallbackGetString(
|
||||
'common.copiedToClipboard',
|
||||
'Copied to clipboard: Stack trace',
|
||||
{ name: 'Stack trace' },
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
// clipboard failure is non-critical
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleShareCrashLogs = async () => {
|
||||
setIsSharing(true);
|
||||
try {
|
||||
await shareCrashLogs(error);
|
||||
} catch {
|
||||
const copiedStackTrace = await copyStackTrace();
|
||||
showToast(
|
||||
copiedStackTrace
|
||||
? fallbackGetString(
|
||||
'errorBoundary.shareCrashLogsFailed',
|
||||
'Could not share crash logs. The stack trace was copied instead.',
|
||||
)
|
||||
: fallbackGetString(
|
||||
'errorBoundary.shareCrashLogsFailedWithoutCopy',
|
||||
'Could not share crash logs or copy the stack trace.',
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsSharing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestart = () => {
|
||||
restartApplication(resetError).catch(resetError);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={[styles.mainCtn, { backgroundColor: theme.background }]}
|
||||
@@ -65,41 +100,49 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
|
||||
<Text style={[styles.errorDesc, { color: theme.onSurface }]}>
|
||||
{fallbackGetString(
|
||||
'errorBoundary.description',
|
||||
'The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.',
|
||||
'The application ran into an unexpected error. Please share the crash logs in our Discord support channel.',
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.versionDetails, { color: theme.onSurfaceVariant }]}
|
||||
>
|
||||
{versionDetails}
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={[
|
||||
styles.errorCtn,
|
||||
{
|
||||
backgroundColor: theme.surfaceVariant,
|
||||
color: theme.onSurfaceVariant,
|
||||
},
|
||||
]}
|
||||
numberOfLines={20}
|
||||
contentContainerStyle={styles.errorContent}
|
||||
>
|
||||
{`${chainMessages.join('\n\nCaused by: ')}\n\n${error.stack}`}
|
||||
</Text>
|
||||
<Text style={[styles.errorText, { color: theme.onSurfaceVariant }]}>
|
||||
{`${chainMessages.join('\n\nCaused by: ')}\n\n${error.stack}`}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<Button
|
||||
onPress={handleCopyStackTrace}
|
||||
title={fallbackGetString(
|
||||
'errorBoundary.copyStackTrace',
|
||||
'Copy stack trace',
|
||||
)}
|
||||
style={styles.copyButtonCtn}
|
||||
mode="outlined"
|
||||
/>
|
||||
<Button
|
||||
onPress={resetError}
|
||||
title={fallbackGetString(
|
||||
'errorBoundary.restart',
|
||||
'Restart the application',
|
||||
)}
|
||||
style={styles.buttonCtn}
|
||||
mode="contained"
|
||||
/>
|
||||
<View style={styles.actionsCtn}>
|
||||
<Button
|
||||
disabled={isSharing}
|
||||
loading={isSharing}
|
||||
onPress={handleShareCrashLogs}
|
||||
title={fallbackGetString(
|
||||
'errorBoundary.shareCrashLogs',
|
||||
'Share crash logs',
|
||||
)}
|
||||
mode="outlined"
|
||||
/>
|
||||
<Button
|
||||
onPress={handleRestart}
|
||||
title={fallbackGetString(
|
||||
'errorBoundary.restart',
|
||||
'Restart the application',
|
||||
)}
|
||||
mode="contained"
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@@ -117,35 +160,42 @@ const AppErrorBoundary: React.FC<AppErrorBoundaryProps> = ({ children }) => {
|
||||
export default AppErrorBoundary;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonCtn: {
|
||||
margin: 16,
|
||||
marginBottom: 32,
|
||||
},
|
||||
copyButtonCtn: {
|
||||
margin: 16,
|
||||
marginBottom: 8,
|
||||
actionsCtn: {
|
||||
gap: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
errorCtn: {
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
lineHeight: 20,
|
||||
marginVertical: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorContent: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
errorText: {
|
||||
lineHeight: 20,
|
||||
},
|
||||
errorDesc: {
|
||||
lineHeight: 20,
|
||||
marginVertical: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
errorInfoCtn: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
paddingTop: 32,
|
||||
},
|
||||
errorTitle: {
|
||||
fontSize: 20,
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
versionDetails: {
|
||||
lineHeight: 24,
|
||||
marginVertical: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
mainCtn: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { PropsWithChildren, useEffect, useMemo, useState } from 'react';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
import { getPendingCrash } from '@services/crashLogs';
|
||||
import { ErrorFallback } from './AppErrorBoundary';
|
||||
|
||||
const CrashRecoveryGate = ({ children }: PropsWithChildren) => {
|
||||
const [pendingCrash, setPendingCrash] = useState<string | null>();
|
||||
|
||||
useEffect(() => {
|
||||
getPendingCrash()
|
||||
.then(crash => {
|
||||
setPendingCrash(crash);
|
||||
if (crash) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
})
|
||||
.catch(() => setPendingCrash(null));
|
||||
}, []);
|
||||
|
||||
const crashError = useMemo(() => {
|
||||
if (!pendingCrash) return null;
|
||||
const error = new Error('LNReader stopped unexpectedly on its last run.');
|
||||
error.stack = pendingCrash;
|
||||
return error;
|
||||
}, [pendingCrash]);
|
||||
|
||||
if (pendingCrash === undefined) return null;
|
||||
if (crashError) {
|
||||
return <ErrorFallback error={crashError} resetError={() => null} />;
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
export default CrashRecoveryGate;
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@test-utils';
|
||||
import { ErrorFallback } from '../AppErrorBoundary';
|
||||
import { version } from '../../../../package.json';
|
||||
|
||||
const mockShareCrashLogs = jest.fn();
|
||||
const mockRestartApplication = jest.fn();
|
||||
|
||||
const mockTheme = {
|
||||
background: '#ffffff',
|
||||
onSurface: '#000000',
|
||||
onSurfaceVariant: '#333333',
|
||||
surfaceVariant: '#eeeeee',
|
||||
outlineVariant: '#cccccc',
|
||||
primary: '#0061a4',
|
||||
};
|
||||
|
||||
jest.mock('@hooks/persisted/useTheme', () => ({
|
||||
ThemeProvider: ({ children }: { children: ReactNode }) => children,
|
||||
useTheme: () => mockTheme,
|
||||
}));
|
||||
|
||||
jest.mock('@hooks/persisted', () => ({
|
||||
useTheme: () => mockTheme,
|
||||
}));
|
||||
|
||||
jest.mock('react-native-device-info', () => ({
|
||||
__esModule: true,
|
||||
default: { getBuildNumber: () => '42' },
|
||||
}));
|
||||
|
||||
jest.mock('@env', () => ({ BUILD_TYPE: 'Debug', GIT_HASH: 'abc123' }));
|
||||
|
||||
jest.mock('@screens/novel/NovelContext', () => ({
|
||||
NovelContextProvider: ({ children }: { children: ReactNode }) => children,
|
||||
}));
|
||||
|
||||
jest.mock('react-native-safe-area-context', () => {
|
||||
const ReactModule = require('react');
|
||||
const { View } = jest.requireActual('react-native');
|
||||
const frame = { height: 800, width: 400, x: 0, y: 0 };
|
||||
const insets = { bottom: 0, left: 0, right: 0, top: 0 };
|
||||
return {
|
||||
SafeAreaFrameContext: ReactModule.createContext(frame),
|
||||
SafeAreaInsetsContext: ReactModule.createContext(insets),
|
||||
SafeAreaProvider: ({ children }: { children: ReactNode }) => children,
|
||||
SafeAreaView: ({ children, ...props }: { children: ReactNode }) =>
|
||||
ReactModule.createElement(View, props, children),
|
||||
initialWindowMetrics: { frame, insets },
|
||||
useSafeAreaFrame: () => frame,
|
||||
useSafeAreaInsets: () => insets,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@services/crashLogs', () => ({
|
||||
shareCrashLogs: (error: Error) => mockShareCrashLogs(error),
|
||||
restartApplication: (fallback: () => void) =>
|
||||
mockRestartApplication(fallback),
|
||||
}));
|
||||
|
||||
describe('ErrorFallback', () => {
|
||||
beforeEach(() => {
|
||||
mockShareCrashLogs.mockResolvedValue(undefined);
|
||||
mockRestartApplication.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('shares a diagnostic report for the displayed error', async () => {
|
||||
const error = new Error('boom');
|
||||
render(<ErrorFallback error={error} resetError={jest.fn()} />);
|
||||
|
||||
fireEvent.press(screen.getByText('Share crash logs'));
|
||||
|
||||
await waitFor(() => expect(mockShareCrashLogs).toHaveBeenCalledWith(error));
|
||||
});
|
||||
|
||||
it('shows version and build details on the error screen', () => {
|
||||
render(<ErrorFallback error={new Error('boom')} resetError={jest.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(`Version: ${version} (42) · Debug · abc123`),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uses the native restart flow', async () => {
|
||||
const resetError = jest.fn();
|
||||
render(<ErrorFallback error={new Error('boom')} resetError={resetError} />);
|
||||
|
||||
fireEvent.press(screen.getByText('Restart the application'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockRestartApplication).toHaveBeenCalledWith(resetError),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -93,23 +93,32 @@ function CustomBottomTabBar({
|
||||
style={styles.pressable}
|
||||
>
|
||||
{/* Icon */}
|
||||
<Animated.View
|
||||
<View
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
{
|
||||
transitionProperty: ['width', 'backgroundColor'],
|
||||
transitionDuration: 250,
|
||||
transitionTimingFunction: 'ease-in-out',
|
||||
marginBottom: showLabel ? 4 : 20,
|
||||
width: isFocused ? 64 : 32,
|
||||
backgroundColor: isFocused
|
||||
? theme.primaryContainer
|
||||
: transparentBg,
|
||||
},
|
||||
{ marginBottom: showLabel ? 4 : 20 },
|
||||
]}
|
||||
>
|
||||
{/* The indicator is absolutely positioned and animated with a
|
||||
transform so that growing it never re-runs layout, which
|
||||
would otherwise make the icon and label tremble sideways as
|
||||
their rounded pixel positions shift on every frame. */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.indicator,
|
||||
{
|
||||
transitionProperty: ['transform', 'backgroundColor'],
|
||||
transitionDuration: 250,
|
||||
transitionTimingFunction: 'ease-in-out',
|
||||
transform: [{ scaleX: isFocused ? 1 : 0.5 }],
|
||||
backgroundColor: isFocused
|
||||
? theme.primaryContainer
|
||||
: transparentBg,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{renderIcon({ color: iconColor, route })}
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Label */}
|
||||
{showLabel ? (
|
||||
@@ -155,7 +164,15 @@ const styles = StyleSheet.create({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 64,
|
||||
height: 32,
|
||||
},
|
||||
indicator: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 16,
|
||||
},
|
||||
label: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UseLibraryReturnType,
|
||||
} from '@screens/library/hooks/useLibrary';
|
||||
import { useLibrarySettings } from '@hooks/persisted';
|
||||
import SetCategoriesModal from '@screens/novel/components/SetCategoriesModal';
|
||||
|
||||
// type Library = Category & { novels: LibraryNovelInfo[] };
|
||||
|
||||
@@ -11,6 +12,8 @@ type LibraryContextType = UseLibraryReturnType & {
|
||||
settings: ReturnType<typeof useLibrarySettings>;
|
||||
};
|
||||
|
||||
const EMPTY_NOVEL_IDS: number[] = [];
|
||||
|
||||
const defaultValue = {} as LibraryContextType;
|
||||
const LibraryContext = createContext<LibraryContextType>(defaultValue);
|
||||
|
||||
@@ -21,11 +24,27 @@ export function LibraryContextProvider({
|
||||
}) {
|
||||
const useLibraryParams = useLibrary();
|
||||
const settings = useLibrarySettings();
|
||||
const {
|
||||
pendingLibraryAddition,
|
||||
cancelPendingLibraryAddition,
|
||||
confirmPendingLibraryAddition,
|
||||
} = useLibraryParams;
|
||||
|
||||
return (
|
||||
<LibraryContext.Provider value={{ ...useLibraryParams, settings }}>
|
||||
{children}
|
||||
</LibraryContext.Provider>
|
||||
<>
|
||||
<LibraryContext.Provider value={{ ...useLibraryParams, settings }}>
|
||||
{children}
|
||||
</LibraryContext.Provider>
|
||||
{pendingLibraryAddition ? (
|
||||
<SetCategoriesModal
|
||||
novelIds={EMPTY_NOVEL_IDS}
|
||||
initialCategoryIds={pendingLibraryAddition.initialCategoryIds}
|
||||
closeModal={cancelPendingLibraryAddition}
|
||||
onSubmit={confirmPendingLibraryAddition}
|
||||
visible
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ const defaultLibraryContext = {
|
||||
refreshCategories: jest.fn().mockResolvedValue(undefined),
|
||||
setLibrary: jest.fn(),
|
||||
novelInLibrary: jest.fn(() => false),
|
||||
switchNovelToLibrary: jest.fn().mockResolvedValue(undefined),
|
||||
switchNovelToLibrary: jest.fn().mockResolvedValue(true),
|
||||
pendingLibraryAddition: undefined,
|
||||
cancelPendingLibraryAddition: jest.fn(),
|
||||
confirmPendingLibraryAddition: jest.fn().mockResolvedValue(undefined),
|
||||
refetchLibrary: jest.fn(),
|
||||
setLibrarySearchText: jest.fn(),
|
||||
settings: {
|
||||
|
||||
@@ -16,6 +16,7 @@ interface ListViewProps {
|
||||
onPress: () => void;
|
||||
isSelected?: boolean;
|
||||
onLongPress?: () => void;
|
||||
continueReadingButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListView = ({
|
||||
@@ -27,6 +28,7 @@ const ListView = ({
|
||||
onPress,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
continueReadingButton,
|
||||
}: ListViewProps) => {
|
||||
const fadedImage = { opacity: inLibraryBadge ? 0.5 : 1 };
|
||||
return (
|
||||
@@ -58,6 +60,7 @@ const ListView = ({
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
{continueReadingButton}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Color from 'color';
|
||||
import ListView from './ListView';
|
||||
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
@@ -19,6 +20,7 @@ import { getString } from '@i18n/translations';
|
||||
import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading';
|
||||
import NovelCoverImage from './NovelCoverImage';
|
||||
import { useNovelCoverLayout } from './NovelCoverLayoutContext';
|
||||
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
|
||||
|
||||
interface UnreadBadgeProps {
|
||||
showDownloadBadges: boolean;
|
||||
@@ -61,6 +63,7 @@ interface INovelCover<TNovel> {
|
||||
selectedNovelIds?: number[];
|
||||
globalSearch?: boolean;
|
||||
imageRequestInit?: ImageRequestInit;
|
||||
onContinueReading?: () => void;
|
||||
}
|
||||
|
||||
function isFromDB(
|
||||
@@ -84,6 +87,7 @@ function NovelCover<
|
||||
globalSearch,
|
||||
selectedNovelIds,
|
||||
imageRequestInit,
|
||||
onContinueReading,
|
||||
}: INovelCover<TNovel>) {
|
||||
const selectionActive =
|
||||
hasSelection ?? (selectedNovelIds != null && selectedNovelIds.length > 0);
|
||||
@@ -108,6 +112,25 @@ function NovelCover<
|
||||
[imageRequestInit],
|
||||
);
|
||||
|
||||
const continueReadingButton =
|
||||
onContinueReading && !selectionActive ? (
|
||||
<Pressable
|
||||
accessibilityLabel={getString('novelScreen.continueReading')}
|
||||
accessibilityRole="button"
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={event => {
|
||||
event.stopPropagation();
|
||||
onContinueReading();
|
||||
}}
|
||||
style={[
|
||||
styles.continueReadingButton,
|
||||
{ backgroundColor: Color(theme.primary).alpha(0.9).string() },
|
||||
]}
|
||||
>
|
||||
<MaterialCommunityIcons name="play" size={20} color={theme.onPrimary} />
|
||||
</Pressable>
|
||||
) : null;
|
||||
|
||||
if (item.completeRow) {
|
||||
if (!addSkeletonLoading) {
|
||||
return <></>;
|
||||
@@ -197,6 +220,13 @@ function NovelCover<
|
||||
width={coverWidth}
|
||||
/>
|
||||
) : null}
|
||||
{continueReadingButton ? (
|
||||
<View
|
||||
style={[styles.continueReadingOverlay, { top: coverHeight - 40 }]}
|
||||
>
|
||||
{continueReadingButton}
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
@@ -227,6 +257,7 @@ function NovelCover<
|
||||
onPress={selectionActive ? selectNovel : onPress}
|
||||
onLongPress={selectNovel}
|
||||
isSelected={isSelected}
|
||||
continueReadingButton={continueReadingButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -373,6 +404,18 @@ const styles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
right: 4,
|
||||
},
|
||||
continueReadingButton: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
height: 36,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: 36,
|
||||
},
|
||||
continueReadingOverlay: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
},
|
||||
downloadBadge: {
|
||||
borderBottomLeftRadius: 4,
|
||||
borderTopLeftRadius: 4,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import './mocks';
|
||||
import { Fragment, type ReactElement } from 'react';
|
||||
import { fireEvent, render, screen } from '@test-utils';
|
||||
import NovelCover from '../NovelCover';
|
||||
import { NovelCoverLayoutProvider } from '../NovelCoverLayoutContext';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
import { getString } from '@i18n/translations';
|
||||
import LibraryNovelItem from '@screens/library/components/LibraryNovelItem';
|
||||
import type { History } from '@database/types';
|
||||
|
||||
jest.mock(
|
||||
'@screens/browse/loadingAnimation/SourceScreenSkeletonLoading',
|
||||
() => 'SourceScreenSkeletonLoading',
|
||||
);
|
||||
jest.mock('@hooks', () => ({ useDeviceOrientation: () => 'portrait' }));
|
||||
|
||||
const novel = {
|
||||
id: 1,
|
||||
name: 'Example Novel',
|
||||
path: '/example',
|
||||
pluginId: 'example',
|
||||
};
|
||||
|
||||
const onPress = jest.fn();
|
||||
const onContinueReading = jest.fn();
|
||||
const lastReadChapter = {
|
||||
id: 7,
|
||||
novelId: novel.id,
|
||||
name: 'Chapter 7',
|
||||
} as History;
|
||||
const renderCover = (element: ReactElement) =>
|
||||
render(element, { wrapper: Fragment });
|
||||
|
||||
const Cover = ({
|
||||
displayMode,
|
||||
hasSelection = false,
|
||||
canContinue = true,
|
||||
}: {
|
||||
displayMode: DisplayModes;
|
||||
hasSelection?: boolean;
|
||||
canContinue?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<NovelCoverLayoutProvider
|
||||
value={{
|
||||
coverHeight: 160,
|
||||
displayMode,
|
||||
numColumns: displayMode === DisplayModes.List ? 1 : 3,
|
||||
showDownloadBadges: false,
|
||||
showUnreadBadges: false,
|
||||
}}
|
||||
>
|
||||
<NovelCover
|
||||
item={novel}
|
||||
theme={theme}
|
||||
isSelected={false}
|
||||
hasSelection={hasSelection}
|
||||
onPress={onPress}
|
||||
onLongPress={jest.fn()}
|
||||
onContinueReading={canContinue ? onContinueReading : undefined}
|
||||
libraryStatus={false}
|
||||
/>
|
||||
</NovelCoverLayoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const LibraryItem = ({
|
||||
showContinueReadingButton,
|
||||
}: {
|
||||
showContinueReadingButton: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<NovelCoverLayoutProvider
|
||||
value={{
|
||||
coverHeight: 160,
|
||||
displayMode: DisplayModes.Comfortable,
|
||||
numColumns: 3,
|
||||
showDownloadBadges: false,
|
||||
showUnreadBadges: false,
|
||||
}}
|
||||
>
|
||||
<LibraryNovelItem
|
||||
item={novel}
|
||||
theme={theme}
|
||||
isSelected={false}
|
||||
hasSelection={false}
|
||||
onSelect={jest.fn()}
|
||||
onNavigate={jest.fn()}
|
||||
onContinueReading={onContinueReading}
|
||||
lastReadChapter={lastReadChapter}
|
||||
showContinueReadingButton={showContinueReadingButton}
|
||||
imageRequestInit={undefined}
|
||||
/>
|
||||
</NovelCoverLayoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('NovelCover continue reading', () => {
|
||||
beforeEach(() => {
|
||||
onPress.mockClear();
|
||||
onContinueReading.mockClear();
|
||||
});
|
||||
|
||||
it.each([DisplayModes.Compact, DisplayModes.Comfortable, DisplayModes.List])(
|
||||
'opens the saved chapter from %s layout',
|
||||
displayMode => {
|
||||
renderCover(<Cover displayMode={displayMode} />);
|
||||
|
||||
fireEvent.press(
|
||||
screen.getByRole('button', {
|
||||
name: getString('novelScreen.continueReading'),
|
||||
}),
|
||||
{ stopPropagation: jest.fn() },
|
||||
);
|
||||
|
||||
expect(onContinueReading).toHaveBeenCalledTimes(1);
|
||||
expect(onPress).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('hides the shortcut during selection and when there is no reading history', () => {
|
||||
const { rerender } = renderCover(
|
||||
<Cover displayMode={DisplayModes.Comfortable} hasSelection />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: getString('novelScreen.continueReading'),
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
rerender(
|
||||
<Cover displayMode={DisplayModes.Comfortable} canContinue={false} />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: getString('novelScreen.continueReading'),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('respects the library display setting', () => {
|
||||
const { rerender } = renderCover(
|
||||
<LibraryItem showContinueReadingButton={false} />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: getString('novelScreen.continueReading'),
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
rerender(<LibraryItem showContinueReadingButton />);
|
||||
fireEvent.press(
|
||||
screen.getByRole('button', {
|
||||
name: getString('novelScreen.continueReading'),
|
||||
}),
|
||||
{ stopPropagation: jest.fn() },
|
||||
);
|
||||
expect(onContinueReading).toHaveBeenCalledWith(novel, lastReadChapter);
|
||||
});
|
||||
});
|
||||
@@ -289,6 +289,27 @@ describe('runDatabaseBootstrap', () => {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('repairs only an empty default category name', () => {
|
||||
const sqlite = open({ name: ':memory:' });
|
||||
try {
|
||||
createSchema(sqlite);
|
||||
sqlite.executeSync(
|
||||
"INSERT INTO Category (id, name, sort) VALUES (1, ' ', 1), (2, 'Legacy user category', 2)",
|
||||
);
|
||||
|
||||
runDatabaseBootstrap(createExecutor(sqlite));
|
||||
|
||||
expect(
|
||||
sqlite.executeSync('SELECT id, name FROM Category ORDER BY id').rows,
|
||||
).toEqual([
|
||||
{ id: 1, name: 'categories.default' },
|
||||
{ id: 2, name: 'Legacy user category' },
|
||||
]);
|
||||
} finally {
|
||||
sqlite.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('production migrations', () => {
|
||||
|
||||
+5
-1
@@ -8,7 +8,10 @@ import { migrate } from 'drizzle-orm/op-sqlite/migrator';
|
||||
import migrations from '../../drizzle/migrations';
|
||||
import { createDbManager } from './manager/manager';
|
||||
import { open } from '@op-engineering/op-sqlite';
|
||||
import { createCategoryDefaultQuery } from './queryStrings/populate';
|
||||
import {
|
||||
createCategoryDefaultQuery,
|
||||
repairDefaultCategoryNameQuery,
|
||||
} from './queryStrings/populate';
|
||||
import {
|
||||
createCategoryTriggerQuery,
|
||||
createNovelTriggerQueryDelete,
|
||||
@@ -332,6 +335,7 @@ const setPragmas = (executor: SqlExecutor) => {
|
||||
const populateDatabase = (executor: SqlExecutor) => {
|
||||
console.log('Populating database');
|
||||
executor.executeSync(createCategoryDefaultQuery);
|
||||
executor.executeSync(repairDefaultCategoryNameQuery);
|
||||
};
|
||||
|
||||
const createDbTriggers = (executor: SqlExecutor) => {
|
||||
|
||||
@@ -8,6 +8,17 @@ import {
|
||||
novelCategorySchema,
|
||||
type CategoryRow,
|
||||
} from '@database/schema';
|
||||
import { BUILT_IN_CATEGORY_IDS } from '@database/constants';
|
||||
|
||||
const DEFAULT_CATEGORY_ID = BUILT_IN_CATEGORY_IDS.default;
|
||||
|
||||
const normalizeCategoryName = (categoryName: string): string => {
|
||||
const normalizedName = categoryName.trim();
|
||||
if (!normalizedName) {
|
||||
throw new Error('Category name cannot be empty');
|
||||
}
|
||||
return normalizedName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all categories with their novel IDs using Drizzle ORM
|
||||
@@ -90,11 +101,13 @@ export const getCategoriesWithCount = async (
|
||||
export const createCategory = async (
|
||||
categoryName: string,
|
||||
): Promise<CategoryRow> => {
|
||||
const normalizedName = normalizeCategoryName(categoryName);
|
||||
|
||||
return await dbManager.write(async tx => {
|
||||
const categoryCount = await tx.$count(categorySchema);
|
||||
const row = await tx
|
||||
.insert(categorySchema)
|
||||
.values({ name: categoryName, sort: categoryCount + 1 })
|
||||
.values({ name: normalizedName, sort: categoryCount + 1 })
|
||||
.returning()
|
||||
.get();
|
||||
return row;
|
||||
@@ -152,10 +165,16 @@ export const updateCategory = async (
|
||||
categoryId: number,
|
||||
categoryName: string,
|
||||
): Promise<void> => {
|
||||
if (categoryId === DEFAULT_CATEGORY_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedName = normalizeCategoryName(categoryName);
|
||||
|
||||
await dbManager.write(async tx => {
|
||||
await tx
|
||||
.update(categorySchema)
|
||||
.set({ name: categoryName })
|
||||
.set({ name: normalizedName })
|
||||
.where(eq(categorySchema.id, categoryId))
|
||||
.run();
|
||||
});
|
||||
@@ -165,11 +184,16 @@ export const updateCategory = async (
|
||||
* Check if a category name already exists using Drizzle ORM
|
||||
*/
|
||||
export const isCategoryNameDuplicate = (categoryName: string): boolean => {
|
||||
const normalizedName = categoryName.trim();
|
||||
if (!normalizedName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = dbManager.getSync(
|
||||
dbManager
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(eq(categorySchema.name, categoryName)),
|
||||
.where(eq(categorySchema.name, normalizedName)),
|
||||
);
|
||||
|
||||
return !!result;
|
||||
@@ -209,39 +233,57 @@ export const getAllNovelCategories = async (): Promise<NovelCategory[]> => {
|
||||
*/
|
||||
export const _restoreCategory = async (
|
||||
category: BackupCategory,
|
||||
): Promise<void> => {
|
||||
await dbManager.write(async tx => {
|
||||
// Delete existing category with same id or sort
|
||||
await tx
|
||||
.delete(categorySchema)
|
||||
.where(
|
||||
sql`${categorySchema.id} = ${category.id} OR ${categorySchema.sort} = ${category.sort}`,
|
||||
)
|
||||
.run();
|
||||
novelIdMap?: ReadonlyMap<number, number>,
|
||||
): Promise<number> => {
|
||||
return dbManager.write(async tx => {
|
||||
const isDefaultCategory = category.id === DEFAULT_CATEGORY_ID;
|
||||
const normalizedName = isDefaultCategory
|
||||
? undefined
|
||||
: normalizeCategoryName(category.name);
|
||||
const existingByName = normalizedName
|
||||
? await tx
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(eq(categorySchema.name, normalizedName))
|
||||
.get()
|
||||
: undefined;
|
||||
const existingDefault =
|
||||
existingByName || !isDefaultCategory
|
||||
? undefined
|
||||
: await tx
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(eq(categorySchema.id, DEFAULT_CATEGORY_ID))
|
||||
.get();
|
||||
|
||||
// Insert the category
|
||||
await tx
|
||||
.insert(categorySchema)
|
||||
.values({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
sort: category.sort,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
let categoryId = existingByName?.id ?? existingDefault?.id;
|
||||
if (!categoryId) {
|
||||
const restoredCategory = await tx
|
||||
.insert(categorySchema)
|
||||
.values({
|
||||
name: normalizedName ?? getString('categories.default'),
|
||||
sort: category.sort,
|
||||
})
|
||||
.returning({ id: categorySchema.id })
|
||||
.get();
|
||||
categoryId = restoredCategory.id;
|
||||
}
|
||||
|
||||
// Insert novel-category associations
|
||||
if (category.novelIds && category.novelIds.length > 0) {
|
||||
for (const novelId of category.novelIds) {
|
||||
for (const backupNovelId of category.novelIds) {
|
||||
const novelId = novelIdMap?.get(backupNovelId) ?? backupNovelId;
|
||||
await tx
|
||||
.insert(novelCategorySchema)
|
||||
.values({
|
||||
categoryId: category.id,
|
||||
categoryId,
|
||||
novelId: novelId,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
return categoryId;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -785,7 +785,9 @@ export const getPrevChapter = async (
|
||||
.orderBy(
|
||||
desc(castInt(chapterSchema.page)),
|
||||
desc(castInt(chapterSchema.position)),
|
||||
desc(chapterSchema.id),
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
};
|
||||
|
||||
@@ -814,7 +816,9 @@ export const getNextChapter = async (
|
||||
.orderBy(
|
||||
asc(castInt(chapterSchema.page)),
|
||||
asc(castInt(chapterSchema.position)),
|
||||
asc(chapterSchema.id),
|
||||
)
|
||||
.limit(1)
|
||||
.get();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import { insertChapters } from './ChapterQueries';
|
||||
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { getString } from '@i18n/translations';
|
||||
import { BackupNovel, DBNovelInfo, NovelInfo } from '../types';
|
||||
import {
|
||||
BackupNovel,
|
||||
DBNovelInfo,
|
||||
NovelInfo,
|
||||
type RestoredNovelMapping,
|
||||
} from '../types';
|
||||
import { SourceNovel } from '@plugins/types';
|
||||
import { NOVEL_STORAGE } from '@utils/Storages';
|
||||
import { downloadFile } from '@plugins/helpers/fetch';
|
||||
@@ -23,7 +28,36 @@ import { getLibraryDefaultCategoryId } from '@hooks/persisted/useSettings';
|
||||
import NativeFile from '@modules/native-file';
|
||||
import { BUILT_IN_CATEGORY_IDS } from '@database/constants';
|
||||
|
||||
const getCategoryForNewNovel = async (tx: TransactionParameter) => {
|
||||
const getBuiltInDefaultCategory = async (tx: TransactionParameter) => {
|
||||
const defaultCategory = await tx
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(eq(categorySchema.id, BUILT_IN_CATEGORY_IDS.default))
|
||||
.get();
|
||||
|
||||
return defaultCategory ? [defaultCategory] : [];
|
||||
};
|
||||
|
||||
const getCategoriesForNewNovel = async (
|
||||
tx: TransactionParameter,
|
||||
categoryIds?: number[],
|
||||
) => {
|
||||
if (categoryIds !== undefined) {
|
||||
if (categoryIds.length) {
|
||||
const selectedCategories = await tx
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(inArray(categorySchema.id, categoryIds))
|
||||
.all();
|
||||
|
||||
if (selectedCategories.length) {
|
||||
return selectedCategories;
|
||||
}
|
||||
}
|
||||
|
||||
return getBuiltInDefaultCategory(tx);
|
||||
}
|
||||
|
||||
const preferredCategoryId = getLibraryDefaultCategoryId();
|
||||
|
||||
if (preferredCategoryId) {
|
||||
@@ -34,17 +68,16 @@ const getCategoryForNewNovel = async (tx: TransactionParameter) => {
|
||||
.get();
|
||||
|
||||
if (preferredCategory) {
|
||||
return preferredCategory;
|
||||
return [preferredCategory];
|
||||
}
|
||||
}
|
||||
|
||||
return tx
|
||||
.select({ id: categorySchema.id })
|
||||
.from(categorySchema)
|
||||
.where(eq(categorySchema.id, BUILT_IN_CATEGORY_IDS.default))
|
||||
.get();
|
||||
return getBuiltInDefaultCategory(tx);
|
||||
};
|
||||
|
||||
const getCategoryForNewNovel = async (tx: TransactionParameter) =>
|
||||
(await getCategoriesForNewNovel(tx))[0];
|
||||
|
||||
/**
|
||||
* Inserts a novel and its chapters into the database using Drizzle ORM.
|
||||
* Also handles downloading the novel cover if available.
|
||||
@@ -138,6 +171,7 @@ export const getNovelByPath = (
|
||||
export const switchNovelToLibraryQuery = async (
|
||||
novelPath: string,
|
||||
pluginId: string,
|
||||
categoryIds?: number[],
|
||||
): Promise<NovelInfo | undefined> => {
|
||||
const novel = await getNovelByPath(novelPath, pluginId);
|
||||
if (novel) {
|
||||
@@ -157,16 +191,18 @@ export const switchNovelToLibraryQuery = async (
|
||||
.run();
|
||||
showToast(getString('browseScreen.removeFromLibrary'));
|
||||
} else {
|
||||
// Add to library: add to default category
|
||||
const defaultCategory = await getCategoryForNewNovel(tx);
|
||||
// Add to library with the selected or configured default categories.
|
||||
const categories = await getCategoriesForNewNovel(tx, categoryIds);
|
||||
|
||||
if (defaultCategory) {
|
||||
if (categories.length) {
|
||||
await tx
|
||||
.insert(novelCategorySchema)
|
||||
.values({
|
||||
novelId: novel.id,
|
||||
categoryId: defaultCategory.id,
|
||||
})
|
||||
.values(
|
||||
categories.map((category: { id: number }) => ({
|
||||
novelId: novel.id,
|
||||
categoryId: category.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -195,15 +231,17 @@ export const switchNovelToLibraryQuery = async (
|
||||
.where(eq(novelSchema.id, novelId))
|
||||
.run();
|
||||
|
||||
const defaultCategory = await getCategoryForNewNovel(tx);
|
||||
const categories = await getCategoriesForNewNovel(tx, categoryIds);
|
||||
|
||||
if (defaultCategory) {
|
||||
if (categories.length) {
|
||||
await tx
|
||||
.insert(novelCategorySchema)
|
||||
.values({
|
||||
novelId: novelId,
|
||||
categoryId: defaultCategory.id,
|
||||
})
|
||||
.values(
|
||||
categories.map((category: { id: number }) => ({
|
||||
novelId: novelId,
|
||||
categoryId: category.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
});
|
||||
@@ -440,18 +478,13 @@ export const updateNovelCategories = async (
|
||||
/**
|
||||
* Restores novel and chapters from a backup object.
|
||||
*/
|
||||
export const _restoreNovelAndChapters = async (backupNovel: BackupNovel) => {
|
||||
const { chapters, ...novel } = backupNovel;
|
||||
await dbManager.write(async tx => {
|
||||
// Delete existing novel data
|
||||
await tx.delete(novelSchema).where(eq(novelSchema.id, novel.id)).run();
|
||||
await tx
|
||||
.delete(chapterSchema)
|
||||
.where(eq(chapterSchema.novelId, novel.id))
|
||||
.run();
|
||||
|
||||
// Restore novel
|
||||
await tx
|
||||
export const _restoreNovelAndChapters = async (
|
||||
backupNovel: BackupNovel,
|
||||
): Promise<RestoredNovelMapping> => {
|
||||
const { chapters, id: backupNovelId, ...novel } = backupNovel;
|
||||
return dbManager.write(async tx => {
|
||||
// Match novels by their stable source identity, not the database-local ID.
|
||||
const restoredNovel = await tx
|
||||
.insert(novelSchema)
|
||||
.values({
|
||||
...novel,
|
||||
@@ -459,15 +492,71 @@ export const _restoreNovelAndChapters = async (backupNovel: BackupNovel) => {
|
||||
chaptersDownloaded: 0,
|
||||
chaptersUnread: 0,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [novelSchema.path, novelSchema.pluginId],
|
||||
set: {
|
||||
...novel,
|
||||
totalChapters: 0,
|
||||
chaptersDownloaded: 0,
|
||||
chaptersUnread: 0,
|
||||
},
|
||||
})
|
||||
.returning({ id: novelSchema.id })
|
||||
.get();
|
||||
|
||||
if (novel.cover?.startsWith(`file://${NOVEL_STORAGE}/`)) {
|
||||
const cacheSuffix = novel.cover.match(/[?#].*$/)?.[0] ?? '';
|
||||
await tx
|
||||
.update(novelSchema)
|
||||
.set({
|
||||
cover: `file://${NOVEL_STORAGE}/${novel.pluginId}/${restoredNovel.id}/cover.png${cacheSuffix}`,
|
||||
})
|
||||
.where(eq(novelSchema.id, restoredNovel.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
await tx
|
||||
.delete(chapterSchema)
|
||||
.where(eq(chapterSchema.novelId, restoredNovel.id))
|
||||
.run();
|
||||
|
||||
const chapterMappings: RestoredNovelMapping['chapters'] = [];
|
||||
|
||||
// Restore chapters in batches
|
||||
if (chapters.length > 0) {
|
||||
const BATCH_SIZE = 100;
|
||||
for (let i = 0; i < chapters.length; i += BATCH_SIZE) {
|
||||
const batch = chapters.slice(i, i + BATCH_SIZE);
|
||||
await tx.insert(chapterSchema).values(batch).run();
|
||||
const restoredChapters = await tx
|
||||
.insert(chapterSchema)
|
||||
.values(
|
||||
batch.map(({ id: _chapterId, novelId: _novelId, ...chapter }) => ({
|
||||
...chapter,
|
||||
novelId: restoredNovel.id,
|
||||
})),
|
||||
)
|
||||
.returning({ id: chapterSchema.id, path: chapterSchema.path })
|
||||
.all();
|
||||
const restoredIdsByPath = new Map(
|
||||
restoredChapters.map(chapter => [chapter.path, chapter.id]),
|
||||
);
|
||||
for (const chapter of batch) {
|
||||
const restoredChapterId = restoredIdsByPath.get(chapter.path);
|
||||
if (restoredChapterId !== undefined) {
|
||||
chapterMappings.push({
|
||||
backupChapterId: chapter.id,
|
||||
restoredChapterId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pluginId: novel.pluginId,
|
||||
backupNovelId,
|
||||
restoredNovelId: restoredNovel.id,
|
||||
chapters: chapterMappings,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
deleteCategoryById,
|
||||
updateCategoryOrderInDb,
|
||||
getAllNovelCategories,
|
||||
_restoreCategory,
|
||||
} from '../CategoryQueries';
|
||||
import { showToast } from '@utils/showToast';
|
||||
|
||||
@@ -56,6 +57,89 @@ describe('CategoryQueries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_restoreCategory', () => {
|
||||
it('preserves the default name and adds restored memberships', async () => {
|
||||
const testDb = getTestDb();
|
||||
const existingNovelId = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
});
|
||||
const restoredNovelId = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
});
|
||||
await insertTestNovelCategory(testDb, existingNovelId, 1);
|
||||
|
||||
await _restoreCategory({
|
||||
id: 1,
|
||||
name: ' ',
|
||||
sort: 1,
|
||||
novelIds: [restoredNovelId],
|
||||
});
|
||||
|
||||
const categories = await getCategoriesFromDb();
|
||||
expect(categories.find(category => category.id === 1)?.name).toBe(
|
||||
'Default',
|
||||
);
|
||||
const memberships = await getAllNovelCategories();
|
||||
expect(
|
||||
memberships
|
||||
.filter(membership => membership.categoryId === 1)
|
||||
.map(membership => membership.novelId),
|
||||
).toEqual(expect.arrayContaining([existingNovelId, restoredNovelId]));
|
||||
});
|
||||
|
||||
it('does not overwrite a custom category with a colliding backup ID', async () => {
|
||||
const testDb = getTestDb();
|
||||
const existingCategoryId = await insertTestCategory(testDb, {
|
||||
name: 'Existing Category',
|
||||
});
|
||||
|
||||
const restoredCategoryId = await _restoreCategory({
|
||||
id: existingCategoryId,
|
||||
name: 'Restored Category',
|
||||
sort: 3,
|
||||
novelIds: [],
|
||||
});
|
||||
|
||||
expect(restoredCategoryId).not.toBe(existingCategoryId);
|
||||
const categories = await getCategoriesFromDb();
|
||||
expect(
|
||||
categories.find(category => category.id === existingCategoryId)?.name,
|
||||
).toBe('Existing Category');
|
||||
expect(
|
||||
categories.find(category => category.id === restoredCategoryId)?.name,
|
||||
).toBe('Restored Category');
|
||||
});
|
||||
|
||||
it('does not treat a legacy category with ID 2 as the Local category', async () => {
|
||||
const restoredCategoryId = await _restoreCategory({
|
||||
id: 2,
|
||||
name: 'Legacy user category',
|
||||
sort: 2,
|
||||
novelIds: [],
|
||||
});
|
||||
|
||||
expect(restoredCategoryId).not.toBe(2);
|
||||
const categories = await getCategoriesFromDb();
|
||||
expect(categories.find(category => category.id === 2)?.name).toBe(
|
||||
'Local',
|
||||
);
|
||||
expect(
|
||||
categories.find(category => category.id === restoredCategoryId)?.name,
|
||||
).toBe('Legacy user category');
|
||||
});
|
||||
|
||||
it('rejects a restored user category with an empty name', async () => {
|
||||
await expect(
|
||||
_restoreCategory({
|
||||
id: 3,
|
||||
name: ' ',
|
||||
sort: 3,
|
||||
novelIds: [],
|
||||
}),
|
||||
).rejects.toThrow('Category name cannot be empty');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCategory', () => {
|
||||
it('should create a new category', async () => {
|
||||
const result = await createCategory('New Category');
|
||||
@@ -71,6 +155,18 @@ describe('CategoryQueries', () => {
|
||||
|
||||
expect(cat2.sort).toBeGreaterThan(cat1.sort!);
|
||||
});
|
||||
|
||||
it('trims category names', async () => {
|
||||
const category = await createCategory(' New Category ');
|
||||
|
||||
expect(category.name).toBe('New Category');
|
||||
});
|
||||
|
||||
it('rejects an empty category name', async () => {
|
||||
await expect(createCategory(' ')).rejects.toThrow(
|
||||
'Category name cannot be empty',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCategoryNameDuplicate', () => {
|
||||
@@ -103,6 +199,38 @@ describe('CategoryQueries', () => {
|
||||
const updated = categories.find(c => c.id === categoryId);
|
||||
expect(updated?.name).toBe('New Name');
|
||||
});
|
||||
|
||||
it('trims the updated category name', async () => {
|
||||
const categoryId = await insertTestCategory(getTestDb(), {
|
||||
name: 'Old Name',
|
||||
});
|
||||
|
||||
await updateCategory(categoryId, ' New Name ');
|
||||
|
||||
const categories = await getCategoriesFromDb();
|
||||
expect(
|
||||
categories.find(category => category.id === categoryId)?.name,
|
||||
).toBe('New Name');
|
||||
});
|
||||
|
||||
it('rejects an empty updated category name', async () => {
|
||||
const categoryId = await insertTestCategory(getTestDb(), {
|
||||
name: 'Old Name',
|
||||
});
|
||||
|
||||
await expect(updateCategory(categoryId, ' ')).rejects.toThrow(
|
||||
'Category name cannot be empty',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not rename the default category', async () => {
|
||||
await updateCategory(1, 'Renamed Default');
|
||||
|
||||
const categories = await getCategoriesFromDb();
|
||||
expect(categories.find(category => category.id === 1)?.name).toBe(
|
||||
'Default',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategoriesWithCount', () => {
|
||||
|
||||
@@ -1008,6 +1008,114 @@ describe('ChapterQueries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('chapter neighbors', () => {
|
||||
it('limits rows transferred by both neighbor lookups to one', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
for (let position = 0; position < 5; position++) {
|
||||
await insertTestChapter(testDb, novelId, { position });
|
||||
}
|
||||
const client = testDb.sqlite as typeof testDb.sqlite & {
|
||||
executeRawAsync: typeof testDb.sqlite.executeRaw;
|
||||
};
|
||||
const executeRaw = jest.spyOn(client, 'executeRawAsync');
|
||||
try {
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.position).toBe(1);
|
||||
expect((await getPrevChapter(novelId, 4, '1'))?.position).toBe(3);
|
||||
expect(executeRaw).toHaveBeenCalledTimes(2);
|
||||
for (const call of executeRaw.mock.calls) {
|
||||
expect(call[0]).toMatch(/limit \?/i);
|
||||
expect(call[1]?.at(-1)).toBe(1);
|
||||
}
|
||||
for (const result of executeRaw.mock.results) {
|
||||
expect(await result.value).toHaveLength(1);
|
||||
}
|
||||
} finally {
|
||||
executeRaw.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns no neighbor before the first or after the last chapter', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
await insertTestChapter(testDb, novelId, { page: '1', position: 0 });
|
||||
await insertTestChapter(testDb, novelId, { page: '2', position: 1 });
|
||||
|
||||
expect(await getPrevChapter(novelId, 0, '1')).toBeUndefined();
|
||||
expect(await getNextChapter(novelId, 1, '2')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('breaks duplicate position ties by id in the navigation direction', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const firstId = await insertTestChapter(testDb, novelId, { position: 1 });
|
||||
const secondId = await insertTestChapter(testDb, novelId, {
|
||||
position: 1,
|
||||
});
|
||||
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(firstId);
|
||||
expect((await getPrevChapter(novelId, 2, '1'))?.id).toBe(secondId);
|
||||
expect(await getNextChapter(novelId, 1, '1')).toBeUndefined();
|
||||
expect(await getPrevChapter(novelId, 1, '1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves numeric-looking page comparisons and integer page ordering', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const page1 = await insertTestChapter(testDb, novelId, { page: '1' });
|
||||
const page02 = await insertTestChapter(testDb, novelId, { page: '02' });
|
||||
const page2 = await insertTestChapter(testDb, novelId, { page: '2' });
|
||||
const page10 = await insertTestChapter(testDb, novelId, { page: '10' });
|
||||
|
||||
// The cast parameter gives comparisons numeric affinity: '02' and '2'
|
||||
// compare equally, and page 10 sorts after page 2.
|
||||
expect((await getPrevChapter(novelId, 0, '2'))?.id).toBe(page1);
|
||||
expect((await getPrevChapter(novelId, 0, '10'))?.id).toBe(page2);
|
||||
expect((await getNextChapter(novelId, 0, '02'))?.id).toBe(page10);
|
||||
expect((await getPrevChapter(novelId, 0, '02'))?.id).toBe(page1);
|
||||
expect((await getNextChapter(novelId, 0, '1'))?.id).toBe(page02);
|
||||
});
|
||||
|
||||
it('preserves integer-cast behavior for source-defined page labels', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
await insertTestChapter(testDb, novelId, {
|
||||
page: 'Volume A',
|
||||
position: 2,
|
||||
});
|
||||
const chapterId = await insertTestChapter(testDb, novelId, {
|
||||
page: 'Volume B',
|
||||
position: 0,
|
||||
});
|
||||
|
||||
// Labels cast to zero, so existing eligibility treats both as later
|
||||
// pages and sorting resolves their equal page casts by position.
|
||||
expect(await getPrevChapter(novelId, 1, 'Volume A')).toBeUndefined();
|
||||
expect((await getNextChapter(novelId, 1, 'Volume A'))?.id).toBe(
|
||||
chapterId,
|
||||
);
|
||||
});
|
||||
|
||||
it('skips excluded chapters at the previous page boundary', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb);
|
||||
const chapterId = await insertTestChapter(testDb, novelId, {
|
||||
page: '1',
|
||||
position: 0,
|
||||
scanlator: null,
|
||||
});
|
||||
await insertTestChapter(testDb, novelId, {
|
||||
page: '1',
|
||||
position: 1,
|
||||
scanlator: 'Excluded',
|
||||
});
|
||||
|
||||
expect((await getPrevChapter(novelId, 0, '2', ['Excluded']))?.id).toBe(
|
||||
chapterId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrevChapter', () => {
|
||||
it('should return previous chapter in same page', async () => {
|
||||
const testDb = getTestDb();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './testData';
|
||||
import {
|
||||
categorySchema,
|
||||
chapterSchema,
|
||||
novelCategorySchema,
|
||||
novelSchema,
|
||||
} from '@database/schema';
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
pickCustomNovelCover,
|
||||
updateNovelCategoryById,
|
||||
updateNovelCategories,
|
||||
_restoreNovelAndChapters,
|
||||
} from '../NovelQueries';
|
||||
|
||||
const mockGetLibraryDefaultCategoryId = jest.fn<number | undefined, []>();
|
||||
@@ -252,6 +254,69 @@ describe('NovelQueries', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should assign the categories selected while adding', async () => {
|
||||
const testDb = getTestDb();
|
||||
const firstCategoryId = await insertTestCategory(testDb, {
|
||||
name: 'First selected category',
|
||||
});
|
||||
const secondCategoryId = await insertTestCategory(testDb, {
|
||||
name: 'Second selected category',
|
||||
});
|
||||
const novelId = await insertTestNovel(testDb, {
|
||||
inLibrary: false,
|
||||
path: '/test/selected-categories',
|
||||
pluginId: 'test-plugin',
|
||||
});
|
||||
|
||||
await switchNovelToLibraryQuery(
|
||||
'/test/selected-categories',
|
||||
'test-plugin',
|
||||
[firstCategoryId, secondCategoryId],
|
||||
);
|
||||
|
||||
const associations = await testDb.drizzleDb
|
||||
.select()
|
||||
.from(novelCategorySchema)
|
||||
.where(eq(novelCategorySchema.novelId, novelId))
|
||||
.all();
|
||||
|
||||
expect(associations.map(association => association.categoryId)).toEqual([
|
||||
firstCategoryId,
|
||||
secondCategoryId,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the built-in default when no categories are selected', async () => {
|
||||
const testDb = getTestDb();
|
||||
const previousDefaultCategoryId = await insertTestCategory(testDb, {
|
||||
name: 'Previous default category',
|
||||
});
|
||||
const novelId = await insertTestNovel(testDb, {
|
||||
inLibrary: false,
|
||||
path: '/test/no-selected-categories',
|
||||
pluginId: 'test-plugin',
|
||||
});
|
||||
mockGetLibraryDefaultCategoryId.mockReturnValue(
|
||||
previousDefaultCategoryId,
|
||||
);
|
||||
|
||||
await switchNovelToLibraryQuery(
|
||||
'/test/no-selected-categories',
|
||||
'test-plugin',
|
||||
[],
|
||||
);
|
||||
|
||||
const associations = await testDb.drizzleDb
|
||||
.select()
|
||||
.from(novelCategorySchema)
|
||||
.where(eq(novelCategorySchema.novelId, novelId))
|
||||
.all();
|
||||
|
||||
expect(associations.map(association => association.categoryId)).toEqual([
|
||||
BUILT_IN_CATEGORY_IDS.default,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fall back when the selected category no longer exists', async () => {
|
||||
const testDb = getTestDb();
|
||||
const novelId = await insertTestNovel(testDb, {
|
||||
@@ -395,6 +460,73 @@ describe('NovelQueries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_restoreNovelAndChapters', () => {
|
||||
it('does not replace an unrelated novel when backup IDs collide', async () => {
|
||||
const testDb = getTestDb();
|
||||
await insertTestNovel(testDb, {
|
||||
path: '/existing/novel',
|
||||
pluginId: 'existing-plugin',
|
||||
name: 'Existing Novel',
|
||||
inLibrary: true,
|
||||
});
|
||||
|
||||
const mapping = await _restoreNovelAndChapters({
|
||||
id: 1,
|
||||
path: '/restored/novel',
|
||||
pluginId: 'restored-plugin',
|
||||
name: 'Restored Novel',
|
||||
cover: null,
|
||||
summary: null,
|
||||
author: null,
|
||||
artist: null,
|
||||
status: 'Ongoing',
|
||||
genres: null,
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
totalPages: 0,
|
||||
chapters: [
|
||||
{
|
||||
id: 10,
|
||||
novelId: 1,
|
||||
path: '/restored/chapter-1',
|
||||
name: 'Chapter 1',
|
||||
releaseTime: null,
|
||||
readTime: null,
|
||||
bookmark: false,
|
||||
unread: true,
|
||||
isDownloaded: true,
|
||||
updatedTime: null,
|
||||
chapterNumber: 1,
|
||||
page: '1',
|
||||
progress: null,
|
||||
position: 0,
|
||||
scanlator: null,
|
||||
timeSpent: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(mapping.restoredNovelId).not.toBe(1);
|
||||
expect(
|
||||
(await getNovelByPath('/existing/novel', 'existing-plugin'))?.name,
|
||||
).toBe('Existing Novel');
|
||||
expect(
|
||||
(await getNovelByPath('/restored/novel', 'restored-plugin'))?.id,
|
||||
).toBe(mapping.restoredNovelId);
|
||||
const restoredChapters = await testDb.drizzleDb
|
||||
.select()
|
||||
.from(chapterSchema)
|
||||
.where(eq(chapterSchema.novelId, mapping.restoredNovelId))
|
||||
.all();
|
||||
expect(mapping.chapters).toEqual([
|
||||
{
|
||||
backupChapterId: 10,
|
||||
restoredChapterId: restoredChapters[0].id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateNovelInfo', () => {
|
||||
it('should update novel information', async () => {
|
||||
const testDb = getTestDb();
|
||||
|
||||
@@ -8,3 +8,9 @@ INSERT OR IGNORE INTO Category (id, name, sort) VALUES
|
||||
(1, '${escapeSqlString(getString('categories.default'))}', 1),
|
||||
(2, '${escapeSqlString(getString('categories.local'))}', 2)
|
||||
`;
|
||||
|
||||
export const repairDefaultCategoryNameQuery = `
|
||||
UPDATE Category
|
||||
SET name = '${escapeSqlString(getString('categories.default'))}'
|
||||
WHERE id = 1 AND TRIM(name) = ''
|
||||
`;
|
||||
|
||||
@@ -121,6 +121,16 @@ export interface BackupNovel extends NovelInfo {
|
||||
chapters: ChapterInfo[];
|
||||
}
|
||||
|
||||
export interface RestoredNovelMapping {
|
||||
pluginId: string;
|
||||
backupNovelId: number;
|
||||
restoredNovelId: number;
|
||||
chapters: {
|
||||
backupChapterId: number;
|
||||
restoredChapterId: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface BackupCategory extends Category {
|
||||
novelIds: number[];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const mockNovel: NovelInfo = {
|
||||
|
||||
const createNovelStore = (
|
||||
overrides: Partial<NovelInfo> = {},
|
||||
switchNovelToLibrary = jest.fn().mockResolvedValue(undefined),
|
||||
switchNovelToLibrary = jest.fn().mockResolvedValue(true),
|
||||
) =>
|
||||
createStore({
|
||||
pluginId: 'plugin-id',
|
||||
@@ -123,7 +123,7 @@ describe('useNovel store', () => {
|
||||
});
|
||||
|
||||
it('toggles follow state after followNovel action', async () => {
|
||||
const switchNovelToLibrary = jest.fn().mockResolvedValue(undefined);
|
||||
const switchNovelToLibrary = jest.fn().mockResolvedValue(true);
|
||||
const store = createNovelStore({ inLibrary: false }, switchNovelToLibrary);
|
||||
|
||||
await store.getState().actions.followNovel();
|
||||
|
||||
@@ -44,6 +44,7 @@ export const defaultLibrarySettings = {
|
||||
displayMode: 'comfortable',
|
||||
showDownloadBadges: true,
|
||||
showUnreadBadges: true,
|
||||
showContinueReadingButton: false,
|
||||
novelsPerRow: 3,
|
||||
sortOrder: 'DateAdded_DESC',
|
||||
};
|
||||
@@ -65,6 +66,8 @@ export const initialChapterGeneralSettings = {
|
||||
bionicReading: false,
|
||||
tapToScroll: false,
|
||||
TTSEnable: true,
|
||||
pageReaderInvertVolumeButtons: false,
|
||||
pageReaderDisableAnimation: false,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings = {
|
||||
|
||||
@@ -38,9 +38,14 @@ export default function useDownload() {
|
||||
const downloadingNovelIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
downloadQueue.flatMap(item =>
|
||||
item.task.data.novelId === undefined ? [] : [item.task.data.novelId],
|
||||
),
|
||||
downloadQueue.flatMap(item => [
|
||||
...(item.task.data.novelId === undefined
|
||||
? []
|
||||
: [item.task.data.novelId]),
|
||||
...item.task.data.chapters.flatMap(chapter =>
|
||||
chapter.novelId === undefined ? [] : [chapter.novelId],
|
||||
),
|
||||
]),
|
||||
),
|
||||
[downloadQueue],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { NovelInfo } from '@database/types';
|
||||
import { getString } from '@i18n/translations';
|
||||
import { createNovelStoreActions } from '../store/novelStore.actions';
|
||||
import { createNovelSlice } from '../store/novelStore';
|
||||
|
||||
interface BootstrapServiceSlice {
|
||||
bootstrapNovelAsync: jest.Mock;
|
||||
bootstrapNovelSync: jest.Mock;
|
||||
getNextChapterBatch: jest.Mock;
|
||||
loadUpToBatch: jest.Mock;
|
||||
}
|
||||
|
||||
const mockNovel: NovelInfo = {
|
||||
id: 1,
|
||||
path: '/novels/x',
|
||||
name: 'X',
|
||||
pluginId: 'plugin.test',
|
||||
};
|
||||
|
||||
interface TestState {
|
||||
novel: NovelInfo | undefined;
|
||||
novelPath: string;
|
||||
pluginId: string;
|
||||
pageIndex: number;
|
||||
pages: string[];
|
||||
novelSettings: { filter: never[]; excludedScanlators: never[] };
|
||||
loading: boolean;
|
||||
fetching: boolean;
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
const createHarness = (overrides: Partial<TestState> = {}) => {
|
||||
let state: TestState = {
|
||||
novel: undefined,
|
||||
novelPath: '/x',
|
||||
pluginId: 'plugin.test',
|
||||
pageIndex: 0,
|
||||
pages: [],
|
||||
novelSettings: { filter: [], excludedScanlators: [] },
|
||||
loading: false,
|
||||
fetching: false,
|
||||
error: undefined,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const set = jest.fn(
|
||||
(partial: Partial<TestState> | ((s: TestState) => Partial<TestState>)) => {
|
||||
const patch = typeof partial === 'function' ? partial(state) : partial;
|
||||
state = { ...state, ...patch };
|
||||
},
|
||||
);
|
||||
const get = () => state;
|
||||
const bootstrapService: jest.Mocked<BootstrapServiceSlice> = {
|
||||
bootstrapNovelAsync: jest.fn(),
|
||||
bootstrapNovelSync: jest.fn(),
|
||||
getNextChapterBatch: jest.fn(),
|
||||
loadUpToBatch: jest.fn(),
|
||||
};
|
||||
let requestVersion = 0;
|
||||
const chapterRequestCoordinator = {
|
||||
current: jest.fn(() => requestVersion),
|
||||
invalidate: jest.fn(() => ++requestVersion),
|
||||
};
|
||||
const transformChapters = jest.fn(chs => chs);
|
||||
|
||||
const actions = createNovelStoreActions({
|
||||
//@ts-expect-error partial state/actions for testing
|
||||
set, //@ts-expect-error
|
||||
get,
|
||||
deps: {
|
||||
bootstrapService,
|
||||
chapterActionsDependencies: {} as never,
|
||||
chapterRequestCoordinator,
|
||||
transformChapters,
|
||||
} as never,
|
||||
defaultChapterSort: 'positionAsc',
|
||||
});
|
||||
|
||||
return {
|
||||
actions,
|
||||
getState: () => state,
|
||||
bootstrapService,
|
||||
chapterRequestCoordinator,
|
||||
};
|
||||
};
|
||||
|
||||
describe('novelStore.bootstrap', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('starts in a loading state until the initial bootstrap completes', () => {
|
||||
const state = createNovelSlice({
|
||||
pluginId: 'plugin.test',
|
||||
novelPath: '/x',
|
||||
initialNovelSettings: {
|
||||
sort: 'positionAsc',
|
||||
filter: [],
|
||||
showChapterTitles: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.loading).toBe(true);
|
||||
expect(state.fetching).toBe(true);
|
||||
});
|
||||
|
||||
it('maps a missing novel to the not-found error', async () => {
|
||||
const harness = createHarness();
|
||||
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
|
||||
ok: false,
|
||||
reason: 'missing-novel',
|
||||
});
|
||||
|
||||
const ok = await harness.actions.bootstrapNovel();
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(harness.getState().error).toBe(getString('novelScreen.notFound'));
|
||||
expect(harness.getState().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('maps a bootstrap error to the load-failed message, never the raw error', async () => {
|
||||
const harness = createHarness();
|
||||
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
|
||||
ok: false,
|
||||
reason: 'error',
|
||||
error: new Error('boom'),
|
||||
});
|
||||
|
||||
const ok = await harness.actions.bootstrapNovel();
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(harness.getState().error).toBe(getString('novelScreen.loadFailed'));
|
||||
expect(harness.getState().error).not.toBe('boom');
|
||||
});
|
||||
|
||||
it('maps a missing-chapters failure to the unable-to-get-novel message', async () => {
|
||||
const harness = createHarness();
|
||||
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
|
||||
ok: false,
|
||||
reason: 'missing-chapters',
|
||||
});
|
||||
|
||||
const ok = await harness.actions.bootstrapNovel();
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(harness.getState().error).toBe(
|
||||
getString('updatesScreen.unableToGetNovel'),
|
||||
);
|
||||
expect(harness.getState().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('clears the error on a successful bootstrap', async () => {
|
||||
const harness = createHarness({ error: 'previous error' });
|
||||
harness.bootstrapService.bootstrapNovelAsync.mockResolvedValue({
|
||||
ok: true,
|
||||
novel: mockNovel,
|
||||
pages: ['1'],
|
||||
pageIndex: 0,
|
||||
scanlators: [],
|
||||
chapters: [],
|
||||
batchInformation: { batch: 0, total: 0, totalChapters: 0 },
|
||||
firstUnreadChapter: undefined,
|
||||
});
|
||||
|
||||
const ok = await harness.actions.bootstrapNovel();
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(harness.getState().error).toBeUndefined();
|
||||
expect(harness.getState().novel).toBe(mockNovel);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,10 @@ interface Props {
|
||||
path: string;
|
||||
novel?: NovelInfo;
|
||||
defaultChapterSort: ChapterOrderKey;
|
||||
switchNovelToLibrary: (novelPath: string, pluginId: string) => Promise<void>;
|
||||
switchNovelToLibrary: (
|
||||
novelPath: string,
|
||||
pluginId: string,
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function createStore({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ChapterFilterKey, ChapterOrderKey } from '@database/constants';
|
||||
import { getString } from '@i18n/translations';
|
||||
import { NovelSettings } from '../types';
|
||||
import { BootstrapFailureResult } from '../store-helper/bootstrapService';
|
||||
import {
|
||||
GetState,
|
||||
NovelStoreDependencies,
|
||||
@@ -14,6 +16,17 @@ interface CreateNovelStoreActionsParams {
|
||||
defaultChapterSort: ChapterOrderKey;
|
||||
}
|
||||
|
||||
const resolveBootstrapError = (result: BootstrapFailureResult): string => {
|
||||
if (result.reason === 'missing-chapters') {
|
||||
return getString('updatesScreen.unableToGetNovel');
|
||||
}
|
||||
if (result.reason === 'error') {
|
||||
// A real fetch/parse failure — don't claim the novel is missing.
|
||||
return getString('novelScreen.loadFailed');
|
||||
}
|
||||
return getString('novelScreen.notFound');
|
||||
};
|
||||
|
||||
export const createNovelStoreActions = ({
|
||||
set,
|
||||
get,
|
||||
@@ -42,7 +55,7 @@ export const createNovelStoreActions = ({
|
||||
|
||||
const requestId = deps.chapterRequestCoordinator.invalidate();
|
||||
inflightBootstrap = (async () => {
|
||||
set({ loading: true, fetching: true });
|
||||
set({ loading: true, fetching: true, error: undefined });
|
||||
|
||||
const state = get();
|
||||
const result = await deps.bootstrapService.bootstrapNovelAsync({
|
||||
@@ -63,6 +76,7 @@ export const createNovelStoreActions = ({
|
||||
set({
|
||||
loading: false,
|
||||
fetching: false,
|
||||
error: resolveBootstrapError(result),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -248,7 +262,11 @@ export const createNovelStoreActions = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await deps.switchNovelToLibrary(state.novelPath, state.pluginId);
|
||||
const completed = await deps.switchNovelToLibrary(
|
||||
state.novelPath,
|
||||
state.pluginId,
|
||||
);
|
||||
if (!completed) return;
|
||||
set(inner => {
|
||||
if (!inner.novel) {
|
||||
return {};
|
||||
|
||||
@@ -21,8 +21,9 @@ export const createNovelSlice = ({
|
||||
initialLastRead,
|
||||
}: CreateNovelStoreParams) => {
|
||||
return {
|
||||
loading: false,
|
||||
fetching: false,
|
||||
loading: true,
|
||||
fetching: true,
|
||||
error: undefined,
|
||||
pluginId,
|
||||
novelPath,
|
||||
novel,
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ChapterSliceState {
|
||||
export interface NovelStoreData extends ChapterSliceState {
|
||||
loading: boolean;
|
||||
fetching: boolean;
|
||||
error: string | undefined;
|
||||
|
||||
pluginId: string;
|
||||
novelPath: string;
|
||||
@@ -95,7 +96,10 @@ export interface NovelStoreDependencies {
|
||||
persistPageIndex?: (value: number) => void;
|
||||
persistNovelSettings?: (value: NovelSettings) => void;
|
||||
persistLastRead?: (value: ChapterInfo) => void;
|
||||
switchNovelToLibrary?: (novelPath: string, pluginId: string) => Promise<void>;
|
||||
switchNovelToLibrary?: (
|
||||
novelPath: string,
|
||||
pluginId: string,
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type SetState = {
|
||||
|
||||
@@ -114,6 +114,8 @@ export interface BrowseSettings {
|
||||
export interface LibrarySettings {
|
||||
/** User-selected category for newly added novels. */
|
||||
defaultCategoryId?: number;
|
||||
/** Prompt for categories whenever a novel is added to the library. */
|
||||
promptForCategoryOnAdd?: boolean;
|
||||
/** Last category viewed in the library. */
|
||||
lastUsedCategoryId?: number;
|
||||
globalUpdateExcludeCategoryIds?: number[];
|
||||
@@ -122,6 +124,7 @@ export interface LibrarySettings {
|
||||
filter?: LibraryFilter;
|
||||
showDownloadBadges?: boolean;
|
||||
showUnreadBadges?: boolean;
|
||||
showContinueReadingButton?: boolean;
|
||||
showNumberOfNovels?: boolean;
|
||||
displayMode?: DisplayModes;
|
||||
novelsPerRow?: number;
|
||||
@@ -191,6 +194,8 @@ export interface ChapterGeneralSettings {
|
||||
bionicReading: boolean;
|
||||
tapToScroll: boolean;
|
||||
TTSEnable: boolean;
|
||||
pageReaderInvertVolumeButtons: boolean;
|
||||
pageReaderDisableAnimation: boolean;
|
||||
}
|
||||
|
||||
export interface ReaderTheme {
|
||||
@@ -316,6 +321,8 @@ export const initialChapterGeneralSettings: ChapterGeneralSettings = {
|
||||
bionicReading: false,
|
||||
tapToScroll: false,
|
||||
TTSEnable: true,
|
||||
pageReaderInvertVolumeButtons: false,
|
||||
pageReaderDisableAnimation: false,
|
||||
};
|
||||
|
||||
export const initialChapterReaderSettings: ChapterReaderSettings = {
|
||||
@@ -376,12 +383,14 @@ export const useBrowseSettings = () => {
|
||||
};
|
||||
|
||||
const defaultLibrarySettings: LibrarySettings = {
|
||||
promptForCategoryOnAdd: false,
|
||||
showNumberOfNovels: false,
|
||||
downloadedOnlyMode: false,
|
||||
incognitoMode: false,
|
||||
displayMode: DisplayModes.Comfortable,
|
||||
showDownloadBadges: true,
|
||||
showUnreadBadges: true,
|
||||
showContinueReadingButton: false,
|
||||
novelsPerRow: 3,
|
||||
sortOrder: LibrarySortOrder.DateAdded_DESC,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"aboutScreen": {
|
||||
"website": "الموقع",
|
||||
"discord": "ديس كورد",
|
||||
"github": "كيت هاب",
|
||||
"github": "Arabic",
|
||||
"helpTranslate": "المساعدة في الترجَمة",
|
||||
"plugins": "Plugins",
|
||||
"version": "آلإصدار",
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "إدارة البيانات",
|
||||
"deleteReadChapters": "حذف الفصول المقروءة",
|
||||
"deleteReadChaptersDialogTitle": "حذف جميع الفصول التي تم قراءتها",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"recreateDBIndexesToast": "تمت إعادة إنشاء فهارس قاعدة البيانات",
|
||||
"recreateDBIndexesDialogTitle": "سيتم إعادة إنشاء جميع فهارس قاعدة البيانات.\nقد يستغرق هذا بعض الوقت.",
|
||||
"recreateDBIndexesDesc": "يعيد إنشاء فهارس قاعدة البيانات. قد يؤدي هذا إلى تحسين الأداء على الأجهزة الأبطأ.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "مظهر",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "تحضير البيانات",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "لا يمكنك حذف التصنيف الافتراضي",
|
||||
"default": "افتراضي",
|
||||
"defaultCategory": "التصنيف الافتراضي",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "هل ترغب في حذف التصنيف",
|
||||
"header": "حذف التصنيف"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "ليس لديك تصنيفات. اضغط على زر تعديل لإنشاء واحد لتنظيم مكتبتك"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "ليس لديك مستودعات. أضف مستودع الإضافات الأول لتبدأ."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "ليس لديك مستودعات. أضف مستودع الإضافات الأول لتبدأ.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "حول",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "إرسال",
|
||||
"loading": "جارٍ التحميل...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "تحذير"
|
||||
"warning": "تحذير",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "تحديث",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "لم يتم العثور على الإضافة!",
|
||||
"removeDownloadsWarning": "هل أنت متأكد؟ سيتم حذف جميع الفصول المحملة."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "العامة",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "جميع المصادر",
|
||||
"searchIn": "البحث عن رواية في"
|
||||
"searchIn": "البحث عن رواية في",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "السجل",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "نقل",
|
||||
"noSummary": "لا يوجد ملخص",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "التمرير إلى الفصل الحالي",
|
||||
"scrollToTop": "التمرير إلى الأعلى"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>تعذر تحميل الفصل</h2><p>لم يتم العثور على محتوى قابل للقراءة. تحقق من الفصل في WebView؛ إذا تم تحميله هناك، <a href='%{reportUrl}'>فأبلغ عن مشكلة الإضافة</a>.</p><p>الإضافة: %{pluginId}<br>الرواية: %{novelName}<br>الفصل: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "انتهى",
|
||||
"nextChapter": "التالي: %{name}",
|
||||
"noNextChapter": "لا يوجد فصل تالي",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "ডিফল্ট শ্ৰেণী",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "আপুনি শ্ৰেণী মচিব বিচাৰে নেকি?",
|
||||
"header": "শ্ৰেণী মচক"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "আপোনাৰ কোনো শ্ৰেণী নাই। সম্পাদনা বুটামত টেপ কৰি এটি সৃষ্টি কৰক"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "জমা দিয়ক",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "সকলো উৎস",
|
||||
"searchIn": "উৎসত উপন্যাস অনুসন্ধান কৰক"
|
||||
"searchIn": "উৎসত উপন্যাস অনুসন্ধান কৰক",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "ইতিহাস",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "স্থানান্তৰিত কৰক",
|
||||
"noSummary": "কোনো সংক্ষিপ্তসাৰ নাই",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "বৰ্তমান অধ্যায়লৈ স্ক্ৰল কৰক",
|
||||
"scrollToTop": "শীৰ্ষলৈ স্ক্ৰল কৰক"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "শেষ",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "পৰবর্তী অধ্যায় নাই",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Výchozí kategorie",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Přejete si odstranit kategorii",
|
||||
"header": "Smazat kategorii"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Nemáte žádné kategorie. Klepněte na tlačítko editovat pro vytvoření nové kategorie"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Odeslat",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "všechny zdroje",
|
||||
"searchIn": "Hledat novelu v"
|
||||
"searchIn": "Hledat novelu v",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Historie",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrovat",
|
||||
"noSummary": "Žádné shrnutí",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Přejít na aktuální kapitolu",
|
||||
"scrollToTop": "Přejít na začátek"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Dokončeno",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Nejsou žádné další kapitoly",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Gelesene Kapitel löschen",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Standard-Kategorie",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Möchten Sie diese Kategorie löschen?",
|
||||
"header": "Kategorie löschen"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Sie haben keine Kategorien. Tippen Sie auf die Plus-Schaltfläche, um eine für die Organisation Ihrer Bibliothek zu erstellen"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Bestätigen",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Allgemein",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "Alle Quellen",
|
||||
"searchIn": "Suche ein Buch in"
|
||||
"searchIn": "Suche ein Buch in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Verlauf",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrieren",
|
||||
"noSummary": "Keine Beschreibung",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Zum aktuellen Kapitel springen",
|
||||
"scrollToTop": "Zum Anfang springen"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Beendet",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Es gibt kein weiteres Kapitel",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -51,7 +55,7 @@
|
||||
"appLanguageDefault": "Default",
|
||||
"appTheme": "App theme",
|
||||
"darkTheme": "Dark Theme",
|
||||
"dateFormat": "Date format",
|
||||
"dateFormat": "Μορφοποίησή ημερομηνίας",
|
||||
"dateFormatDefault": "Default (%{date})",
|
||||
"dynamicColors": "Dynamic",
|
||||
"hideBackdrop": "Hide backdrop",
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Υποβολή",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Ιστορικό",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Κύλιση στο τρέχον κεφάλαιο",
|
||||
"scrollToTop": "Κύλιση προς τα πάνω"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Δεν υπάρχει επόμενο κεφάλαιο",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -383,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -429,6 +443,7 @@
|
||||
"jumpToLastReadChapter": "Jump to last read chapter in list",
|
||||
"novel": "Novel",
|
||||
"novelBadges": "Novel Badges",
|
||||
"showContinueReadingButton": "Show continue reading button",
|
||||
"novelSort": "Novel Sort",
|
||||
"refreshMetadata": "Automatically refresh metadata",
|
||||
"refreshMetadataDescription": "Check for new cover and details when updating library",
|
||||
@@ -440,7 +455,8 @@
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low-end devices",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library",
|
||||
"useFAB": "Show library resume button",
|
||||
"useFABDescription": "Show a floating button that opens the most recently read chapter.",
|
||||
"timeTracking": "Time Tracking",
|
||||
"enableTimeTracking": "Enable time tracking",
|
||||
"enableTimeTrackingDesc": "Tracks time spent reading and displays it in the statistics screen",
|
||||
@@ -450,7 +466,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -469,12 +488,14 @@
|
||||
"badges": "Badges",
|
||||
"comfortable": "Comfortable grid",
|
||||
"compact": "Compact grid",
|
||||
"continueReadingButton": "Continue reading button",
|
||||
"displayMode": "Display mode",
|
||||
"download": "Download",
|
||||
"downloadBadges": "Download badges",
|
||||
"list": "List",
|
||||
"noTitle": "Cover only grid",
|
||||
"numberOfItems": "Number of Items",
|
||||
"overlays": "Overlays",
|
||||
"showNoOfItems": "Show number of items",
|
||||
"unread": "Unread",
|
||||
"unreadBadges": "Unread badges"
|
||||
@@ -600,9 +621,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -645,6 +668,10 @@
|
||||
"padding": "Padding",
|
||||
"pageReader": "Paged reading (Experimental)",
|
||||
"pageReaderDescription": "Split the chapter into pages instead of scrolling. Experimental — may break formatting in some novels",
|
||||
"invertVolumeButtons": "Invert volume buttons",
|
||||
"invertVolumeButtonsDescription": "Swap volume up and down buttons for previous and next page",
|
||||
"disablePageTransitions": "Disable page transition animation",
|
||||
"disablePageTransitionsDescription": "Turn pages instantly without slide animation",
|
||||
"removeExtraSpacing": "Remove extra spacing",
|
||||
"removeExtraSpacingDescription": "Collapse extra blank lines between paragraphs",
|
||||
"scrollAmount": "Scroll amount (screen height by default)",
|
||||
@@ -713,9 +740,14 @@
|
||||
"genreDistribution": "Genre distribution",
|
||||
"novels": "novels",
|
||||
"readChapters": "Read chapters",
|
||||
"readLabel": "Read",
|
||||
"downloadedLabel": "Downloaded",
|
||||
"sources": "Sources",
|
||||
"statusDistribution": "Status distribution",
|
||||
"title": "Statistics",
|
||||
"readingTime": "Reading time",
|
||||
"chaptersInLibrary": "chapters in library",
|
||||
"customizeGenres": "Customize",
|
||||
"titlesInLibrary": "Titles in library",
|
||||
"totalChapters": "Total chapters",
|
||||
"unreadChapters": "Unread chapters",
|
||||
@@ -789,7 +821,20 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"customCodeSettings": {
|
||||
"description": "Rules and snippets run on every chapter you read, in the order shown.",
|
||||
"textRules": "Text Rules",
|
||||
"ruleCount": {
|
||||
"one": "%{count} rule",
|
||||
"other": "%{count} rules"
|
||||
},
|
||||
"activeSnippetCount": {
|
||||
"one": "%{count} of %{total} active",
|
||||
"other": "%{count} of %{total} active"
|
||||
},
|
||||
"addReplaceRule": "Add replacement",
|
||||
"addRemoveRule": "Add removal",
|
||||
"renameHint": "Long press to rename",
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
@@ -804,11 +849,16 @@
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"regexHint": "Plain text affects all matches. For regex, use /pattern/flags and add g to affect all matches.",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"beautifyCode": "Beautify code",
|
||||
"beautifyFailed": "Couldn't format the code. Check its syntax and try again.",
|
||||
"syntaxHighlighting": "Syntax highlighting",
|
||||
"importCode": "Import code",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
@@ -828,12 +878,21 @@
|
||||
"one": "%{count} subgenre",
|
||||
"other": "%{count} subgenres"
|
||||
},
|
||||
"novelsIn": "Novels in %{genre}",
|
||||
"novels": "Novels",
|
||||
"noNovels": "No novels found for this category",
|
||||
"seeAllNovels": "See all",
|
||||
"editTaxonomy": "Edit genre taxonomy",
|
||||
"taxonomyTitle": "Genre taxonomy",
|
||||
"parentCategories": "Parent categories",
|
||||
"parentCategories": "Genre groups",
|
||||
"genreGroups": "Genre groups",
|
||||
"taxonomyDescription": "Statistics group each novel's genres under these groups. Genres outside a group are counted individually.",
|
||||
"newGroup": "New group",
|
||||
"editGenreGroup": "Edit genre group",
|
||||
"genresInGroup": "Genres in this group",
|
||||
"noGenresInGroup": "No genres yet",
|
||||
"foundInLibrary": "Found in your library",
|
||||
"ungroupedGenresDescription": "Genres your sources use that aren't in any group yet",
|
||||
"deleteGroupConfirm": "Delete this genre group? Its genres will be counted individually.",
|
||||
"normalization": "Normalization rules",
|
||||
"addCategory": "Add category",
|
||||
"addChild": "Add subgenre",
|
||||
@@ -842,11 +901,11 @@
|
||||
"deleteCategoryConfirm": "Delete this category and its subgenres?",
|
||||
"deleteChildConfirm": "Delete this subgenre?",
|
||||
"deleteNormalizationConfirm": "Delete this normalization rule?",
|
||||
"parentNamePlaceholder": "Category name",
|
||||
"childNamePlaceholder": "Add Subgenre",
|
||||
"parentNamePlaceholder": "Group name",
|
||||
"childNamePlaceholder": "Add genre",
|
||||
"normalizationKeyPlaceholder": "e.g. sci-fi",
|
||||
"normalizationValuePlaceholder": "e.g. Sci-Fi",
|
||||
"noCategories": "No parent categories configured",
|
||||
"noCategories": "No genre groups yet",
|
||||
"noNormalization": "No normalization rules configured"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Administración de datos",
|
||||
"deleteReadChapters": "Eliminar capítulos leídos",
|
||||
"deleteReadChaptersDialogTitle": "Todos los capítulos marcados como leídos serán borrados.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"recreateDBIndexesToast": "Índices de la Base de Datos Recreados",
|
||||
"recreateDBIndexesDialogTitle": "Todos los índices de la base de datos serán Recreados. Esto puede tomar un tiempo.",
|
||||
"recreateDBIndexesDesc": "Recrea los índices de la Base de Datos. Esto puede mejorar el rendimiento en dispositivos más lentos.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Apariencia",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparando datos",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "No se puede eliminar la categoría predeterminada",
|
||||
"default": "Predeterminado",
|
||||
"defaultCategory": "Categoría por defecto",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "¿Deseas eliminar la categoría?",
|
||||
"header": "Eliminar categoría"
|
||||
@@ -287,13 +292,17 @@
|
||||
"setModalEmptyMsg": "No tienes categorías. Pulsa el botón Editar para crear una para organizar tu biblioteca"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "No tienes repositorios. Añade tu primer repositorio de plugins para empezar."
|
||||
"disable": "Deshabilitar",
|
||||
"disableTitle": "¿Desactivar repositorio?",
|
||||
"disableWarning": "LNReader dejará de buscar actualizaciones en %{name}. Sus plugins se ocultarán a menos que otro repositorio activo los proporcione. Los plugins instalados permanecerán instalados.",
|
||||
"emptyMsg": "No tienes repositorios. Añade tu primer repositorio de plugins para empezar.",
|
||||
"toggle": "Alternar repositorio %{name}"
|
||||
},
|
||||
"common": {
|
||||
"about": "Acerca de",
|
||||
"add": "Añadir",
|
||||
"all": "Todos",
|
||||
"back": "Back",
|
||||
"back": "Volver",
|
||||
"backup": "Copia de seguridad",
|
||||
"cancel": "Cancelar",
|
||||
"categories": "Categorías",
|
||||
@@ -311,14 +320,14 @@
|
||||
"filter": "Filtrar",
|
||||
"globally": "globalmente",
|
||||
"install": "Instalar",
|
||||
"imported": "Imported",
|
||||
"later": "Later",
|
||||
"imported": "Importado",
|
||||
"later": "Más tarde",
|
||||
"logout": "Cerrar sesión",
|
||||
"name": "Nombre",
|
||||
"newUpdateAvailable": "Nueva actualización disponible",
|
||||
"none": "None",
|
||||
"none": "Ninguno",
|
||||
"ok": "Ok",
|
||||
"close": "Close",
|
||||
"close": "Cerrar",
|
||||
"pause": "Pausa",
|
||||
"preparing": "Preparando",
|
||||
"remove": "Eliminar",
|
||||
@@ -327,75 +336,89 @@
|
||||
"resume": "Reanudar",
|
||||
"retry": "Reintentar",
|
||||
"save": "Guardar",
|
||||
"saved": "Saved",
|
||||
"saved": "Guardar",
|
||||
"search": "Buscar",
|
||||
"skipVersion": "Skip version",
|
||||
"skipVersion": "Saltar versión",
|
||||
"searchFor": "Buscar por",
|
||||
"searchResults": "Resultados de la búsqueda",
|
||||
"settings": "Configuración",
|
||||
"show": "Mostrar",
|
||||
"signIn": "Sign in",
|
||||
"signOut": "Sign out",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Salir",
|
||||
"sort": "Ordenar",
|
||||
"submit": "Enviar",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"loading": "Cargando ...",
|
||||
"moreOptions": "Más opciones",
|
||||
"warning": "Aviso",
|
||||
"code": "Código",
|
||||
"custom_code": "Cód. Propio",
|
||||
"textToReplace": "Texto a buscar",
|
||||
"replaceWith": "Remplazar con",
|
||||
"replaceText": "Remplazar texto"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
"share": "Share",
|
||||
"openInWebView": "Open in WebView",
|
||||
"openInBrowser": "Open in browser",
|
||||
"clearCookies": "Clear cookies",
|
||||
"cookiesCleared": "Cookies cleared",
|
||||
"clearData": "Clear WebView data",
|
||||
"dataDeleted": "WebView data cleared"
|
||||
"refresh": "Recargar",
|
||||
"share": "Compartir",
|
||||
"openInWebView": "Abrir en vista web",
|
||||
"openInBrowser": "Abrir en el navegador",
|
||||
"clearCookies": "Eliminar cookies",
|
||||
"cookiesCleared": "Cookies borradas",
|
||||
"clearData": "Borrar datos de vista web",
|
||||
"dataDeleted": "Datos de vista web borrados"
|
||||
},
|
||||
"date": {
|
||||
"calendar": {
|
||||
"lastDay": "[Yesterday]",
|
||||
"lastWeek": "[Last] dddd",
|
||||
"nextDay": "[Tomorrow]",
|
||||
"sameDay": "[Today]"
|
||||
"lastDay": "[Ayer]",
|
||||
"lastWeek": "[El] dddd [pasado]",
|
||||
"nextDay": "[Mañana]",
|
||||
"sameDay": "[Hoy]"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"storageInfo": "Downloaded chapters are stored in the app's private storage on this device.",
|
||||
"downloading": "Downloading",
|
||||
"downloadingNovel": "Downloading: %{name}",
|
||||
"cancelDownloads": "Cancelar descargas",
|
||||
"cancelled": "Descarga cancelada.",
|
||||
"chapterEmptyOrScrapeError": "El capítulo está vacío o la app no pudo extraer el contenido",
|
||||
"chapterName": "Capítulo: %{name}",
|
||||
"completed": "Descarta completada",
|
||||
"storageInfo": "Los capítulos descargados se guardan en el almacenamiento privado de la app en este dispositivo.",
|
||||
"downloading": "Descargando",
|
||||
"downloadingNovel": "Descargando:%{name}",
|
||||
"downloadsLower": "descargas",
|
||||
"noDownloads": "No hay descargas",
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
"pluginNotFound": "¡Plugin no funciona!",
|
||||
"removeDownloadsWarning": "¿Estás seguro? Se eliminarán todos los capítulos descargados."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancelar tarea",
|
||||
"cancelTaskTitle": "¿Cancelar tarea?",
|
||||
"cancelTaskConfirmation": "¿Estás seguro de que quieres cancelar %{task}?",
|
||||
"keepTaskAction": "Conservar tarea"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"restart": "Restart the application"
|
||||
"title": "Ocurrió un error inesperado",
|
||||
"description": "La aplicación experimentó un error inesperado. Por favor, comparte los registros de fallo en nuestro canal de soporte de Discord.",
|
||||
"copyStackTrace": "Copiar seguimiento del error",
|
||||
"shareCrashLogs": "Compartir registro de fallos",
|
||||
"shareCrashLogsFailed": "No se pudieron compartir los registros de fallo. En su lugar, se copió el seguimiento del error.",
|
||||
"shareCrashLogsFailedWithoutCopy": "No se pudieron compartir los registros de fallo ni copiar el seguimiento del error.",
|
||||
"restart": "Reiniciar aplicación"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
"generalSettingsScreen": {
|
||||
"asc": "(Ascendente)",
|
||||
"autoDownload": "Descarga automática",
|
||||
"automaticUpdates": "Automatic updates",
|
||||
"automaticUpdatesDaily": "Daily",
|
||||
"automaticUpdatesDescription": "Runs when a network connection is available. Android may delay background work to save battery.",
|
||||
"automaticUpdatesEvery12Hours": "Every 12 hours",
|
||||
"automaticUpdatesEvery2Days": "Every 2 days",
|
||||
"automaticUpdatesEvery3Days": "Every 3 days",
|
||||
"automaticUpdatesOff": "Off",
|
||||
"automaticUpdatesWeekly": "Weekly",
|
||||
"automaticUpdates": "Actualizaciones automáticas",
|
||||
"automaticUpdatesDaily": "Diariamente",
|
||||
"automaticUpdatesDescription": "Ejecuta cuando hay disponible una conexión de red. Android puede retrasar el trabajo en segundo plano para ahorrar batería.",
|
||||
"automaticUpdatesEvery12Hours": "Cada 12 horas",
|
||||
"automaticUpdatesEvery2Days": "Cada 2 días",
|
||||
"automaticUpdatesEvery3Days": "Cada 3 días",
|
||||
"automaticUpdatesOff": "Desactivar",
|
||||
"automaticUpdatesWeekly": "Semanal",
|
||||
"bySource": "Por fuente",
|
||||
"chapterSort": "Orden de capítulo por defecto",
|
||||
"desc": "(Descendente)",
|
||||
"disableLoadingAnimations": "Disable loading animations",
|
||||
"disableLoadingAnimations": "Desactivar animación de carga",
|
||||
"disableLoadingAnimationsDesc": "May improve performance on slower devices",
|
||||
"disableHapticFeedback": "Deshabilitar la respuesta háptica",
|
||||
"disableHapticFeedbackDescription": "Turn off vibrations for touch interactions.",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "Todas las fuentes",
|
||||
"searchIn": "Buscar una novela en"
|
||||
"searchIn": "Buscar una novela en",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Historial",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrar",
|
||||
"noSummary": "Sin resumen",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Desplazar al capítulo actual",
|
||||
"scrollToTop": "Desplazar a arriba"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finalizado",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "No hay próximo capítulo",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"advancedSettings": "Avancé",
|
||||
"advancedSettingsScreen": {
|
||||
"cachedNovelsDeletedToast": "Romans en cache supprimés",
|
||||
"chapterInsertFailed": "Échec de l'insertion du chapitre",
|
||||
"chapterInsertFailed": "Insertion de chapitre échoué",
|
||||
"clearCachedNovels": "Vider les romans en cache",
|
||||
"clearCachedNovelsDesc": "Supprimer les romans en cache qui ne sont pas dans votre bibliothèque",
|
||||
"clearDatabaseWarning": "Les progrès et les chapitres lus et téléchargés hors de la bibliothèque, seront perdus.",
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Gestion de données",
|
||||
"deleteReadChapters": "Supprimer les chapitres lus",
|
||||
"deleteReadChaptersDialogTitle": "Tous les chapitres marqués comme lus seront supprimés.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"recreateDBIndexesToast": "Index de la base de données recréés",
|
||||
"recreateDBIndexesDialogTitle": "Tous les index de la base de données seront recréés.\nCela peut prendre un certain temps.",
|
||||
"recreateDBIndexesDesc": "Recrée les index de la base de données. Cela peut améliorer les performances sur les appareils plus lents.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Apparence",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "Vous ne pouvez pas supprimer la catégorie par défaut",
|
||||
"default": "Défaut",
|
||||
"defaultCategory": "Catégorie par défaut",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Voulez-vous supprimer cette catégorie",
|
||||
"header": "Supprimer la catégorie"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Vous n'avez pas de catégories. Appuyez sur le bouton Modifier pour en créer une pour organiser votre bibliothèque"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "À propos",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Soumettre",
|
||||
"loading": "Chargement ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Actualiser",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin introuvable!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Général",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "toutes les sources",
|
||||
"searchIn": "Rechercher un novel dans"
|
||||
"searchIn": "Rechercher un novel dans",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Historique",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrer",
|
||||
"noSummary": "Aucun résumé",
|
||||
"noCoverFound": "Aucune couverture trouvée",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Commencer à lire %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Défiler vers le chapitre actuel",
|
||||
"scrollToTop": "Défiler jusqu'en haut"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Impossible de charger le chapitre</h2><p>Aucun contenu lisible n'a été trouvé. Vérifiez le chapitre dans la WebView ; s'il s'y charge, <a href='%{reportUrl}'>signalez le problème du plugin</a>.</p><p>Plugin : %{pluginId}<br>Roman : %{novelName}<br>Chapitre : %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Terminé",
|
||||
"nextChapter": "Suivant: %{name}",
|
||||
"noNextChapter": "Il n'y a pas de prochain chapitre",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "ניהול מידע",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "תצוגה",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Olvasott fejezetek törlése",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Alapértelmezett kategória",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Kategória törlése"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Küldés",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Általános",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "összes forrás",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Előzmények",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Áttelepítés",
|
||||
"noSummary": "Nincs összefoglaló",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Aktuális fejezethez ugrás",
|
||||
"scrollToTop": "Elejére ugrás"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Befejezett",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Nincs következő fejezet",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Elimina capitoli letti",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Categoria predefinita",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Vuoi eliminare la categoria",
|
||||
"header": "Elimina categoria"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Non hai categorie. Tocca il pulsante Modifica per crearne una per organizzare la tua libreria"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -308,7 +317,7 @@
|
||||
"downloads": "Downloads",
|
||||
"edit": "Modifica",
|
||||
"example": "Example",
|
||||
"filter": "Filtra",
|
||||
"filter": "Filtra",
|
||||
"globally": "globale",
|
||||
"install": "Installa",
|
||||
"imported": "Imported",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Invia",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Generale",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "Tutte le fonti",
|
||||
"searchIn": "Cerca una novel in"
|
||||
"searchIn": "Cerca una novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Cronologia",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migra",
|
||||
"noSummary": "Nessuna trama",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scorri al capitolo corrente",
|
||||
"scrollToTop": "Scorri in cima"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finito",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Non c'è un capitolo successivo",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "デフォルトカテゴリー",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "カテゴリーを削除しますか?",
|
||||
"header": "カテゴリーを削除"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "カテゴリーがありません。ライブラリを整理したい場合、プラスボタンをタップしてカテゴリーを作成してください。"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "履歴",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "移行",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "次の章はありません",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"aboutScreen": {
|
||||
"website": "Website",
|
||||
"discord": "Discord",
|
||||
"github": "GitHub",
|
||||
"helpTranslate": "Help translate",
|
||||
"plugins": "Plugins",
|
||||
"version": "Version",
|
||||
"whatsNew": "What's new"
|
||||
"website": "웹사이트",
|
||||
"discord": "디스코드",
|
||||
"github": "깃허브",
|
||||
"helpTranslate": "번역에 도움을 주세요.",
|
||||
"plugins": "플러그인",
|
||||
"version": "버전 정보",
|
||||
"whatsNew": "새로운 소식"
|
||||
},
|
||||
"advancedSettings": "Advanced",
|
||||
"advancedSettings": "고급 설정",
|
||||
"advancedSettingsScreen": {
|
||||
"cachedNovelsDeletedToast": "Cached novels deleted",
|
||||
"chapterInsertFailed": "chapter insert failed",
|
||||
@@ -19,10 +19,11 @@
|
||||
"clearUpdatesTab": "Clear updates tab",
|
||||
"clearUpdatesWarning": "Updates tab will be cleared.",
|
||||
"clearupdatesTabDesc": "Clears chapter entries in updates tab",
|
||||
"dataManagement": "Data Management",
|
||||
"dataManagement": "데이터 관리",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"disabled": "Disabled",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "비활성화",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
"importEpub": "Import EPUB",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Verwijder gelezen hoofdstukken",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,7 +242,7 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Standaard categorie",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Wilt u deze categorie verwijderen",
|
||||
"header": "Categorie verwijderen"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Je hebt geen categorieën. Tik op de plus-knop om er een te maken om je bibliotheek te organiseren"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Algemeen",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "alle bronnen",
|
||||
"searchIn": "Zoek een roman in"
|
||||
"searchIn": "Zoek een roman in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Geschiedenis",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migreer",
|
||||
"noSummary": "Geen samenvatting",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Voltooid",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Er is geen volgend hoofdstuk",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Manejo de informação",
|
||||
"deleteReadChapters": "Excluir capítulos lidos",
|
||||
"deleteReadChaptersDialogTitle": "Você tem certeza? Todos os capítulos marcados como lidos serão excluídos.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -31,13 +32,16 @@
|
||||
"importStaticFiles": "Importar arquivo estático",
|
||||
"novelInsertFailed": "falha ao inserir livro",
|
||||
"networking": "Networking",
|
||||
"useFAB": "Utilize FAB em vez do botão",
|
||||
"useFAB": "Utilize FAV invez do botão",
|
||||
"userAgent": "Agente usuário",
|
||||
"recreateDBIndexes": "Recriar índices do BD",
|
||||
"recreateDBIndexesToast": "Índices do BD foram recriados",
|
||||
"recreateDBIndexesDialogTitle": "Todos os índices do BD serão recriados.\nIsso pode demorar um pouco.",
|
||||
"recreateDBIndexesToast": "Índices do BD foi recriado",
|
||||
"recreateDBIndexesDialogTitle": "Todos os índices do BD vai ser Recriado.\nIsso pode demorar um pouco.",
|
||||
"recreateDBIndexesDesc": "Recrie os índices do BD. Isso pode melhorar o desempenho em dispositivos mais lentos.",
|
||||
"restartRequired": "Reinicie o LNReader para aplicar esta alteração."
|
||||
"restartRequired": "Reinicie o LNReader para aplicar esta alteração.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Aparência",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparando os dados",
|
||||
"preparingSelectedFiles": "Preparando arquivos selecionados",
|
||||
@@ -189,7 +193,7 @@
|
||||
},
|
||||
"remoteBackup": "Backup remoto",
|
||||
"restoreBackup": "Restaurar backup",
|
||||
"restoreBackupDesc": "Restaure a biblioteca a partir de um arquivo de backup",
|
||||
"restoreBackupDesc": "Restaure a biblioteca a partir de um arquivo de backup ",
|
||||
"restorinBackup": "Restaurando cópia de segurança",
|
||||
"restoringData": "Restaurando dados",
|
||||
"restoringSelectedFiles": "Restaurando arquivos selecionados",
|
||||
@@ -245,7 +249,7 @@
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removido da biblioteca",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Pesquisar fontes e plugins",
|
||||
"searchbar": "Pesquisar nas fontes",
|
||||
"searchResults": "Resultados da pesquisa",
|
||||
"selectNovel": "Selecionar novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "Categoria padrão não pode ser deletada",
|
||||
"default": "Padrão",
|
||||
"defaultCategory": "Categoria padrão",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Você quer deletar esta categoria",
|
||||
"header": "Deletar categoria"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "Você não possui categorias. Clique no botão de editar para criar uma para organizar sua biblioteca"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "Você não tem fontes. Adicione sua primeira para iniciar."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "Você não tem fontes. Adicione sua primeira para iniciar.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "Sobre",
|
||||
@@ -304,7 +313,7 @@
|
||||
"deleted": "Deletado %{name}",
|
||||
"deprecated": "Depreciado",
|
||||
"display": "Exibir",
|
||||
"done": "Concluído",
|
||||
"done": "Depreciado",
|
||||
"downloads": "Baixados",
|
||||
"edit": "Editar",
|
||||
"example": "Exemplo",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Aplicar",
|
||||
"loading": "Carregando...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Aviso"
|
||||
"warning": "Aviso",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Rever",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Extensão não encontrada!",
|
||||
"removeDownloadsWarning": "Tem certeza? Todos os capítulos transferidos serão deletados"
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Geral",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "todas as fontes",
|
||||
"searchIn": "Procurar uma novel em"
|
||||
"searchIn": "Procurar uma novel em",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "Histórico",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrar",
|
||||
"noSummary": "Nenhum resumo",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Rolar para o capítulo atual",
|
||||
"scrollToTop": "Rolar para o topo"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Não foi possível carregar o capítulo</h2><p>Nenhum conteúdo legível foi encontrado. Verifique o capítulo no WebView; se ele carregar lá, <a href='%{reportUrl}'>relate o problema do plugin</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Capítulo: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finalizado",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "Não há próximo capítulo",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"cachedNovelsDeletedToast": "Кэшированные новеллы удалены",
|
||||
"chapterInsertFailed": "не удалось вставить главу",
|
||||
"clearCachedNovels": "Очистить кэш новелл",
|
||||
"clearCachedNovelsDesc": "Удалить кэшированные новеллы, которые не находятся в вашей библиотеке",
|
||||
"clearCachedNovelsDesc": "Удалить кэш новелл, которых нет в вашей библиотеке",
|
||||
"clearDatabaseWarning": "Вы уверены? Прочитанные и загруженные главы и прогресс не библиотечных новелл будут потеряны.",
|
||||
"clearUpdatesMessage": "Обновления очищены.",
|
||||
"clearUpdatesTab": "Очистить вкладку обновления",
|
||||
@@ -22,8 +22,9 @@
|
||||
"dataManagement": "Управление данными",
|
||||
"deleteReadChapters": "Удалять прочитанные главы",
|
||||
"deleteReadChaptersDialogTitle": "Вы уверены? Все главы, помеченные как прочитанные, будут удалены.",
|
||||
"diagnostics": "Диагностика",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttps": "DNS через HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
"importEpub": "Импорт EPUB",
|
||||
"importFailed": "Error importing EPUB:",
|
||||
@@ -37,7 +38,10 @@
|
||||
"recreateDBIndexesToast": "Индексы базы данных пересозданы",
|
||||
"recreateDBIndexesDialogTitle": "Все индексы базы данных будут пересозданы.\nЭто может занять некоторое время.",
|
||||
"recreateDBIndexesDesc": "Пересоздаёт индексы базы данных. Это может улучшить производительность на слабых устройствах.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Внешний вид",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Подготовка данных",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "Вы не можете удалить категорию по умолчанию",
|
||||
"default": "По умолчанию",
|
||||
"defaultCategory": "Категория по умолчанию",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Вы хотите удалить эту категорию",
|
||||
"header": "Удалить категорию"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "У вас нет категорий. Нажмите кнопку Изменить, чтобы создать ее для организации вашей библиотеки"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "У вас нет репозиториев. Добавьте ваш первый репозиторий плагинов, чтобы начать работу."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "У вас нет репозиториев. Добавьте ваш первый репозиторий плагинов, чтобы начать работу.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "О программе",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Продолжить",
|
||||
"loading": "Загрузка...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Внимание"
|
||||
"warning": "Внимание",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Обновить",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Плагин не найден!",
|
||||
"removeDownloadsWarning": "Вы уверены? Все загруженные главы будут удалены."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "Основные",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "все источники",
|
||||
"searchIn": "Поиск новеллы в"
|
||||
"searchIn": "Поиск новеллы в",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "История",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Перенос",
|
||||
"noSummary": "Нет сводки",
|
||||
"noCoverFound": "Обложка не найдена",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Прогресс %{progress}%",
|
||||
"readChaptersDeleted": "Прочитанные главы удалены",
|
||||
"startReadingChapters": "Начать %{name}",
|
||||
@@ -662,9 +690,9 @@
|
||||
"scrollToCurrentChapter": "Прокрутка к текущей главе",
|
||||
"scrollToTop": "Прокрутить вверх"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Не удалось загрузить главу</h2><p>Читаемый контент не найден. Проверьте главу в WebView; если она там загружается, <a href='%{reportUrl}'>сообщите о проблеме плагина</a>.</p><p>Плагин: %{pluginId}<br>Новелла: %{novelName}<br>Глава: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Завершено",
|
||||
"nextChapter": "Следующая глава: %{name}",
|
||||
"nextChapter": "%{name}Следующий: %{name}",
|
||||
"noNextChapter": "Следующей главы нет",
|
||||
"noPreviousChapter": "Предыдущей главы нет",
|
||||
"searchMinLength": "Enter at least %{count} characters to search",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"diagnostics": "Diagnostics",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
@@ -37,7 +38,10 @@
|
||||
"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.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
"restartRequired": "Restart LNReader to apply this change.",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsDescription": "Create a diagnostic report with app, device, plugin, exception, and app-accessible logcat information.",
|
||||
"shareCrashLogsFailed": "Failed to create crash logs."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
@@ -172,13 +176,13 @@
|
||||
"description": "Choose what to include in this backup.",
|
||||
"selectAll": "Select all",
|
||||
"library": "Library data",
|
||||
"libraryDescription": "Novels, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"libraryDescription": "Novels, covers, chapter lists, reading progress, bookmarks, and categories.",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "App, reader, and update preferences.",
|
||||
"plugins": "Plugins",
|
||||
"pluginsDescription": "Installed source plugin files.",
|
||||
"downloadedFiles": "Downloaded chapters and covers",
|
||||
"downloadedFilesDescription": "Downloaded chapter content and saved novel covers. This may be large."
|
||||
"downloadedFiles": "Downloaded chapters",
|
||||
"downloadedFilesDescription": "Downloaded chapter content for offline reading. This may be large."
|
||||
},
|
||||
"preparingData": "Preparing Data",
|
||||
"preparingSelectedFiles": "Preparing selected files",
|
||||
@@ -238,14 +242,14 @@
|
||||
"openPluginSettings": "Open settings for %{name}",
|
||||
"openWebsite": "Open the website for %{name}",
|
||||
"pinSource": "Pin %{name}",
|
||||
"pinnedPlugins": "Pinned",
|
||||
"pinnedPlugins": "Pinned plugins",
|
||||
"pluginDetails": "Plugin details",
|
||||
"pluginIcon": "plugin icon",
|
||||
"pluginNotInstalled": "This plugin is not installed",
|
||||
"plugins": "Plugins",
|
||||
"removeFromLibrary": "Removed from library",
|
||||
"repositories": "Repositories",
|
||||
"searchbar": "Search sources and plugins",
|
||||
"searchbar": "Search sources",
|
||||
"searchResults": "Search results",
|
||||
"selectNovel": "Select Novel",
|
||||
"sources": "Sources",
|
||||
@@ -274,6 +278,7 @@
|
||||
"cantDeleteDefault": "You can't delete the default category",
|
||||
"default": "Default",
|
||||
"defaultCategory": "Default category",
|
||||
"alwaysAsk": "Always ask",
|
||||
"deleteModal": {
|
||||
"desc": "Do you wish to delete category",
|
||||
"header": "Delete category"
|
||||
@@ -287,7 +292,11 @@
|
||||
"setModalEmptyMsg": "You have no categories. Tap the Edit button to create one for organizing your library"
|
||||
},
|
||||
"repositories": {
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started."
|
||||
"disable": "Disable",
|
||||
"disableTitle": "Disable repository?",
|
||||
"disableWarning": "LNReader will stop checking %{name}. Its plugins will be hidden unless another enabled repository provides them. Installed plugins will remain installed.",
|
||||
"emptyMsg": "You have no repositories. Add your first plugin repository to get started.",
|
||||
"toggle": "Toggle %{name} repository"
|
||||
},
|
||||
"common": {
|
||||
"about": "About",
|
||||
@@ -340,7 +349,12 @@
|
||||
"submit": "Submit",
|
||||
"loading": "Loading ...",
|
||||
"moreOptions": "More options",
|
||||
"warning": "Warning"
|
||||
"warning": "Warning",
|
||||
"code": "Code",
|
||||
"custom_code": "Custom Code",
|
||||
"textToReplace": "Text to replace",
|
||||
"replaceWith": "Replace with",
|
||||
"replaceText": "Replace text"
|
||||
},
|
||||
"webview": {
|
||||
"refresh": "Refresh",
|
||||
@@ -374,10 +388,19 @@
|
||||
"pluginNotFound": "Plugin not found!",
|
||||
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
|
||||
},
|
||||
"taskQueue": {
|
||||
"cancelTaskAction": "Cancel task",
|
||||
"cancelTaskTitle": "Cancel task?",
|
||||
"cancelTaskConfirmation": "Are you sure you want to cancel %{task}?",
|
||||
"keepTaskAction": "Keep task"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "An Unexpected Error Occurred",
|
||||
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
|
||||
"description": "The application ran into an unexpected error. Please share the crash logs in our Discord support channel.",
|
||||
"copyStackTrace": "Copy stack trace",
|
||||
"shareCrashLogs": "Share crash logs",
|
||||
"shareCrashLogsFailed": "Could not share crash logs. The stack trace was copied instead.",
|
||||
"shareCrashLogsFailedWithoutCopy": "Could not share crash logs or copy the stack trace.",
|
||||
"restart": "Restart the application"
|
||||
},
|
||||
"generalSettings": "General",
|
||||
@@ -441,7 +464,10 @@
|
||||
},
|
||||
"globalSearch": {
|
||||
"allSources": "all sources",
|
||||
"searchIn": "Search a novel in"
|
||||
"searchIn": "Search a novel in",
|
||||
"openNovel": "Open novel",
|
||||
"openCopiedNovel": "Open copied novel",
|
||||
"noSourceForUrl": "No source found for this URL"
|
||||
},
|
||||
"history": "History",
|
||||
"historyScreen": {
|
||||
@@ -591,9 +617,11 @@
|
||||
"searchByName": "Search by Chapter Name",
|
||||
"searchByNameDescription": "Turn off to search by chapter number."
|
||||
},
|
||||
"loadFailed": "Unable to load novel",
|
||||
"migrate": "Migrate",
|
||||
"noSummary": "No summary",
|
||||
"noCoverFound": "No cover found",
|
||||
"notFound": "Novel not found on the source",
|
||||
"progress": "Progress %{progress} %",
|
||||
"readChaptersDeleted": "Read chapters deleted",
|
||||
"startReadingChapters": "Start reading %{name}",
|
||||
@@ -662,7 +690,7 @@
|
||||
"scrollToCurrentChapter": "Scroll to current chapter",
|
||||
"scrollToTop": "Scroll to top"
|
||||
},
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"emptyChapterMessage": "<h2>Chapter couldn't be loaded</h2><p>No readable content was found. Check the chapter in WebView; if it loads there, <a href='%{reportUrl}'>report the plugin issue</a>.</p><p><a href='%{refreshUrl}'>Refresh chapter</a></p><p>Plugin: %{pluginId}<br>Novel: %{novelName}<br>Chapter: %{chapterName}</p>",
|
||||
"finished": "Finished",
|
||||
"nextChapter": "Next: %{name}",
|
||||
"noNextChapter": "There's no next chapter",
|
||||
@@ -780,6 +808,28 @@
|
||||
"other": "%{count} days"
|
||||
}
|
||||
},
|
||||
"customCodeSettings": {
|
||||
"textManipulation": "Text Manipulation",
|
||||
"codeSnippets": "Code Snippets",
|
||||
"cssSnippets": "CSS Snippets",
|
||||
"javascriptSnippets": "JavaScript Snippets",
|
||||
"createCSSSnippet": "Create CSS Snippet",
|
||||
"addCssCode": "Add custom CSS code that applies to all chapters",
|
||||
"createJSSnippet": "Create JS Snippet",
|
||||
"addJavascriptCode": "Add custom JavaScript code that applies to all chapters",
|
||||
"noCodeSnippets": "No code snippets",
|
||||
"renameSnippet": "Rename Snippet",
|
||||
"yourCodeHere": "Your code here",
|
||||
"editReplace": "Edit Replace/Remove",
|
||||
"enterAMatch": "Enter a match pattern",
|
||||
"enterAReplace": "Enter a replace value",
|
||||
"itemAlreadyExists": "Item already exists",
|
||||
"replace": "Replace",
|
||||
"removeText": "Remove Text",
|
||||
"snippetSaved": "Snippet saved",
|
||||
"snippetUpdated": "Snippet updated",
|
||||
"imported": "Imported"
|
||||
},
|
||||
"genreStats": {
|
||||
"title": "Genre statistics",
|
||||
"totalNovels": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user