Skip to content

Commit 6e02b82

Browse files
Animate logo disappearance,
Media images gallery
1 parent 3c7054b commit 6e02b82

File tree

9 files changed

+305
-18
lines changed

9 files changed

+305
-18
lines changed

core/src/commonMain/kotlin/com/mrboomdev/awery/core/utils/Log.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fun logger() = LoggerDelegate()
102102
*/
103103
fun logger(tag: String) = Logger(tag)
104104

105-
fun Log.debug(vararg variables: Pair<String, Any>) = d(
105+
fun Log.debug(vararg variables: Pair<String, Any?>) = d(
106106
tag = "AweryDebug",
107107
message = buildString {
108108
if(variables.isEmpty()) {

ui/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ kotlin {
4646
implementation(libs.kotlin.reflect)
4747
implementation(libs.kotlinx.datetime)
4848
implementation(libs.kotlinx.serialization.json)
49+
implementation("org.jetbrains.kotlinx:kotlinx-io-okio:0.8.0")
4950

5051
// ViewModel
5152
implementation(libs.lifecycle.viewmodel)
@@ -66,6 +67,7 @@ kotlin {
6667
implementation(composeLibs.confetti)
6768
implementation(libs.filekit.core)
6869
implementation(libs.filekit.dialogs)
70+
implementation("me.saket.telephoto:zoomable:0.18.0")
6971

7072
// Navigation
7173
implementation(composeLibs.navigation.jetpack)

ui/src/commonMain/kotlin/com/mrboomdev/awery/ui/App.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,9 @@ private fun AweryTopBar(
567567
contentDescription = null
568568
)
569569

570-
if(!showSearch || windowSize.width >= WindowSizeType.Large) {
570+
AnimatedVisibility(
571+
visible = !showSearch || windowSize.width >= WindowSizeType.Large
572+
) {
571573
Text(
572574
style = MaterialTheme.typography.titleLarge,
573575
text = "Awery"

ui/src/commonMain/kotlin/com/mrboomdev/awery/ui/Navigation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ sealed interface Routes {
8181
val initialPage: SettingsPages = SettingsPages.Main()
8282
): Routes {
8383
@Composable
84-
fun Content(contentPadding: PaddingValues) = SettingsScreen(initialPage)
84+
fun Content(contentPadding: PaddingValues) = SettingsScreen(initialPage, contentPadding)
8585
}
8686

8787
@Serializable
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package com.mrboomdev.awery.ui.screens
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.Crossfade
5+
import androidx.compose.animation.slideInVertically
6+
import androidx.compose.animation.slideOutVertically
7+
import androidx.compose.foundation.layout.*
8+
import androidx.compose.foundation.pager.HorizontalPager
9+
import androidx.compose.foundation.pager.rememberPagerState
10+
import androidx.compose.material3.CircularProgressIndicator
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.*
13+
import androidx.compose.runtime.saveable.rememberSaveable
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.focus.FocusRequester
17+
import androidx.compose.ui.focus.focusRequester
18+
import coil3.SingletonImageLoader
19+
import coil3.compose.AsyncImage
20+
import coil3.compose.LocalPlatformContext
21+
import coil3.request.ImageRequest
22+
import coil3.request.crossfade
23+
import com.mrboomdev.awery.resources.Res
24+
import com.mrboomdev.awery.resources.ic_close
25+
import com.mrboomdev.awery.resources.ic_download
26+
import com.mrboomdev.awery.ui.components.IconButton
27+
import com.mrboomdev.awery.ui.components.LocalToaster
28+
import com.mrboomdev.awery.ui.components.toast
29+
import com.mrboomdev.awery.ui.utils.classify
30+
import com.mrboomdev.awery.ui.utils.niceSideInset
31+
import com.mrboomdev.awery.ui.utils.padding
32+
import io.github.vinceglb.filekit.FileKit
33+
import io.github.vinceglb.filekit.saveImageToGallery
34+
import io.ktor.http.*
35+
import kotlinx.coroutines.runBlocking
36+
import me.saket.telephoto.ExperimentalTelephotoApi
37+
import me.saket.telephoto.zoomable.ZoomSpec
38+
import me.saket.telephoto.zoomable.rememberZoomableState
39+
import me.saket.telephoto.zoomable.zoomable
40+
import okio.FileSystem
41+
import okio.SYSTEM
42+
import org.jetbrains.compose.resources.painterResource
43+
import kotlin.coroutines.cancellation.CancellationException
44+
45+
private sealed interface GalleryElementState {
46+
data object Loading: GalleryElementState
47+
data class Loaded(val width: Int, val height: Int, val diskCacheKey: String?): GalleryElementState
48+
data class Error(val throwable: Throwable): GalleryElementState
49+
}
50+
51+
@OptIn(ExperimentalTelephotoApi::class)
52+
@Composable
53+
fun GalleryScreen(
54+
onDismissRequest: () -> Unit,
55+
elements: List<String>
56+
) {
57+
var showUi by rememberSaveable { mutableStateOf(true) }
58+
59+
HorizontalPager(
60+
modifier = Modifier.fillMaxSize(),
61+
state = rememberPagerState { elements.size },
62+
key = { elements[it] }
63+
) { index ->
64+
val context = LocalPlatformContext.current
65+
val focusRequester = remember { FocusRequester() }
66+
var state by remember(elements, index) { mutableStateOf<GalleryElementState>(GalleryElementState.Loading) }
67+
68+
val zoomableState = rememberZoomableState(
69+
zoomSpec = ZoomSpec(maxZoomFactor = 10f)
70+
)
71+
72+
val model = remember(elements, index) {
73+
ImageRequest.Builder(context)
74+
.data(elements[index])
75+
.memoryCacheKey(elements[index])
76+
.placeholderMemoryCacheKey(elements[index])
77+
.crossfade(1000)
78+
.listener(
79+
onStart = {
80+
state = GalleryElementState.Loading
81+
},
82+
83+
onSuccess = { _, result ->
84+
state = GalleryElementState.Loaded(
85+
result.image.width,
86+
result.image.height,
87+
result.diskCacheKey
88+
)
89+
90+
// zoomableState.setContentLocation(
91+
// ZoomableContentLocation.scaledInsideAndCenterAligned(
92+
// result.image.asPainter(context).intrinsicSize
93+
// )
94+
// )
95+
},
96+
97+
onCancel = { _ ->
98+
state = GalleryElementState.Error(CancellationException())
99+
},
100+
101+
onError = { _, error ->
102+
state = GalleryElementState.Error(error.throwable)
103+
}
104+
).build()
105+
}
106+
107+
LaunchedEffect(Unit) {
108+
focusRequester.requestFocus()
109+
}
110+
111+
Box(
112+
modifier = Modifier.fillMaxSize(),
113+
contentAlignment = Alignment.Center
114+
) {
115+
AsyncImage(
116+
modifier = Modifier
117+
.fillMaxSize()
118+
.focusRequester(focusRequester)
119+
.zoomable(
120+
state = zoomableState,
121+
onClick = { showUi = !showUi },
122+
onLongClick = { showUi = !showUi }
123+
),
124+
125+
model = model,
126+
contentDescription = null
127+
)
128+
129+
AnimatedVisibility(
130+
modifier = Modifier
131+
.fillMaxWidth()
132+
.align(Alignment.TopStart),
133+
enter = slideInVertically(initialOffsetY = { -it }),
134+
exit = slideOutVertically(targetOffsetY = { -it }),
135+
visible = showUi
136+
) {
137+
Row(
138+
modifier = Modifier
139+
.fillMaxWidth()
140+
.padding(horizontal = niceSideInset()),
141+
horizontalArrangement = Arrangement.SpaceBetween
142+
) {
143+
IconButton(
144+
painter = painterResource(Res.drawable.ic_close),
145+
contentDescription = null,
146+
onClick = onDismissRequest
147+
)
148+
149+
(state as? GalleryElementState.Loaded)?.diskCacheKey?.also { diskCacheKey ->
150+
val toaster = LocalToaster.current
151+
152+
IconButton(
153+
painter = painterResource(Res.drawable.ic_download),
154+
contentDescription = null,
155+
onClick = {
156+
runBlocking {
157+
try {
158+
SingletonImageLoader.get(context).diskCache!!.openSnapshot(diskCacheKey)!!.use {
159+
FileKit.saveImageToGallery(
160+
bytes = FileSystem.SYSTEM.read(it.data) { readByteArray() },
161+
filename = buildString {
162+
val url = Url(elements[index]).encodedPathAndQuery
163+
append(url)
164+
165+
append(when(url.substringAfterLast(".")) {
166+
"png", "jpg", "jpeg", "gif", "webp" -> ""
167+
else -> ".png"
168+
})
169+
}
170+
)
171+
}
172+
173+
toaster.toast("Downloaded successfully!")
174+
} catch(t: Throwable) {
175+
toaster.toast(
176+
title = "Failed to download!",
177+
message = t.classify().message
178+
)
179+
}
180+
}
181+
}
182+
)
183+
}
184+
}
185+
}
186+
187+
Crossfade(state) { state ->
188+
when(state) {
189+
GalleryElementState.Loading -> {
190+
CircularProgressIndicator()
191+
}
192+
193+
is GalleryElementState.Error -> {
194+
Text(state.throwable.classify().message)
195+
}
196+
197+
is GalleryElementState.Loaded -> {}
198+
}
199+
}
200+
}
201+
}
202+
}

ui/src/commonMain/kotlin/com/mrboomdev/awery/ui/screens/media/DefaultMediaScreen.kt

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateDpAsState
66
import androidx.compose.animation.core.animateFloatAsState
77
import androidx.compose.animation.core.tween
88
import androidx.compose.foundation.Canvas
9+
import androidx.compose.foundation.clickable
910
import androidx.compose.foundation.layout.*
1011
import androidx.compose.foundation.lazy.LazyColumn
1112
import androidx.compose.foundation.lazy.LazyItemScope
@@ -34,6 +35,7 @@ import androidx.compose.ui.text.style.TextAlign
3435
import androidx.compose.ui.unit.Dp
3536
import androidx.compose.ui.unit.dp
3637
import androidx.compose.ui.window.Dialog
38+
import androidx.compose.ui.window.DialogProperties
3739
import be.digitalia.compose.htmlconverter.htmlToAnnotatedString
3840
import coil3.compose.AsyncImage
3941
import coil3.compose.LocalPlatformContext
@@ -64,6 +66,7 @@ import com.mrboomdev.awery.ui.Navigation
6466
import com.mrboomdev.awery.ui.Routes
6567
import com.mrboomdev.awery.ui.components.*
6668
import com.mrboomdev.awery.ui.effects.BackEffect
69+
import com.mrboomdev.awery.ui.screens.GalleryScreen
6770
import com.mrboomdev.awery.ui.theme.SeedAweryTheme
6871
import com.mrboomdev.awery.ui.utils.*
6972
import com.mrboomdev.navigation.core.safePop
@@ -95,6 +98,7 @@ internal fun DefaultMediaScreen(
9598
MediaScreenTabs.getVisibleFor(media)
9699
}
97100

101+
var showGallery by rememberSaveable { mutableStateOf(false) }
98102
val pagerState = rememberPagerState { tabs.count() }
99103

100104
val defaultColor = remember(media) {
@@ -128,6 +132,24 @@ internal fun DefaultMediaScreen(
128132
}
129133
}
130134

135+
if(showGallery) {
136+
Dialog(
137+
onDismissRequest = { showGallery = false },
138+
properties = DialogProperties(
139+
usePlatformDefaultWidth = false
140+
)
141+
) {
142+
GalleryScreen(
143+
onDismissRequest = { showGallery = false },
144+
elements = listOfNotNull(
145+
media.largePoster,
146+
media.poster,
147+
media.banner
148+
)
149+
)
150+
}
151+
}
152+
131153
LaunchedEffect(media) {
132154
media.getLargePoster()?.also { poster ->
133155
dominantColorState.updateFrom(Url(poster))
@@ -242,15 +264,28 @@ internal fun DefaultMediaScreen(
242264
}, tween()).value))
243265

244266
media.getLargePoster()?.also { poster ->
267+
val context = LocalPlatformContext.current
268+
269+
val model = remember(poster) {
270+
poster.let {
271+
ImageRequest.Builder(context)
272+
.placeholderMemoryCacheKey(it)
273+
.memoryCacheKey(it)
274+
.data(it)
275+
.build()
276+
}
277+
}
278+
245279
if(isPosterFuckedUp) {
246280
AsyncImage(
247281
modifier = Modifier
248-
.padding(top = 8.dp, bottom = 16.dp)
249-
.padding(horizontal = 8.dp)
282+
.padding(top = 8.dp, bottom = 16.dp, horizontal = 8.dp)
250283
.clip(RoundedCornerShape(16.dp))
251284
.fillMaxWidth()
285+
.clickable { showGallery = true }
252286
.animateContentSize(),
253-
model = poster,
287+
288+
model = model,
254289
contentDescription = null
255290
)
256291

@@ -263,16 +298,10 @@ internal fun DefaultMediaScreen(
263298
.padding(horizontal = 56.dp)
264299
.heightIn(max = currentWindowHeight() - 250.dp)
265300
.fillMaxWidth()
301+
.clickable { showGallery = true }
266302
.animateContentSize(),
267303

268-
model = poster.let {
269-
ImageRequest.Builder(LocalPlatformContext.current)
270-
.placeholderMemoryCacheKey(it)
271-
.memoryCacheKey(it)
272-
.data(it)
273-
.build()
274-
},
275-
304+
model = model,
276305
contentDescription = null,
277306

278307
onError = {
@@ -423,7 +452,8 @@ internal fun DefaultMediaScreen(
423452
modifier = Modifier
424453
.clip(RoundedCornerShape(16.dp))
425454
.fillMaxHeight()
426-
.animateContentSize(),
455+
.animateContentSize()
456+
.clickable { showGallery = true },
427457

428458
model = poster.let {
429459
ImageRequest.Builder(LocalPlatformContext.current)

0 commit comments

Comments
 (0)