Compare commits

...

28 Commits
v6.1 ... v6.1.2

Author SHA1 Message Date
Koitharu
a435435496 Fix webtoon zoom controls visibility 2023-09-18 13:57:07 +03:00
Koitharu
81e8c25563 Reorder reader settings items 2023-09-18 13:51:02 +03:00
Koitharu
e3504c3b1e Translated using Weblate (Russian)
Currently translated at 100.0% (487 of 487 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-18 13:48:30 +03:00
Макар Разин
2601c12348 Translated using Weblate (Russian)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-18 13:48:30 +03:00
Koitharu
138cf44e37 Fix crash with ActivityNotFoundException 2023-09-18 13:42:28 +03:00
Koitharu
65d83e0921 Fix search action #495 2023-09-18 13:34:08 +03:00
Koitharu
6e1cd05fa8 Zoom control buttons in reader 2023-09-18 13:25:53 +03:00
Koitharu
8398c01929 Improve keyboard control in reader 2023-09-18 12:49:37 +03:00
Koitharu
835c49ae79 Download updates directly 2023-09-15 13:34:13 +03:00
Koitharu
36065ccf6c Pin source shortcuts 2023-09-15 12:12:06 +03:00
Koitharu
4ab40566f7 Fix sync server address configuration 2023-09-15 11:14:06 +03:00
return_null
bf01a4d1ab Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (480 of 483 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-09-14 13:41:58 +03:00
Koitharu
8dce9dcc3f Fix pages thumbnails loading 2023-09-14 13:38:02 +03:00
Koitharu
d872044252 Improve mouse interaction 2023-09-14 13:31:18 +03:00
Koitharu
f4313525c2 Update dependencies 2023-09-14 09:05:50 +03:00
Koitharu
4eb4ec7de0 Fix nsfw sources filtering 2023-09-12 19:33:49 +03:00
Koitharu
ecb4dd87d9 Update parsers 2023-09-12 19:28:45 +03:00
Nayuki
3d0f5f75cd Translated using Weblate (Thai)
Currently translated at 63.3% (306 of 483 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Bander AL-shreef
c5462e8454 Translated using Weblate (Arabic)
Currently translated at 37.4% (181 of 483 strings)

Co-authored-by: Bander AL-shreef <bander.alshreef@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
return_null
5039e324fb Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (480 of 483 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 93.1% (450 of 483 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.5% (447 of 483 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-09-12 18:31:26 +03:00
Макар Разин
b251b3e654 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
gallegonovato
5f10070564 Translated using Weblate (Spanish)
Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Clxff H3r4ld0
3da6f80eb6 Translated using Weblate (Indonesian)
Currently translated at 100.0% (481 of 481 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
InfinityDouki56
4b2cfdb972 Translated using Weblate (Filipino)
Currently translated at 89.7% (428 of 477 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
kuragehime
51387ace7e Translated using Weblate (Japanese)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (481 of 481 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (477 of 477 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Koitharu
2bdb83ff28 Fix navigation reordering 2023-09-12 13:38:03 +03:00
Koitharu
a1b85433ec Fix bookmarks crash #492 2023-09-12 13:38:03 +03:00
Isira Seneviratne
ca5207c658 Use ancestors and descendants extensions 2023-09-09 17:52:12 +03:00
84 changed files with 1273 additions and 408 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 577 versionCode = 580
versionName = '6.1' versionName = '6.1.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp { ksp {
@@ -81,7 +81,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:aae3fa3b05') { implementation('com.github.KotatsuApp:kotatsu-parsers:7fbeb2e266') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -89,13 +89,13 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.7.2' implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.fragment:fragment-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1' implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1' implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.3.1'
@@ -103,7 +103,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1' implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
// TODO https://issuetracker.google.com/issues/254846063 // TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.8.1'
@@ -125,19 +125,19 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.47' implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.47' kapt 'com.google.dagger:hilt-compiler:2.48'
implementation 'androidx.hilt:hilt-work:1.0.0' implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.4.0' implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0' implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67' implementation 'com.github.KotatsuApp:subsampling-scale-image-view:169806d928'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
implementation 'ch.acra:acra-http:5.11.1' implementation 'ch.acra:acra-http:5.11.2'
implementation 'ch.acra:acra-dialog:5.11.1' implementation 'ch.acra:acra-dialog:5.11.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
@@ -155,6 +155,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.5.2' androidTestImplementation 'androidx.room:room-testing:2.5.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.47' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.47' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48'
} }

View File

@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
@@ -95,7 +96,12 @@
android:label="@string/search" /> android:label="@string/search" />
<activity <activity
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity" android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:label="@string/search_manga" /> android:exported="true"
android:label="@string/manga_list">
<intent-filter>
<action android:name="${applicationId}.action.EXPLORE_MANGA" />
</intent-filter>
</activity>
<activity <activity
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity" android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
android:label="@string/history" /> android:label="@string/history" />
@@ -138,8 +144,8 @@
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity" android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:autoRemoveFromRecents="true" android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity" android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
@@ -314,6 +320,13 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" /> android:resource="@xml/widget_recent" />
</receiver> </receiver>
<receiver
android:name="org.koitharu.kotatsu.settings.about.UpdateDownloadReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<meta-data <meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing" android:name="android.webkit.WebView.EnableSafeBrowsing"

View File

@@ -14,7 +14,6 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@@ -25,8 +24,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
@@ -38,7 +36,6 @@ import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import javax.inject.Inject import javax.inject.Inject
@@ -105,7 +102,7 @@ class BookmarksFragment :
viewLifecycleOwner, viewLifecycleOwner,
SnackbarErrorObserver(binding.recyclerView, this) SnackbarErrorObserver(binding.recyclerView, this)
) )
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -184,17 +181,6 @@ class BookmarksFragment :
} }
} }
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar =
Snackbar.make((activity as SnackbarOwner).snackbarHost, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable { private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable {
init { init {

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
@@ -29,8 +30,10 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -77,6 +80,10 @@ class AppShortcutManager @Inject constructor(
return ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(manga), null) return ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(manga), null)
} }
suspend fun requestPinShortcut(source: MangaSource): Boolean {
return ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(source), null)
}
@VisibleForTesting @VisibleForTesting
suspend fun await(): Boolean { suspend fun await(): Boolean {
return shortcutsUpdateJob?.join() != null return shortcutsUpdateJob?.join() != null
@@ -86,6 +93,11 @@ class AppShortcutManager @Inject constructor(
ShortcutManagerCompat.reportShortcutUsed(context, mangaId.toString()) ShortcutManagerCompat.reportShortcutUsed(context, mangaId.toString())
} }
fun isDynamicShortcutsAvailable(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 &&
context.getSystemService(ShortcutManager::class.java).maxShortcutCountPerActivity > 0
}
private suspend fun updateShortcutsImpl() = runCatchingCancellable { private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context).coerceAtLeast(5) val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context).coerceAtLeast(5)
val shortcuts = historyRepository.getList(0, maxShortcuts) val shortcuts = historyRepository.getList(0, maxShortcuts)
@@ -132,8 +144,25 @@ class AppShortcutManager @Inject constructor(
.build() .build()
} }
fun isDynamicShortcutsAvailable(): Boolean { private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && val icon = runCatchingCancellable {
context.getSystemService(ShortcutManager::class.java).maxShortcutCountPerActivity > 0 coil.execute(
ImageRequest.Builder(context)
.data(source.faviconUri())
.size(iconSize)
.scale(Scale.FIT)
.build(),
).getDrawableOrThrow().toBitmap()
}.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
return ShortcutInfoCompat.Builder(context, source.name)
.setShortLabel(source.title)
.setLongLabel(source.title)
.setIcon(icon)
.setLongLived(true)
.setIntent(MangaListActivity.newIntent(context, source))
.build()
} }
} }

View File

@@ -90,6 +90,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val readerPageSwitch: Set<String> val readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS) get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
val isReaderZoomButtonsEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_ZOOM_BUTTONS, false)
val isReaderTapsAdaptive: Boolean val isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false) get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
@@ -161,7 +164,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_APP_PASSWORD, null) get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit { set(value) = prefs.edit {
if (value != null) putString(KEY_APP_PASSWORD, value) else remove( if (value != null) putString(KEY_APP_PASSWORD, value) else remove(
KEY_APP_PASSWORD KEY_APP_PASSWORD,
) )
} }
@@ -314,7 +317,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit { set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit {
putFloat( putFloat(
KEY_READER_AUTOSCROLL_SPEED, KEY_READER_AUTOSCROLL_SPEED,
value value,
) )
} }
@@ -325,7 +328,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
} }
val policy = NetworkPolicy.from( val policy = NetworkPolicy.from(
prefs.getString(KEY_PAGES_PRELOAD, null), prefs.getString(KEY_PAGES_PRELOAD, null),
NetworkPolicy.NON_METERED NetworkPolicy.NON_METERED,
) )
return policy.isNetworkAllowed(connectivityManager) return policy.isNetworkAllowed(connectivityManager)
} }
@@ -409,6 +412,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers" const val KEY_READER_SWITCHERS = "reader_switchers"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_TRACKER_ENABLED = "tracker_enabled" const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi" const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
const val KEY_TRACK_SOURCES = "track_sources" const val KEY_TRACK_SOURCES = "track_sources"

View File

@@ -19,13 +19,13 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ancestors
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isLayoutReversed import org.koitharu.kotatsu.core.util.ext.isLayoutReversed
import org.koitharu.kotatsu.core.util.ext.parents
import org.koitharu.kotatsu.databinding.FastScrollerBinding import org.koitharu.kotatsu.databinding.FastScrollerBinding
import kotlin.math.roundToInt import kotlin.math.roundToInt
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -522,7 +522,7 @@ class FastScroller @JvmOverloads constructor(
return BubbleSize.entries.getOrNull(ordinal) ?: defaultValue return BubbleSize.entries.getOrNull(ordinal) ?: defaultValue
} }
private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p -> private fun findValidParent(view: View): ViewGroup? = view.ancestors.firstNotNullOfOrNull { p ->
if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) { if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) {
p as ViewGroup p as ViewGroup
} else { } else {

View File

@@ -104,6 +104,7 @@ sealed class AdaptiveSheetBehavior {
companion object { companion object {
const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED
const val STATE_COLLAPSED = BottomSheetBehavior.STATE_COLLAPSED
const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
@@ -114,10 +115,11 @@ sealed class AdaptiveSheetBehavior {
else -> null else -> null
} }
fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) { fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? =
is BottomSheetBehavior<*> -> Bottom(behavior) when (val behavior = lp.behavior) {
is SideSheetBehavior<*> -> Side(behavior) is BottomSheetBehavior<*> -> Bottom(behavior)
else -> null is SideSheetBehavior<*> -> Side(behavior)
} else -> null
}
} }
} }

View File

@@ -2,17 +2,19 @@ package org.koitharu.kotatsu.core.ui.sheet
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.InputDevice
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.ancestors
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.parents
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding
class AdaptiveSheetHeaderBar @JvmOverloads constructor( class AdaptiveSheetHeaderBar @JvmOverloads constructor(
@@ -21,7 +23,8 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0, @AttrRes defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback { ) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback {
private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this) private val binding =
LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this)
private var sheetBehavior: AdaptiveSheetBehavior? = null private var sheetBehavior: AdaptiveSheetBehavior? = null
var title: CharSequence? var title: CharSequence?
@@ -60,6 +63,28 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor(
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
val behavior = sheetBehavior ?: return super.onGenericMotionEvent(event)
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) {
behavior.state = if (
behavior is AdaptiveSheetBehavior.Bottom
&& behavior.state == AdaptiveSheetBehavior.STATE_EXPANDED
) {
AdaptiveSheetBehavior.STATE_COLLAPSED
} else {
AdaptiveSheetBehavior.STATE_HIDDEN
}
} else {
behavior.state = AdaptiveSheetBehavior.STATE_EXPANDED
}
return true
}
}
return super.onGenericMotionEvent(event)
}
override fun onStateChanged(sheet: View, newState: Int) { override fun onStateChanged(sheet: View, newState: Int) {
} }
@@ -81,14 +106,9 @@ class AdaptiveSheetHeaderBar @JvmOverloads constructor(
} }
private fun findParentSheetBehavior(): AdaptiveSheetBehavior? { private fun findParentSheetBehavior(): AdaptiveSheetBehavior? {
for (p in parents) { return ancestors.firstNotNullOfOrNull {
val layoutParams = (p as? View)?.layoutParams ((it as? View)?.layoutParams as? CoordinatorLayout.LayoutParams)
if (layoutParams is CoordinatorLayout.LayoutParams) { ?.let { params -> AdaptiveSheetBehavior.from(params) }
AdaptiveSheetBehavior.from(layoutParams)?.let {
return it
}
}
} }
return null
} }
} }

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ViewZoomBinding
class ZoomControl @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : LinearLayout(context, attrs), View.OnClickListener {
private val binding = ViewZoomBinding.inflate(LayoutInflater.from(context), this)
var listener: ZoomControlListener? = null
init {
binding.buttonZoomIn.setOnClickListener(this)
binding.buttonZoomOut.setOnClickListener(this)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_zoom_in -> listener?.onZoomIn()
R.id.button_zoom_out -> listener?.onZoomOut()
}
}
interface ZoomControlListener {
fun onZoomIn()
fun onZoomOut()
}
}

View File

@@ -2,13 +2,14 @@ package org.koitharu.kotatsu.core.util.ext
import android.app.Activity import android.app.Activity
import android.graphics.Rect import android.graphics.Rect
import android.os.Build
import android.view.View import android.view.View
import android.view.View.MeasureSpec import android.view.View.MeasureSpec
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewParent
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Checkable import android.widget.Checkable
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.descendants
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
@@ -89,23 +90,8 @@ fun Slider.setValueRounded(newValue: Float) {
value = roundedValue.coerceIn(valueFrom, valueTo) value = roundedValue.coerceIn(valueFrom, valueTo)
} }
fun <T : View> ViewGroup.findViewsByType(clazz: Class<T>): Sequence<T> {
if (childCount == 0) {
return emptySequence()
}
return sequence {
for (view in children) {
if (clazz.isInstance(view)) {
yield(clazz.cast(view)!!)
} else if (view is ViewGroup && view.childCount != 0) {
yieldAll(view.findViewsByType(clazz))
}
}
}
}
fun RecyclerView.invalidateNestedItemDecorations() { fun RecyclerView.invalidateNestedItemDecorations() {
findViewsByType(RecyclerView::class.java).forEach { descendants.filterIsInstance<RecyclerView>().forEach {
it.invalidateItemDecorations() it.invalidateItemDecorations()
} }
} }
@@ -113,15 +99,6 @@ fun RecyclerView.invalidateNestedItemDecorations() {
val View.parentView: ViewGroup? val View.parentView: ViewGroup?
get() = parent as? ViewGroup get() = parent as? ViewGroup
val View.parents: Sequence<ViewParent>
get() = sequence {
var p: ViewParent? = parent
while (p != null) {
yield(p)
p = p.parent
}
}
fun View.measureDimension(desiredSize: Int, measureSpec: Int): Int { fun View.measureDimension(desiredSize: Int, measureSpec: Int): Int {
var result: Int var result: Int
val specMode = MeasureSpec.getMode(measureSpec) val specMode = MeasureSpec.getMode(measureSpec)
@@ -164,3 +141,9 @@ fun BaseProgressIndicator<*>.showOrHide(value: Boolean) {
if (isVisible) hide() if (isVisible) hide()
} }
} }
fun View.setOnContextClickListenerCompat(listener: View.OnLongClickListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnContextClickListener(listener::onLongClick)
}
}

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.view.InputDevice
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.OnLayoutChangeListener import android.view.View.OnLayoutChangeListener
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
@@ -12,7 +14,7 @@ class ChaptersBottomSheetMediator(
private val behavior: BottomSheetBehavior<*>, private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(false), ) : OnBackPressedCallback(false),
ActionModeListener, ActionModeListener,
OnLayoutChangeListener { OnLayoutChangeListener, View.OnGenericMotionListener {
private var lockCounter = 0 private var lockCounter = 0
@@ -55,6 +57,20 @@ class ChaptersBottomSheetMediator(
} }
} }
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0f) {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
} else {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
return true
}
}
return false
}
fun lock() { fun lock() {
lockCounter++ lockCounter++
behavior.isDraggable = lockCounter <= 0 behavior.isDraggable = lockCounter <= 0

View File

@@ -49,6 +49,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.service.MangaPrefetchService
@@ -89,6 +90,7 @@ class DetailsActivity :
} }
viewBinding.buttonRead.setOnClickListener(this) viewBinding.buttonRead.setOnClickListener(this)
viewBinding.buttonRead.setOnLongClickListener(this) viewBinding.buttonRead.setOnLongClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
viewBinding.buttonDropdown.setOnClickListener(this) viewBinding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(viewBinding.buttonRead, this) viewBadge = ViewBadge(viewBinding.buttonRead, this)
@@ -103,6 +105,7 @@ class DetailsActivity :
viewBinding.toolbarChapters?.setNavigationOnClickListener { viewBinding.toolbarChapters?.setNavigationOnClickListener {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED behavior.state = BottomSheetBehavior.STATE_COLLAPSED
} }
viewBinding.toolbarChapters?.setOnGenericMotionListener(bsMediator)
} else { } else {
chaptersMenuProvider = ChaptersMenuProvider(viewModel, null) chaptersMenuProvider = ChaptersMenuProvider(viewModel, null)
addMenuProvider(chaptersMenuProvider) addMenuProvider(chaptersMenuProvider)
@@ -134,13 +137,19 @@ class DetailsActivity :
viewBinding.toolbarChapters?.subtitle = it viewBinding.toolbarChapters?.subtitle = it
viewBinding.textViewSubtitle?.textAndVisible = it viewBinding.textViewSubtitle?.textAndVisible = it
} }
viewModel.isChaptersReversed.observe(this, MenuInvalidator(viewBinding.toolbarChapters ?: this)) viewModel.isChaptersReversed.observe(
this,
MenuInvalidator(viewBinding.toolbarChapters ?: this)
)
viewModel.favouriteCategories.observe(this, MenuInvalidator(this)) viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
viewModel.branches.observe(this) { viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1 viewBinding.buttonDropdown.isVisible = it.size > 1
} }
viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.containerDetails)) viewModel.onDownloadStarted.observeEvent(
this,
DownloadStartedObserver(viewBinding.containerDetails)
)
addMenuProvider( addMenuProvider(
DetailsMenuProvider( DetailsMenuProvider(
@@ -243,7 +252,11 @@ class DetailsActivity :
right = insets.right, right = insets.right,
) )
if (insets.bottom > 0) { if (insets.bottom > 0) {
window.setNavigationBarTransparentCompat(this, viewBinding.layoutBottom?.elevation ?: 0f, 0.9f) window.setNavigationBarTransparentCompat(
this,
viewBinding.layoutBottom?.elevation ?: 0f,
0.9f
)
} }
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> { viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom + marginEnd bottomMargin = insets.bottom + marginEnd
@@ -265,9 +278,18 @@ class DetailsActivity :
} }
val text = when { val text = when {
!info.isValid -> getString(R.string.loading_) !info.isValid -> getString(R.string.loading_)
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) info.currentChapter >= 0 -> getString(
R.string.chapter_d_of_d,
info.currentChapter + 1,
info.totalChapters
)
info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) else -> resources.getQuantityString(
R.plurals.chapters,
info.totalChapters,
info.totalChapters
)
} }
viewBinding.toolbarChapters?.title = text viewBinding.toolbarChapters?.title = text
viewBinding.textViewTitle?.text = text viewBinding.textViewTitle?.text = text
@@ -286,7 +308,12 @@ class DetailsActivity :
append(' ') append(' ')
append(' ') append(' ')
inSpans( inSpans(
ForegroundColorSpan(v.context.getThemeColor(android.R.attr.textColorSecondary, Color.LTGRAY)), ForegroundColorSpan(
v.context.getThemeColor(
android.R.attr.textColorSecondary,
Color.LTGRAY
)
),
RelativeSizeSpan(0.74f), RelativeSizeSpan(0.74f),
) { ) {
append(branch.count.toString()) append(branch.count.toString())
@@ -305,7 +332,8 @@ class DetailsActivity :
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
val chapterId = viewModel.historyInfo.value.history?.chapterId val chapterId = viewModel.historyInfo.value.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT) val snackbar =
makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show() snackbar.show()
} else { } else {
startActivity( startActivity(
@@ -331,7 +359,10 @@ class DetailsActivity :
view.isVisible = isVisible view.isVisible = isVisible
} }
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar { private fun makeSnackbar(
text: CharSequence,
@BaseTransientBottomBar.Duration duration: Int,
): Snackbar {
val sb = Snackbar.make(viewBinding.containerDetails, text, duration) val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
if (viewBinding.layoutBottom?.isVisible == true) { if (viewBinding.layoutBottom?.isVisible == true) {
sb.anchorView = viewBinding.toolbarChapters sb.anchorView = viewBinding.toolbarChapters

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.explore.data
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -11,12 +12,12 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.move
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import javax.inject.Inject import javax.inject.Inject
@@ -76,14 +77,6 @@ class MangaSourcesRepository @Inject constructor(
} }
} }
suspend fun setSourcesEnabled(sources: Iterable<MangaSource>, isEnabled: Boolean) {
db.withTransaction {
for (s in sources) {
dao.setEnabled(s.name, isEnabled)
}
}
}
suspend fun disableAllSources() { suspend fun disableAllSources() {
db.withTransaction { db.withTransaction {
assimilateNewSources() assimilateNewSources()
@@ -99,46 +92,20 @@ class MangaSourcesRepository @Inject constructor(
} }
} }
suspend fun setPosition(source: MangaSource, index: Int) { fun observeNewSources(): Flow<Set<MangaSource>> = combine(
db.withTransaction { dao.observeAll(),
val all = dao.findAll().toMutableList() observeIsNsfwDisabled(),
val sourceIndex = all.indexOfFirst { x -> x.source == source.name } ) { entities, skipNsfw ->
if (sourceIndex !in all.indices) {
val entity = MangaSourceEntity(
source = source.name,
isEnabled = false,
sortKey = index,
)
all.add(index, entity)
dao.upsert(entity)
} else {
all.move(sourceIndex, index)
}
for ((i, e) in all.withIndex()) {
if (e.sortKey != i) {
dao.setSortKey(e.source, i)
}
}
}
}
fun observeNewSources(): Flow<Set<MangaSource>> = dao.observeAll().map { entities ->
val result = EnumSet.copyOf(remoteSources) val result = EnumSet.copyOf(remoteSources)
for (e in entities) { for (e in entities) {
result.remove(MangaSource(e.source)) result.remove(MangaSource(e.source))
} }
if (skipNsfw) {
result.removeAll { x -> x.isNsfw() }
}
result result
}.distinctUntilChanged() }.distinctUntilChanged()
suspend fun getNewSources(): Set<MangaSource> {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
}
return result
}
suspend fun assimilateNewSources(): Set<MangaSource> { suspend fun assimilateNewSources(): Set<MangaSource> {
val new = getNewSources() val new = getNewSources()
if (new.isEmpty()) { if (new.isEmpty()) {
@@ -153,6 +120,9 @@ class MangaSourcesRepository @Inject constructor(
) )
} }
dao.insertIfAbsent(entities) dao.insertIfAbsent(entities)
if (settings.isNsfwContentDisabled) {
new.removeAll { x -> x.isNsfw() }
}
return new return new
} }
@@ -160,6 +130,15 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAll().isEmpty() return dao.findAll().isEmpty()
} }
private suspend fun getNewSources(): MutableSet<MangaSource> {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
}
return result
}
private fun List<MangaSourceEntity>.toSources(skipNsfwSources: Boolean): List<MangaSource> { private fun List<MangaSourceEntity>.toSources(skipNsfwSources: Boolean): List<MangaSource> {
val result = ArrayList<MangaSource>(size) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@@ -15,9 +16,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -28,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentExploreBinding import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
@@ -55,6 +59,9 @@ class ExploreFragment :
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject
lateinit var shortcutManager: AppShortcutManager
private val viewModel by viewModels<ExploreViewModel>() private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null private var exploreAdapter: ExploreAdapter? = null
@@ -141,6 +148,8 @@ class ExploreFragment :
override fun onItemLongClick(item: MangaSourceItem, view: View): Boolean { override fun onItemLongClick(item: MangaSourceItem, view: View): Boolean {
val menu = PopupMenu(view.context, view) val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_source) menu.inflate(R.menu.popup_source)
menu.menu.findItem(R.id.action_shortcut)
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(view.context)
menu.setOnMenuItemClickListener(SourceMenuListener(item)) menu.setOnMenuItemClickListener(SourceMenuListener(item))
menu.show() menu.show()
return true return true
@@ -195,6 +204,12 @@ class ExploreFragment :
viewModel.hideSource(sourceItem.source) viewModel.hideSource(sourceItem.source)
} }
R.id.action_shortcut -> {
viewLifecycleScope.launch {
shortcutManager.requestPinShortcut(sourceItem.source)
}
}
else -> return false else -> return false
} }
return true return true

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
@@ -44,7 +45,12 @@ fun exploreButtonsAD(
if (item.isRandomLoading) { if (item.isRandomLoading) {
val icon = CircularProgressDrawable(context) val icon = CircularProgressDrawable(context)
icon.strokeWidth = context.resources.resolveDp(2f) icon.strokeWidth = context.resources.resolveDp(2f)
icon.setColorSchemeColors(context.getThemeColor(materialR.attr.colorPrimary, Color.DKGRAY)) icon.setColorSchemeColors(
context.getThemeColor(
materialR.attr.colorPrimary,
Color.DKGRAY
)
)
binding.buttonRandom.icon = icon binding.buttonRandom.icon = icon
icon.start() icon.start()
} else { } else {
@@ -88,7 +94,13 @@ fun exploreSourceListItemAD(
listener: OnListItemClickListener<MangaSourceItem>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>(
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent ->
ItemExploreSourceListBinding.inflate(
layoutInflater,
parent,
false
)
},
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
) { ) {
@@ -96,6 +108,7 @@ fun exploreSourceListItemAD(
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
binding.root.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
@@ -115,7 +128,13 @@ fun exploreSourceGridItemAD(
listener: OnListItemClickListener<MangaSourceItem>, listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>( ) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>(
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent ->
ItemExploreSourceGridBinding.inflate(
layoutInflater,
parent,
false
)
},
on = { item, _, _ -> item is MangaSourceItem && item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
) { ) {
@@ -123,6 +142,7 @@ fun exploreSourceGridItemAD(
binding.root.setOnClickListener(eventListener) binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener) binding.root.setOnLongClickListener(eventListener)
binding.root.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
@@ -28,12 +30,13 @@ fun mangaGridItemAD(
) { ) {
var badge: BadgeDrawable? = null var badge: BadgeDrawable? = null
itemView.setOnClickListener { val eventListener = object : View.OnClickListener, View.OnLongClickListener {
clickListener.onItemClick(item.manga, it) override fun onClick(v: View) = clickListener.onItemClick(item.manga, v)
} override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
} }
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
itemView.setOnContextClickListenerCompat(eventListener)
sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView) sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView)
bind { payloads -> bind { payloads ->

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
@@ -45,6 +46,7 @@ fun mangaListDetailedItemAD(
} }
itemView.setOnClickListener(listenerAdapter) itemView.setOnClickListener(listenerAdapter)
itemView.setOnLongClickListener(listenerAdapter) itemView.setOnLongClickListener(listenerAdapter)
itemView.setOnContextClickListenerCompat(listenerAdapter)
binding.buttonRead.setOnClickListener(listenerAdapter) binding.buttonRead.setOnClickListener(listenerAdapter)
binding.chipsTags.onChipClickListener = listenerAdapter binding.chipsTags.onChipClickListener = listenerAdapter

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
@@ -9,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.databinding.ItemMangaListBinding
@@ -25,12 +27,13 @@ fun mangaListItemAD(
) { ) {
var badge: BadgeDrawable? = null var badge: BadgeDrawable? = null
itemView.setOnClickListener { val eventListener = object : View.OnClickListener, View.OnLongClickListener {
clickListener.onItemClick(item.manga, it) override fun onClick(v: View) = clickListener.onItemClick(item.manga, v)
} override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
} }
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
itemView.setOnContextClickListenerCompat(eventListener)
bind { bind {
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.DialogImportBinding import org.koitharu.kotatsu.databinding.DialogImportBinding
class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener { class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener {
@@ -40,9 +41,13 @@ class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.On
} }
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { val res = when (v.id) {
R.id.button_file -> importFileCall.launch(arrayOf("*/*")) R.id.button_file -> importFileCall.tryLaunch(arrayOf("*/*"))
R.id.button_dir -> importDirCall.launch(null) R.id.button_dir -> importDirCall.tryLaunch(null)
else -> true
}
if (!res) {
Toast.makeText(v.context, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -239,10 +239,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
override fun onQueryClick(query: String, submit: Boolean) { override fun onQueryClick(query: String, submit: Boolean) {
viewBinding.searchView.query = query viewBinding.searchView.query = query
if (submit) { if (submit && query.isNotEmpty()) {
if (query.isNotEmpty()) { startActivity(MultiSearchActivity.newIntent(this, query))
startActivity(MultiSearchActivity.newIntent(this, query)) searchSuggestionViewModel.saveQuery(query)
searchSuggestionViewModel.saveQuery(query) viewBinding.searchView.post {
closeSearchCallback.handleOnBackPressed()
} }
} }
} }
@@ -376,9 +377,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal) val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal)
viewBinding.appbar.updatePadding(left = padding, right = padding) viewBinding.appbar.updatePadding(left = padding, right = padding)
adjustFabVisibility(isSearchOpened = isOpened) adjustFabVisibility(isSearchOpened = isOpened)
supportActionBar?.setHomeAsUpIndicator( supportActionBar?.apply {
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, setHomeAsUpIndicator(
) if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
)
setHomeActionContentDescription(
if (isOpened) R.string.back else R.string.search,
)
}
viewBinding.searchView.setHintCompat( viewBinding.searchView.setHintCompat(
if (isOpened) R.string.search_hint else R.string.search_manga, if (isOpened) R.string.search_hint else R.string.search_manga,
) )
@@ -394,7 +400,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
ActivityCompat.requestPermissions( ActivityCompat.requestPermissions(
this, this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS), arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1 1,
) )
} }
} }

View File

@@ -107,7 +107,7 @@ class ReaderActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this) touchHelper = GridTouchHelper(this, this)
scrollTimer = scrollTimerFactory.create(this, this) scrollTimer = scrollTimerFactory.create(this, this)
controlDelegate = ReaderControlDelegate(settings, this, this) controlDelegate = ReaderControlDelegate(resources, settings, this, this)
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
viewBinding.slider.setLabelFormatter(PageLabelFormatter()) viewBinding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider) ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
@@ -347,8 +347,8 @@ class ReaderActivity :
readerManager.currentReader?.switchPageBy(delta) readerManager.currentReader?.switchPageBy(delta)
} }
override fun scrollBy(delta: Int): Boolean { override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
return readerManager.currentReader?.scrollBy(delta) ?: false return readerManager.currentReader?.scrollBy(delta, smooth) ?: false
} }
override fun toggleUiVisibility() { override fun toggleUiVisibility() {

View File

@@ -1,16 +1,19 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.ui
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources
import android.view.KeyEvent import android.view.KeyEvent
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
import android.view.View import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.GridTouchHelper import org.koitharu.kotatsu.core.util.GridTouchHelper
class ReaderControlDelegate( class ReaderControlDelegate(
resources: Resources,
private val settings: AppSettings, private val settings: AppSettings,
private val listener: OnInteractionListener, private val listener: OnInteractionListener,
owner: LifecycleOwner, owner: LifecycleOwner,
@@ -19,6 +22,7 @@ class ReaderControlDelegate(
private var isTapSwitchEnabled: Boolean = true private var isTapSwitchEnabled: Boolean = true
private var isVolumeKeysSwitchEnabled: Boolean = false private var isVolumeKeysSwitchEnabled: Boolean = false
private var isReaderTapsAdaptive: Boolean = true private var isReaderTapsAdaptive: Boolean = true
private var minScrollDelta = resources.getDimensionPixelSize(R.dimen.reader_scroll_delta_min)
init { init {
owner.lifecycle.addObserver(this) owner.lifecycle.addObserver(this)
@@ -82,8 +86,6 @@ class ReaderControlDelegate(
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN,
-> { -> {
listener.switchPageBy(1) listener.switchPageBy(1)
true true
@@ -95,8 +97,6 @@ class ReaderControlDelegate(
} }
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP,
-> { -> {
listener.switchPageBy(-1) listener.switchPageBy(-1)
true true
@@ -112,6 +112,22 @@ class ReaderControlDelegate(
true true
} }
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP -> {
if (!listener.scrollBy(-minScrollDelta, smooth = true)) {
listener.switchPageBy(-1)
}
true
}
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!listener.scrollBy(minScrollDelta, smooth = true)) {
listener.switchPageBy(1)
}
true
}
else -> false else -> false
} }
@@ -139,7 +155,7 @@ class ReaderControlDelegate(
fun switchPageBy(delta: Int) fun switchPageBy(delta: Int)
fun scrollBy(delta: Int): Boolean fun scrollBy(delta: Int, smooth: Boolean): Boolean
fun toggleUiVisibility() fun toggleUiVisibility()

View File

@@ -120,6 +120,12 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isWebtoonZoomEnable }, valueProducer = { isWebtoonZoomEnable },
) )
val isZoomControlEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_ZOOM_BUTTONS,
valueProducer = { isReaderZoomButtonsEnabled },
)
val readerSettings = ReaderSettings( val readerSettings = ReaderSettings(
parentScope = viewModelScope, parentScope = viewModelScope,
settings = settings, settings = settings,

View File

@@ -96,7 +96,7 @@ class ScrollTimer @AssistedInject constructor(
if (!listener.isReaderResumed()) { if (!listener.isReaderResumed()) {
continue continue
} }
if (!listener.scrollBy(1)) { if (!listener.scrollBy(1, false)) {
accumulator += delayMs accumulator += delayMs
} }
if (accumulator >= pageSwitchDelay) { if (accumulator >= pageSwitchDelay) {

View File

@@ -32,6 +32,9 @@ class ReaderSettings(
val isPagesNumbersEnabled: Boolean val isPagesNumbersEnabled: Boolean
get() = settings.isPagesNumbersEnabled get() = settings.isPagesNumbersEnabled
val isZoomControlsEnabled: Boolean
get() = settings.isReaderZoomButtonsEnabled
fun applyBackground(view: View) { fun applyBackground(view: View) {
val bg = settings.readerBackground val bg = settings.readerBackground
view.background = bg.resolve(view.context) view.background = bg.resolve(view.context)
@@ -74,6 +77,7 @@ class ReaderSettings(
key == AppSettings.KEY_ZOOM_MODE || key == AppSettings.KEY_ZOOM_MODE ||
key == AppSettings.KEY_PAGES_NUMBERS || key == AppSettings.KEY_PAGES_NUMBERS ||
key == AppSettings.KEY_WEBTOON_ZOOM || key == AppSettings.KEY_WEBTOON_ZOOM ||
key == AppSettings.KEY_READER_ZOOM_BUTTONS ||
key == AppSettings.KEY_READER_BACKGROUND key == AppSettings.KEY_READER_BACKGROUND
) { ) {
notifyChanged() notifyChanged()

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
abstract class BasePageHolder<B : ViewBinding>( abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B, protected val binding: B,
loader: PageLoader, loader: PageLoader,
private val settings: ReaderSettings, protected val settings: ReaderSettings,
networkState: NetworkState, networkState: NetworkState,
exceptionResolver: ExceptionResolver, exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback { ) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {

View File

@@ -66,7 +66,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
abstract fun switchPageTo(position: Int, smooth: Boolean) abstract fun switchPageTo(position: Int, smooth: Boolean)
open fun scrollBy(delta: Int): Boolean = false open fun scrollBy(delta: Int, smooth: Boolean): Boolean = false
abstract fun getCurrentState(): ReaderState? abstract fun getCurrentState(): ReaderState?

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.reader.ui.pager.reversed package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -23,12 +28,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint @AndroidEntryPoint
class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() { class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@@ -47,6 +55,11 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
adapter = readerAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@ReversedReaderFragment)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
recyclerView?.defaultFocusHighlightEnabled = false
}
PagerEventSupplier(this).attach()
} }
viewModel.pageAnimation.observe(viewLifecycleOwner) { viewModel.pageAnimation.observe(viewLifecycleOwner) {
@@ -69,6 +82,20 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
super.onDestroyView() super.onDestroyView()
} }
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
if (!withCtrl) {
switchPageBy(-axisValue.sign.toInt())
return true
}
}
}
return false
}
override fun onCreateAdapter() = ReversedPagesAdapter( override fun onCreateAdapter() = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
loader = pageLoader, loader = pageLoader,

View File

@@ -40,6 +40,12 @@ open class PageHolder(
@Suppress("LeakingThis") @Suppress("LeakingThis")
bindingInfo.buttonErrorDetails.setOnClickListener(this) bindingInfo.buttonErrorDetails.setOnClickListener(this)
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
binding.zoomControl.listener = SsivZoomListener(binding.ssiv)
}
override fun onConfigChanged() {
super.onConfigChanged()
binding.zoomControl.isVisible = settings.isZoomControlsEnabled
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.viewpager2.widget.ViewPager2
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.util.ext.recyclerView
class PagerEventSupplier(private val pager: ViewPager2) : View.OnKeyListener {
fun attach() {
pager.recyclerView?.setOnKeyListener(this)
}
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
val rootView = pager.recyclerView?.findViewHolderForAdapterPosition(pager.currentItem)?.itemView as? ViewGroup
?: return false
return rootView.children.firstNotNullOfOrNull { x ->
x as? SubsamplingScaleImageView
}?.dispatchKeyEvent(event) == true
}
}

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.reader.ui.pager.standard package org.koitharu.kotatsu.reader.ui.pager.standard
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -24,9 +29,11 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.sign
@AndroidEntryPoint @AndroidEntryPoint
class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() { class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
View.OnGenericMotionListener {
@Inject @Inject
lateinit var networkState: NetworkState lateinit var networkState: NetworkState
@@ -39,12 +46,20 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
container: ViewGroup?, container: ViewGroup?,
) = FragmentReaderStandardBinding.inflate(inflater, container, false) ) = FragmentReaderStandardBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(
binding: FragmentReaderStandardBinding,
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
with(binding.pager) { with(binding.pager) {
adapter = readerAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
setOnGenericMotionListener(this@PagerReaderFragment)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
recyclerView?.defaultFocusHighlightEnabled = false
}
PagerEventSupplier(this).attach()
} }
viewModel.pageAnimation.observe(viewLifecycleOwner) { viewModel.pageAnimation.observe(viewLifecycleOwner) {
@@ -67,28 +82,43 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
super.onDestroyView() super.onDestroyView()
} }
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope { override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
val items = async { if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
requireAdapter().setItems(pages) if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
yield() val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
} val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
if (pendingState != null) { if (!withCtrl) {
val position = pages.indexOfFirst { switchPageBy(-axisValue.sign.toInt())
it.chapterId == pendingState.chapterId && it.index == pendingState.page return true
}
} }
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
}
} else {
items.await()
} }
return false
} }
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) =
coroutineScope {
val items = async {
requireAdapter().setItems(pages)
yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
}
items.await()
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
}
} else {
items.await()
}
}
override fun onCreateAdapter() = PagesAdapter( override fun onCreateAdapter() = PagesAdapter(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
loader = pageLoader, loader = pageLoader,

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.animation.DecelerateInterpolator
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
class SsivZoomListener(
private val ssiv: SubsamplingScaleImageView,
) : ZoomControl.ZoomControlListener {
override fun onZoomIn() {
scaleBy(1.2f)
}
override fun onZoomOut() {
scaleBy(0.8f)
}
private fun scaleBy(factor: Float) {
val center = ssiv.getCenter() ?: return
val newScale = ssiv.scale * factor
ssiv.animateScaleAndCenter(newScale, center)?.apply {
withDuration(ssiv.resources.getInteger(android.R.integer.config_shortAnimTime).toLong())
withInterpolator(DecelerateInterpolator())
start()
}
}
}

View File

@@ -12,8 +12,8 @@ class WebtoonFrameLayout @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0, @AttrRes defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
val target by lazy(LazyThreadSafetyMode.NONE) { val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
findViewById<WebtoonImageView>(R.id.ssiv) findViewById(R.id.ssiv)
} }
fun dispatchVerticalScroll(dy: Int): Int { fun dispatchVerticalScroll(dy: Int): Int {

View File

@@ -3,9 +3,9 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context import android.content.Context
import android.graphics.PointF import android.graphics.PointF
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.view.ancestors
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.util.ext.parents
import org.koitharu.kotatsu.parsers.util.toIntUp import org.koitharu.kotatsu.parsers.util.toIntUp
private const val SCROLL_UNKNOWN = -1 private const val SCROLL_UNKNOWN = -1
@@ -93,7 +93,7 @@ class WebtoonImageView @JvmOverloads constructor(
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
computeScrollRange() computeScrollRange()
val container = parents.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return val container = ancestors.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
val parentHeight = parentHeight() val parentHeight = parentHeight()
if (scrollPos != 0 && container.bottom < parentHeight) { if (scrollPos != 0 && container.bottom < parentHeight) {
scrollTo(scrollRange) scrollTo(scrollRange)
@@ -115,6 +115,6 @@ class WebtoonImageView @JvmOverloads constructor(
} }
private fun parentHeight(): Int { private fun parentHeight(): Int {
return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0 return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
} }
} }

View File

@@ -3,11 +3,13 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
@@ -31,7 +33,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
@Inject @Inject
lateinit var pageLoader: PageLoader lateinit var pageLoader: PageLoader
private val scrollInterpolator = AccelerateDecelerateInterpolator() private val scrollInterpolator = DecelerateInterpolator()
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -45,10 +47,15 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
adapter = readerAdapter adapter = readerAdapter
addOnPageScrollListener(PageScrollListener()) addOnPageScrollListener(PageScrollListener())
} }
binding.zoomControl.listener = binding.frame
viewModel.isWebtoonZoomEnabled.observe(viewLifecycleOwner) { viewModel.isWebtoonZoomEnabled.observe(viewLifecycleOwner) {
binding.frame.isZoomEnable = it binding.frame.isZoomEnable = it
} }
combine(viewModel.isWebtoonZoomEnabled, viewModel.isZoomControlEnabled, Boolean::and)
.observe(viewLifecycleOwner) {
binding.zoomControl.isVisible = it
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -122,8 +129,12 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
requireViewBinding().recyclerView.firstVisibleItemPosition = position requireViewBinding().recyclerView.firstVisibleItemPosition = position
} }
override fun scrollBy(delta: Int): Boolean { override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
requireViewBinding().recyclerView.nestedScrollBy(0, delta) if (smooth && isAnimationEnabled()) {
requireViewBinding().recyclerView.smoothScrollBy(0, delta, scrollInterpolator)
} else {
requireViewBinding().recyclerView.nestedScrollBy(0, delta)
}
return true return true
} }

View File

@@ -7,12 +7,19 @@ import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector import android.view.GestureDetector
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.ViewConfiguration
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.OverScroller import android.widget.OverScroller
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewConfigurationCompat
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
private const val MAX_SCALE = 2.5f private const val MAX_SCALE = 2.5f
private const val MIN_SCALE = 0.5f private const val MIN_SCALE = 0.5f
@@ -21,7 +28,9 @@ class WebtoonScalingFrame @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyles: Int = 0, defStyles: Int = 0,
) : FrameLayout(context, attrs, defStyles), ScaleGestureDetector.OnScaleGestureListener { ) : FrameLayout(context, attrs, defStyles),
ScaleGestureDetector.OnScaleGestureListener,
ZoomControl.ZoomControlListener {
private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) as WebtoonRecyclerView } private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) as WebtoonRecyclerView }
@@ -40,6 +49,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private var halfHeight = 0f private var halfHeight = 0f
private val translateBounds = RectF() private val translateBounds = RectF()
private val targetHitRect = Rect() private val targetHitRect = Rect()
private var animator: ValueAnimator? = null
var isZoomEnable = true var isZoomEnable = true
set(value) { set(value) {
@@ -77,10 +87,79 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return super.dispatchTouchEvent(ev) return super.dispatchTouchEvent(ev)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onGenericMotionEvent(event: MotionEvent): Boolean {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) if (isZoomEnable && event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
halfWidth = measuredWidth / 2f if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
halfHeight = measuredHeight / 2f val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0
if (withCtrl) {
val axisValue =
event.getAxisValue(MotionEvent.AXIS_VSCROLL) * ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(context), context,
)
val newScale = (scale + axisValue).coerceIn(MIN_SCALE, MAX_SCALE)
scaleChild(newScale, event.x, event.y)
return true
}
}
}
return super.onGenericMotionEvent(event)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (!isZoomEnable) {
return super.onKeyDown(keyCode, event)
}
return when (keyCode) {
KeyEvent.KEYCODE_ZOOM_IN,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_PLUS -> {
onZoomIn()
true
}
KeyEvent.KEYCODE_ZOOM_OUT,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_MINUS -> {
onZoomOut()
true
}
KeyEvent.KEYCODE_ESCAPE -> {
smoothScaleTo(1f)
true
}
else -> super.onKeyDown(keyCode, event)
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
return if (isZoomEnable) {
keyCode == KeyEvent.KEYCODE_NUMPAD_ADD
|| keyCode == KeyEvent.KEYCODE_PLUS
|| keyCode == KeyEvent.KEYCODE_NUMPAD_SUBTRACT
|| keyCode == KeyEvent.KEYCODE_MINUS
|| keyCode == KeyEvent.KEYCODE_ZOOM_IN
|| keyCode == KeyEvent.KEYCODE_ZOOM_OUT
|| keyCode == KeyEvent.KEYCODE_ESCAPE
|| super.onKeyUp(keyCode, event)
} else {
super.onKeyUp(keyCode, event)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
halfWidth = w / 2f
halfHeight = h / 2f
}
override fun onZoomIn() {
smoothScaleTo(scale * 1.1f)
}
override fun onZoomOut() {
smoothScaleTo(scale * 0.9f)
} }
private fun invalidateTarget() { private fun invalidateTarget() {
@@ -154,14 +233,33 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true return true
} }
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
animator?.cancel()
animator = null
return true
}
override fun onScaleEnd(p0: ScaleGestureDetector) = Unit override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
private fun smoothScaleTo(target: Float) {
val newScale = target.coerceIn(MIN_SCALE, MAX_SCALE)
animator?.cancel()
animator = ValueAnimator.ofFloat(scale, newScale).apply {
setDuration(context.getAnimationDuration(android.R.integer.config_shortAnimTime))
interpolator = DecelerateInterpolator()
addUpdateListener { scaleChild(it.animatedValue as Float, halfWidth, halfHeight) }
start()
}
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable { private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
if (scale <= 1f) return false if (scale <= 1f) return false
transformMatrix.postTranslate(-distanceX, -distanceY) transformMatrix.postTranslate(-distanceX, -distanceY)
invalidateTarget() invalidateTarget()
@@ -181,7 +279,12 @@ class WebtoonScalingFrame @JvmOverloads constructor(
return true return true
} }
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
if (scale <= 1) return false if (scale <= 1) return false
overScroller.fling( overScroller.fling(
@@ -200,7 +303,10 @@ class WebtoonScalingFrame @JvmOverloads constructor(
override fun run() { override fun run() {
if (overScroller.computeScrollOffset()) { if (overScroller.computeScrollOffset()) {
transformMatrix.postTranslate(overScroller.currX - transX, overScroller.currY - transY) transformMatrix.postTranslate(
overScroller.currX - transX,
overScroller.currY - transY,
)
invalidateTarget() invalidateTarget()
postOnAnimation(this) postOnAnimation(this)
} }

View File

@@ -152,13 +152,6 @@ class PagesThumbnailsSheet :
override fun onScrolledToEnd(recyclerView: RecyclerView) { override fun onScrolledToEnd(recyclerView: RecyclerView) {
viewModel.loadNextChapter() viewModel.loadNextChapter()
} }
override fun onPostScrolled(recyclerView: RecyclerView, firstVisibleItemPosition: Int, visibleItemCount: Int) {
super.onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
if (firstVisibleItemPosition > offsetTop) {
viewModel.allowLoadAbove()
}
}
} }
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() { private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {

View File

@@ -16,7 +16,6 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.firstNotNull import org.koitharu.kotatsu.core.util.ext.firstNotNull
import org.koitharu.kotatsu.core.util.ext.firstNotNullOrNull
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
@@ -29,10 +28,11 @@ class PagesThumbnailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
private val chaptersLoader: ChaptersLoader, private val chaptersLoader: ChaptersLoader,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
) : BaseViewModel() { ) : BaseViewModel() {
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1 private val currentPageIndex: Int =
savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
@@ -46,7 +46,6 @@ class PagesThumbnailsViewModel @Inject constructor(
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var loadingPrevJob: Job? = null private var loadingPrevJob: Job? = null
private var loadingNextJob: Job? = null private var loadingNextJob: Job? = null
private var isLoadAboveAllowed = false
val thumbnails = MutableStateFlow<List<ListModel>>(emptyList()) val thumbnails = MutableStateFlow<List<ListModel>>(emptyList())
val branch = MutableStateFlow<String?>(null) val branch = MutableStateFlow<String?>(null)
@@ -60,17 +59,8 @@ class PagesThumbnailsViewModel @Inject constructor(
} }
} }
fun allowLoadAbove() {
if (!isLoadAboveAllowed) {
loadingJob = launchLoadingJob(Dispatchers.Default) {
isLoadAboveAllowed = true
updateList()
}
}
}
fun loadPrevChapter() { fun loadPrevChapter() {
if (!isLoadAboveAllowed || loadingJob?.isActive == true || loadingPrevJob?.isActive == true) { if (loadingJob?.isActive == true || loadingPrevJob?.isActive == true) {
return return
} }
loadingPrevJob = loadPrevNextChapter(isNext = false) loadingPrevJob = loadPrevNextChapter(isNext = false)
@@ -91,7 +81,6 @@ class PagesThumbnailsViewModel @Inject constructor(
private suspend fun updateList() { private suspend fun updateList() {
val snapshot = chaptersLoader.snapshot() val snapshot = chaptersLoader.snapshot()
val mangaChapters = mangaDetails.firstNotNullOrNull()?.chapters.orEmpty()
val pages = buildList(snapshot.size + chaptersLoader.size + 2) { val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
var previousChapterId = 0L var previousChapterId = 0L
for (page in snapshot) { for (page in snapshot) {

View File

@@ -17,14 +17,15 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -64,7 +65,7 @@ class MangaListActivity :
if (viewBinding.containerFilterHeader != null) { if (viewBinding.containerFilterHeader != null) {
viewBinding.appbar.addOnOffsetChangedListener(this) viewBinding.appbar.addOnOffsetChangedListener(this)
} }
val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source val source = intent.getStringExtra(EXTRA_SOURCE)?.let(::MangaSource) ?: tags?.firstOrNull()?.source
if (source == null) { if (source == null) {
finishAfterTransition() finishAfterTransition()
return return
@@ -186,11 +187,14 @@ class MangaListActivity :
private const val EXTRA_TAGS = "tags" private const val EXTRA_TAGS = "tags"
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"
const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
fun newIntent(context: Context, tags: Set<MangaTag>) = Intent(context, MangaListActivity::class.java) fun newIntent(context: Context, tags: Set<MangaTag>) = Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(EXTRA_TAGS, ParcelableMangaTags(tags)) .putExtra(EXTRA_TAGS, ParcelableMangaTags(tags))
fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java) fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java)
.putExtra(EXTRA_SOURCE, source) .setAction(ACTION_MANGA_EXPLORE)
.putExtra(EXTRA_SOURCE, source.name)
} }
} }

View File

@@ -7,6 +7,7 @@ import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.TextAppearanceSpan import android.text.style.TextAppearanceSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.SoundEffectConstants import android.view.SoundEffectConstants
@@ -59,7 +60,11 @@ class SearchEditText @JvmOverloads constructor(
} }
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers() && query.isNotEmpty()) { if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)
&& (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
&& event.hasNoModifiers()
&& query.isNotEmpty()
) {
cancelLongPress() cancelLongPress()
searchSuggestionListener?.onQueryClick(query, submit = true) searchSuggestionListener?.onQueryClick(query, submit = true)
clearFocus() clearFocus()

View File

@@ -30,7 +30,7 @@ class SyncSettingsFragment : BasePreferenceFragment(R.string.sync_settings), Fra
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) { return when (preference.key) {
SyncSettings.KEY_HOST -> { SyncSettings.KEY_HOST -> {
SyncHostDialogFragment.show(childFragmentManager) SyncHostDialogFragment.show(childFragmentManager, null)
true true
} }

View File

@@ -1,7 +1,10 @@
package org.koitharu.kotatsu.settings.about package org.koitharu.kotatsu.settings.about
import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Environment
import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -29,13 +32,26 @@ class AppUpdateDialog(private val context: Context) {
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(message) .setMessage(message)
.setIcon(R.drawable.ic_app_update) .setIcon(R.drawable.ic_app_update)
.setPositiveButton(R.string.download) { _, _ -> .setNeutralButton(R.string.open_in_browser) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri()) val intent = Intent(Intent.ACTION_VIEW, version.url.toUri())
context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser))) context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser)))
} }.setPositiveButton(R.string.update) { _, _ ->
.setNegativeButton(R.string.close, null) downloadUpdate(version)
}.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false) .setCancelable(false)
.create() .create()
.show() .show()
} }
private fun downloadUpdate(version: AppVersion) {
val url = version.apkUrl.toUri()
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(url)
.setTitle("${context.getString(R.string.app_name)} v${version.name}")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, url.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setMimeType("application/vnd.android.package-archive")
dm.enqueue(request)
Toast.makeText(context, R.string.download_started, Toast.LENGTH_SHORT).show()
}
} }

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.settings.about
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
class UpdateDownloadReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
if (downloadId == 0L) {
return
}
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
@Suppress("DEPRECATION")
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
installIntent.setDataAndType(
dm.getUriForDownloadedFile(downloadId),
dm.getMimeTypeForDownloadedFile(downloadId),
)
installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
try {
context.startActivity(installIntent)
} catch (e: ActivityNotFoundException) {
e.printStackTraceDebug()
}
}
}
}
}

View File

@@ -114,7 +114,7 @@ class NavConfigFragment : BaseFragment<FragmentSettingsSourcesBinding>(), Recycl
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,
): Boolean = true ): Boolean = target.itemViewType == ListItemType.NAV_ITEM.ordinal
override fun onMoved( override fun onMoved(
recyclerView: RecyclerView, recyclerView: RecyclerView,

View File

@@ -63,9 +63,13 @@ class NavConfigViewModel @Inject constructor(
} }
fun removeItem(item: NavItem) { fun removeItem(item: NavItem) {
items.value = items.value.minus(item).also { val newList = items.value.toMutableList()
commit(it) newList.remove(item)
if (newList.isEmpty()) {
newList.add(NavItem.EXPLORE)
} }
items.value = newList
commit(newList)
} }
private fun commit(value: List<NavItem>) { private fun commit(value: List<NavItem>) {

View File

@@ -59,6 +59,8 @@ class NewSourcesDialogFragment :
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemShortcutClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.onItemEnabledChanged(item, isEnabled) viewModel.onItemEnabledChanged(item, isEnabled)
} }

View File

@@ -15,7 +15,9 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
@@ -24,6 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.getItem import org.koitharu.kotatsu.core.util.ext.getItem
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
@@ -44,6 +47,9 @@ class SourcesManageFragment :
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@Inject
lateinit var shortcutManager: AppShortcutManager
private var reorderHelper: ItemTouchHelper? = null private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModels<SourcesManageViewModel>() private val viewModel by viewModels<SourcesManageViewModel>()
@@ -103,6 +109,12 @@ class SourcesManageFragment :
viewModel.bringToTop(item.source) viewModel.bringToTop(item.source)
} }
override fun onItemShortcutClick(item: SourceConfigItem.SourceItem) {
viewLifecycleScope.launch {
shortcutManager.requestPinShortcut(item.source)
}
}
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.setEnabled(item.source, isEnabled) viewModel.setEnabled(item.source, isEnabled)
} }

View File

@@ -8,6 +8,7 @@ import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan import android.text.style.SuperscriptSpan
import android.view.View import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import androidx.core.view.isGone import androidx.core.view.isGone
@@ -39,7 +40,7 @@ fun sourceConfigHeaderDelegate() =
ItemFilterHeaderBinding.inflate( ItemFilterHeaderBinding.inflate(
layoutInflater, layoutInflater,
parent, parent,
false false,
) )
}, },
) { ) {
@@ -76,7 +77,7 @@ fun sourceConfigItemCheckableDelegate(
ItemSourceConfigCheckableBinding.inflate( ItemSourceConfigCheckableBinding.inflate(
layoutInflater, layoutInflater,
parent, parent,
false false,
) )
}, },
) { ) {
@@ -121,7 +122,7 @@ fun sourceConfigItemDelegate2(
ItemSourceConfigBinding.inflate( ItemSourceConfigBinding.inflate(
layoutInflater, layoutInflater,
parent, parent,
false false,
) )
}, },
) { ) {
@@ -189,8 +190,8 @@ fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
ForegroundColorSpan( ForegroundColorSpan(
context.getThemeColor( context.getThemeColor(
com.google.android.material.R.attr.colorError, com.google.android.material.R.attr.colorError,
Color.RED Color.RED,
) ),
), ),
RelativeSizeSpan(0.74f), RelativeSizeSpan(0.74f),
SuperscriptSpan(), SuperscriptSpan(),
@@ -205,10 +206,13 @@ private fun showSourceMenu(
) { ) {
val menu = PopupMenu(anchor.context, anchor) val menu = PopupMenu(anchor.context, anchor)
menu.inflate(R.menu.popup_source_config) menu.inflate(R.menu.popup_source_config)
menu.menu.findItem(R.id.action_shortcut)
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context)
menu.setOnMenuItemClickListener { menu.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_settings -> listener.onItemSettingsClick(item) R.id.action_settings -> listener.onItemSettingsClick(item)
R.id.action_lift -> listener.onItemLiftClick(item) R.id.action_lift -> listener.onItemLiftClick(item)
R.id.action_shortcut -> listener.onItemShortcutClick(item)
} }
true true
} }

View File

@@ -9,6 +9,8 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
fun onItemLiftClick(item: SourceConfigItem.SourceItem) fun onItemLiftClick(item: SourceConfigItem.SourceItem)
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
fun onHeaderClick(header: SourceConfigItem.LocaleGroup) fun onHeaderClick(header: SourceConfigItem.LocaleGroup)

View File

@@ -7,6 +7,7 @@ import android.content.ContentProviderResult
import android.content.Context import android.content.Context
import android.content.OperationApplicationException import android.content.OperationApplicationException
import android.content.SyncResult import android.content.SyncResult
import android.content.SyncStats
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
@@ -67,7 +68,7 @@ class SyncHelper @AssistedInject constructor(
get() = TimeUnit.DAYS.toMillis(4) get() = TimeUnit.DAYS.toMillis(4)
@WorkerThread @WorkerThread
fun syncFavourites(syncResult: SyncResult) { fun syncFavourites(stats: SyncStats) {
val data = JSONObject() val data = JSONObject()
data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories()) data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories())
data.put(TABLE_FAVOURITES, getFavourites()) data.put(TABLE_FAVOURITES, getFavourites())
@@ -81,17 +82,18 @@ class SyncHelper @AssistedInject constructor(
val timestamp = response.getLong(FIELD_TIMESTAMP) val timestamp = response.getLong(FIELD_TIMESTAMP)
val categoriesResult = val categoriesResult =
upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp) upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
syncResult.stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES), timestamp) val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES), timestamp)
syncResult.stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L
syncResult.stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
} }
gcFavourites() gcFavourites()
} }
@WorkerThread @WorkerThread
fun syncHistory(syncResult: SyncResult) { fun syncHistory(stats: SyncStats) {
val data = JSONObject() val data = JSONObject()
data.put(TABLE_HISTORY, getHistory()) data.put(TABLE_HISTORY, getHistory())
data.put(FIELD_TIMESTAMP, System.currentTimeMillis()) data.put(FIELD_TIMESTAMP, System.currentTimeMillis())
@@ -105,8 +107,9 @@ class SyncHelper @AssistedInject constructor(
json = response.getJSONArray(TABLE_HISTORY), json = response.getJSONArray(TABLE_HISTORY),
timestamp = response.getLong(FIELD_TIMESTAMP), timestamp = response.getLong(FIELD_TIMESTAMP),
) )
syncResult.stats.numDeletes += result.first().count?.toLong() ?: 0L stats.numDeletes += result.first().count?.toLong() ?: 0L
syncResult.stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L } stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
} }
gcHistory() gcHistory()
} }

View File

@@ -104,7 +104,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
} }
R.id.button_settings -> { R.id.button_settings -> {
SyncHostDialogFragment.show(supportFragmentManager) SyncHostDialogFragment.show(supportFragmentManager, viewModel.host.value)
} }
} }
} }

View File

@@ -10,7 +10,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.domain.SyncAuthResult import org.koitharu.kotatsu.sync.domain.SyncAuthResult
import javax.inject.Inject import javax.inject.Inject
@@ -23,9 +22,7 @@ class SyncAuthViewModel @Inject constructor(
val onAccountAlreadyExists = MutableEventFlow<Unit>() val onAccountAlreadyExists = MutableEventFlow<Unit>()
val onTokenObtained = MutableEventFlow<SyncAuthResult>() val onTokenObtained = MutableEventFlow<SyncAuthResult>()
val host = MutableStateFlow("") val host = MutableStateFlow(context.getString(R.string.sync_host_default))
private val defaultHost = context.getString(R.string.sync_host_default)
init { init {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
@@ -38,7 +35,7 @@ class SyncAuthViewModel @Inject constructor(
} }
fun obtainToken(email: String, password: String) { fun obtainToken(email: String, password: String) {
val hostValue = host.value.ifNullOrEmpty { defaultHost } val hostValue = host.value
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
val token = api.authenticate(hostValue, email, password) val token = api.authenticate(hostValue, email, password)
val result = SyncAuthResult(host.value, email, password, token) val result = SyncAuthResult(host.value, email, password, token)

View File

@@ -13,6 +13,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.sync.data.SyncSettings
@@ -50,7 +52,7 @@ class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletet
binding.message.setText(R.string.sync_host_description) binding.message.setText(R.string.sync_host_description)
val entries = binding.root.resources.getStringArray(R.array.sync_host_list) val entries = binding.root.resources.getStringArray(R.array.sync_host_list)
val editText = binding.edit val editText = binding.edit
editText.setText(syncSettings.host) editText.setText(arguments?.getString(KEY_HOST).ifNullOrEmpty { syncSettings.host })
editText.threshold = 0 editText.threshold = 0
editText.setAdapter(ArrayAdapter(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries)) editText.setAdapter(ArrayAdapter(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries))
binding.dropdown.setOnClickListener { binding.dropdown.setOnClickListener {
@@ -76,6 +78,8 @@ class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletet
const val REQUEST_KEY = "sync_host" const val REQUEST_KEY = "sync_host"
const val KEY_HOST = "host" const val KEY_HOST = "host"
fun show(fm: FragmentManager) = SyncHostDialogFragment().show(fm, TAG) fun show(fm: FragmentManager, host: String?) = SyncHostDialogFragment().withArgs(1) {
putString(KEY_HOST, host)
}.show(fm, TAG)
} }
} }

View File

@@ -28,7 +28,7 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java) val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
val syncHelper = entryPoint.syncHelperFactory.create(account, provider) val syncHelper = entryPoint.syncHelperFactory.create(account, provider)
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncFavourites(syncResult) syncHelper.syncFavourites(syncResult.stats)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure { e -> }.onFailure { e ->
syncResult.onError(e) syncResult.onError(e)

View File

@@ -28,7 +28,7 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java) val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
val syncHelper = entryPoint.syncHelperFactory.create(account, provider) val syncHelper = entryPoint.syncHelperFactory.create(account, provider)
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncHistory(syncResult) syncHelper.syncHistory(syncResult.stats)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure { e -> }.onFailure { e ->
syncResult.onError(e) syncResult.onError(e)

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5A6.5,6.5 0 0,0 9.5,3A6.5,6.5 0 0,0 3,9.5A6.5,6.5 0 0,0 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.5L20.5,19L15.5,14M9.5,14C7,14 5,12 5,9.5C5,7 7,5 9.5,5C12,5 14,7 14,9.5C14,12 12,14 9.5,14M7,9H12V10H7V9Z" />
</vector>

View File

@@ -26,6 +26,17 @@
<solid android:color="@color/selector_overlay" /> <solid android:color="@color/selector_overlay" />
</shape> </shape>
</item> </item>
<item
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:state_hovered="true"
android:top="2dp">
<shape android:shape="rectangle">
<corners android:radius="@dimen/list_selector_corner" />
<solid android:color="@color/selector_overlay" />
</shape>
</item>
<item <item
android:bottom="2dp" android:bottom="2dp"
android:left="2dp" android:left="2dp"

View File

@@ -64,6 +64,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:navigationContentDescription="@string/search"
app:navigationIcon="?attr/actionModeWebSearchDrawable"> app:navigationIcon="?attr/actionModeWebSearchDrawable">
<org.koitharu.kotatsu.search.ui.widget.SearchEditText <org.koitharu.kotatsu.search.ui.widget.SearchEditText

View File

@@ -48,6 +48,7 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:navigationContentDescription="@string/search"
app:navigationIcon="?attr/actionModeWebSearchDrawable"> app:navigationIcon="?attr/actionModeWebSearchDrawable">
<org.koitharu.kotatsu.search.ui.widget.SearchEditText <org.koitharu.kotatsu.search.ui.widget.SearchEditText

View File

@@ -3,4 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false" />

View File

@@ -2,13 +2,29 @@
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame <org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frame" android:id="@+id/frame"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:focusable="true">
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView <org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" /> app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
android:id="@+id/zoomControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible" />
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame> </org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>

View File

@@ -10,6 +10,8 @@
android:id="@+id/ssiv" android:id="@+id/ssiv"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
app:restoreStrategy="deferred" /> app:restoreStrategy="deferred" />
<TextView <TextView
@@ -25,4 +27,14 @@
<include layout="@layout/layout_page_info" /> <include layout="@layout/layout_page_info" />
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
android:id="@+id/zoomControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>

View File

@@ -3,12 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false">
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonImageView <org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonImageView
android:id="@+id/ssiv" android:id="@+id/ssiv"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false"
android:minHeight="1dp" android:minHeight="1dp"
app:panEnabled="false" app:panEnabled="false"
app:quickScaleEnabled="false" app:quickScaleEnabled="false"

View File

@@ -60,6 +60,7 @@
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/abc_ic_menu_overflow_material" android:src="@drawable/abc_ic_menu_overflow_material"
android:tooltipText="@string/more"
app:tint="?colorControlNormal" /> app:tint="?colorControlNormal" />
<ImageView <ImageView
@@ -70,7 +71,8 @@
android:contentDescription="@string/add" android:contentDescription="@string/add"
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_add" /> android:src="@drawable/ic_add"
android:tooltipText="@string/add" />
<ImageView <ImageView
android:id="@+id/imageView_remove" android:id="@+id/imageView_remove"
@@ -80,6 +82,7 @@
android:contentDescription="@string/remove" android:contentDescription="@string/remove"
android:padding="@dimen/margin_small" android:padding="@dimen/margin_small"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_delete" /> android:src="@drawable/ic_delete"
android:tooltipText="@string/remove" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.LinearLayout">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/button_zoom_in"
android:layout_width="?minTouchTargetSize"
android:layout_height="?minTouchTargetSize"
android:layout_margin="4dp"
android:alpha="0.8"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/zoom_in"
android:padding="1dp"
android:scaleType="centerInside"
android:src="@drawable/ic_zoom_in"
android:tooltipText="@string/zoom_in"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/button_zoom_out"
android:layout_width="?minTouchTargetSize"
android:layout_height="?minTouchTargetSize"
android:layout_margin="4dp"
android:alpha="0.8"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/zoom_out"
android:padding="1dp"
android:scaleType="centerInside"
android:src="@drawable/ic_zoom_out"
android:tooltipText="@string/zoom_out"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp" />
</merge>

View File

@@ -7,7 +7,8 @@
android:id="@+id/action_app_update" android:id="@+id/action_app_update"
android:icon="@drawable/ic_app_update" android:icon="@drawable/ic_app_update"
android:orderInCategory="1" android:orderInCategory="1"
android:title="@string/update" android:title="@string/app_update_available"
android:titleCondensed="@string/update"
android:visible="false" android:visible="false"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

View File

@@ -6,6 +6,10 @@
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:title="@string/settings" /> android:title="@string/settings" />
<item
android:id="@+id/action_shortcut"
android:title="@string/create_shortcut" />
<item <item
android:id="@+id/action_hide" android:id="@+id/action_hide"
android:title="@string/hide" /> android:title="@string/hide" />

View File

@@ -6,6 +6,10 @@
android:id="@+id/action_lift" android:id="@+id/action_lift"
android:title="@string/to_top" /> android:title="@string/to_top" />
<item
android:id="@+id/action_shortcut"
android:title="@string/create_shortcut" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:title="@string/settings" /> android:title="@string/settings" />

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="detailed_list">تفاصيل القائمة</string> <string name="detailed_list">تفاصيل القائمة</string>
<string name="error_occurred">حدث خطأ</string> <string name="error_occurred">حدث خطأ</string>
<string name="details">التفاصيل</string> <string name="details">التفاصيل</string>
@@ -19,7 +19,7 @@
<string name="history">السجل</string> <string name="history">السجل</string>
<string name="list">قائمة</string> <string name="list">قائمة</string>
<string name="clear_history">محو سجل</string> <string name="clear_history">محو سجل</string>
<string name="add_to_favourites">ضع هذا في المفضلة</string> <string name="add_to_favourites">أضف للمفضلة</string>
<string name="add">أضف</string> <string name="add">أضف</string>
<string name="save">حفظ</string> <string name="save">حفظ</string>
<string name="history_is_empty">لا سجل بعد</string> <string name="history_is_empty">لا سجل بعد</string>
@@ -48,11 +48,11 @@
<string name="clear">أزل</string> <string name="clear">أزل</string>
<string name="remove">ازالة</string> <string name="remove">ازالة</string>
<string name="popular">شائع</string> <string name="popular">شائع</string>
<string name="add_new_category">قائمة جديدة</string> <string name="add_new_category">أضف فئة جديدة</string>
<string name="download_complete">تم التنزيل</string> <string name="download_complete">تم التنزيل</string>
<string name="text_clear_history_prompt">هل تريد محو سجل القراءة بالكامل بشكل دائم؟</string> <string name="text_clear_history_prompt">هل تريد محو سجل القراءة بالكامل بشكل دائم؟</string>
<string name="save_page">احفظ الصفحة</string> <string name="save_page">احفظ الصفحة</string>
<string name="page_saved">حفظت</string> <string name="page_saved">تم الحفظ</string>
<string name="standard">اساسي</string> <string name="standard">اساسي</string>
<string name="no_description">لا يوجد وصف</string> <string name="no_description">لا يوجد وصف</string>
<string name="clear_pages_cache">مسح ذاكرة التخزين المؤقت للصفحة</string> <string name="clear_pages_cache">مسح ذاكرة التخزين المؤقت للصفحة</string>
@@ -61,7 +61,7 @@
<string name="search_on_s">بحث على %s</string> <string name="search_on_s">بحث على %s</string>
<string name="delete_manga">حذف المانغا</string> <string name="delete_manga">حذف المانغا</string>
<string name="text_delete_local_manga">حذف \"%s\" من الجهاز نهائيا؟</string> <string name="text_delete_local_manga">حذف \"%s\" من الجهاز نهائيا؟</string>
<string name="reader_settings">إعدادات القراءة</string> <string name="reader_settings">إعدادات القارىء</string>
<string name="switch_pages">تغییر صفحات</string> <string name="switch_pages">تغییر صفحات</string>
<string name="delete">حذف</string> <string name="delete">حذف</string>
<string name="share_image">شارك الصورة</string> <string name="share_image">شارك الصورة</string>
@@ -71,8 +71,110 @@
<string name="grid_size">حجم الشبكة</string> <string name="grid_size">حجم الشبكة</string>
<string name="volume_buttons">أزرار الصوت</string> <string name="volume_buttons">أزرار الصوت</string>
<string name="taps_on_edges">النقر على حواف الشاشة</string> <string name="taps_on_edges">النقر على حواف الشاشة</string>
<string name="_continue">يكمل</string> <string name="_continue">أكمل</string>
<string name="error">خطاء</string> <string name="error">خطأ</string>
<string name="clear_search_history">مسح تاريخ البحث</string> <string name="clear_search_history">مسح تاريخ البحث</string>
<string name="disable_nsfw">تعطيل NSFW</string> <string name="disable_nsfw">تعطيل NSFW</string>
<string name="updates">التحديثات</string>
<string name="sync_host_description">يمكنك استخدام سيرفر التزامن ذاتياً أو السيرفر الافتراضي. لا تغير هذا إن لم تكن متأكداً مما تفعله.</string>
<string name="text_clear_cookies_prompt">سيتم تسجيل خروجك من جميع المصادر</string>
<string name="clear_cookies">مسح ملفات تعريف الارتباط</string>
<string name="favourites_categories">الفئات المفضلة</string>
<string name="enabled_sources">المصادر المستخدمة</string>
<string name="gestures_only">الإيماءات فقط</string>
<string name="clear_thumbs_cache">مسح ذاكرة التخزين المؤقت للصور المصغرة</string>
<string name="rotate_screen">تدوير الشاشة</string>
<string name="text_clear_updates_feed_prompt">هل تريد مسح سجل التحديث بشكل دائم؟</string>
<string name="suggestions_enable">تفعيل الاقتراحات</string>
<string name="clear_feed">مسح الخلاصة</string>
<string name="welcome">مرحبا</string>
<string name="about_app_translation_summary">ترجمة هذا التطبيق</string>
<string name="vibration">اهتزاز</string>
<string name="no_update_available">لا يوجد تحديثات</string>
<string name="remove_category">حذف الفئة</string>
<string name="internal_storage">التخزين الداخلي</string>
<string name="read_later">اقرأ لاحقا</string>
<string name="backup_saved">تم حفظ النسخة الاحتياطية</string>
<string name="create_backup">إنشاء نسخة احتياطية</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">تمكين %1$d من %2$d</string>
<string name="tap_to_try_again">انقر لإعادة المحاولة</string>
<string name="ignore_ssl_errors">تجاهل أخطاء SSL</string>
<string name="auth_required">سجل الدخول لمشاهدة المحتوى</string>
<string name="next">التالي</string>
<string name="restore_backup">استعادة من نسخة احتياطية</string>
<string name="password_length_hint">كلمة السر يجب أن تكون 4 أحرف أو أكثر</string>
<string name="server_address">عنوان السيرفر</string>
<string name="text_feed_holder">فصول جديدة من ما تقرأه تظهر هنا</string>
<string name="text_suggestion_holder">ابدأ بقراءة المانجا وستحصل على اقتراحات مخصصة</string>
<string name="find_similar">ابحث عن متشابه</string>
<string name="data_restored">تم الاستعادة</string>
<string name="protect_application_subtitle">أدخل كلمة السر لبدء التطبيق</string>
<string name="suggestions">الاقتراحات</string>
<string name="enabled">مفعل</string>
<string name="text_clear_search_history_prompt">هل تريد إزالة استعلامات البحث الأخيرة نهائيًا؟</string>
<string name="updates_feed_cleared">تم المسح</string>
<string name="update">تحديث</string>
<string name="feed_will_update_soon">سيبدأ تحديث الخلاصة قريبًا</string>
<string name="app_update_available">تتوفر نسخة جديدة من التطبيق</string>
<string name="new_version_s">نسخة جديدة: %s</string>
<string name="sync_settings">إعدادات التزامن</string>
<string name="create_category">أضف فئة جديدة</string>
<string name="notification_sound">صوت الإشعار</string>
<string name="backup_restore">النسخ الاحتياطي و الاستعادة</string>
<string name="show_pages_numbers">إظهار أرقام الصفحات</string>
<string name="search_history_cleared">تم المسح</string>
<string name="_s_deleted_from_local_storage">تم ازالة \"%s\" من التخزين المحلي</string>
<string name="saved_manga">المانجا المحفوظة</string>
<string name="open_in_browser">الفتح في المتصفح</string>
<string name="about_app_translation">ترجمة</string>
<string name="notifications">الإشعارات</string>
<string name="reverse">العكس</string>
<string name="track_sources">البحث عن تحديثات</string>
<string name="wrong_password">كلمة سر خاطئة</string>
<string name="group">المجموعة</string>
<string name="just_now">الآن</string>
<string name="download">تنزيل</string>
<string name="chapter_is_missing">الفصل مفقود</string>
<string name="size_s">الحجم: %s</string>
<string name="about">حول</string>
<string name="check_for_new_chapters">التحقق من الفصول الجديدة</string>
<string name="captcha_solve">حل</string>
<string name="data_restored_with_errors">أستعيدت البيانات، لكن هناك أخطاء</string>
<string name="new_chapters">فصول جديدة</string>
<string name="exit_confirmation">تأكيد الخروج</string>
<string name="protect_application">حماية التطبيق</string>
<string name="passwords_mismatch">كلمة السر غير مطابقة</string>
<string name="yesterday">أمس</string>
<string name="check_for_updates">تحقق من وجود تحديثات</string>
<string name="protect_application_summary">اطلب كلمة السر عند تشغيل كوتاتسو</string>
<string name="right_to_left">من اليمين الى اليسار</string>
<string name="reader_mode_hint">سيتم تذكر الاعدادات المختارة لهذه المانجا</string>
<string name="default_s">الافتراضي: %s</string>
<string name="confirm">تأكد</string>
<string name="clear_updates_feed">مسح موجز التحديثات</string>
<string name="disabled">معطل</string>
<string name="long_ago">منذ فترة</string>
<string name="notifications_settings">إعدادات الإشعارات</string>
<string name="save_manga">حفظ</string>
<string name="large_manga_save_confirm">تحتوي هذه المانجا على s%. حفظ الكل؟</string>
<string name="read_more">اقرأ المزيد</string>
<string name="search_results">نتائج البحث</string>
<string name="file_not_found">الملف غير موجود</string>
<string name="app_version">نسخة %s</string>
<string name="cookies_cleared">تمت إزالة جميع ملفات تعريف الارتباط</string>
<string name="state_finished">انتهت</string>
<string name="state_ongoing">مستمرة</string>
<string name="suggestions_summary">أقترح المانجا بناء على تفضيلاتك</string>
<string name="preparing_">جارٍ التحضير…</string>
<string name="exit_confirmation_summary">اضغط مرتين للخروج من التطبيق</string>
<string name="enter_password">ادخل كلمة السر</string>
<string name="repeat_password">كرر كلمة السر</string>
<string name="data_restored_success">استعيدت جميع البيانات</string>
<string name="backup_information">يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمفضلة واستعادتها</string>
<string name="available_sources">المصادر المتاحة</string>
<string name="external_storage">التخزين الخارجي</string>
<string name="silent">صامت</string>
<string name="today">اليوم</string>
<string name="system_default">الافتراضي</string>
<string name="sign_in">تسجبل الدخول</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="local_storage">На прыладзе</string> <string name="local_storage">Лакальнае сховішча</string>
<string name="favourites">Абраныя</string> <string name="favourites">Абраныя</string>
<string name="history">Гісторыя</string> <string name="history">Гісторыя</string>
<string name="error_occurred">Адбылася памылка</string> <string name="error_occurred">Адбылася памылка</string>
@@ -50,7 +50,7 @@
<string name="clear">Ачысціць</string> <string name="clear">Ачысціць</string>
<string name="text_clear_history_prompt">Вы ўпэўненыя, што жадаеце ачысціць гісторыю\?</string> <string name="text_clear_history_prompt">Вы ўпэўненыя, што жадаеце ачысціць гісторыю\?</string>
<string name="remove">Выдаліць</string> <string name="remove">Выдаліць</string>
<string name="_s_deleted_from_local_storage">«%s» выдалена з прылады</string> <string name="_s_deleted_from_local_storage">«%s» выдалены з лакальнага сховішча</string>
<string name="save_page">Захаваць старонку</string> <string name="save_page">Захаваць старонку</string>
<string name="page_saved">Старонка захавана</string> <string name="page_saved">Старонка захавана</string>
<string name="share_image">Падзяліцца выявай</string> <string name="share_image">Падзяліцца выявай</string>
@@ -472,4 +472,11 @@
<string name="advanced">Пашыраныя</string> <string name="advanced">Пашыраныя</string>
<string name="default_section">Раздзел па змаўчанні</string> <string name="default_section">Раздзел па змаўчанні</string>
<string name="manga_list">Спіс мангі</string> <string name="manga_list">Спіс мангі</string>
<string name="error_corrupted_file">Вяртаюцца няправільныя дадзеныя ці файл пашкоджаны</string>
<string name="on_device">На прыладзе</string>
<string name="moved_to_top">Перанесены ўверх</string>
<string name="items_limit_exceeded">Больш нельга дадаваць элементы</string>
<string name="directories">Каталогі</string>
<string name="main_screen_sections">Раздзелы галоўнага экрана</string>
<string name="to_top">Уверх</string>
</resources> </resources>

View File

@@ -472,4 +472,11 @@
<string name="advanced">Avanzado</string> <string name="advanced">Avanzado</string>
<string name="default_section">Sección predeterminada</string> <string name="default_section">Sección predeterminada</string>
<string name="manga_list">Lista de mangas</string> <string name="manga_list">Lista de mangas</string>
<string name="error_corrupted_file">Los datos que devuelve el archivo no son válidos o el archivo está dañado</string>
<string name="on_device">En el dispositivo</string>
<string name="moved_to_top">Movido hacia arriba</string>
<string name="items_limit_exceeded">No se pueden añadir más elementos</string>
<string name="directories">Directorios</string>
<string name="main_screen_sections">Secciones de la pantalla principal</string>
<string name="to_top">Hasta arriba</string>
</resources> </resources>

View File

@@ -467,4 +467,10 @@
<string name="unknown">Di-alam</string> <string name="unknown">Di-alam</string>
<string name="in_progress">Isinasagawa</string> <string name="in_progress">Isinasagawa</string>
<string name="disable_nsfw">I-disable ang NSFW</string> <string name="disable_nsfw">I-disable ang NSFW</string>
<string name="error_corrupted_file">Ang hindi wastong data ay ibinalik nito o ang file ay nasira</string>
<string name="related_manga_summary">Magpakita ng listahan ng mga kaugnay na manga. Sa ilang mga kaso, ito ay maaaring may mali o nawawala</string>
<string name="advanced">Advanced</string>
<string name="too_many_requests_message">Masyadong maraming request. Subukang ulit mamaya</string>
<string name="default_section">Default na seksyon</string>
<string name="manga_list">Listahan ng Manga</string>
</resources> </resources>

View File

@@ -472,4 +472,9 @@
<string name="advanced">Lanjutan</string> <string name="advanced">Lanjutan</string>
<string name="default_section">Bagian default</string> <string name="default_section">Bagian default</string>
<string name="manga_list">Daftar manga</string> <string name="manga_list">Daftar manga</string>
<string name="error_corrupted_file">Data yang dikembalikan tidak valid atau file rusak</string>
<string name="on_device">Dari perangkat</string>
<string name="items_limit_exceeded">Tidak ada lagi item yang bisa ditambahkan</string>
<string name="directories">Direktori</string>
<string name="main_screen_sections">Bagian layar utama</string>
</resources> </resources>

View File

@@ -273,7 +273,7 @@
<string name="show_reading_indicators_summary">履歴とお気に入りに既読率を表示する</string> <string name="show_reading_indicators_summary">履歴とお気に入りに既読率を表示する</string>
<string name="clear_cookies_summary">いくつかの問題の場合に助けることができる。すべての認証が無効になります</string> <string name="clear_cookies_summary">いくつかの問題の場合に助けることができる。すべての認証が無効になります</string>
<string name="show_reading_indicators">読書の進行状況インジケーターを表示</string> <string name="show_reading_indicators">読書の進行状況インジケーターを表示</string>
<string name="exclude_nsfw_from_history_summary">NSFWとマークされたマンガは履歴に追加されず、進行状況保存されない</string> <string name="exclude_nsfw_from_history_summary">NSFWとしてマークされたマンガは履歴に追加されず、進行状況保存されません</string>
<string name="show_all">すべて表示</string> <string name="show_all">すべて表示</string>
<string name="invalid_domain_message">無効なドメイン</string> <string name="invalid_domain_message">無効なドメイン</string>
<string name="select_range">範囲を選択</string> <string name="select_range">範囲を選択</string>
@@ -350,13 +350,13 @@
<string name="nothing_here">ここには何もありません</string> <string name="nothing_here">ここには何もありません</string>
<string name="scrobbling_empty_hint">読書の進捗状況を確認するには、マンガの詳細画面で「メニュー」→「追跡」を選択します。</string> <string name="scrobbling_empty_hint">読書の進捗状況を確認するには、マンガの詳細画面で「メニュー」→「追跡」を選択します。</string>
<string name="services">サービス</string> <string name="services">サービス</string>
<string name="enable_logging_summary">デバッグ目的でいくつかのアクションを記録する</string> <string name="enable_logging_summary">デバッグ目的でいくつかのアクションを記録します。 何をしているのか分からない場合はオンにしないでください</string>
<string name="theme_name_miku">ミク</string> <string name="theme_name_miku">ミク</string>
<string name="user_agent">ユーザー エージェント ヘッダー</string> <string name="user_agent">ユーザー エージェント ヘッダー</string>
<string name="prefetch_content">コンテンツのプリロード</string> <string name="prefetch_content">コンテンツのプリロード</string>
<string name="share_logs">ログを共有</string> <string name="share_logs">ログを共有</string>
<string name="allow_unstable_updates">不安定な更新を許可</string> <string name="allow_unstable_updates">不安定な更新を許可</string>
<string name="allow_unstable_updates_summary">アプリのベータ版へのアップデートを提案す</string> <string name="allow_unstable_updates_summary">不安定なビルドに関する通知を受け取</string>
<string name="download_started">ダウンロードが開始されました</string> <string name="download_started">ダウンロードが開始されました</string>
<string name="language">言語</string> <string name="language">言語</string>
<string name="source_disabled">ソースが無効になっています</string> <string name="source_disabled">ソースが無効になっています</string>
@@ -401,7 +401,7 @@
<string name="speed">速度</string> <string name="speed">速度</string>
<string name="ignore_ssl_errors">SSLエラーを無視する</string> <string name="ignore_ssl_errors">SSLエラーを無視する</string>
<string name="mirror_switching">ミラーを自動的に選択する</string> <string name="mirror_switching">ミラーを自動的に選択する</string>
<string name="mirror_switching_summary">ミラーがある場合、エラー時にリモートソースのドメインを自動切り替える</string> <string name="mirror_switching_summary">ミラーが利用可能な場合、エラー時にマンガ ソースのドメインを自動的に切り替える</string>
<string name="resume">履歴書</string> <string name="resume">履歴書</string>
<string name="paused">一時停止</string> <string name="paused">一時停止</string>
<string name="cancel_all">全てキャンセル</string> <string name="cancel_all">全てキャンセル</string>
@@ -466,4 +466,17 @@
<string name="languages">言語</string> <string name="languages">言語</string>
<string name="unknown">不明</string> <string name="unknown">不明</string>
<string name="in_progress">進行状況</string> <string name="in_progress">進行状況</string>
<string name="error_corrupted_file">無効なデータが返されたか、ファイルが破損しています</string>
<string name="related_manga_summary">関連漫画のリストを表示します。場合によっては不正確または欠落している可能性があります</string>
<string name="advanced">高度</string>
<string name="too_many_requests_message">リクエストが多すぎます。 後でもう一度お試しください</string>
<string name="default_section">デフォルトセクション</string>
<string name="manga_list">漫画一覧</string>
<string name="disable_nsfw">NSFWを非表示にする</string>
<string name="on_device">デバイス上</string>
<string name="items_limit_exceeded">これ以上アイテムを追加出来ません</string>
<string name="directories">ディレクトリ</string>
<string name="main_screen_sections">メイン画面のセクション</string>
<string name="moved_to_top">上部に移動しました</string>
<string name="to_top">最上部へ移動</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="local_storage">На устройстве</string> <string name="local_storage">Локальное хранилище</string>
<string name="favourites">Избранное</string> <string name="favourites">Избранное</string>
<string name="history">История</string> <string name="history">История</string>
<string name="error_occurred">Произошла ошибка</string> <string name="error_occurred">Произошла ошибка</string>
@@ -472,4 +472,15 @@
<string name="advanced">Расширенные</string> <string name="advanced">Расширенные</string>
<string name="default_section">Раздел по умолчанию</string> <string name="default_section">Раздел по умолчанию</string>
<string name="manga_list">Список манги</string> <string name="manga_list">Список манги</string>
<string name="error_corrupted_file">Возвращаются неверные данные или файл поврежден</string>
<string name="on_device">На устройстве</string>
<string name="moved_to_top">Перемещено наверх</string>
<string name="items_limit_exceeded">Больше нельзя добавлять элементы</string>
<string name="directories">Каталоги</string>
<string name="main_screen_sections">Разделы главного экрана</string>
<string name="to_top">Наверх</string>
<string name="zoom_in">Приблизить</string>
<string name="reader_zoom_buttons_summary">Показывать или нет кнопки управления масштабом в правом нижнем углу</string>
<string name="reader_zoom_buttons">Отображать кнопки масштабирования</string>
<string name="zoom_out">Отдалить</string>
</resources> </resources>

View File

@@ -269,4 +269,36 @@
<string name="status_on_hold">พักไว้</string> <string name="status_on_hold">พักไว้</string>
<string name="disable_all">ปิดการใช้งานทั้งหมด</string> <string name="disable_all">ปิดการใช้งานทั้งหมด</string>
<string name="report">รีพอร์ต</string> <string name="report">รีพอร์ต</string>
<string name="text_clear_cookies_prompt">คุณจะออกจากระบบจากทุกแหล่ง</string>
<string name="clear_all_history">ลบประวัติทั้งหมด</string>
<string name="data_deletion">การลบข้อมูล</string>
<string name="show_reading_indicators">แสดงตัวบอกความคืบหน้าในการอ่าน</string>
<string name="show_notification_new_chapters_off">คุณจะไม่ได้รับการแจ้งเตือน แต่บทใหม่จะถูกเน้นในรายการ</string>
<string name="text_clear_search_history_prompt">ลบคำค้นหาล่าสุดทั้งหมดอย่างถาวรไหม\?</string>
<string name="filter_load_error">ไม่สามารถโหลดรายการประเภทได้</string>
<string name="detect_reader_mode_summary">ตรวจสอบอัตโนมัติว่ามังงะเป็นเว็บตูนหรือไม่</string>
<string name="appwidget_recent_description">มังงะที่คุณอ่านล่าสุด</string>
<string name="appearance">รูปร่าง</string>
<string name="bookmark_remove">ลบบุ๊คมาร์ค</string>
<string name="auth_not_supported_by">%s ไม่รองรับการเข้าสู่ระบบ</string>
<string name="last_2_hours">2 ชั่วโมงที่ผ่านมา</string>
<string name="edit_category">แก้ไขหมวดหมู่</string>
<string name="bookmark_removed">บุ๊คมาร์คถูกลบแล้ว</string>
<string name="suggestions_excluded_genres_summary">ระบุประเภทที่คุณไม่ต้องการเห็นในคำแนะนำ</string>
<string name="dns_over_https">DNS บน HTTPS</string>
<string name="appwidget_shelf_description">มังงะจากรายการโปรดของคุณ</string>
<string name="bookmark_add">เพิ่มบุ๊คมาร์ค</string>
<string name="logged_in_as">เข้าสู่ระบบด้วย %s</string>
<string name="history_cleared">ลบประวัติแล้ว</string>
<string name="bookmark_added">บุ๊คมาร์คได้ถูกเพิ่มแล้ว</string>
<string name="suggestions_excluded_genres">ยกเว้นประเภท</string>
<string name="exclude_nsfw_from_history_summary">มังงะที่เป็น NSFW จะไม่ถูกเพิ่มเข้าไปในประวัติ และความคืบหน้าของคุณจะไม่ถูกบันทึก</string>
<string name="show_reading_indicators_summary">แสดงเปอร์เซ็นต์การอ่านในประวัติและรายการโปรด</string>
<string name="manage">จัดการ</string>
<string name="logout">ออกจากระบบ</string>
<string name="no_bookmarks_yet">ยังไม่มีบุ๊คมาร์ก</string>
<string name="bookmarks">บุ๊คมาร์ค</string>
<string name="show_all">แสดงทั้งหมด</string>
<string name="empty_favourite_categories">ไม่มีหมวดหมู่ที่ชื่นชอบ</string>
<string name="invalid_domain_message">โดเมนไม่ถูกต้อง</string>
</resources> </resources>

View File

@@ -472,4 +472,11 @@
<string name="advanced">Розширені</string> <string name="advanced">Розширені</string>
<string name="default_section">Розділ за замовчуванням</string> <string name="default_section">Розділ за замовчуванням</string>
<string name="manga_list">Список манґи</string> <string name="manga_list">Список манґи</string>
<string name="error_corrupted_file">Повертаються неправильні дані або файл пошкоджено</string>
<string name="on_device">На пристрої</string>
<string name="moved_to_top">Перенесено вгору</string>
<string name="items_limit_exceeded">Більше не можна додавати елементи</string>
<string name="directories">Каталоги</string>
<string name="main_screen_sections">Розділи головного екрана</string>
<string name="to_top">Вгору</string>
</resources> </resources>

View File

@@ -1,21 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<plurals name="items"> <plurals name="items">
<item quantity="other">%1$d 个项目</item> <item quantity="other">%1$d 个项目</item>
</plurals> </plurals>
<plurals name="new_chapters"> <plurals name="new_chapters">
<item quantity="other">%1$d 个新章节</item> <item quantity="other">%1$d 个新章节</item>
</plurals> </plurals>
<plurals name="chapters"> <plurals name="chapters">
<item quantity="other">%1$d 个章节</item> <item quantity="other">%1$d 个章节</item>
</plurals> </plurals>
<plurals name="minutes_ago"> <plurals name="minutes_ago">
<item quantity="other">%1$d 分钟前</item> <item quantity="other">%1$d 分钟前</item>
</plurals> </plurals>
<plurals name="hours_ago"> <plurals name="hours_ago">
<item quantity="other">%1$d 小时前</item> <item quantity="other">%1$d 小时前</item>
</plurals> </plurals>
<plurals name="days_ago"> <plurals name="days_ago">
<item quantity="other">%1$d 天前</item> <item quantity="other">%1$d 天前</item>
</plurals> </plurals>
</resources> <plurals name="months_ago">
<item quantity="other">%1$d 个月前</item>
</plurals>
</resources>

View File

@@ -2,31 +2,31 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="settings">设置</string> <string name="settings">设置</string>
<string name="local_storage">本地存储</string> <string name="local_storage">本地存储</string>
<string name="favourites">喜欢</string> <string name="favourites">收藏</string>
<string name="history">历史</string> <string name="history">历史</string>
<string name="error_occurred">发生了一个错误</string> <string name="error_occurred">发生了一个错误</string>
<string name="network_error">网络错误</string> <string name="network_error">网络错误</string>
<string name="chapters">章节</string> <string name="chapters">章节</string>
<string name="list">列表</string> <string name="list">紧凑</string>
<string name="data_restored_with_errors">数据恢复,但有错误</string> <string name="data_restored_with_errors">数据恢复,但有一些错误</string>
<string name="processing_">正在处理…</string> <string name="processing_">正在处理…</string>
<string name="newest">最新</string> <string name="newest">最新</string>
<string name="by_rating">评分</string> <string name="by_rating">评分</string>
<string name="cookies_cleared">除所有 cookie</string> <string name="cookies_cleared">除所有 cookie</string>
<string name="data_restored_success">所有数据都被恢复</string> <string name="data_restored_success">数据已全部恢复</string>
<string name="silent">无声</string> <string name="silent">无声</string>
<string name="preparing_">准备…</string> <string name="preparing_">准备…</string>
<string name="file_not_found">未找到文件</string> <string name="file_not_found">未找到文件</string>
<string name="yesterday">昨日</string> <string name="yesterday">昨日</string>
<string name="backup_information">你可以创建你的历史和收藏的备份并恢复它</string> <string name="backup_information">你可以创建你的历史和收藏的备份并恢复它</string>
<string name="just_now">现在</string> <string name="just_now">刚刚</string>
<string name="long_ago">很久以前</string> <string name="long_ago">很久以前</string>
<string name="group"></string> <string name="group"></string>
<string name="tap_to_try_again">点以重试</string> <string name="tap_to_try_again">重试</string>
<string name="reader_mode_hint">所选择的配置将这部漫画而被记住</string> <string name="reader_mode_hint">所选配置将这部漫画记住</string>
<string name="captcha_required">需要验证码</string> <string name="captcha_required">需要验证码</string>
<string name="captcha_solve">解决</string> <string name="captcha_solve">解决</string>
<string name="today"></string> <string name="today"></string>
<string name="clear_cookies">清除cookies</string> <string name="clear_cookies">清除cookies</string>
<string name="new_sources_text">有新的漫画源可用</string> <string name="new_sources_text">有新的漫画源可用</string>
<string name="suggestions_summary">根据你的喜好推荐漫画</string> <string name="suggestions_summary">根据你的喜好推荐漫画</string>
@@ -36,24 +36,24 @@
<string name="nsfw">18+</string> <string name="nsfw">18+</string>
<string name="various_languages">各种语言</string> <string name="various_languages">各种语言</string>
<string name="search_chapters">查找章节</string> <string name="search_chapters">查找章节</string>
<string name="suggestions_excluded_genres">排除流派</string> <string name="suggestions_excluded_genres">排除分类</string>
<string name="suggestions_updating">建议更新</string> <string name="suggestions_updating">漫画推荐更新</string>
<string name="check_new_chapters_title">检查新章节并通知有关情况</string> <string name="check_new_chapters_title">检查新章节并通知检查结果</string>
<string name="details">详细内容</string> <string name="details">详细</string>
<string name="detailed_list">详细列表</string> <string name="detailed_list">详细</string>
<string name="grid">网格</string> <string name="grid">网格</string>
<string name="list_mode">列表模式</string> <string name="list_mode">列表模式</string>
<string name="remote_sources">漫画源</string> <string name="remote_sources">漫画源</string>
<string name="loading_">加载中…</string> <string name="loading_">加载中…</string>
<string name="computing_">计算中…</string> <string name="computing_">计算中…</string>
<string name="chapter_d_of_d">%1$d/%2$d章节</string> <string name="chapter_d_of_d">章节 %1$d/%2$d</string>
<string name="close">关闭</string> <string name="close">关闭</string>
<string name="try_again">再试一次</string> <string name="try_again">再试一次</string>
<string name="clear_history">清除历史</string> <string name="clear_history">清除历史</string>
<string name="nothing_found">没有发现</string> <string name="nothing_found">未找到</string>
<string name="history_is_empty">尚无历史</string> <string name="history_is_empty">尚无历史</string>
<string name="read">阅读</string> <string name="read">阅读</string>
<string name="you_have_not_favourites_yet">尚无收藏</string> <string name="you_have_not_favourites_yet">尚无收藏</string>
<string name="add_to_favourites">收藏此漫画</string> <string name="add_to_favourites">收藏此漫画</string>
<string name="add_new_category">新分类</string> <string name="add_new_category">新分类</string>
<string name="add">添加</string> <string name="add">添加</string>
@@ -68,14 +68,14 @@
<string name="downloads">下载</string> <string name="downloads">下载</string>
<string name="by_name">名称</string> <string name="by_name">名称</string>
<string name="popular">热门</string> <string name="popular">热门</string>
<string name="updated">更新</string> <string name="updated">更新</string>
<string name="sort_order">排序顺序</string> <string name="sort_order">排序</string>
<string name="filter">过滤器</string> <string name="filter">筛选</string>
<string name="theme">主题</string> <string name="theme">主题</string>
<string name="dark">深色</string> <string name="dark">深色</string>
<string name="light">浅色</string> <string name="light">浅色</string>
<string name="automatic">跟随系统</string> <string name="automatic">跟随系统</string>
<string name="pages"></string> <string name="pages"></string>
<string name="clear">清除</string> <string name="clear">清除</string>
<string name="text_clear_history_prompt">永久清除所有阅读历史\?</string> <string name="text_clear_history_prompt">永久清除所有阅读历史\?</string>
<string name="remove">删除</string> <string name="remove">删除</string>
@@ -86,8 +86,8 @@
<string name="_import">导入</string> <string name="_import">导入</string>
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="operation_not_supported">不支持此操作</string> <string name="operation_not_supported">不支持此操作</string>
<string name="text_file_not_supported">选择 ZIP 或 CBZ 文件.</string> <string name="text_file_not_supported">选择 ZIP 或 CBZ 文件</string>
<string name="no_description">描述</string> <string name="no_description">简介</string>
<string name="clear_pages_cache">清除页面缓存</string> <string name="clear_pages_cache">清除页面缓存</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string> <string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">标准</string> <string name="standard">标准</string>
@@ -98,21 +98,21 @@
<string name="delete_manga">删除漫画</string> <string name="delete_manga">删除漫画</string>
<string name="text_delete_local_manga">从设备中永久删除\"%s\"\?</string> <string name="text_delete_local_manga">从设备中永久删除\"%s\"\?</string>
<string name="reader_settings">阅读器设置</string> <string name="reader_settings">阅读器设置</string>
<string name="switch_pages">切换页面</string> <string name="switch_pages">翻页方式</string>
<string name="volume_buttons">音量按钮</string> <string name="volume_buttons">音量按钮</string>
<string name="_continue">继续</string> <string name="_continue">继续</string>
<string name="taps_on_edges">边缘点击</string> <string name="taps_on_edges">边缘点击</string>
<string name="error">错误</string> <string name="error">错误</string>
<string name="clear_thumbs_cache">清除缩略图缓存</string> <string name="clear_thumbs_cache">清除缩略图缓存</string>
<string name="clear_search_history">清除搜索历史</string> <string name="clear_search_history">清除搜索历史</string>
<string name="search_history_cleared">清除</string> <string name="search_history_cleared">清除</string>
<string name="gestures_only">仅限手势</string> <string name="gestures_only">仅限手势</string>
<string name="internal_storage">内部存储</string> <string name="internal_storage">内部存储</string>
<string name="external_storage">外部存储</string> <string name="external_storage">外部存储</string>
<string name="domain">范围</string> <string name="domain">范围</string>
<string name="app_update_available">新版本应用程序已经推出</string> <string name="app_update_available">新版本应用程序已经推出</string>
<string name="open_in_browser">在网络浏览器中打开</string> <string name="open_in_browser">在网络浏览器中打开</string>
<string name="large_manga_save_confirm">这部漫画有%s.全部保存\?</string> <string name="large_manga_save_confirm">这部漫画有 %s全部保存</string>
<string name="save_manga">保存</string> <string name="save_manga">保存</string>
<string name="notifications">通知</string> <string name="notifications">通知</string>
<string name="new_chapters">新章节</string> <string name="new_chapters">新章节</string>
@@ -121,34 +121,34 @@
<string name="notification_sound">通知声音</string> <string name="notification_sound">通知声音</string>
<string name="light_indicator">LED指示器</string> <string name="light_indicator">LED指示器</string>
<string name="vibration">振动</string> <string name="vibration">振动</string>
<string name="favourites_categories">收藏分类</string> <string name="favourites_categories">收藏分类</string>
<string name="remove_category">删除</string> <string name="remove_category">删除</string>
<string name="text_empty_holder_primary">这里有点空…</string> <string name="text_empty_holder_primary">这里有点空…</string>
<string name="text_search_holder_secondary">尝试重新表述查询。</string> <string name="text_search_holder_secondary">尝试重新表述查询。</string>
<string name="text_history_holder_primary">所读的内容将在这里显示</string> <string name="text_history_holder_primary">看过的内容将在这里显示</string>
<string name="text_history_holder_secondary"> «探索»部分找到要读的内容</string> <string name="text_history_holder_secondary">【浏览】部分找到要读的内容</string>
<string name="text_local_holder_primary">先保存内容</string> <string name="text_local_holder_primary">先保存内容</string>
<string name="text_local_holder_secondary">从在线来源保存或导入文件.</string> <string name="text_local_holder_secondary">从在线来源保存或导入文件</string>
<string name="manga_shelf">书架</string> <string name="manga_shelf">书架</string>
<string name="recent_manga">最近</string> <string name="recent_manga">最近</string>
<string name="pages_animation">动画</string> <string name="pages_animation">页动画</string>
<string name="manga_save_location">下载文件夹</string> <string name="manga_save_location">下载文件夹</string>
<string name="not_available"></string> <string name="not_available">可用</string>
<string name="cannot_find_available_storage">没有可用的存储空间</string> <string name="cannot_find_available_storage">没有可用的存储空间</string>
<string name="other_storage">其他存储</string> <string name="other_storage">其他存储</string>
<string name="done">完成</string> <string name="done">完成</string>
<string name="all_favourites">所有收藏</string> <string name="all_favourites">所有收藏</string>
<string name="favourites_category_empty">空分类</string> <string name="favourites_category_empty">分类</string>
<string name="read_later">稍后阅读</string> <string name="read_later">稍后阅读</string>
<string name="updates">更新</string> <string name="updates">更新内容</string>
<string name="text_feed_holder">你正在阅读的新章节显示在这里</string> <string name="text_feed_holder">你正在阅读的新章节显示在这里</string>
<string name="search_results">搜索结果</string> <string name="search_results">搜索结果</string>
<string name="new_version_s">新版本: %s</string> <string name="new_version_s">新版本: %s</string>
<string name="clear_updates_feed">清除更新源</string> <string name="clear_updates_feed">清除订阅更新记录</string>
<string name="updates_feed_cleared">已清除</string> <string name="updates_feed_cleared">已清除</string>
<string name="rotate_screen">旋转屏幕</string> <string name="rotate_screen">旋转屏幕</string>
<string name="update">更新</string> <string name="update">更新</string>
<string name="feed_will_update_soon">更新即将开始</string> <string name="feed_will_update_soon">订阅更新即将开始</string>
<string name="track_sources">查找更新</string> <string name="track_sources">查找更新</string>
<string name="dont_check">不要检查</string> <string name="dont_check">不要检查</string>
<string name="enter_password">输入密码</string> <string name="enter_password">输入密码</string>
@@ -167,14 +167,14 @@
<string name="zoom_mode_fit_center">适应中心</string> <string name="zoom_mode_fit_center">适应中心</string>
<string name="zoom_mode_fit_height">适应高度</string> <string name="zoom_mode_fit_height">适应高度</string>
<string name="zoom_mode_fit_width">适应宽度</string> <string name="zoom_mode_fit_width">适应宽度</string>
<string name="zoom_mode_keep_start">从头开始</string> <string name="zoom_mode_keep_start">保持原始比例</string>
<string name="black_dark_theme">黑色</string> <string name="black_dark_theme">黑色</string>
<string name="black_dark_theme_summary">在AMOLED屏幕上使用更少电池</string> <string name="black_dark_theme_summary">在AMOLED屏幕上更省电</string>
<string name="backup_restore">备份和还原</string> <string name="backup_restore">备份与恢复</string>
<string name="create_backup">创建数据备份</string> <string name="create_backup">创建数据备份</string>
<string name="restore_backup">从备份中恢复</string> <string name="restore_backup">从备份中恢复</string>
<string name="data_restored">恢复</string> <string name="data_restored">恢复</string>
<string name="clear_feed">清除文件</string> <string name="clear_feed">清除订阅</string>
<string name="text_clear_updates_feed_prompt">永久地清除所有的更新历史?</string> <string name="text_clear_updates_feed_prompt">永久地清除所有的更新历史?</string>
<string name="check_for_new_chapters">检查新的章节</string> <string name="check_for_new_chapters">检查新的章节</string>
<string name="reverse">倒序</string> <string name="reverse">倒序</string>
@@ -185,64 +185,64 @@
<string name="protect_application_subtitle">输入密码以启动应用程序</string> <string name="protect_application_subtitle">输入密码以启动应用程序</string>
<string name="confirm">确认</string> <string name="confirm">确认</string>
<string name="password_length_hint">密码必须是4个字符或以上</string> <string name="password_length_hint">密码必须是4个字符或以上</string>
<string name="text_clear_search_history_prompt">永久删除所有最近的搜索查询</string> <string name="text_clear_search_history_prompt">永久删除所有搜索记录</string>
<string name="welcome">欢迎</string> <string name="welcome">欢迎</string>
<string name="backup_saved">保存备份</string> <string name="backup_saved">备份已保存</string>
<string name="tracker_warning">一些设备有不同的系统行为, 这可能会破坏后台任务.</string> <string name="tracker_warning">一些设备有不同的系统行为这可能会破坏后台任务</string>
<string name="read_more">阅读更多</string> <string name="read_more">了解详情</string>
<string name="queued">排队</string> <string name="queued">排队</string>
<string name="chapter_is_missing">该章缺失</string> <string name="chapter_is_missing">该章缺失</string>
<string name="about_app_translation_summary">翻译此应用程序</string> <string name="about_app_translation_summary">翻译此应用程序</string>
<string name="about_app_translation">翻译</string> <string name="about_app_translation">翻译</string>
<string name="auth_complete">授权</string> <string name="auth_complete">授权</string>
<string name="auth_not_supported_by">不支持在%s上登录</string> <string name="auth_not_supported_by">不支持在%s上登录</string>
<string name="text_clear_cookies_prompt">你将退出登录所有来源</string> <string name="text_clear_cookies_prompt">你将在所有漫画源退出登录</string>
<string name="genres">类型</string> <string name="genres">类型</string>
<string name="state_ongoing">连载中</string> <string name="state_ongoing">连载中</string>
<string name="state_finished">已完结</string> <string name="state_finished">已完结</string>
<string name="system_default">默认</string> <string name="system_default">默认</string>
<string name="exclude_nsfw_from_history">将NSFW漫画排除在历史之外</string> <string name="exclude_nsfw_from_history">从历史中排除NSFW漫画</string>
<string name="show_pages_numbers">页数</string> <string name="show_pages_numbers">页数</string>
<string name="enabled_sources">使用图</string> <string name="enabled_sources">已用漫画</string>
<string name="available_sources">现有图</string> <string name="available_sources">可用漫画</string>
<string name="screenshots_policy">屏幕截图</string> <string name="screenshots_policy">截图策略</string>
<string name="screenshots_allow">允许</string> <string name="screenshots_allow">允许</string>
<string name="screenshots_block_nsfw">禁止18+</string> <string name="screenshots_block_nsfw">屏蔽 NSFW</string>
<string name="screenshots_block_all">始终阻止</string> <string name="screenshots_block_all">始终屏蔽</string>
<string name="suggestions">建议</string> <string name="suggestions">漫画推荐</string>
<string name="suggestions_enable">启用建议</string> <string name="suggestions_enable">启用漫画推荐</string>
<string name="text_suggestion_holder">开始阅读漫画,你会得到个性化的建议</string> <string name="text_suggestion_holder">开始阅读漫画,你会获取个性化推荐</string>
<string name="exclude_nsfw_from_suggestions">请勿推荐18+漫画</string> <string name="exclude_nsfw_from_suggestions">请勿推荐 NSFW 漫画</string>
<string name="enabled">启用</string> <string name="enabled">启用</string>
<string name="disabled">禁用</string> <string name="disabled">禁用</string>
<string name="filter_load_error">无法加载流派列表</string> <string name="filter_load_error">无法加载分类列表</string>
<string name="reset_filter">重置过滤器</string> <string name="reset_filter">重置筛选</string>
<string name="onboard_text">选择你想看的漫画的语言. 你可以在以后的设置中改变它.</string> <string name="onboard_text">选择您想阅读漫画的语言。稍候您可以在设置中更改它。</string>
<string name="only_using_wifi">只在Wi-Fi上使用</string> <string name="only_using_wifi">Wi-Fi</string>
<string name="always">总是</string> <string name="always">总是</string>
<string name="preload_pages">预加载页面</string> <string name="preload_pages">预加载页面</string>
<string name="logged_in_as">以%s身份登录</string> <string name="logged_in_as">以%s身份登录</string>
<string name="chapters_empty">这部漫画中没有章节</string> <string name="chapters_empty">这部漫画中没有章节</string>
<string name="appearance">外观</string> <string name="appearance">外观</string>
<string name="suggestions_excluded_genres_summary">指定您不希望在建议中看到的类</string> <string name="suggestions_excluded_genres_summary">指定您不希望在推荐中看到的</string>
<string name="text_delete_local_manga_batch">从设备中永久删除所选项目\?</string> <string name="text_delete_local_manga_batch">从设备中永久删除所选项目</string>
<string name="removal_completed">删除已完成</string> <string name="removal_completed">删除完毕</string>
<string name="download_slowdown">下载速度减慢</string> <string name="download_slowdown">下载速度减慢</string>
<string name="download_slowdown_summary">有助于避免阻断你的IP地址</string> <string name="download_slowdown_summary">有助于避免阻断你的IP地址</string>
<string name="local_manga_processing">保存漫画处理</string> <string name="local_manga_processing">保存漫画处理</string>
<string name="chapters_will_removed_background">章节将在后台被删除</string> <string name="chapters_will_removed_background">章节将在后台被删除</string>
<string name="hide">隐藏</string> <string name="hide">隐藏</string>
<string name="show_notification_new_chapters_off">你将不会收到通知但新的章节将在列表中突出显示</string> <string name="show_notification_new_chapters_off">你将不会收到通知但新的章节将在列表中突出显示</string>
<string name="notifications_enable">启用通知</string> <string name="notifications_enable">启用通知</string>
<string name="name"></string> <string name="name"></string>
<string name="edit">编辑</string> <string name="edit">编辑</string>
<string name="edit_category">编辑分类</string> <string name="edit_category">编辑分类</string>
<string name="empty_favourite_categories">没有收藏分类</string> <string name="empty_favourite_categories">没有收藏分类</string>
<string name="bookmark_add">添加书签</string> <string name="bookmark_add">添加书签</string>
<string name="bookmark_remove">删除书签</string> <string name="bookmark_remove">删除书签</string>
<string name="bookmarks">书签</string> <string name="bookmarks">书签</string>
<string name="bookmark_removed">删除书签</string> <string name="bookmark_removed">书签已删除</string>
<string name="bookmark_added">添加书签</string> <string name="bookmark_added">书签已添加</string>
<string name="undo">撤销</string> <string name="undo">撤销</string>
<string name="removed_from_history">从历史中删除</string> <string name="removed_from_history">从历史中删除</string>
<string name="dns_over_https">DNS over HTTPS</string> <string name="dns_over_https">DNS over HTTPS</string>
@@ -250,8 +250,8 @@
<string name="detect_reader_mode">自动检测阅读器模式</string> <string name="detect_reader_mode">自动检测阅读器模式</string>
<string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string> <string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string>
<string name="disable_battery_optimization">禁用电池优化</string> <string name="disable_battery_optimization">禁用电池优化</string>
<string name="disable_battery_optimization_summary">帮助进行背景更新检查</string> <string name="disable_battery_optimization_summary">有助于进行后台更新检查</string>
<string name="crash_text">出了点问题. 请向开发人员提交一份错误报告以帮助我们修复它.</string> <string name="crash_text">出了点问题请向开发人员提交一份错误报告以帮助我们修复它</string>
<string name="send">发送</string> <string name="send">发送</string>
<string name="disable_all">全部禁用</string> <string name="disable_all">全部禁用</string>
<string name="status_planned">计划</string> <string name="status_planned">计划</string>
@@ -263,13 +263,13 @@
<string name="status_re_reading">重读</string> <string name="status_re_reading">重读</string>
<string name="status_completed">完成</string> <string name="status_completed">完成</string>
<string name="use_fingerprint">使用指纹</string> <string name="use_fingerprint">使用指纹</string>
<string name="appwidget_shelf_description">喜欢的漫画</string> <string name="appwidget_shelf_description">收藏的漫画</string>
<string name="appwidget_recent_description">您最近阅读的漫画</string> <string name="appwidget_recent_description">您最近阅读的漫画</string>
<string name="show_reading_indicators_summary">在历史和收藏中显示阅读百分比</string> <string name="show_reading_indicators_summary">在历史和收藏中显示阅读百分比</string>
<string name="show_reading_indicators">显示阅读进度指标</string> <string name="show_reading_indicators">显示阅读进度</string>
<string name="data_deletion">数据删除</string> <string name="data_deletion">数据删除</string>
<string name="exclude_nsfw_from_history_summary">标记为NSFW的漫画将永远不会被添加到历史中你的进度也不会被保存</string> <string name="exclude_nsfw_from_history_summary">标记为 NSFW 的漫画将永远不会被添加到历史中你的阅读进度也不会被保存</string>
<string name="clear_cookies_summary">可以在出现一些问题时提供帮助. 所有授权将被视为无</string> <string name="clear_cookies_summary">能对部分问题起到一点作用。所有网站授权将失</string>
<string name="show_all">显示全部</string> <string name="show_all">显示全部</string>
<string name="manga_error_description_pattern">错误详情:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1.尝试&lt;a href=%2$s&gt;在网络浏览器中打开漫画&lt;/a&gt;以确保在其来源中可用&lt;br&gt;2. 请确保您使用的是&lt;a href=kotatsu://about&gt;最新版本的Kotatsu&lt;/a&gt;&lt;br&gt;3.如果可用,请向开发人员发送错误报告。</string> <string name="manga_error_description_pattern">错误详情:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1.尝试&lt;a href=%2$s&gt;在网络浏览器中打开漫画&lt;/a&gt;以确保在其来源中可用&lt;br&gt;2. 请确保您使用的是&lt;a href=kotatsu://about&gt;最新版本的Kotatsu&lt;/a&gt;&lt;br&gt;3.如果可用,请向开发人员发送错误报告。</string>
<string name="invalid_domain_message">无效域名</string> <string name="invalid_domain_message">无效域名</string>
@@ -289,23 +289,23 @@
<string name="bookmarks_removed">书签已移除</string> <string name="bookmarks_removed">书签已移除</string>
<string name="history_cleared">历史已清除</string> <string name="history_cleared">历史已清除</string>
<string name="manage">管理</string> <string name="manage">管理</string>
<string name="no_bookmarks_yet">还没有书签</string> <string name="no_bookmarks_yet">尚无书签</string>
<string name="no_bookmarks_summary">您可以在阅读漫画时创建书签</string> <string name="no_bookmarks_summary">您可以在阅读漫画时创建书签</string>
<string name="no_manga_sources">无漫画源</string> <string name="no_manga_sources">无漫画源</string>
<string name="no_manga_sources_text">启用漫画源在线阅读漫画</string> <string name="no_manga_sources_text">启用漫画源即可在线阅读漫画</string>
<string name="random">随机</string> <string name="random">随机</string>
<string name="categories_delete_confirm">您确定要删除选定的收藏吗? <string name="categories_delete_confirm">您确定要删除选定的收藏分类吗?
\n所有收藏夹中的漫画将丢失且无法恢复。</string> \n该分类中的所有漫画将丢失且无法恢复。</string>
<string name="reorder">重新排序</string> <string name="reorder">重新排序</string>
<string name="empty"></string> <string name="empty">分类</string>
<string name="explore">浏览</string> <string name="explore">浏览</string>
<string name="automatic_scroll">自动滚动</string> <string name="automatic_scroll">自动滚动</string>
<string name="reader_info_bar">在阅读器中显示信息栏</string> <string name="reader_info_bar">在阅读器中显示信息栏</string>
<string name="comics_archive">漫画压缩包</string> <string name="comics_archive">漫画压缩包</string>
<string name="folder_with_images">图片文件夹</string> <string name="folder_with_images">图片文件夹</string>
<string name="importing_manga">漫画导入中</string> <string name="importing_manga">漫画导入中</string>
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string> <string name="reader_info_pattern">章节 %1$d/%2$d 页码 %3$d/%4$d</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d %2$d 启用</string> <string name="enabled_d_of_d" tools:ignore="PluralsCandidate">已启用 %1$d / %2$d</string>
<string name="size_s">大小:%s</string> <string name="size_s">大小:%s</string>
<string name="confirm_exit">再按一次返回键退出</string> <string name="confirm_exit">再按一次返回键退出</string>
<string name="exit_confirmation_summary">按两次返回键退出应用</string> <string name="exit_confirmation_summary">按两次返回键退出应用</string>
@@ -322,10 +322,10 @@
<string name="import_completed">导入完毕</string> <string name="import_completed">导入完毕</string>
<string name="import_completed_hint">您可以从存储中删除原文件以节省空间</string> <string name="import_completed_hint">您可以从存储中删除原文件以节省空间</string>
<string name="import_will_start_soon">即将开始导入</string> <string name="import_will_start_soon">即将开始导入</string>
<string name="feed">订阅</string> <string name="feed">订阅</string>
<string name="memory_usage_pattern">%s - %s</string> <string name="memory_usage_pattern">%s - %s</string>
<string name="not_found_404">内容未找到或已移除</string> <string name="not_found_404">内容未找到或已移除</string>
<string name="reader_control_ltr_summary">点击屏幕右侧或按下右键翻到下一页</string> <string name="reader_control_ltr_summary">点击屏幕右侧或按下右键始终翻到下一页</string>
<string name="reader_control_ltr">高效阅读器控制</string> <string name="reader_control_ltr">高效阅读器控制</string>
<string name="history_shortcuts_summary">长按应用图标显示最近阅读的漫画</string> <string name="history_shortcuts_summary">长按应用图标显示最近阅读的漫画</string>
<string name="history_shortcuts">显示最近阅读漫画的快捷方式</string> <string name="history_shortcuts">显示最近阅读漫画的快捷方式</string>
@@ -337,21 +337,21 @@
<string name="text_unsaved_changes_prompt">保存还是放弃未保存的更改?</string> <string name="text_unsaved_changes_prompt">保存还是放弃未保存的更改?</string>
<string name="discard">放弃</string> <string name="discard">放弃</string>
<string name="error_no_space_left">设备上没有剩余空间</string> <string name="error_no_space_left">设备上没有剩余空间</string>
<string name="reader_slider">显示页滑块</string> <string name="reader_slider">显示页滑块</string>
<string name="webtoon_zoom">Webtoon 缩放</string> <string name="webtoon_zoom">条漫缩放</string>
<string name="different_languages">不同语言</string> <string name="different_languages">不同语言</string>
<string name="network_unavailable">网络不可用</string> <string name="network_unavailable">网络不可用</string>
<string name="network_unavailable_hint">打开 Wi-Fi 或移动网络在线阅读漫画</string> <string name="network_unavailable_hint">打开 Wi-Fi 或移动网络在线阅读漫画</string>
<string name="clear_new_chapters_counters">清除新章节信息</string> <string name="clear_new_chapters_counters">清除新章节信息</string>
<string name="server_error">服务器端错误 (%1$d)。请稍后再试</string> <string name="server_error">服务器端错误 (%1$d)。请稍后再试</string>
<string name="compact">紧凑</string> <string name="compact">紧凑</string>
<string name="source_disabled">已禁用图源</string> <string name="source_disabled">漫画源已禁用</string>
<string name="prefetch_content">内容预加载</string> <string name="prefetch_content">内容预加载</string>
<string name="mark_as_current">标为当前</string> <string name="mark_as_current">标为当前</string>
<string name="language">语言</string> <string name="language">语言</string>
<string name="enable_logging">启用日志记录</string> <string name="enable_logging">启用日志记录</string>
<string name="share_logs">分享日志</string> <string name="share_logs">分享日志</string>
<string name="enable_logging_summary">出于调试目的记录某些操作</string> <string name="enable_logging_summary">出于调试目的记录某些操作,如果你不了解你在做什么,请不要启用该选项</string>
<string name="show_suspicious_content">显示可疑内容</string> <string name="show_suspicious_content">显示可疑内容</string>
<string name="theme_name_dynamic">动态</string> <string name="theme_name_dynamic">动态</string>
<string name="color_theme">颜色方案</string> <string name="color_theme">颜色方案</string>
@@ -365,10 +365,10 @@
<string name="theme_name_mamimi">Mamimi</string> <string name="theme_name_mamimi">Mamimi</string>
<string name="theme_name_kanade">Kanade</string> <string name="theme_name_kanade">Kanade</string>
<string name="nothing_here">这里什么也没有</string> <string name="nothing_here">这里什么也没有</string>
<string name="scrobbling_empty_hint">要跟踪阅读进度,在漫画详情屏幕上选中菜单跟踪。</string> <string name="scrobbling_empty_hint">要跟踪阅读进度,在漫画详情屏幕上选中菜单】→【跟踪</string>
<string name="allow_unstable_updates">允许不稳定更新</string> <string name="allow_unstable_updates">允许不稳定更新</string>
<string name="allow_unstable_updates_summary">提示更新到测试版</string> <string name="allow_unstable_updates_summary">接收不稳定版本更新的通知</string>
<string name="download_started">已开始下载</string> <string name="download_started">下载已开始</string>
<string name="user_agent">UserAgent 标头</string> <string name="user_agent">UserAgent 标头</string>
<string name="settings_apply_restart_required">要应用这些更改请重启程序</string> <string name="settings_apply_restart_required">要应用这些更改请重启程序</string>
<string name="sources_reorder_tip">点击并长按项目排序</string> <string name="sources_reorder_tip">点击并长按项目排序</string>
@@ -383,10 +383,10 @@
<string name="web_view_unavailable">WebView不可用检查是否已安装WebView</string> <string name="web_view_unavailable">WebView不可用检查是否已安装WebView</string>
<string name="sync_host_description">你可以使用自建同步服务器或默认服务器。如果你不知道自己在干什么请不要修改此处。</string> <string name="sync_host_description">你可以使用自建同步服务器或默认服务器。如果你不知道自己在干什么请不要修改此处。</string>
<string name="mirror_switching">自动选择镜像</string> <string name="mirror_switching">自动选择镜像</string>
<string name="mirror_switching_summary">如果存在可用镜像,在出错时自动切换域名</string> <string name="mirror_switching_summary">如果存在可用镜像,在出错时自动切换漫画源域名</string>
<string name="paused">已暂停</string> <string name="paused">已暂停</string>
<string name="downloads_wifi_only_summary">切换到移动网络时停止下载</string> <string name="downloads_wifi_only_summary">切换到移动网络时停止下载</string>
<string name="remove_completed">移除已完成</string> <string name="remove_completed">删除完毕</string>
<string name="cancel_all">取消所有</string> <string name="cancel_all">取消所有</string>
<string name="downloads_wifi_only">仅通过Wi-Fi下载</string> <string name="downloads_wifi_only">仅通过Wi-Fi下载</string>
<string name="enable">启用</string> <string name="enable">启用</string>
@@ -397,17 +397,17 @@
<string name="resume">恢复</string> <string name="resume">恢复</string>
<string name="ignore_ssl_errors">忽略SSL错误</string> <string name="ignore_ssl_errors">忽略SSL错误</string>
<string name="text_downloads_list_holder">没有下载项</string> <string name="text_downloads_list_holder">没有下载项</string>
<string name="downloads_resumed">下载已恢复</string> <string name="downloads_resumed">下载已恢复</string>
<string name="downloads_paused">暂停下载</string> <string name="downloads_paused">下载已暂停</string>
<string name="downloads_removed">下载已移除</string> <string name="downloads_removed">下载已移除</string>
<string name="downloads_cancelled">下载取消</string> <string name="downloads_cancelled">下载取消</string>
<string name="suggestions_enable_prompt">你想要接收个性化的漫画推荐吗?</string> <string name="suggestions_enable_prompt">你想要接收个性化的漫画推荐吗?</string>
<string name="suggestion_manga">推荐:%s</string> <string name="suggestion_manga">推荐:%s</string>
<string name="suggestions_notifications_summary">偶尔显示建议漫画通知</string> <string name="suggestions_notifications_summary">偶尔显示漫画推荐通知</string>
<string name="more">更多</string> <string name="more">更多</string>
<string name="cancel_all_downloads_confirm">所有进行中的下载都将被取消,未下载完成的数据将丢失</string> <string name="cancel_all_downloads_confirm">所有进行中的下载都将被取消,未下载完成的数据将丢失</string>
<string name="remove_completed_downloads_confirm">你的下载历史将会永久删除</string> <string name="remove_completed_downloads_confirm">你的下载历史将会永久删除</string>
<string name="sync_auth_hint">你可以登陆一个已有账号或创建新账号</string> <string name="sync_auth_hint">你可以登陆已有账号或创建新账号</string>
<string name="address">地址</string> <string name="address">地址</string>
<string name="clear_network_cache">清除网络缓存</string> <string name="clear_network_cache">清除网络缓存</string>
<string name="proxy">代理</string> <string name="proxy">代理</string>
@@ -420,10 +420,61 @@
<string name="clear_source_cookies_summary">仅清除特定域名的 cookies。大多数情况下会使网站授权失效</string> <string name="clear_source_cookies_summary">仅清除特定域名的 cookies。大多数情况下会使网站授权失效</string>
<string name="data_and_privacy">数据与隐私</string> <string name="data_and_privacy">数据与隐私</string>
<string name="reader_info_bar_summary">在屏幕顶部显示当前时间和阅读进度</string> <string name="reader_info_bar_summary">在屏幕顶部显示当前时间和阅读进度</string>
<string name="webtoon_zoom_summary">条漫模式下允许以手势缩放</string> <string name="webtoon_zoom_summary">条漫模式下允许使用缩放手势</string>
<string name="invalid_port_number">无效端口</string> <string name="invalid_port_number">无效端口</string>
<string name="no_access_to_file">您无法访问该文件或目录</string> <string name="no_access_to_file">您无法访问该文件或目录</string>
<string name="local_manga_directories">本地漫画目录</string> <string name="local_manga_directories">本地漫画目录</string>
<string name="port">端口</string> <string name="port">端口</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string> <string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="download_option_all_unread">所有未读章节</string>
<string name="pick_custom_directory">选择自定义目录</string>
<string name="password">密码</string>
<string name="download_option_whole_manga">整部漫画</string>
<string name="download_option_manual_selection">手动选择章节</string>
<string name="description">简介</string>
<string name="images_proxy_title">图片加载优化</string>
<string name="username">用户名</string>
<string name="download_option_all_unread_b">所有未读章节 (%s)</string>
<string name="authorization_optional">授权 (可选项)</string>
<string name="download_option_first_n_chapters">前 %s 章</string>
<string name="downloaded">已下载</string>
<string name="custom_directory">自定义目录</string>
<string name="pages_animation_summary">翻页动画</string>
<string name="download_option_next_unread_n_chapters">后 %s 章</string>
<string name="images_procy_description">尽可能使用 wsrv.nl 代理服务减少流量使用并加快图片加载</string>
<string name="invert_colors">颜色反转</string>
<string name="related_manga">相关漫画</string>
<string name="voice_search">语音搜索</string>
<string name="this_month">本月</string>
<string name="languages">语言</string>
<string name="captcha_required_summary">%s 需通过验证码才能正常工作</string>
<string name="progress">阅读进度</string>
<string name="error_corrupted_file">无效数据回传或文件已损坏</string>
<string name="related_manga_summary">显示相关漫画。可能并不准确或缺失</string>
<string name="tracker_wifi_only_summary">使用计量网络时停止检查新章节</string>
<string name="order_added">添加日期</string>
<string name="on_device">本地</string>
<string name="moved_to_top">移动到顶部</string>
<string name="data_not_restored_text">请确认你选择了正确的备份文件</string>
<string name="unknown">未知</string>
<string name="in_progress">进行中</string>
<string name="items_limit_exceeded">无可添加项</string>
<string name="data_not_restored">数据未恢复</string>
<string name="directories">目录</string>
<string name="manage_categories">漫画类别</string>
<string name="color_light">浅色</string>
<string name="search_hint">输入漫画名、漫画类型或漫画源名称</string>
<string name="main_screen_sections">主页栏目</string>
<string name="advanced">高级</string>
<string name="color_dark">深色</string>
<string name="too_many_requests_message">请求次数过多,稍候再尝试</string>
<string name="suggestions_wifi_only_summary">使用计量网络时停止推荐漫画</string>
<string name="default_section">默认栏目</string>
<string name="background">阅读背景色</string>
<string name="manga_list">漫画列表</string>
<string name="disable_nsfw">禁用 NSFW 源</string>
<string name="color_white">白色</string>
<string name="to_top">置顶</string>
<string name="show">显示</string>
<string name="color_black">黑色</string>
</resources> </resources>

View File

@@ -80,4 +80,6 @@
<dimen name="fastscroll_scrollbar_padding_end">6dp</dimen> <dimen name="fastscroll_scrollbar_padding_end">6dp</dimen>
<dimen name="m3_side_sheet_width">400dp</dimen> <dimen name="m3_side_sheet_width">400dp</dimen>
<dimen name="reader_scroll_delta_min">200dp</dimen>
</resources> </resources>

View File

@@ -485,4 +485,8 @@
<string name="items_limit_exceeded">No more items can be added</string> <string name="items_limit_exceeded">No more items can be added</string>
<string name="to_top">To top</string> <string name="to_top">To top</string>
<string name="moved_to_top">Moved to top</string> <string name="moved_to_top">Moved to top</string>
<string name="zoom_out">Zoom out</string>
<string name="zoom_in">Zoom in</string>
<string name="reader_zoom_buttons">Show zoom buttons</string>
<string name="reader_zoom_buttons_summary">Whether to show zoom control buttons in the bottom right corner</string>
</resources> </resources>

View File

@@ -28,6 +28,12 @@
android:summary="@string/webtoon_zoom_summary" android:summary="@string/webtoon_zoom_summary"
android:title="@string/webtoon_zoom" /> android:title="@string/webtoon_zoom" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_zoom_buttons"
android:summary="@string/reader_zoom_buttons_summary"
android:title="@string/reader_zoom_buttons" />
<MultiSelectListPreference <MultiSelectListPreference
android:defaultValue="@array/values_reader_switchers_default" android:defaultValue="@array/values_reader_switchers_default"
android:entries="@array/reader_switchers" android:entries="@array/reader_switchers"

View File

@@ -6,8 +6,8 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.1.1' classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.47' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48'
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.0-1.0.13' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.10-1.0.13'
} }
} }