Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion komelia-app/androidApp/src/main/kotlin/snd/komelia/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class App : Application() {
private fun initWorkManager() {
val config = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(MyWorkerFactory(dependencies.filterNotNull().map { it.offlineDependencies }))
.setWorkerFactory(MyWorkerFactory(dependencies.filterNotNull().map { it.offlineDependencies }.filterNotNull()))
.setWorkerCoroutineContext(Dispatchers.IO)
.build()
WorkManager.initialize(this, config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ interface ImageReaderSettingsRepository {
fun getPagedReaderScaleType(): Flow<LayoutScaleType>
suspend fun putPagedReaderScaleType(type: LayoutScaleType)

fun getGtcModeEnabled(): Flow<Boolean>
suspend fun putGtcModeEnabled(enabled: Boolean)

fun getPagedReaderReadingDirection(): Flow<PagedReadingDirection>
suspend fun putPagedReaderReadingDirection(direction: PagedReadingDirection)

Expand Down
14 changes: 14 additions & 0 deletions komelia-epub-reader/ttu-ebook-reader/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ data class ImageReaderSettings(
val readerType: ReaderType = PAGED,
val stretchToFit: Boolean = true,
val pagedScaleType: LayoutScaleType = LayoutScaleType.SCREEN,
val gtcModeEnabled: Boolean = false,
val pagedReadingDirection: PagedReadingDirection = PagedReadingDirection.LEFT_TO_RIGHT,
val pagedPageLayout: PageDisplayLayout = PageDisplayLayout.SINGLE_PAGE,
val continuousReadingDirection: ContinuousReadingDirection = ContinuousReadingDirection.TOP_TO_BOTTOM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class ReaderSettingsRepositoryWrapper(
wrapper.transform { it.copy(pagedScaleType = type) }
}

override fun getGtcModeEnabled(): Flow<Boolean> {
return wrapper.mapState { it.gtcModeEnabled }
}

override suspend fun putGtcModeEnabled(enabled: Boolean) {
wrapper.transform { it.copy(gtcModeEnabled = enabled) }
}

override fun getPagedReaderReadingDirection(): Flow<PagedReadingDirection> {
return wrapper.mapState { it.pagedReadingDirection }
}
Expand Down
4 changes: 2 additions & 2 deletions komelia-infra/database/sqlite/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ kotlin {
implementation(libs.exposed.kotlin.datetime)
implementation(libs.hikariCP)
implementation(libs.flyway.core)
// implementation(libs.sqlite.xerial.jdbc)
implementation(files("sqlite-jdbc-3.51.3.0-linux.jar"))
implementation(libs.sqlite.xerial.jdbc)
//// implementation(files("sqlite-jdbc-3.51.3.0-linux.jar"))
}
}
}
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE ImageReaderSettings
ADD COLUMN gtc_mode_enabled BOOLEAN DEFAULT 0 NOT NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AppMigrations : MigrationResourcesProvider() {
"V10__komf_settings.sql",
"V11__home_filters.sql",
"V12__offline_mode.sql",
"V13__gtc_page_orientation.sql",
)

override suspend fun getMigration(name: String): ByteArray? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ExposedImageReaderSettingsRepository(database: Database) : ExposedReposito
readerType = ReaderType.valueOf(it[ImageReaderSettingsTable.readerType]),
stretchToFit = it[ImageReaderSettingsTable.stretchToFit],
pagedScaleType = LayoutScaleType.valueOf(it[ImageReaderSettingsTable.pagedScaleType]),
gtcModeEnabled = it[ImageReaderSettingsTable.gtcModeEnabled],
pagedReadingDirection = PagedReadingDirection.valueOf(it[ImageReaderSettingsTable.pagedReadingDirection]),
pagedPageLayout = PageDisplayLayout.valueOf(it[ImageReaderSettingsTable.pagedPageLayout]),
continuousReadingDirection = ContinuousReadingDirection.valueOf(it[ImageReaderSettingsTable.continuousReadingDirection]),
Expand Down Expand Up @@ -65,6 +66,7 @@ class ExposedImageReaderSettingsRepository(database: Database) : ExposedReposito
it[readerType] = settings.readerType.name
it[stretchToFit] = settings.stretchToFit
it[pagedScaleType] = settings.pagedScaleType.name
it[gtcModeEnabled] = settings.gtcModeEnabled
it[pagedReadingDirection] = settings.pagedReadingDirection.name
it[pagedPageLayout] = settings.pagedPageLayout.name
it[continuousReadingDirection] = settings.continuousReadingDirection.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ object ImageReaderSettingsTable : Table("ImageReaderSettings") {
val stretchToFit = bool("stretch_to_fit")

val pagedScaleType = text("paged_scale_type")
val gtcModeEnabled = bool("gtc_mode_enabled")
val pagedReadingDirection = text("paged_reading_direction")
val pagedPageLayout = text("paged_page_layout")

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package snd.komelia.ui.platform

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext

@Composable
actual fun LockScreenOrientation(locked: Boolean) {
val activity = LocalContext.current.findActivity() ?: return

DisposableEffect(locked) {
val previousOrientation = activity.requestedOrientation
activity.requestedOrientation =
if (locked) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED

onDispose {
activity.requestedOrientation = previousOrientation
}
}
}

private tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
123 changes: 123 additions & 0 deletions komelia-ui/src/commonMain/composeResources/files/komga.html

Large diffs are not rendered by default.

205 changes: 205 additions & 0 deletions komelia-ui/src/commonMain/composeResources/files/ttsu.html

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package snd.komelia.ui.platform

import androidx.compose.runtime.Composable

/**
* Locks the app to portrait orientation while [locked] is true, restoring whatever
* orientation setting was in effect before once [locked] goes back to false (or the
* composable leaves composition). No-op on platforms without an OS-level concept of
* screen orientation (desktop, web).
*/
@Composable
expect fun LockScreenOrientation(locked: Boolean)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package snd.komelia.ui.reader.image.paged

import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
import snd.komelia.image.ReaderImage
import snd.komelia.settings.model.PageDisplayLayout
import snd.komelia.settings.model.PageDisplayLayout.SINGLE_PAGE
import snd.komelia.ui.reader.image.PageMetadata
import snd.komelia.ui.reader.image.ScreenScaleState

/**
* GTC (device-height orientation): when enabled, a landscape page (its longest side
* horizontal) viewed on a portrait device is rotated so its longest side aligns with
* the device height, and the image is scaled to fit before display.
* Only applies to single page layout - rotating a two-page spread isn't well-defined.
*
* All GTC-specific decision/scaling logic lives here so future upstream merges only
* need to reconcile the small one-line call sites left in PagedReaderState.kt and
* PagedReaderContent.kt, rather than colliding with inlined GTC logic.
*/
object GtcReaderSupport {

fun isApplicable(
gtcModeEnabled: Boolean,
layout: PageDisplayLayout,
pageMetadata: PageMetadata,
containerSize: IntSize,
): Boolean {
return gtcModeEnabled &&
layout == SINGLE_PAGE &&
pageMetadata.isLandscape() &&
containerSize.height > containerSize.width
}

fun adjustedContainerSize(
gtcModeEnabled: Boolean,
layout: PageDisplayLayout,
pages: List<PageMetadata>,
containerSize: IntSize,
): IntSize {
val page = pages.singleOrNull() ?: return containerSize
return if (isApplicable(gtcModeEnabled, layout, page, containerSize))
IntSize(containerSize.height, containerSize.width)
else containerSize
}

fun swapIfApplicable(applicable: Boolean, size: IntSize): IntSize =
if (applicable) IntSize(size.height, size.width) else size

suspend fun updateImage(
image: ReaderImage,
maxPageSize: IntSize,
stretchToFit: Boolean,
) {
val imageDisplaySize = image.calculateSizeForArea(maxPageSize, stretchToFit) ?: maxPageSize
image.requestUpdate(
visibleDisplaySize = IntRect(0, 0, imageDisplaySize.width, imageDisplaySize.height),
zoomFactor = 1f,
maxDisplaySize = maxPageSize
)
}

/**
* fitToScreenSize is computed against a width/height-swapped maxPageSize (see
* adjustedContainerSize), so it is expressed in rotated-page space. Locks the view
* to a fit-to-screen scale, swapping the target size back to real screen space first
* so the zoom-limit math compares matching axes.
*/
fun applyFixedScale(scaleState: ScreenScaleState, fitToScreenSize: IntSize) {
scaleState.setTargetSize(swapIfApplicable(true, fitToScreenSize).toSize())
scaleState.setZoom(0f)
}

data class RotationLayout(
val rotate: Boolean,
val childConstraints: Constraints,
)

fun computeRotationLayout(
gtcModeEnabled: Boolean,
pageMetadata: PageMetadata,
constraints: Constraints,
): RotationLayout {
val containerIsPortrait = constraints.maxHeight > constraints.maxWidth
val rotate = gtcModeEnabled && pageMetadata.isLandscape() && containerIsPortrait
val childConstraints = if (rotate) {
constraints.copy(
minWidth = constraints.minHeight,
maxWidth = constraints.maxHeight,
minHeight = constraints.minWidth,
maxHeight = constraints.maxWidth,
)
} else constraints
return RotationLayout(rotate, childConstraints)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import snd.komelia.settings.model.PagedReadingDirection.RIGHT_TO_LEFT
import snd.komelia.ui.reader.image.ScreenScaleState
import snd.komelia.ui.reader.image.common.PagedReaderHelpDialog
import snd.komelia.ui.reader.image.common.ReaderControlsOverlay
import snd.komelia.ui.platform.LockScreenOrientation
import snd.komelia.ui.reader.image.common.ReaderImageContent
import snd.komelia.ui.reader.image.common.ScalableContainer
import snd.komelia.ui.reader.image.paged.PagedReaderState.Page
Expand Down Expand Up @@ -64,6 +65,8 @@ fun BoxScope.PagedReaderContent(
val layoutOffset = pagedReaderState.layoutOffset.collectAsState().value

val currentContainerSize = screenScaleState.areaSize.collectAsState().value
val gtcModeEnabled = pagedReaderState.gtcModeEnabled.collectAsState().value
LockScreenOrientation(locked = gtcModeEnabled)

val coroutineScope = rememberCoroutineScope()
ReaderControlsOverlay(
Expand Down Expand Up @@ -96,7 +99,7 @@ fun BoxScope.PagedReaderContent(
TransitionPage(transitionPage)
} else {
when (layout) {
SINGLE_PAGE -> pages.firstOrNull()?.let { SinglePageLayout(it) }
SINGLE_PAGE -> pages.firstOrNull()?.let { SinglePageLayout(it, gtcModeEnabled) }
DOUBLE_PAGES, DOUBLE_PAGES_NO_COVER -> DoublePageLayout(pages, readingDirection)
}
}
Expand Down Expand Up @@ -165,13 +168,34 @@ private fun TransitionPage(page: TransitionPage) {
}

@Composable
private fun SinglePageLayout(page: Page) {
private fun SinglePageLayout(page: Page, gtcModeEnabled: Boolean) {
Layout(content = { ReaderImageContent(page.imageResult) }) { measurable, constraints ->
val placeable = measurable.first().measure(constraints)
val startPadding = (constraints.maxWidth - placeable.width) / 2
val topPadding = ((constraints.maxHeight - placeable.height) / 2).coerceAtLeast(0)
layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(startPadding, topPadding)
val (rotate, childConstraints) = GtcReaderSupport.computeRotationLayout(
gtcModeEnabled, page.metadata, constraints
)

val placeable = measurable.first().measure(childConstraints)

if (rotate) {
// after rotation the placeable's width/height swap on screen
val effectiveWidth = placeable.height
val effectiveHeight = placeable.width
val startPadding = (constraints.maxWidth - effectiveWidth) / 2
val topPadding = ((constraints.maxHeight - effectiveHeight) / 2).coerceAtLeast(0)
layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelativeWithLayer(
x = startPadding + (effectiveWidth - placeable.width) / 2,
y = topPadding + (effectiveHeight - placeable.height) / 2,
) {
rotationZ = 90f
}
}
} else {
val startPadding = (constraints.maxWidth - placeable.width) / 2
val topPadding = ((constraints.maxHeight - placeable.height) / 2).coerceAtLeast(0)
layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(startPadding, topPadding)
}
}
}
}
Expand Down
Loading