Skip to content

Commit

Permalink
Extract fullscreen image to another screen
Browse files Browse the repository at this point in the history
  • Loading branch information
HeroBrine1st committed Jan 24, 2024
1 parent 4ece443 commit cf0f736
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 112 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/ru/herobrine1st/e621/api/model/File.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ data class File(
val url: String = ""
)

@Serializable
data class NormalizedFile(
val name: String,
val width: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package ru.herobrine1st.e621.ui.screen.post
package ru.herobrine1st.e621.navigation.component

enum class FullscreenState {
OPEN,
CLOSED,
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.arkivanov.decompose.ComponentContext
import ru.herobrine1st.e621.api.model.NormalizedFile
import ru.herobrine1st.e621.api.model.Post

class PostMediaComponent(
post: Post,
initialFile: NormalizedFile,
componentContext: ComponentContext,
) : ComponentContext by componentContext {
val files = post.files

init {
check(initialFile in files) { "Initial file does not belong to post" }
}


var currentFile by mutableStateOf(initialFile)
private set

fun setFile(file: NormalizedFile) {
check(file in files) { "File does not belong to post" }
currentFile = file
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.navigate
import com.arkivanov.decompose.router.stack.StackNavigator
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.lifecycle.doOnResume
Expand Down Expand Up @@ -308,6 +309,16 @@ class PostComponent(
slotNavigation.navigate { null }
}

fun openToFullscreen() {
val post = (state as PostState.Ready).post
navigator.push(
Config.PostMedia(
post = post,
initialFile = currentFile
)
)
}

class Instance(
postId: PostId,
api: API,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.arkivanov.decompose.value.Value
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import ru.herobrine1st.e621.navigation.component.BlacklistTogglesDialogComponent
import ru.herobrine1st.e621.navigation.component.PostMediaComponent
import ru.herobrine1st.e621.navigation.component.WikiComponent
import ru.herobrine1st.e621.navigation.component.home.HomeComponent
import ru.herobrine1st.e621.navigation.component.post.PostComponent
Expand All @@ -48,6 +49,7 @@ interface RootComponent {
class PostListing(val component: PostListingComponent) : Child
class Post(val component: PostComponent) : Child
class Wiki(val component: WikiComponent) : Child
class PostMedia(val component: PostMediaComponent) : Child
class Settings(val component: SettingsComponent) : Child {
class Blacklist(val component: SettingsBlacklistComponent) : Child {
class Entry(val component: SettingsBlacklistEntryComponent) : Child
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import ru.herobrine1st.e621.module.ActivityInjectionCompanion
import ru.herobrine1st.e621.navigation.component.BlacklistTogglesDialogComponent
import ru.herobrine1st.e621.navigation.component.PostMediaComponent
import ru.herobrine1st.e621.navigation.component.WikiComponent
import ru.herobrine1st.e621.navigation.component.home.HomeComponent
import ru.herobrine1st.e621.navigation.component.post.PostComponent
Expand Down Expand Up @@ -152,6 +153,7 @@ class RootComponentImpl(
is Settings.AboutLibraries -> Child.Settings.AboutLibraries(
SettingsAboutLibrariesComponent(context)
)

is Wiki -> Child.Wiki(
WikiComponent(
configuration.tag,
Expand All @@ -162,6 +164,14 @@ class RootComponentImpl(
navigation
)
)

is Config.PostMedia -> Child.PostMedia(
PostMediaComponent(
configuration.post,
configuration.initialFile,
context
)
)
}
}

Expand Down
16 changes: 10 additions & 6 deletions app/src/main/java/ru/herobrine1st/e621/navigation/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package ru.herobrine1st.e621.navigation.config

import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import ru.herobrine1st.e621.api.model.NormalizedFile
import ru.herobrine1st.e621.api.model.PostId
import ru.herobrine1st.e621.api.model.Tag
import ru.herobrine1st.e621.api.search.PostsSearchOptions
Expand All @@ -39,13 +40,13 @@ sealed interface Config {
@Serializable
data class Search(
val initialSearch: PostsSearchOptions = PostsSearchOptions(),
private val index: Int
private val index: Int,
) : Config

@Serializable
data class PostListing(
val search: SearchOptions,
private val index: Int
private val index: Int,
) : Config

@Serializable
Expand All @@ -60,11 +61,14 @@ sealed interface Config {
@Serializable
data class Wiki(
val tag: Tag,
private val index: Int
private val index: Int,
) : Config

// Not needed: already covered by [PostListing]
// data class Favourites
@Serializable
data class PostMedia(
val post: ModelPost,
val initialFile: NormalizedFile,
) : Config

// These are used only once in a stack
@Serializable
Expand All @@ -75,7 +79,7 @@ sealed interface Config {
data class Entry(
val id: Long,
val query: String,
val enabled: Boolean
val enabled: Boolean,
) : Config
}

Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/ru/herobrine1st/e621/ui/Navigator.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/*
* This file is part of ru.herobrine1st.e621.
*
Expand Down Expand Up @@ -43,6 +42,7 @@ import ru.herobrine1st.e621.navigation.config.Config
import ru.herobrine1st.e621.preference.LocalPreferences
import ru.herobrine1st.e621.ui.animation.reducedSlide
import ru.herobrine1st.e621.ui.component.scaffold.rememberScreenSharedState
import ru.herobrine1st.e621.ui.screen.PostMediaScreen
import ru.herobrine1st.e621.ui.screen.WikiScreen
import ru.herobrine1st.e621.ui.screen.home.Home
import ru.herobrine1st.e621.ui.screen.post.Post
Expand All @@ -58,7 +58,7 @@ import ru.herobrine1st.e621.ui.screen.settings.SettingsLicenses
@Composable
fun Navigator(
rootComponent: RootComponent,
snackbarHostState: androidx.compose.material3.SnackbarHostState
snackbarHostState: androidx.compose.material3.SnackbarHostState,
) {
val preferences = LocalPreferences.current
val navigation = rootComponent.navigation
Expand Down Expand Up @@ -109,6 +109,7 @@ fun Navigator(
component = instance.component,
isAuthorized = preferences.hasAuth()
)

is Settings -> Settings(
screenSharedState = sharedState,
onNavigateToBlacklistSettings = {
Expand All @@ -118,15 +119,18 @@ fun Navigator(
navigation.push(Config.Settings.About)
}
)

is Settings.Blacklist ->
SettingsBlacklist(
screenSharedState = sharedState,
component = instance.component
)

is Settings.Blacklist.Entry -> SettingsBlacklistEntry(
sharedState,
instance.component
)

is Settings.About -> SettingsAbout(
screenSharedState = sharedState,
navigateToLicense = {
Expand All @@ -136,9 +140,11 @@ fun Navigator(
navigation.push(Config.Settings.AboutLibraries)
}
)

is Settings.License -> SettingsLicense(sharedState)
is Settings.AboutLibraries -> SettingsLicenses(sharedState)
is Wiki -> WikiScreen(sharedState, instance.component)
is RootComponent.Child.PostMedia -> PostMediaScreen(instance.component)
}
}
}
162 changes: 162 additions & 0 deletions app/src/main/java/ru/herobrine1st/e621/ui/screen/PostMediaScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* This file is part of ru.herobrine1st.e621.
*
* ru.herobrine1st.e621 is an android client for https://e621.net
* Copyright (C) 2022-2024 HeroBrine1st Erquilenne <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package ru.herobrine1st.e621.ui.screen

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ru.herobrine1st.e621.R
import ru.herobrine1st.e621.navigation.component.PostMediaComponent
import ru.herobrine1st.e621.ui.component.MAX_SCALE_DEFAULT
import ru.herobrine1st.e621.ui.component.post.PostImage
import ru.herobrine1st.e621.ui.component.rememberZoomableState
import ru.herobrine1st.e621.ui.component.zoomable

@Composable
fun PostMediaScreen(
component: PostMediaComponent,
) {
BoxWithConstraints {
var showOverlay by remember { mutableStateOf(false) }

val file = component.currentFile
val initialTranslation: Offset
val initialScale: Float
// if image size would exceed screen height, it is true
val matchHeightConstraintsFirst = file.aspectRatio < maxWidth / maxHeight
if (!matchHeightConstraintsFirst) {
initialTranslation = Offset.Zero
initialScale = 1f
} else {
val width = constraints.maxHeight * file.aspectRatio
initialScale = constraints.maxWidth / width
initialTranslation = Offset(-(constraints.maxWidth - width) * initialScale / 2, 0f)
}
val maxScale = initialScale * MAX_SCALE_DEFAULT

val modifier = Modifier
.zoomable(
state = rememberZoomableState(
maxScale = maxScale,
initialScale = initialScale,
initialTranslation = initialTranslation
),
onTap = { showOverlay = !showOverlay }
)
.background(Color.Black)
.fillMaxSize()

Box {
when {
file.type.isVideo -> {
// Not supported
}

file.type.isImage -> PostImage(
file = file,
contentDescription = null,
modifier = modifier,
actualPostFileType = null,
matchHeightConstraintsFirst = matchHeightConstraintsFirst,
setSizeOriginal = true
)

else -> {}
}
AnimatedVisibility(
visible = showOverlay,
enter = fadeIn() + slideInVertically(),
exit = slideOutVertically() + fadeOut()
) {
CompositionLocalProvider(LocalContentColor provides Color.White) {
Row(
Modifier
.background(Color.Black.copy(alpha = 0.5f))
.systemBarsPadding()
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
file.name.replaceFirstChar { it.titlecase() },
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.weight(1f))
Box {
var showVariants by remember { mutableStateOf(false) }
IconButton(onClick = { showVariants = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.image_fullscreen_select_image_variant)
)
}
DropdownMenu(
expanded = showVariants,
onDismissRequest = { showVariants = false }
) {
component.files.forEach {
DropdownMenuItem(
text = {
Text(it.name.replaceFirstChar { it.uppercase() })
},
onClick = {
component.setFile(it)
}
)
}
}
}
}
}
}
}
}
}
Loading

0 comments on commit cf0f736

Please sign in to comment.