mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Remove the WebGPU reader and go back to the old image decoder
Brings back SSIV and the image decoder from v0.20.4 with their settings, except "Use legacy decoder for long strip reader", which long strip pages drawn as strips replace.
This commit is contained in:
+1
-18
@@ -12,22 +12,6 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Add experimental high quality reader renderer (Settings → Advanced → Use high quality renderer) with better image scaling, page transition animations, dual page view and display cutout handling ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3388](https://github.com/mihonapp/mihon/pull/3388))
|
|
||||||
- Match background and transition page colors to the old reader ([@AntsyLich](https://github.com/AntsyLich)) ([#3765](https://github.com/mihonapp/mihon/pull/3765))
|
|
||||||
- Support the Fit width, Fit height and Original size scale types and the zoom start position ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3801](https://github.com/mihonapp/mihon/pull/3801))
|
|
||||||
- Fix preloading and navigation across chapters, and right-to-left navigation ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3823](https://github.com/mihonapp/mihon/pull/3823))
|
|
||||||
- Add a "None" page transition and fix the background color of animated images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3843](https://github.com/mihonapp/mihon/pull/3843))
|
|
||||||
- Improve transition and error pages, dual page animations and border cropping ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3852](https://github.com/mihonapp/mihon/pull/3852))
|
|
||||||
- Fix navigation directions and long strip bounds, and fade in loaded pages ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3870](https://github.com/mihonapp/mihon/pull/3870))
|
|
||||||
- Add a "Page flip" transition, a long strip minimum width setting and separate dual page settings, and show wide pages on their own in dual page view ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3886](https://github.com/mihonapp/mihon/pull/3886))
|
|
||||||
- Fix freezes and running out of memory when jumping between pages with the slider ([@Danidukiyu](https://github.com/Danidukiyu)) ([#3872](https://github.com/mihonapp/mihon/pull/3872))
|
|
||||||
- Add HDR, ICC color profile and EXIF orientation support ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3933](https://github.com/mihonapp/mihon/pull/3933))
|
|
||||||
- Add a page gap setting for long strip with gaps and a "Disable zoom out" setting for long strip modes ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3933](https://github.com/mihonapp/mihon/pull/3933))
|
|
||||||
- Fix a crash on Android 15 and long strip progress and background color ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3949](https://github.com/mihonapp/mihon/pull/3949))
|
|
||||||
- Keep the reader menu open while moving through pages with the slider ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3956](https://github.com/mihonapp/mihon/pull/3956))
|
|
||||||
- Show pages progressively while they download ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4023](https://github.com/mihonapp/mihon/pull/4023))
|
|
||||||
- Lower memory use for animated images and support cropping borders in long strip ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4029](https://github.com/mihonapp/mihon/pull/4029))
|
|
||||||
- Pair more wide pages in dual page view by comparing them with neighbouring pages ([@wwww-wwww](https://github.com/wwww-wwww)) ([#4051](https://github.com/mihonapp/mihon/pull/4051))
|
|
||||||
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya), [@BrutuZ](https://github.com/BrutuZ)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
|
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya), [@BrutuZ](https://github.com/BrutuZ)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
|
||||||
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||||
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
|
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
|
||||||
@@ -45,7 +29,6 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Download app updates from the update screen instead of a background job, and remove the "App updates" notification channel ([@AntsyLich](https://github.com/AntsyLich)) ([#3707](https://github.com/mihonapp/mihon/pull/3707))
|
- Download app updates from the update screen instead of a background job, and remove the "App updates" notification channel ([@AntsyLich](https://github.com/AntsyLich)) ([#3707](https://github.com/mihonapp/mihon/pull/3707))
|
||||||
- Use a new decoder for images Android can't decode itself, like AVIF, JPEG XL and HEIF, and support JPEG 2000 images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3786](https://github.com/mihonapp/mihon/pull/3786))
|
|
||||||
- Switch app icons to Material Symbols ([@AntsyLich](https://github.com/AntsyLich)) ([#3873](https://github.com/mihonapp/mihon/pull/3873))
|
- Switch app icons to Material Symbols ([@AntsyLich](https://github.com/AntsyLich)) ([#3873](https://github.com/mihonapp/mihon/pull/3873))
|
||||||
- Use the filled icon for the library continue reading button ([@AntsyLich](https://github.com/AntsyLich)) ([`34a33ed`](https://github.com/mihonapp/mihon/commit/34a33ed))
|
- Use the filled icon for the library continue reading button ([@AntsyLich](https://github.com/AntsyLich)) ([`34a33ed`](https://github.com/mihonapp/mihon/commit/34a33ed))
|
||||||
- Move notification, launcher shortcut, bottom navigation and reading mode icons to Material Symbols, bring back filled icons where the switch dropped them, and show a compact grid icon for the compact display modes ([@AntsyLich](https://github.com/AntsyLich)) ([#4057](https://github.com/mihonapp/mihon/pull/4057))
|
- Move notification, launcher shortcut, bottom navigation and reading mode icons to Material Symbols, bring back filled icons where the switch dropped them, and show a compact grid icon for the compact display modes ([@AntsyLich](https://github.com/AntsyLich)) ([#4057](https://github.com/mihonapp/mihon/pull/4057))
|
||||||
@@ -102,7 +85,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Speed up queueing many chapters for download at once ([@AntsyLich](https://github.com/AntsyLich)) ([`f02bf5b`](https://github.com/mihonapp/mihon/commit/f02bf5b))
|
- Speed up queueing many chapters for download at once ([@AntsyLich](https://github.com/AntsyLich)) ([`f02bf5b`](https://github.com/mihonapp/mihon/commit/f02bf5b))
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Remove "Custom hardware bitmap threshold", "Use legacy decoder for long strip reader" and "Custom display profile" advanced settings ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3786](https://github.com/mihonapp/mihon/pull/3786))
|
- Remove "Use legacy decoder for long strip reader" advanced setting, since long strip pages no longer load as one big image ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed app and extension update check running again on configuration change ([@AntsyLich](https://github.com/AntsyLich)) ([#3708](https://github.com/mihonapp/mihon/pull/3708))
|
- Fixed app and extension update check running again on configuration change ([@AntsyLich](https://github.com/AntsyLich)) ([#3708](https://github.com/mihonapp/mihon/pull/3708))
|
||||||
|
|||||||
@@ -291,8 +291,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
implementation(libs.webgpuviewer)
|
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.flexibleAdapter)
|
implementation(libs.flexibleAdapter)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dev.icerock.moko.resources.StringResource
|
|||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -37,7 +38,12 @@ class BasePreferences(
|
|||||||
PRIVATE(MR.strings.ext_installer_private, false),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
|
|
||||||
val highQualityRenderer: Preference<Boolean> = preferenceStore.getBoolean("pref_high_quality_renderer_key", false)
|
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
|
||||||
|
|
||||||
|
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
|
||||||
|
"pref_hardware_bitmap_threshold",
|
||||||
|
GLUtil.SAFE_TEXTURE_LIMIT,
|
||||||
|
)
|
||||||
|
|
||||||
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -26,9 +25,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
@@ -36,27 +33,16 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import ca.mpreg.webgpuviewer.renderer.GainmapInput
|
|
||||||
import ca.mpreg.webgpuviewer.renderer.Image
|
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImageViewer
|
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImageViewerState
|
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.SuccessResult
|
|
||||||
import coil3.size.Size
|
import coil3.size.Size
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
|
||||||
import eu.kanade.tachiyomi.data.coil.newDecoder
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import mihon.app.di.appGraph
|
|
||||||
import mihon.icons.materialsymbols.MaterialSymbols
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
import mihon.icons.materialsymbols.rounded.Close
|
import mihon.icons.materialsymbols.rounded.Close
|
||||||
import mihon.icons.materialsymbols.rounded.Edit
|
import mihon.icons.materialsymbols.rounded.Edit
|
||||||
@@ -79,9 +65,6 @@ fun MangaCoverDialog(
|
|||||||
onEditClick: ((EditCoverAction) -> Unit)?,
|
onEditClick: ((EditCoverAction) -> Unit)?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val useNewRenderer = LocalContext.current.appGraph.basePreferences.highQualityRenderer.get()
|
|
||||||
val view = LocalView.current
|
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
@@ -168,59 +151,6 @@ fun MangaCoverDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (useNewRenderer) {
|
|
||||||
val state = remember(manga) { ImageViewerState() }
|
|
||||||
LaunchedEffect(manga) {
|
|
||||||
val request = ImageRequest.Builder(view.context)
|
|
||||||
.data(manga)
|
|
||||||
.size(Size.ORIGINAL)
|
|
||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
|
||||||
.newDecoder(true)
|
|
||||||
.build()
|
|
||||||
val result = view.context.imageLoader.execute(request) as? SuccessResult ?: return@LaunchedEffect
|
|
||||||
val res = result.image as? ImageDecoder.DecodeResultImage ?: return@LaunchedEffect
|
|
||||||
// Held, then freed, around the upload: its buffer alone doesn't keep
|
|
||||||
// the frame's native pixels alive.
|
|
||||||
val page = res.frame.use {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
ImagePage.ImageSingle(
|
|
||||||
Image(
|
|
||||||
res.image,
|
|
||||||
res.width,
|
|
||||||
res.height,
|
|
||||||
createMipMaps = true,
|
|
||||||
backgroundColor = 0,
|
|
||||||
hdr = res.isHdr,
|
|
||||||
hdrHeadroom = res.hdrHeadroom,
|
|
||||||
gainmap = res.gainmap?.let {
|
|
||||||
GainmapInput(
|
|
||||||
pixels = it.pixels,
|
|
||||||
width = it.width,
|
|
||||||
height = it.height,
|
|
||||||
channels = it.channels,
|
|
||||||
gamma = it.gamma,
|
|
||||||
minContentBoost = it.minContentBoost,
|
|
||||||
maxContentBoost = it.maxContentBoost,
|
|
||||||
offsetSdr = it.offsetSdr,
|
|
||||||
offsetHdr = it.offsetHdr,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.apply {
|
|
||||||
fetchPage = { index ->
|
|
||||||
if (index == 0) page else null
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageViewer(state = state)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
|
||||||
|
|||||||
+38
-3
@@ -6,6 +6,8 @@ import android.content.Intent
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.webkit.WebStorage
|
import android.webkit.WebStorage
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -41,6 +43,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
|||||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
@@ -53,6 +56,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -333,12 +337,43 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
basePreferences: BasePreferences,
|
basePreferences: BasePreferences,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val chooseColorProfile = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
basePreferences.displayProfile.set(uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_reader),
|
title = stringResource(MR.strings.pref_category_reader),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
preference = basePreferences.highQualityRenderer,
|
preference = basePreferences.hardwareBitmapThreshold,
|
||||||
title = stringResource(MR.strings.pref_high_quality_renderer),
|
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
||||||
|
.mapIndexed { index, option ->
|
||||||
|
val display = if (index == 0) {
|
||||||
|
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
||||||
|
} else {
|
||||||
|
option.toString()
|
||||||
|
}
|
||||||
|
option to display
|
||||||
|
}
|
||||||
|
.toMap(),
|
||||||
|
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
||||||
|
subtitleProvider = { value, options ->
|
||||||
|
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
||||||
|
},
|
||||||
|
visible = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
||||||
|
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
||||||
|
),
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.pref_display_profile),
|
||||||
|
subtitle = basePreferences.displayProfile.get(),
|
||||||
|
onClick = {
|
||||||
|
chooseColorProfile.launch(arrayOf("*/*"))
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -31,7 +30,6 @@ import java.text.NumberFormat
|
|||||||
internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
|
internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
|
||||||
HeadingItem(MR.strings.pref_category_for_this_series)
|
HeadingItem(MR.strings.pref_category_for_this_series)
|
||||||
val manga by viewModel.mangaFlow.collectAsState()
|
val manga by viewModel.mangaFlow.collectAsState()
|
||||||
val viewer by viewModel.viewerFlow.collectAsState()
|
|
||||||
|
|
||||||
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
|
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
|
||||||
SettingsChipRow(MR.strings.pref_category_reading_mode) {
|
SettingsChipRow(MR.strings.pref_category_reading_mode) {
|
||||||
@@ -44,62 +42,6 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewer is WebGpuViewer) {
|
|
||||||
val default = LocalContext.current.appGraph.readerPreferences.defaultReadingMode.get()
|
|
||||||
val resolved = ReadingMode.fromPreference(
|
|
||||||
when {
|
|
||||||
readingMode == ReadingMode.DEFAULT -> default
|
|
||||||
else -> manga?.readingMode?.toInt() ?: default
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (resolved == ReadingMode.LEFT_TO_RIGHT || resolved == ReadingMode.RIGHT_TO_LEFT) {
|
|
||||||
val dualPageView by viewModel.preferences.dualPageView.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_dual_page_view) {
|
|
||||||
ReaderPreferences.DualPageView.entries.map {
|
|
||||||
FilterChip(
|
|
||||||
selected = it == dualPageView,
|
|
||||||
onClick = { viewModel.preferences.dualPageView.set(it) },
|
|
||||||
label = { Text(stringResource(it.titleRes)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolved == ReadingMode.WEBTOON || resolved == ReadingMode.CONTINUOUS_VERTICAL) {
|
|
||||||
val numberFormat = remember { NumberFormat.getPercentInstance() }
|
|
||||||
val continuousMinWidth by viewModel.preferences.continuousMinWidth.collectAsState()
|
|
||||||
SliderItem(
|
|
||||||
value = continuousMinWidth,
|
|
||||||
valueRange = ReaderPreferences.let { 1..100 },
|
|
||||||
label = stringResource(MR.strings.pref_continuous_minwidth),
|
|
||||||
valueString = numberFormat.format(continuousMinWidth / 100f),
|
|
||||||
onChange = {
|
|
||||||
viewModel.preferences.continuousMinWidth.set(it)
|
|
||||||
},
|
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (resolved == ReadingMode.CONTINUOUS_VERTICAL) {
|
|
||||||
val continuousGap by viewModel.preferences.continuousGap.collectAsState()
|
|
||||||
SliderItem(
|
|
||||||
value = continuousGap,
|
|
||||||
valueRange = ReaderPreferences.let { 1..100 },
|
|
||||||
label = stringResource(MR.strings.pref_continuous_gap),
|
|
||||||
valueString = numberFormat.format(continuousGap / 100f),
|
|
||||||
onChange = {
|
|
||||||
viewModel.preferences.continuousGap.set(it)
|
|
||||||
},
|
|
||||||
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
|
||||||
pref = viewModel.preferences.webtoonDisableZoomOut,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
|
val orientation = remember(manga) { ReaderOrientation.fromPreference(manga?.readerOrientation?.toInt()) }
|
||||||
SettingsChipRow(MR.strings.rotation_type) {
|
SettingsChipRow(MR.strings.rotation_type) {
|
||||||
ReaderOrientation.entries.map {
|
ReaderOrientation.entries.map {
|
||||||
@@ -111,10 +53,11 @@ internal fun ColumnScope.ReadingModePage(viewModel: ReaderSettingsViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (viewer) {
|
val viewer by viewModel.viewerFlow.collectAsState()
|
||||||
is WebtoonViewer -> WebtoonViewerSettings(viewModel)
|
if (viewer is WebtoonViewer) {
|
||||||
is WebGpuViewer -> WebGpuViewerSettings(viewModel)
|
WebtoonViewerSettings(viewModel)
|
||||||
else -> PagerViewerSettings(viewModel)
|
} else {
|
||||||
|
PagerViewerSettings(viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,120 +235,3 @@ private fun ColumnScope.TapZonesItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ColumnScope.WebGpuViewerSettings(viewModel: ReaderSettingsViewModel) {
|
|
||||||
HeadingItem(MR.strings.webgpu_viewer)
|
|
||||||
|
|
||||||
val manga by viewModel.mangaFlow.collectAsState()
|
|
||||||
val viewer by viewModel.viewerFlow.collectAsState()
|
|
||||||
|
|
||||||
val readingMode = remember(manga) { ReadingMode.fromPreference(manga?.readingMode?.toInt()) }
|
|
||||||
val default = LocalContext.current.appGraph.readerPreferences.defaultReadingMode.get()
|
|
||||||
val resolved = ReadingMode.fromPreference(
|
|
||||||
when {
|
|
||||||
readingMode == ReadingMode.DEFAULT -> default
|
|
||||||
else -> manga?.readingMode?.toInt() ?: default
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val isDual = (viewer as? WebGpuViewer)?.isDualPageMode() == true
|
|
||||||
|
|
||||||
val navigationModePager by viewModel.preferences.navigationModePager.collectAsState()
|
|
||||||
val pagerNavInverted by viewModel.preferences.pagerNavInverted.collectAsState()
|
|
||||||
TapZonesItems(
|
|
||||||
selected = navigationModePager,
|
|
||||||
onSelect = viewModel.preferences.navigationModePager::set,
|
|
||||||
invertMode = pagerNavInverted,
|
|
||||||
onSelectInvertMode = viewModel.preferences.pagerNavInverted::set,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isDual) {
|
|
||||||
val transitionAnimation by viewModel.preferences.transitionAnimationDual.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_transition_animation_dual) {
|
|
||||||
(
|
|
||||||
ReaderPreferences.TransitionAnimation.entries - ReaderPreferences.TransitionAnimation.FLIP_LEFT -
|
|
||||||
ReaderPreferences.TransitionAnimation.FLIP_RIGHT
|
|
||||||
).forEach {
|
|
||||||
FilterChip(
|
|
||||||
selected = it == transitionAnimation,
|
|
||||||
onClick = { viewModel.preferences.transitionAnimationDual.set(it) },
|
|
||||||
label = { Text(stringResource(it.titleRes)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val cutoutMode by viewModel.preferences.cutoutModeDual.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_cutout_mode_dual) {
|
|
||||||
ReaderPreferences.CutoutMode.entries.forEach {
|
|
||||||
FilterChip(
|
|
||||||
selected = it == cutoutMode,
|
|
||||||
onClick = { viewModel.preferences.cutoutModeDual.set(it) },
|
|
||||||
label = { Text(stringResource(it.titleRes)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolved != ReadingMode.WEBTOON && resolved != ReadingMode.CONTINUOUS_VERTICAL) {
|
|
||||||
val imageScaleType by viewModel.preferences.imageScaleType.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_image_scale_type) {
|
|
||||||
ReaderPreferences.ImageScaleTypeWebGpuViewer.forEach {
|
|
||||||
FilterChip(
|
|
||||||
selected = ReaderPreferences.ImageScaleType[imageScaleType - 1] == it,
|
|
||||||
onClick = {
|
|
||||||
viewModel.preferences.imageScaleType.set(ReaderPreferences.ImageScaleType.indexOf(it) + 1)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(it)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val zoomStart by viewModel.preferences.zoomStart.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_zoom_start) {
|
|
||||||
ReaderPreferences.ZoomStart.mapIndexed { index, it ->
|
|
||||||
FilterChip(
|
|
||||||
selected = zoomStart == index + 1,
|
|
||||||
onClick = { viewModel.preferences.zoomStart.set(index + 1) },
|
|
||||||
label = { Text(stringResource(it)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(MR.strings.pref_crop_borders),
|
|
||||||
pref = viewModel.preferences.cropBorders,
|
|
||||||
)
|
|
||||||
|
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(MR.strings.pref_landscape_zoom),
|
|
||||||
pref = viewModel.preferences.landscapeZoom,
|
|
||||||
)
|
|
||||||
|
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(MR.strings.pref_navigate_pan),
|
|
||||||
pref = viewModel.preferences.navigateToPan,
|
|
||||||
)
|
|
||||||
|
|
||||||
val transitionAnimation by viewModel.preferences.transitionAnimation.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_transition_animation) {
|
|
||||||
ReaderPreferences.TransitionAnimation.entries.forEach {
|
|
||||||
FilterChip(
|
|
||||||
selected = it == transitionAnimation,
|
|
||||||
onClick = { viewModel.preferences.transitionAnimation.set(it) },
|
|
||||||
label = { Text(stringResource(it.titleRes)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cutoutMode by viewModel.preferences.cutoutMode.collectAsState()
|
|
||||||
SettingsChipRow(MR.strings.pref_cutout_mode) {
|
|
||||||
ReaderPreferences.CutoutMode.entries.forEach {
|
|
||||||
FilterChip(
|
|
||||||
selected = it == cutoutMode,
|
|
||||||
onClick = { viewModel.preferences.cutoutMode.set(it) },
|
|
||||||
label = { Text(stringResource(it.titleRes)) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
@@ -68,6 +69,7 @@ import org.conscrypt.Conscrypt
|
|||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -184,6 +186,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
|
basePreferences.hardwareBitmapThreshold.let { preference ->
|
||||||
|
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
basePreferences.hardwareBitmapThreshold.changes()
|
||||||
|
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
||||||
|
.launchIn(scope)
|
||||||
|
|
||||||
setAppCompatDelegateThemeMode(uiPreferences.themeMode.get())
|
setAppCompatDelegateThemeMode(uiPreferences.themeMode.get())
|
||||||
|
|
||||||
// Updates widget update
|
// Updates widget update
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import dev.zacsweers.metro.AppScope
|
|||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.DownloadStream
|
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -151,11 +150,10 @@ class ChapterCache(
|
|||||||
*
|
*
|
||||||
* @param imageUrl url of image.
|
* @param imageUrl url of image.
|
||||||
* @param response http response from page.
|
* @param response http response from page.
|
||||||
* @param live also receives the bytes as they arrive.
|
|
||||||
* @throws IOException image error.
|
* @throws IOException image error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun putImageToCache(imageUrl: String, response: Response, live: DownloadStream? = null) {
|
fun putImageToCache(imageUrl: String, response: Response) {
|
||||||
// Initialize editor (edits the values for an entry).
|
// Initialize editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
@@ -165,22 +163,7 @@ class ChapterCache(
|
|||||||
editor = diskCache.edit(key) ?: return
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
// Get OutputStream and write image with Okio.
|
// Get OutputStream and write image with Okio.
|
||||||
if (live == null) {
|
response.body.source().saveTo(editor.newOutputStream(0))
|
||||||
response.body.source().saveTo(editor.newOutputStream(0))
|
|
||||||
} else {
|
|
||||||
live.reserve(response.body.contentLength())
|
|
||||||
response.body.source().use { source ->
|
|
||||||
editor.newOutputStream(0).use { out ->
|
|
||||||
val buffer = ByteArray(1 shl 16)
|
|
||||||
while (true) {
|
|
||||||
val n = source.read(buffer)
|
|
||||||
if (n < 0) break
|
|
||||||
out.write(buffer, 0, n)
|
|
||||||
live.append(buffer, 0, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diskCache.flush()
|
diskCache.flush()
|
||||||
editor.commit()
|
editor.commit()
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.data.coil
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import androidx.core.graphics.createBitmap
|
|
||||||
import androidx.core.graphics.scale
|
|
||||||
import ca.mpreg.imagedecoder.ImageDecoder
|
|
||||||
import coil3.Canvas
|
import coil3.Canvas
|
||||||
import coil3.Image
|
import coil3.Image
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
@@ -15,39 +12,20 @@ import coil3.decode.Decoder
|
|||||||
import coil3.decode.ImageSource
|
import coil3.decode.ImageSource
|
||||||
import coil3.fetch.SourceFetchResult
|
import coil3.fetch.SourceFetchResult
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
|
import coil3.request.bitmapConfig
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Decoder] that uses [ImageDecoder] to decode image formats not supported
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
* by the Android system decoder (AVIF, JXL, HEIF, etc.).
|
|
||||||
*/
|
*/
|
||||||
class ImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
class ImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a raw [ImageDecoder.Frame] as a Coil [Image] for callers that want
|
|
||||||
* direct access to the RGBA [java.nio.ByteBuffer] (e.g. the new-decoder path).
|
|
||||||
*/
|
|
||||||
class DecodeResultImage(
|
|
||||||
val frame: ImageDecoder.Frame,
|
|
||||||
val isHdr: Boolean,
|
|
||||||
val hdrHeadroom: Float,
|
|
||||||
val gainmap: ImageDecoder.Gainmap?,
|
|
||||||
) : Image {
|
|
||||||
val image: java.nio.ByteBuffer get() = frame.image
|
|
||||||
|
|
||||||
// Taken now: a caller may close the frame once it has the pixels.
|
|
||||||
override val size: Long = frame.image.capacity().toLong()
|
|
||||||
override val width: Int get() = frame.width
|
|
||||||
override val height: Int get() = frame.height
|
|
||||||
override val shareable: Boolean get() = true
|
|
||||||
override fun draw(canvas: Canvas) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A page cut into immutable strips of about [STRIP_BYTES] each. The renderer uploads an
|
* A page cut into immutable strips of about [STRIP_BYTES] each. The renderer uploads an
|
||||||
* immutable bitmap only when it is drawn, so only the strips on screen reach the GPU instead of
|
* immutable bitmap only when it is drawn, so only the strips on screen reach the GPU instead of
|
||||||
@@ -64,32 +42,18 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
override suspend fun decode(): DecodeResult {
|
override suspend fun decode(): DecodeResult {
|
||||||
if (options.webtoonStrips) return decodeStrips()
|
if (options.webtoonStrips) return decodeStrips()
|
||||||
|
|
||||||
val res = resources.source().use {
|
val decoder = resources.sourceOrNull()?.use {
|
||||||
ImageDecoder.open(it.inputStream()).use { dec ->
|
ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile)
|
||||||
DecodeResultImage(
|
|
||||||
dec.decodeNext(),
|
|
||||||
dec.isHdr,
|
|
||||||
dec.hdrHeadroom,
|
|
||||||
if (dec.hdrKind == ImageDecoder.HdrKind.GAINMAP) dec.getGainmap() else null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val srcWidth = res.width
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||||
val srcHeight = res.height
|
|
||||||
|
|
||||||
// newDecoder path: caller wants the raw DecodeResult (e.g. for custom rendering).
|
val srcWidth = decoder.width
|
||||||
// Hand it back as-is; sampling is the caller's responsibility.
|
val srcHeight = decoder.height
|
||||||
if (options.newDecoder) {
|
|
||||||
return DecodeResult(
|
|
||||||
image = res,
|
|
||||||
isSampled = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal path: produce a Bitmap scaled to the requested output size.
|
|
||||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||||
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||||
|
|
||||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||||
srcWidth = srcWidth,
|
srcWidth = srcWidth,
|
||||||
srcHeight = srcHeight,
|
srcHeight = srcHeight,
|
||||||
@@ -98,25 +62,17 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
scale = options.scale,
|
scale = options.scale,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Copy RGBA pixels from the native buffer into a full-resolution bitmap.
|
var bitmap = decoder.decode(sampleSize = sampleSize)
|
||||||
// We must do this while `res` (and its native memory) is still alive.
|
decoder.recycle()
|
||||||
// HDR frames are half-float RGBA.
|
|
||||||
val config = if (res.isHdr) Bitmap.Config.RGBA_F16 else Bitmap.Config.ARGB_8888
|
|
||||||
val fullBitmap = createBitmap(srcWidth, srcHeight, config)
|
|
||||||
res.image.rewind()
|
|
||||||
fullBitmap.copyPixelsFromBuffer(res.image)
|
|
||||||
res.frame.close()
|
|
||||||
|
|
||||||
// Downsample if needed. sampleSize is a power-of-two factor; the target
|
check(bitmap != null) { "Failed to decode image" }
|
||||||
// dimensions are src / sampleSize, matching BitmapFactory inSampleSize behaviour.
|
|
||||||
val bitmap = if (sampleSize > 1) {
|
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
|
||||||
val scaledWidth = (srcWidth / sampleSize).coerceAtLeast(1)
|
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
||||||
val scaledHeight = (srcHeight / sampleSize).coerceAtLeast(1)
|
if (hwBitmap != null) {
|
||||||
val scaled = fullBitmap.scale(scaledWidth, scaledHeight)
|
bitmap.recycle()
|
||||||
fullBitmap.recycle()
|
bitmap = hwBitmap
|
||||||
scaled
|
}
|
||||||
} else {
|
|
||||||
fullBitmap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DecodeResult(
|
return DecodeResult(
|
||||||
@@ -126,66 +82,62 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies bands of rows straight from the native frame into strips, so the page never exists as
|
* Decodes a webtoon page and cuts it into strips, freeing the decoded page once they're copied.
|
||||||
* one Java bitmap on top of the frame.
|
|
||||||
*
|
*
|
||||||
* Strips keep the resolution a power-of-two sample size leaves, like the other paths, so zooming
|
* Strips keep the resolution a power-of-two sample size leaves, like the other paths, so zooming
|
||||||
* in still shows the page's detail, unless that would make the page take more than
|
* in still shows the page's detail, unless that would make the page take more than
|
||||||
* [MAX_PAGE_BYTES]. They are never narrower than the view, so the page stays sharp unzoomed.
|
* [MAX_PAGE_BYTES]. They are never narrower than the view, so the page stays sharp unzoomed.
|
||||||
*/
|
*/
|
||||||
private suspend fun decodeStrips(): DecodeResult = stripDecodes.withPermit {
|
private suspend fun decodeStrips(): DecodeResult = stripDecodes.withPermit {
|
||||||
val (frame, isHdr) = resources.source().use {
|
val decoder = resources.sourceOrNull()?.use {
|
||||||
ImageDecoder.open(it.inputStream()).use { dec -> dec.decodeNext() to dec.isHdr }
|
ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile)
|
||||||
}
|
}
|
||||||
frame.use {
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
||||||
val config = if (isHdr) Bitmap.Config.RGBA_F16 else Bitmap.Config.ARGB_8888
|
|
||||||
val bytesPerPixel = if (isHdr) 8 else 4
|
|
||||||
val rowBytes = frame.width * bytesPerPixel
|
|
||||||
val viewWidth = options.size.widthPx(options.scale) { frame.width }
|
|
||||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
|
||||||
srcWidth = frame.width,
|
|
||||||
srcHeight = frame.height,
|
|
||||||
dstWidth = viewWidth,
|
|
||||||
dstHeight = options.size.heightPx(options.scale) { frame.height },
|
|
||||||
scale = options.scale,
|
|
||||||
)
|
|
||||||
val budgetWidth = sqrt(MAX_PAGE_BYTES.toDouble() / bytesPerPixel * frame.width / frame.height).toInt()
|
|
||||||
val stripWidth = maxOf(viewWidth, min(frame.width / sampleSize, budgetWidth))
|
|
||||||
// Older GPUs can't draw a texture wider than this
|
|
||||||
.coerceAtMost(MAX_STRIP_WIDTH)
|
|
||||||
.coerceIn(1, frame.width)
|
|
||||||
val scale = stripWidth.toFloat() / frame.width
|
|
||||||
|
|
||||||
// Strips of about the same size in bytes, so each takes about as long to upload
|
val srcWidth = decoder.width
|
||||||
val stripRows = (STRIP_BYTES / (stripWidth * bytesPerPixel)).coerceAtLeast(1)
|
val viewWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||||
val bandRows = (stripRows / scale).toInt().coerceIn(1, frame.height)
|
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||||
val matrix = Matrix().apply { setScale(scale, scale) }
|
srcWidth = decoder.width,
|
||||||
val band = createBitmap(frame.width, bandRows, config)
|
srcHeight = decoder.height,
|
||||||
val strips = (0..<frame.height step bandRows).map { top ->
|
dstWidth = viewWidth,
|
||||||
val rows = min(bandRows, frame.height - top)
|
dstHeight = options.size.heightPx(options.scale) { decoder.height },
|
||||||
if (rows != band.height) band.reconfigure(frame.width, rows, config)
|
scale = options.scale,
|
||||||
band.copyPixelsFromBuffer(
|
)
|
||||||
frame.image.duplicate().apply {
|
val page = decoder.decode(sampleSize = sampleSize)
|
||||||
position(top * rowBytes)
|
decoder.recycle()
|
||||||
limit((top + rows) * rowBytes)
|
check(page != null) { "Failed to decode image" }
|
||||||
},
|
|
||||||
)
|
val config = page.config ?: Bitmap.Config.ARGB_8888
|
||||||
val strip = if (stripWidth < frame.width) {
|
val bytesPerPixel = page.rowBytes / page.width
|
||||||
Bitmap.createBitmap(band, 0, 0, frame.width, rows, matrix, true)
|
val budgetWidth = sqrt(MAX_PAGE_BYTES.toDouble() / bytesPerPixel * page.width / page.height).toInt()
|
||||||
} else {
|
val stripWidth = maxOf(viewWidth, min(page.width, budgetWidth))
|
||||||
band
|
// Older GPUs can't draw a texture wider than this
|
||||||
}
|
.coerceAtMost(MAX_STRIP_WIDTH)
|
||||||
// createBitmap returns a mutable bitmap despite its docs, and copy is the only way to an immutable one
|
.coerceIn(1, page.width)
|
||||||
strip.copy(config, false).also { if (strip !== band) strip.recycle() }
|
val scale = stripWidth.toFloat() / page.width
|
||||||
|
|
||||||
|
// Strips of about the same size in bytes, so each takes about as long to upload
|
||||||
|
val stripRows = (STRIP_BYTES / (stripWidth * bytesPerPixel)).coerceAtLeast(1)
|
||||||
|
val bandRows = (stripRows / scale).toInt().coerceIn(1, page.height)
|
||||||
|
val matrix = Matrix().apply { setScale(scale, scale) }
|
||||||
|
val strips = (0..<page.height step bandRows).map { top ->
|
||||||
|
val rows = min(bandRows, page.height - top)
|
||||||
|
val band = if (stripWidth < page.width) {
|
||||||
|
Bitmap.createBitmap(page, 0, top, page.width, rows, matrix, true)
|
||||||
|
} else {
|
||||||
|
Bitmap.createBitmap(page, 0, top, page.width, rows)
|
||||||
}
|
}
|
||||||
band.recycle()
|
// createBitmap returns a mutable bitmap despite its docs, and copy is the only way to an immutable one
|
||||||
DecodeResult(image = StripsImage(strips), isSampled = stripWidth < frame.width)
|
band.copy(config, false).also { if (band !== page) band.recycle() }
|
||||||
}
|
}
|
||||||
|
page.recycle()
|
||||||
|
DecodeResult(image = StripsImage(strips), isSampled = stripWidth < srcWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Decoder.Factory {
|
class Factory : Decoder.Factory {
|
||||||
|
|
||||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||||
return if (options.newDecoder || options.webtoonStrips || isApplicable(result.source.source())) {
|
return if (options.webtoonStrips || isApplicable(result.source.source())) {
|
||||||
ImageDecoder(result.source, options)
|
ImageDecoder(result.source, options)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -197,12 +149,7 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
ImageUtil.findImageType(it)
|
ImageUtil.findImageType(it)
|
||||||
}
|
}
|
||||||
return when (type) {
|
return when (type) {
|
||||||
ImageUtil.ImageType.AVIF,
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL, ImageUtil.ImageType.HEIF -> true
|
||||||
ImageUtil.ImageType.JXL,
|
|
||||||
ImageUtil.ImageType.HEIF,
|
|
||||||
ImageUtil.ImageType.JP2,
|
|
||||||
-> true
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +158,10 @@ class ImageDecoder(private val resources: ImageSource, private val options: Opti
|
|||||||
|
|
||||||
override fun hashCode() = javaClass.hashCode()
|
override fun hashCode() = javaClass.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var displayProfile: ByteArray? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val STRIP_BYTES = 4 * 1024 * 1024
|
private const val STRIP_BYTES = 4 * 1024 * 1024
|
||||||
@@ -219,5 +170,5 @@ private const val MAX_PAGE_BYTES = 128 * 1024 * 1024
|
|||||||
|
|
||||||
private const val MAX_STRIP_WIDTH = 4096
|
private const val MAX_STRIP_WIDTH = 4096
|
||||||
|
|
||||||
// Each decode holds the whole native frame until its strips are copied, and the reader loads pages ahead in parallel
|
// Each decode holds the whole page until its strips are copied, and the reader loads pages ahead in parallel
|
||||||
private val stripDecodes = Semaphore(1)
|
private val stripDecodes = Semaphore(1)
|
||||||
|
|||||||
@@ -42,11 +42,3 @@ val Options.webtoonStrips: Boolean
|
|||||||
get() = getExtra(webtoonStripsKey)
|
get() = getExtra(webtoonStripsKey)
|
||||||
|
|
||||||
private val webtoonStripsKey = Extras.Key(default = false)
|
private val webtoonStripsKey = Extras.Key(default = false)
|
||||||
|
|
||||||
fun ImageRequest.Builder.newDecoder(enable: Boolean) = apply {
|
|
||||||
extras[newDecoderKey] = enable
|
|
||||||
}
|
|
||||||
val Options.newDecoder: Boolean
|
|
||||||
get() = getExtra(newDecoderKey)
|
|
||||||
|
|
||||||
private val newDecoderKey = Extras.Key(default = false)
|
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.presentation.reader.DisplayRefreshHost
|
import eu.kanade.presentation.reader.DisplayRefreshHost
|
||||||
@@ -56,6 +58,7 @@ import eu.kanade.presentation.reader.appbars.ReaderAppBars
|
|||||||
import eu.kanade.presentation.reader.components.ChapterNavigatorType
|
import eu.kanade.presentation.reader.components.ChapterNavigatorType
|
||||||
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||||
@@ -74,7 +77,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
|
|||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
|
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
|
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
|
||||||
@@ -99,6 +101,7 @@ import tachiyomi.core.common.i18n.stringResource
|
|||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class ReaderActivity : BaseActivity() {
|
class ReaderActivity : BaseActivity() {
|
||||||
@@ -473,7 +476,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
onShare = ::shareChapter.takeIf { isHttpSource },
|
onShare = ::shareChapter.takeIf { isHttpSource },
|
||||||
|
|
||||||
chapterNavigatorType = if (!verticalNavigator) {
|
chapterNavigatorType = if (!verticalNavigator) {
|
||||||
if (state.viewer is R2LPagerViewer || (state.viewer as? WebGpuViewer)?.isReversed ?: false) {
|
if (state.viewer is R2LPagerViewer) {
|
||||||
ChapterNavigatorType.HORIZONTAL_RTL
|
ChapterNavigatorType.HORIZONTAL_RTL
|
||||||
} else {
|
} else {
|
||||||
ChapterNavigatorType.HORIZONTAL_LTR
|
ChapterNavigatorType.HORIZONTAL_LTR
|
||||||
@@ -860,6 +863,10 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
|
preferences.displayProfile.changes()
|
||||||
|
.onEach { setDisplayProfile(it) }
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
readerPreferences.keepScreenOn.changes()
|
readerPreferences.keepScreenOn.changes()
|
||||||
.onEach(::setKeepScreenOn)
|
.onEach(::setKeepScreenOn)
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
@@ -887,6 +894,25 @@ class ReaderActivity : BaseActivity() {
|
|||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display profile to [path].
|
||||||
|
*/
|
||||||
|
private fun setDisplayProfile(path: String) {
|
||||||
|
val file = UniFile.fromUri(baseContext, path.toUri())
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
val inputStream = file.openInputStream()
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
inputStream.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val data = outputStream.toByteArray()
|
||||||
|
SubsamplingScaleImageView.setDisplayProfile(data)
|
||||||
|
ImageDecoder.displayProfile = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the keep screen on mode according to [enabled].
|
* Sets the keep screen on mode according to [enabled].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -231,23 +231,14 @@ internal class HttpPageLoader(
|
|||||||
val imageUrl = page.imageUrl!!
|
val imageUrl = page.imageUrl!!
|
||||||
|
|
||||||
if (force || !chapterCache.isImageInCache(imageUrl)) {
|
if (force || !chapterCache.isImageInCache(imageUrl)) {
|
||||||
val live = page.downloadStream
|
|
||||||
page.status = Page.State.DownloadImage
|
page.status = Page.State.DownloadImage
|
||||||
try {
|
val imageResponse = source.getImage(page)
|
||||||
val imageResponse = source.getImage(page)
|
chapterCache.putImageToCache(imageUrl, imageResponse)
|
||||||
chapterCache.putImageToCache(imageUrl, imageResponse, live)
|
|
||||||
live?.finish()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
live?.finish(e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
page.downloadStream = null
|
|
||||||
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
|
||||||
page.status = Page.State.Ready
|
page.status = Page.State.Ready
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.downloadStream = null
|
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException) {
|
||||||
// Chunk chains get cancelled on every page turn, so a cancelled page isn't an error. Put it back in
|
// Chunk chains get cancelled on every page turn, so a cancelled page isn't an error. Put it back in
|
||||||
// the queue, otherwise it's stuck in a state no later pass will pick up.
|
// the queue, otherwise it's stuck in a state no later pass will pick up.
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.model
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A download's bytes as they arrive, for one reader. Past [MAX_BYTES], or once that reader is
|
|
||||||
* closed, it stops keeping them; [append] never throws, so the download itself carries on.
|
|
||||||
*/
|
|
||||||
class DownloadStream {
|
|
||||||
private val lock = Object()
|
|
||||||
private var data = ByteArray(1 shl 16)
|
|
||||||
private var size = 0
|
|
||||||
private var done = false
|
|
||||||
private var error: Throwable? = null
|
|
||||||
|
|
||||||
/** Presizes for [bytes], e.g. Content-Length; ignored if implausible. */
|
|
||||||
fun reserve(bytes: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (error == null && bytes > data.size && bytes <= MAX_RESERVE) {
|
|
||||||
try {
|
|
||||||
data = data.copyOf(bytes.toInt())
|
|
||||||
} catch (e: OutOfMemoryError) {
|
|
||||||
// Only a hint.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun append(src: ByteArray, offset: Int, length: Int) {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (error != null || done) return
|
|
||||||
val needed = size.toLong() + length
|
|
||||||
if (needed > MAX_BYTES) {
|
|
||||||
// Too big to decode while downloading; the whole-file decode takes over.
|
|
||||||
drop("Download too large to stream")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (needed > data.size) {
|
|
||||||
data = try {
|
|
||||||
data.copyOf(maxOf(needed, minOf(data.size.toLong() * 2, MAX_BYTES)).toInt())
|
|
||||||
} catch (e: OutOfMemoryError) {
|
|
||||||
// As too large: the download mustn't fail over this copy.
|
|
||||||
drop("Out of memory buffering the download")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.arraycopy(src, offset, data, size, length)
|
|
||||||
size += length
|
|
||||||
lock.notifyAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ends the bytes; readers throw [error] if given. */
|
|
||||||
fun finish(error: Throwable? = null) {
|
|
||||||
synchronized(lock) {
|
|
||||||
done = true
|
|
||||||
if (this.error == null) this.error = error
|
|
||||||
lock.notifyAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** From the start; [Reader.read] blocks for bytes. Closing it drops what's kept. */
|
|
||||||
fun reader(): Reader = Reader()
|
|
||||||
|
|
||||||
// Rather than hold a second copy of the page.
|
|
||||||
private fun drop(why: String) {
|
|
||||||
if (error == null) error = IOException(why)
|
|
||||||
data = ByteArray(0)
|
|
||||||
size = 0
|
|
||||||
lock.notifyAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
// As the decoder's own input limit.
|
|
||||||
const val MAX_BYTES = 1L shl 30
|
|
||||||
const val MAX_RESERVE = 64L shl 20
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Reader : InputStream() {
|
|
||||||
private var position = 0
|
|
||||||
|
|
||||||
/** All read; throws if the download failed. */
|
|
||||||
val ended: Boolean
|
|
||||||
get() = synchronized(lock) {
|
|
||||||
checkError()
|
|
||||||
done && position >= size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun read(): Int {
|
|
||||||
val b = ByteArray(1)
|
|
||||||
return if (read(b, 0, 1) < 0) -1 else b[0].toInt() and 0xFF
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (len == 0) return 0
|
|
||||||
while (position >= size && !done && error == null) lock.wait()
|
|
||||||
checkError()
|
|
||||||
if (position >= size) return -1
|
|
||||||
val n = minOf(len, size - position)
|
|
||||||
System.arraycopy(data, position, b, off, n)
|
|
||||||
position += n
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun available(): Int = synchronized(lock) { if (error != null) 0 else size - position }
|
|
||||||
|
|
||||||
/** Waits up to [timeoutMs] for [min] unread bytes or the end. */
|
|
||||||
fun await(min: Int, timeoutMs: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
val until = System.nanoTime() + timeoutMs * 1_000_000
|
|
||||||
while (size - position < min && !done && error == null) {
|
|
||||||
val left = (until - System.nanoTime()) / 1_000_000
|
|
||||||
if (left <= 0) return
|
|
||||||
lock.wait(left)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
synchronized(lock) { drop("No longer read") }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkError() {
|
|
||||||
error?.let { throw IOException("Download failed", it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,4 @@ open class ReaderPage(
|
|||||||
) : Page(index, url, imageUrl, null) {
|
) : Page(index, url, imageUrl, null) {
|
||||||
|
|
||||||
open lateinit var chapter: ReaderChapter
|
open lateinit var chapter: ReaderChapter
|
||||||
|
|
||||||
/** Set by a viewer before the download; filled during [Page.State.DownloadImage]. */
|
|
||||||
@Volatile
|
|
||||||
var downloadStream: DownloadStream? = null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,6 @@ class ReaderPreferences(
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dualPageView: Preference<DualPageView> = preferenceStore.getEnum(
|
|
||||||
"pref_dual_page_view",
|
|
||||||
DualPageView.NEVER,
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Color filter
|
// region Color filter
|
||||||
@@ -204,24 +199,6 @@ class ReaderPreferences(
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region WebGpu
|
|
||||||
|
|
||||||
val transitionAnimation: Preference<TransitionAnimation> =
|
|
||||||
preferenceStore.getEnum("webgpu_transition_animation", TransitionAnimation.BASIC)
|
|
||||||
|
|
||||||
val transitionAnimationDual: Preference<TransitionAnimation> =
|
|
||||||
preferenceStore.getEnum("webgpu_dual_transition_animation", TransitionAnimation.BASIC)
|
|
||||||
|
|
||||||
val cutoutMode: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_cutout_mode", CutoutMode.AVOID)
|
|
||||||
|
|
||||||
val cutoutModeDual: Preference<CutoutMode> = preferenceStore.getEnum("webgpu_dual_cutout_mode", CutoutMode.IGNORE)
|
|
||||||
|
|
||||||
val continuousMinWidth: Preference<Int> = preferenceStore.getInt("webgpu_continuous_minwidth", 100)
|
|
||||||
|
|
||||||
val continuousGap: Preference<Int> = preferenceStore.getInt("webgpu_continuous_gap", 10)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
enum class FlashColor {
|
enum class FlashColor {
|
||||||
BLACK,
|
BLACK,
|
||||||
WHITE,
|
WHITE,
|
||||||
@@ -234,14 +211,8 @@ class ReaderPreferences(
|
|||||||
val shouldInvertVertical: Boolean = false,
|
val shouldInvertVertical: Boolean = false,
|
||||||
) {
|
) {
|
||||||
NONE(MR.strings.tapping_inverted_none),
|
NONE(MR.strings.tapping_inverted_none),
|
||||||
HORIZONTAL(
|
HORIZONTAL(MR.strings.tapping_inverted_horizontal, shouldInvertHorizontal = true),
|
||||||
MR.strings.tapping_inverted_horizontal,
|
VERTICAL(MR.strings.tapping_inverted_vertical, shouldInvertVertical = true),
|
||||||
shouldInvertHorizontal = true,
|
|
||||||
),
|
|
||||||
VERTICAL(
|
|
||||||
MR.strings.tapping_inverted_vertical,
|
|
||||||
shouldInvertVertical = true,
|
|
||||||
),
|
|
||||||
BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true),
|
BOTH(MR.strings.tapping_inverted_both, shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,43 +223,6 @@ class ReaderPreferences(
|
|||||||
LOWEST(47),
|
LOWEST(47),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TransitionAnimation(val titleRes: StringResource) {
|
|
||||||
BASIC(MR.strings.transition_animation_basic),
|
|
||||||
FLIP(MR.strings.transition_animation_flip),
|
|
||||||
FLIP_LEFT(MR.strings.transition_animation_flip_left),
|
|
||||||
FLIP_RIGHT(
|
|
||||||
MR.strings.transition_animation_flip_right,
|
|
||||||
),
|
|
||||||
STACK_LEFT(MR.strings.transition_animation_stack_left),
|
|
||||||
STACK_RIGHT(MR.strings.transition_animation_stack_right),
|
|
||||||
STACK_UP(
|
|
||||||
MR.strings.transition_animation_stack_up,
|
|
||||||
),
|
|
||||||
STACK_DOWN(MR.strings.transition_animation_stack_down),
|
|
||||||
SPHERE(MR.strings.transition_animation_sphere),
|
|
||||||
CUBE_INSIDE(
|
|
||||||
MR.strings.transition_animation_cube_inside,
|
|
||||||
),
|
|
||||||
CUBE_OUTSIDE(MR.strings.transition_animation_cube_outside),
|
|
||||||
FADE(MR.strings.transition_animation_fade),
|
|
||||||
FADE_WHITE(
|
|
||||||
MR.strings.transition_animation_fade_white,
|
|
||||||
),
|
|
||||||
NONE(MR.strings.transition_animation_none),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class CutoutMode(val titleRes: StringResource) {
|
|
||||||
IGNORE(MR.strings.cutout_mode_ignore),
|
|
||||||
AVOID(MR.strings.cutout_mode_avoid),
|
|
||||||
SHIFT(MR.strings.cutout_mode_shift),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class DualPageView(val titleRes: StringResource) {
|
|
||||||
NEVER(MR.strings.dual_page_view_never),
|
|
||||||
ALWAYS(MR.strings.dual_page_view_always),
|
|
||||||
WIDE(MR.strings.dual_page_view_wide),
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val WEBTOON_PADDING_MIN = 0
|
const val WEBTOON_PADDING_MIN = 0
|
||||||
const val WEBTOON_PADDING_MAX = 25
|
const val WEBTOON_PADDING_MAX = 25
|
||||||
@@ -313,13 +247,6 @@ class ReaderPreferences(
|
|||||||
MR.strings.scale_type_smart_fit,
|
MR.strings.scale_type_smart_fit,
|
||||||
)
|
)
|
||||||
|
|
||||||
val ImageScaleTypeWebGpuViewer = listOf(
|
|
||||||
MR.strings.scale_type_fit_screen,
|
|
||||||
MR.strings.scale_type_fit_width,
|
|
||||||
MR.strings.scale_type_fit_height,
|
|
||||||
MR.strings.scale_type_original_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
val ZoomStart = listOf(
|
val ZoomStart = listOf(
|
||||||
MR.strings.zoom_start_automatic,
|
MR.strings.zoom_start_automatic,
|
||||||
MR.strings.zoom_start_left,
|
MR.strings.zoom_start_left,
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
|||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewer
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webgpu.WebGpuViewerContinuous
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
||||||
import mihon.app.di.appGraph
|
|
||||||
import mihon.icons.custommaterialsymbols.CustomMaterialSymbols
|
import mihon.icons.custommaterialsymbols.CustomMaterialSymbols
|
||||||
import mihon.icons.custommaterialsymbols.rounded.ReaderLeftToRight
|
import mihon.icons.custommaterialsymbols.rounded.ReaderLeftToRight
|
||||||
import mihon.icons.custommaterialsymbols.rounded.ReaderLongStrip
|
import mihon.icons.custommaterialsymbols.rounded.ReaderLongStrip
|
||||||
@@ -77,16 +74,6 @@ enum class ReadingMode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
|
fun toViewer(preference: Int?, activity: ReaderActivity): Viewer {
|
||||||
if (activity.appGraph.basePreferences.highQualityRenderer.get()) {
|
|
||||||
return when (fromPreference(preference)) {
|
|
||||||
LEFT_TO_RIGHT -> WebGpuViewer(activity, isReversed = false, isVertical = false)
|
|
||||||
RIGHT_TO_LEFT -> WebGpuViewer(activity, isReversed = true, isVertical = false)
|
|
||||||
VERTICAL -> WebGpuViewer(activity, isReversed = false, isVertical = true)
|
|
||||||
WEBTOON -> WebGpuViewerContinuous(activity, useGap = false)
|
|
||||||
CONTINUOUS_VERTICAL -> WebGpuViewerContinuous(activity, useGap = true)
|
|
||||||
DEFAULT -> throw IllegalStateException("Preference value must be resolved: $preference")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return when (fromPreference(preference)) {
|
return when (fromPreference(preference)) {
|
||||||
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
|
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
|
||||||
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
|
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonStripImageView
|
|||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper view for showing page image.
|
* A wrapper view for showing page image.
|
||||||
@@ -240,6 +241,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
removeView(pageView)
|
removeView(pageView)
|
||||||
|
|
||||||
pageView = SubsamplingScaleImageView(context).apply {
|
pageView = SubsamplingScaleImageView(context).apply {
|
||||||
|
setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
|
||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumTileDpi(180)
|
setMinimumTileDpi(180)
|
||||||
@@ -300,6 +302,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
isVisible = true
|
isVisible = true
|
||||||
}
|
}
|
||||||
is BufferedSource -> {
|
is BufferedSource -> {
|
||||||
|
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
|
||||||
setImage(ImageSource.inputStream(data.inputStream()))
|
setImage(ImageSource.inputStream(data.inputStream()))
|
||||||
isVisible = true
|
isVisible = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration used by pager viewers.
|
|
||||||
*/
|
|
||||||
class WebGpuConfig(
|
|
||||||
private val viewer: WebGpuViewer,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
readerPreferences: ReaderPreferences,
|
|
||||||
) : ViewerConfig(readerPreferences, scope) {
|
|
||||||
|
|
||||||
var theme = readerPreferences.readerTheme.get()
|
|
||||||
private set
|
|
||||||
|
|
||||||
var automaticBackground = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
var imageScaleType = 1
|
|
||||||
private set
|
|
||||||
|
|
||||||
var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
|
|
||||||
private set
|
|
||||||
|
|
||||||
var imageCropBorders = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var imageCropBordersWebtoon = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var navigateToPan = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var landscapeZoom = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var transitionAnimation = ReaderPreferences.TransitionAnimation.BASIC
|
|
||||||
private set
|
|
||||||
|
|
||||||
var transitionAnimationDual = ReaderPreferences.TransitionAnimation.BASIC
|
|
||||||
private set
|
|
||||||
|
|
||||||
var cutoutMode = ReaderPreferences.CutoutMode.AVOID
|
|
||||||
private set
|
|
||||||
|
|
||||||
var cutoutModeDual = ReaderPreferences.CutoutMode.AVOID
|
|
||||||
private set
|
|
||||||
|
|
||||||
var dualPageView = ReaderPreferences.DualPageView.NEVER
|
|
||||||
private set
|
|
||||||
|
|
||||||
var continuousMinWidth = 100
|
|
||||||
private set
|
|
||||||
|
|
||||||
var zoomOutDisabled = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
var continuousGap = 10
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
readerPreferences.readerTheme.register(
|
|
||||||
{
|
|
||||||
theme = it
|
|
||||||
automaticBackground = it == 3
|
|
||||||
},
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.imageScaleType.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
|
|
||||||
|
|
||||||
readerPreferences.zoomStart.register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() })
|
|
||||||
|
|
||||||
readerPreferences.cropBorders.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
|
||||||
|
|
||||||
readerPreferences.cropBordersWebtoon.register(
|
|
||||||
{ imageCropBordersWebtoon = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.navigateToPan.register({ navigateToPan = it })
|
|
||||||
|
|
||||||
readerPreferences.landscapeZoom.register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() })
|
|
||||||
|
|
||||||
readerPreferences.navigationModePager.register({ navigationMode = it }, { updateNavigation(navigationMode) })
|
|
||||||
|
|
||||||
readerPreferences.pagerNavInverted.register({ tappingInverted = it }, { navigator.invertMode = it })
|
|
||||||
readerPreferences.pagerNavInverted.changes().drop(1).onEach { navigationModeChangedListener?.invoke() }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
readerPreferences.dualPageSplitPaged.register(
|
|
||||||
{ dualPageSplit = it },
|
|
||||||
{
|
|
||||||
imagePropertyChangedListener?.invoke()
|
|
||||||
dualPageSplitChangedListener?.invoke(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.dualPageInvertPaged.register(
|
|
||||||
{ dualPageInvert = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.dualPageRotateToFit.register(
|
|
||||||
{ dualPageRotateToFit = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.dualPageRotateToFitInvert.register(
|
|
||||||
{ dualPageRotateToFitInvert = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.transitionAnimation.register(
|
|
||||||
{ transitionAnimation = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.transitionAnimationDual.register(
|
|
||||||
{ transitionAnimationDual = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.cutoutMode.register(
|
|
||||||
{ cutoutMode = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.cutoutModeDual.register(
|
|
||||||
{ cutoutModeDual = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.dualPageView.register(
|
|
||||||
{ dualPageView = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.continuousMinWidth.register(
|
|
||||||
{ continuousMinWidth = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.webtoonDisableZoomOut.register(
|
|
||||||
{ zoomOutDisabled = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
|
|
||||||
readerPreferences.continuousGap.register(
|
|
||||||
{ continuousGap = it },
|
|
||||||
{ imagePropertyChangedListener?.invoke() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun zoomTypeFromPreference(value: Int) {
|
|
||||||
imageZoomType = when (value) {
|
|
||||||
// Auto
|
|
||||||
1 -> if (viewer.isReversed) {
|
|
||||||
ReaderPageImageView.ZoomStartPosition.RIGHT
|
|
||||||
} else {
|
|
||||||
ReaderPageImageView.ZoomStartPosition.LEFT
|
|
||||||
}
|
|
||||||
// Left
|
|
||||||
2 -> ReaderPageImageView.ZoomStartPosition.LEFT
|
|
||||||
// Right
|
|
||||||
3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
|
|
||||||
// Center
|
|
||||||
else -> ReaderPageImageView.ZoomStartPosition.CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var navigator: ViewerNavigation = defaultNavigation()
|
|
||||||
set(value) {
|
|
||||||
field = value.also { it.invertMode = this.tappingInverted }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun defaultNavigation(): ViewerNavigation {
|
|
||||||
return if (viewer.isVertical) {
|
|
||||||
LNavigation()
|
|
||||||
} else {
|
|
||||||
RightAndLeftNavigation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateNavigation(navigationMode: Int) {
|
|
||||||
navigator = when (navigationMode) {
|
|
||||||
0 -> defaultNavigation()
|
|
||||||
1 -> LNavigation()
|
|
||||||
2 -> KindlishNavigation()
|
|
||||||
3 -> EdgeNavigation()
|
|
||||||
4 -> RightAndLeftNavigation()
|
|
||||||
5 -> DisabledNavigation()
|
|
||||||
else -> defaultNavigation()
|
|
||||||
}
|
|
||||||
navigationModeChangedListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
-102
@@ -1,102 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
|
||||||
|
|
||||||
import ca.mpreg.webgpuviewer.ImageViewContinuous
|
|
||||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = false) :
|
|
||||||
WebGpuViewer(activity, isReversed = false, isVertical = true, pager = ImageViewContinuous(activity)) {
|
|
||||||
|
|
||||||
override val isContinuous: Boolean get() = true
|
|
||||||
|
|
||||||
// How many pages the viewport shows depends on the zoom, and a page on screen has to be
|
|
||||||
// decoded rather than merely reserved - so the window follows what the last frame reached.
|
|
||||||
override val preloadAhead get() = max(3, state.pagesBelow)
|
|
||||||
override val preloadBehind get() = max(2, state.pagesAbove)
|
|
||||||
|
|
||||||
override val cacheSize get() = 2 + 2 * max(4, state.pagesAbove + state.pagesBelow)
|
|
||||||
|
|
||||||
private val state get() = (pager as ImageViewContinuous).state
|
|
||||||
|
|
||||||
init {
|
|
||||||
state.backgroundColor = readerBackgroundColor()
|
|
||||||
|
|
||||||
state.onViewport = { readThrough ->
|
|
||||||
val pageChanged = readThrough !== lastReadThrough
|
|
||||||
if (pageChanged) {
|
|
||||||
lastReadThrough = readThrough
|
|
||||||
readThrough?.let { select(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val page0 = state.getPage(0)
|
|
||||||
val edge = (page0 is TransitionPage && page0.prevChapter == null) ||
|
|
||||||
(readThrough is TransitionPage && readThrough.nextChapter == null)
|
|
||||||
|
|
||||||
val first = wasAtEdge == null
|
|
||||||
val edgeChanged = edge != wasAtEdge
|
|
||||||
wasAtEdge = edge
|
|
||||||
if (edge) {
|
|
||||||
if (edgeChanged) scope.launch { activity.showMenu() }
|
|
||||||
} else if (!first && (edgeChanged || pageChanged) && !activity.isScrollingThroughPages) {
|
|
||||||
scope.launch { activity.hideMenu() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Null until the first frame, so opening mid-document neither shows nor hides. */
|
|
||||||
@Volatile
|
|
||||||
private var wasAtEdge: Boolean? = null
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var lastReadThrough: ImagePage? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark [imagePage] read - its bottom has cleared the viewport, which is later than
|
|
||||||
* onPageChange's "top arrived" and the reason this mode selects from here: scrolling up to
|
|
||||||
* reveal the page above would otherwise walk progress back past pages still on screen.
|
|
||||||
*/
|
|
||||||
private fun select(imagePage: ImagePage) {
|
|
||||||
val page = readerPageFor(imagePage) ?: return
|
|
||||||
scope.launch { activity.onPageSelected(page) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readerPageFor(imagePage: ImagePage): ReaderPage? {
|
|
||||||
// Off the page itself: a transition page is never currentPage (the scroll walk stops
|
|
||||||
// before it), so it is the first thing a preload evicts.
|
|
||||||
if (imagePage is TransitionPage) return imagePage.prevChapter?.pages?.lastOrNull()
|
|
||||||
|
|
||||||
// Only an image page needs the lookup - nothing on it names its ReaderPage. Evicted
|
|
||||||
// means scrolled well clear of, so there is no progress left to report.
|
|
||||||
return (viewerPageFor(imagePage) as? ViewerReaderPage)?.page
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun destroy() {
|
|
||||||
state.onViewport = null
|
|
||||||
lastReadThrough = null
|
|
||||||
wasAtEdge = null
|
|
||||||
super.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollByHalfPage(direction: Int) {
|
|
||||||
state.animateScroll(direction * state.height / 2f)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun moveRight() = scrollByHalfPage(1)
|
|
||||||
|
|
||||||
override fun moveLeft() = scrollByHalfPage(-1)
|
|
||||||
|
|
||||||
override fun moveToPage(page: ReaderPage) {
|
|
||||||
super.moveToPage(page)
|
|
||||||
wasAtEdge = null
|
|
||||||
lastReadThrough = null
|
|
||||||
// Also for a jump to the page already showing, which turns nothing to slide in.
|
|
||||||
state.resetScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun animateTurn(direction: Int, fromSpread: ImagePage) {
|
|
||||||
state.animateSlideIn(direction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import javax.microedition.khronos.egl.EGL10
|
||||||
|
import javax.microedition.khronos.egl.EGLConfig
|
||||||
|
import javax.microedition.khronos.egl.EGLContext
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
object GLUtil {
|
||||||
|
val DEVICE_TEXTURE_LIMIT: Int by lazy {
|
||||||
|
// Get EGL Display
|
||||||
|
val egl = EGLContext.getEGL() as EGL10
|
||||||
|
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
|
||||||
|
|
||||||
|
// Initialise
|
||||||
|
val version = IntArray(2)
|
||||||
|
egl.eglInitialize(display, version)
|
||||||
|
|
||||||
|
// Query total number of configurations
|
||||||
|
val totalConfigurations = IntArray(1)
|
||||||
|
egl.eglGetConfigs(display, null, 0, totalConfigurations)
|
||||||
|
|
||||||
|
// Query actual list configurations
|
||||||
|
val configurationsList = arrayOfNulls<EGLConfig>(totalConfigurations[0])
|
||||||
|
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations)
|
||||||
|
|
||||||
|
val textureSize = IntArray(1)
|
||||||
|
var maximumTextureSize = 0
|
||||||
|
|
||||||
|
// Iterate through all the configurations to located the maximum texture size
|
||||||
|
for (i in 0..<totalConfigurations[0]) {
|
||||||
|
// Only need to check for width since opengl textures are always squared
|
||||||
|
egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize)
|
||||||
|
|
||||||
|
// Keep track of the maximum texture size
|
||||||
|
if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release
|
||||||
|
egl.eglTerminate(display)
|
||||||
|
|
||||||
|
// Return largest texture size found (after making it a multiplier of [Multiplier]), or default
|
||||||
|
max(maximumTextureSize, SAFE_TEXTURE_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
const val SAFE_TEXTURE_LIMIT: Int = 2048
|
||||||
|
|
||||||
|
val CUSTOM_TEXTURE_LIMIT_OPTIONS: List<Int> by lazy {
|
||||||
|
val steps = DEVICE_TEXTURE_LIMIT / MULTIPLIER
|
||||||
|
buildList(steps) {
|
||||||
|
add(DEVICE_TEXTURE_LIMIT)
|
||||||
|
for (step in steps downTo 2) {
|
||||||
|
val value = step * MULTIPLIER
|
||||||
|
if (value >= DEVICE_TEXTURE_LIMIT) continue
|
||||||
|
add(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val MULTIPLIER: Int = 1024
|
||||||
@@ -21,11 +21,13 @@ import androidx.core.graphics.drawable.toDrawable
|
|||||||
import androidx.core.graphics.get
|
import androidx.core.graphics.get
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
import ca.mpreg.imagedecoder.ImageDecoder
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.decoder.Format
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -47,20 +49,17 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun findImageType(stream: InputStream): ImageType? {
|
fun findImageType(stream: InputStream): ImageType? {
|
||||||
return try {
|
return try {
|
||||||
val format = ImageDecoder.open(stream).use { dec -> dec.format }
|
when (getImageType(stream)?.format) {
|
||||||
when (format) {
|
Format.Avif -> ImageType.AVIF
|
||||||
ImageDecoder.Format.JPEG -> ImageType.JPEG
|
Format.Gif -> ImageType.GIF
|
||||||
ImageDecoder.Format.PNG -> ImageType.PNG
|
Format.Heif -> ImageType.HEIF
|
||||||
ImageDecoder.Format.WEBP -> ImageType.WEBP
|
Format.Jpeg -> ImageType.JPEG
|
||||||
ImageDecoder.Format.GIF -> ImageType.GIF
|
Format.Jxl -> ImageType.JXL
|
||||||
ImageDecoder.Format.AVIF -> ImageType.AVIF
|
Format.Png -> ImageType.PNG
|
||||||
ImageDecoder.Format.HEIF -> ImageType.HEIF
|
Format.Webp -> ImageType.WEBP
|
||||||
ImageDecoder.Format.JXL -> ImageType.JXL
|
|
||||||
ImageDecoder.Format.JP2 -> ImageType.JP2
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR) { "findImageType: ${e.message}" }
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,22 +71,38 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun isAnimatedAndSupported(source: BufferedSource): Boolean {
|
fun isAnimatedAndSupported(source: BufferedSource): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val type = findImageType(source.peek().inputStream()) ?: return false
|
val type = getImageType(source.peek().inputStream()) ?: return false
|
||||||
when (type) {
|
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
|
||||||
ImageType.GIF -> true
|
when (type.format) {
|
||||||
ImageType.WEBP, ImageType.HEIF -> {
|
Format.Gif -> true
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false
|
// Animated WebP on Android 9+
|
||||||
ImageDecoder.open(source.peek().inputStream()).use { dec -> dec.pages > 1 }
|
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
}
|
// Animated Heif on Android 11+
|
||||||
|
Format.Heif -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR) { "isAnimatedAndSupported: ${e.message}" }
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getImageType(stream: InputStream): tachiyomi.decoder.ImageType? {
|
||||||
|
val bytes = ByteArray(32)
|
||||||
|
|
||||||
|
val length = if (stream.markSupported()) {
|
||||||
|
stream.mark(bytes.size)
|
||||||
|
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
||||||
|
} else {
|
||||||
|
stream.read(bytes, 0, bytes.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageDecoder.findType(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
enum class ImageType(val mime: String, val extension: String) {
|
enum class ImageType(val mime: String, val extension: String) {
|
||||||
AVIF("image/avif", "avif"),
|
AVIF("image/avif", "avif"),
|
||||||
GIF("image/gif", "gif"),
|
GIF("image/gif", "gif"),
|
||||||
@@ -96,8 +111,6 @@ object ImageUtil {
|
|||||||
JXL("image/jxl", "jxl"),
|
JXL("image/jxl", "jxl"),
|
||||||
PNG("image/png", "png"),
|
PNG("image/png", "png"),
|
||||||
WEBP("image/webp", "webp"),
|
WEBP("image/webp", "webp"),
|
||||||
JP2("image/jp2", "jp2"),
|
|
||||||
JPX("image/jpx", "jpx"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -299,24 +312,30 @@ object ImageUtil {
|
|||||||
val bottomOffset = topOffset + splitHeight
|
val bottomOffset = topOffset + splitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun canUseHardwareBitmap(bitmap: Bitmap): Boolean {
|
||||||
|
return canUseHardwareBitmap(bitmap.width, bitmap.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canUseHardwareBitmap(imageSource: BufferedSource): Boolean {
|
||||||
|
return with(extractImageOptions(imageSource)) {
|
||||||
|
canUseHardwareBitmap(outWidth, outHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT
|
||||||
|
|
||||||
|
private fun canUseHardwareBitmap(width: Int, height: Int): Boolean {
|
||||||
|
if (HARDWARE_BITMAP_UNSUPPORTED) return false
|
||||||
|
return maxOf(width, height) <= hardwareBitmapThreshold
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Algorithm for determining what background to accompany a comic/manga page
|
* Algorithm for determining what background to accompany a comic/manga page
|
||||||
*/
|
*/
|
||||||
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
||||||
val image = try {
|
val decoder = ImageDecoder.newInstance(imageStream)
|
||||||
ImageDecoder.open(imageStream).use { it.decodeNext() to it.isHdr }.let { (res, hdr) ->
|
val image = decoder?.decode()
|
||||||
res.use {
|
decoder?.recycle()
|
||||||
val config = if (hdr) Bitmap.Config.RGBA_F16 else Bitmap.Config.ARGB_8888
|
|
||||||
createBitmap(res.width, res.height, config).also { bitmap ->
|
|
||||||
res.image.rewind()
|
|
||||||
bitmap.copyPixelsFromBuffer(res.image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR) { "chooseBackground: ${e.message}" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image == null) return Color.WHITE.toDrawable()
|
if (image == null) return Color.WHITE.toDrawable()
|
||||||
return try {
|
return try {
|
||||||
@@ -564,6 +583,121 @@ object ImageUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
|
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taken from Coil
|
||||||
|
* (https://github.com/coil-kt/coil/blob/1674d3516f061aeacbe749a435b1924f9648fd41/coil-core/src/androidMain/kotlin/coil3/util/hardwareBitmaps.kt)
|
||||||
|
* ---
|
||||||
|
* Maintains a list of devices with broken/incomplete/unstable hardware bitmap implementations.
|
||||||
|
*
|
||||||
|
* Model names are retrieved from
|
||||||
|
* [Google's official device list](https://support.google.com/googleplay/answer/1727131?hl=en).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
val HARDWARE_BITMAP_UNSUPPORTED = when (Build.VERSION.SDK_INT) {
|
||||||
|
26 -> run {
|
||||||
|
val model = Build.MODEL ?: return@run false
|
||||||
|
|
||||||
|
// Samsung Galaxy (ALL)
|
||||||
|
if (model.removePrefix("SAMSUNG-").startsWith("SM-")) return@run true
|
||||||
|
|
||||||
|
val device = Build.DEVICE ?: return@run false
|
||||||
|
|
||||||
|
return@run device in arrayOf(
|
||||||
|
"nora", "nora_8917", "nora_8917_n", // Moto E5
|
||||||
|
"james", "rjames_f", "rjames_go", "pettyl", // Moto E5 Play
|
||||||
|
"hannah", "ahannah", "rhannah", // Moto E5 Plus
|
||||||
|
|
||||||
|
"ali", "ali_n", // Moto G6
|
||||||
|
"aljeter", "aljeter_n", "jeter", // Moto G6 Play
|
||||||
|
"evert", "evert_n", "evert_nt", // Moto G6 Plus
|
||||||
|
|
||||||
|
"G3112", "G3116", "G3121", "G3123", "G3125", // Xperia XA1
|
||||||
|
"G3412", "G3416", "G3421", "G3423", "G3426", // Xperia XA1 Plus
|
||||||
|
"G3212", "G3221", "G3223", "G3226", // Xperia XA1 Ultra
|
||||||
|
|
||||||
|
"BV6800Pro", // BlackView BV6800Pro
|
||||||
|
"CatS41", // Cat S41
|
||||||
|
"Hi9Pro", // CHUWI Hi9 Pro
|
||||||
|
"manning", // Lenovo K8 Note
|
||||||
|
"N5702L", // NUU Mobile G3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
27 -> run {
|
||||||
|
val device = Build.DEVICE ?: return@run false
|
||||||
|
|
||||||
|
return@run device in arrayOf(
|
||||||
|
"mcv1s", // LG Tribute Empire
|
||||||
|
"mcv3", // LG K11
|
||||||
|
"mcv5a", // LG Q7
|
||||||
|
"mcv7a", // LG Stylo 4
|
||||||
|
|
||||||
|
"A30ATMO", // T-Mobile REVVL 2
|
||||||
|
"A70AXLTMO", // T-Mobile REVVL 2 PLUS
|
||||||
|
|
||||||
|
"A3A_8_4G_TMO", // Alcatel 9027W
|
||||||
|
"Edison_CKT", // Alcatel ONYX
|
||||||
|
"EDISON_TF", // Alcatel TCL XL2
|
||||||
|
"FERMI_TF", // Alcatel A501DL
|
||||||
|
"U50A_ATT", // Alcatel TETRA
|
||||||
|
"U50A_PLUS_ATT", // Alcatel 5059R
|
||||||
|
"U50A_PLUS_TF", // Alcatel TCL LX
|
||||||
|
"U50APLUSTMO", // Alcatel 5059Z
|
||||||
|
"U5A_PLUS_4G", // Alcatel 1X
|
||||||
|
|
||||||
|
"RCT6513W87DK5e", // RCA Galileo Pro
|
||||||
|
"RCT6873W42BMF9A", // RCA Voyager
|
||||||
|
"RCT6A03W13", // RCA 10 Viking
|
||||||
|
"RCT6B03W12", // RCA Atlas 10 Pro
|
||||||
|
"RCT6B03W13", // RCA Atlas 10 Pro+
|
||||||
|
"RCT6T06E13", // RCA Artemis 10
|
||||||
|
|
||||||
|
"A3_Pro", // Umidigi A3 Pro
|
||||||
|
"One", // Umidigi One
|
||||||
|
"One_Max", // Umidigi One Max
|
||||||
|
"One_Pro", // Umidigi One Pro
|
||||||
|
"Z2", // Umidigi Z2
|
||||||
|
"Z2_PRO", // Umidigi Z2 Pro
|
||||||
|
|
||||||
|
"Armor_3", // Ulefone Armor 3
|
||||||
|
"Armor_6", // Ulefone Armor 6
|
||||||
|
|
||||||
|
"Blackview", // Blackview BV6000
|
||||||
|
"BV9500", // Blackview BV9500
|
||||||
|
"BV9500Pro", // Blackview BV9500Pro
|
||||||
|
|
||||||
|
"A6L-C", // Nuu A6L-C
|
||||||
|
"N5002LA", // Nuu A7L
|
||||||
|
"N5501LA", // Nuu A5L
|
||||||
|
|
||||||
|
"Power_2_Pro", // Leagoo Power 2 Pro
|
||||||
|
"Power_5", // Leagoo Power 5
|
||||||
|
"Z9", // Leagoo Z9
|
||||||
|
|
||||||
|
"V0310WW", // Blu VIVO VI+
|
||||||
|
"V0330WW", // Blu VIVO XI
|
||||||
|
|
||||||
|
"A3", // BenQ A3
|
||||||
|
"ASUS_X018_4", // Asus ZenFone Max Plus M1 (ZB570TL)
|
||||||
|
"C210AE", // Wiko Life
|
||||||
|
"fireball", // DROID Incredible 4G LTE
|
||||||
|
"ILA_X1", // iLA X1
|
||||||
|
"Infinix-X605_sprout", // Infinix NOTE 5 Stylus
|
||||||
|
"j7maxlte", // Samsung Galaxy J7 Max
|
||||||
|
"KING_KONG_3", // Cubot King Kong 3
|
||||||
|
"M10500", // Packard Bell M10500
|
||||||
|
"S70", // Altice ALTICE S70
|
||||||
|
"S80Lite", // Doogee S80Lite
|
||||||
|
"SGINO6", // SGiNO 6
|
||||||
|
"st18c10bnn", // Barnes and Noble BNTV650
|
||||||
|
"TECNO-CA8", // Tecno CAMON X Pro,
|
||||||
|
"SHIFT6m", // SHIFT 6m
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val getDisplayMaxHeightInPx: Int
|
val getDisplayMaxHeightInPx: Int
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ firebase-bom = "35.0.0"
|
|||||||
firebase-crashlytics = "3.0.8"
|
firebase-crashlytics = "3.0.8"
|
||||||
flexibleAdapter = "5.1.0"
|
flexibleAdapter = "5.1.0"
|
||||||
google-services = "4.5.0"
|
google-services = "4.5.0"
|
||||||
image-decoder = "16"
|
image-decoder = "e03b81e18a"
|
||||||
injekt = "1.16.1"
|
injekt = "1.16.1"
|
||||||
jsoup = "1.23.2"
|
jsoup = "1.23.2"
|
||||||
junit = "6.1.3"
|
junit = "6.1.3"
|
||||||
@@ -70,13 +70,12 @@ spotless = "8.10.4"
|
|||||||
sqldelight = "2.4.1"
|
sqldelight = "2.4.1"
|
||||||
sqldelight-androidx-driver = "0.2.2"
|
sqldelight-androidx-driver = "0.2.2"
|
||||||
stringSimilarity = "0.1.0"
|
stringSimilarity = "0.1.0"
|
||||||
subsamplingScaleImageView = "1b283e2bc4"
|
subsamplingScaleImageView = "66e0db195d"
|
||||||
swipe = "1.3.0"
|
swipe = "1.3.0"
|
||||||
tapmoc = "0.4.2"
|
tapmoc = "0.4.2"
|
||||||
unifile = "08f224c8f9"
|
unifile = "08f224c8f9"
|
||||||
valkyrie = "0.5.2"
|
valkyrie = "0.5.2"
|
||||||
voyager = "2.2.21-1.10.3"
|
voyager = "2.2.21-1.10.3"
|
||||||
webgpuviewer = "52"
|
|
||||||
xmlutil = "1.0.2"
|
xmlutil = "1.0.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -140,7 +139,7 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
|||||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||||
flexibleAdapter = { module = "eu.davidea:flexible-adapter", version.ref = "flexibleAdapter" }
|
flexibleAdapter = { module = "eu.davidea:flexible-adapter", version.ref = "flexibleAdapter" }
|
||||||
image-decoder = { module = "ca.mpreg:imagedecoder", version.ref = "image-decoder" }
|
image-decoder = { module = "com.github.mihonapp:image-decoder", version.ref = "image-decoder" }
|
||||||
injekt = { module = "uy.kohesive.injekt:injekt-core", version.ref = "injekt" }
|
injekt = { module = "uy.kohesive.injekt:injekt-core", version.ref = "injekt" }
|
||||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
@@ -190,14 +189,13 @@ sqldelight-async = { module = "app.cash.sqldelight:async-extensions", version.re
|
|||||||
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||||
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
||||||
subsamplingScaleImageView = { module = "com.github.mihonapp:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
||||||
swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
|
swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
|
||||||
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
|
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
|
||||||
unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
|
unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
|
||||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||||
webgpuviewer = { module = "ca.mpreg:webgpuviewer", version.ref = "webgpuviewer" }
|
|
||||||
xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" }
|
xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core-android", version.ref = "xmlutil" }
|
||||||
xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" }
|
xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" }
|
||||||
|
|
||||||
|
|||||||
@@ -427,7 +427,9 @@
|
|||||||
<string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string>
|
<string name="pref_webtoon_vertical_navigator_on_left">Place vertical navigator on the left side</string>
|
||||||
<string name="pref_show_reading_mode">Show reading mode</string>
|
<string name="pref_show_reading_mode">Show reading mode</string>
|
||||||
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
||||||
<string name="pref_high_quality_renderer">Use high quality renderer</string>
|
<string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string>
|
||||||
|
<string name="pref_hardware_bitmap_threshold_default">Default (%d)</string>
|
||||||
|
<string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string>
|
||||||
<string name="pref_display_profile">Custom display profile</string>
|
<string name="pref_display_profile">Custom display profile</string>
|
||||||
<string name="pref_crop_borders">Crop borders</string>
|
<string name="pref_crop_borders">Crop borders</string>
|
||||||
<string name="pref_custom_brightness">Custom brightness</string>
|
<string name="pref_custom_brightness">Custom brightness</string>
|
||||||
@@ -517,34 +519,6 @@
|
|||||||
<string name="pref_low">Low</string>
|
<string name="pref_low">Low</string>
|
||||||
<string name="pref_lowest">Lowest</string>
|
<string name="pref_lowest">Lowest</string>
|
||||||
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
|
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
|
||||||
<string name="webgpu_viewer">WebGPU</string>
|
|
||||||
<string name="pref_transition_animation">Transition animation</string>
|
|
||||||
<string name="pref_transition_animation_dual">Transition animation (dual)</string>
|
|
||||||
<string name="transition_animation_basic">Basic</string>
|
|
||||||
<string name="transition_animation_flip_left">Page flip (left)</string>
|
|
||||||
<string name="transition_animation_flip_right">Page flip (right)</string>
|
|
||||||
<string name="transition_animation_stack_left">Stack (left)</string>
|
|
||||||
<string name="transition_animation_stack_right">Stack (right)</string>
|
|
||||||
<string name="transition_animation_stack_up">Stack (up)</string>
|
|
||||||
<string name="transition_animation_stack_down">Stack (down)</string>
|
|
||||||
<string name="transition_animation_sphere">Sphere</string>
|
|
||||||
<string name="transition_animation_cube_inside">Cube (Inside)</string>
|
|
||||||
<string name="transition_animation_cube_outside">Cube (Outside)</string>
|
|
||||||
<string name="transition_animation_fade">Fade</string>
|
|
||||||
<string name="transition_animation_fade_white">Fade to white</string>
|
|
||||||
<string name="transition_animation_none">None</string>
|
|
||||||
<string name="transition_animation_flip">Page flip</string>
|
|
||||||
<string name="pref_cutout_mode">Display cutout mode</string>
|
|
||||||
<string name="pref_cutout_mode_dual">Display cutout mode (dual)</string>
|
|
||||||
<string name="cutout_mode_ignore">Ignore</string>
|
|
||||||
<string name="cutout_mode_avoid">Avoid</string>
|
|
||||||
<string name="cutout_mode_shift">Shift</string>
|
|
||||||
<string name="pref_dual_page_view">Dual page view</string>
|
|
||||||
<string name="dual_page_view_never">Never</string>
|
|
||||||
<string name="dual_page_view_always">Always</string>
|
|
||||||
<string name="dual_page_view_wide">When wide</string>
|
|
||||||
<string name="pref_continuous_minwidth">Min width</string>
|
|
||||||
<string name="pref_continuous_gap">Gap</string>
|
|
||||||
|
|
||||||
<!-- Downloads section -->
|
<!-- Downloads section -->
|
||||||
<string name="pref_category_delete_chapters">Delete chapters</string>
|
<string name="pref_category_delete_chapters">Delete chapters</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user