Skip to content

Commit f8e5a06

Browse files
committed
add image view
1 parent ad31dee commit f8e5a06

7 files changed

Lines changed: 169 additions & 9 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package me.lkl.dalvikus.tabs
2+
3+
import androidx.compose.material.icons.Icons
4+
import androidx.compose.material.icons.filled.Image
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.MutableState
7+
import androidx.compose.runtime.mutableStateOf
8+
import androidx.compose.ui.graphics.vector.ImageVector
9+
import me.lkl.dalvikus.tree.archive.ZipEntryImageNode
10+
11+
class ImageTab(
12+
val tabName: String,
13+
override val tabId: String,
14+
override val contentProvider: ZipEntryImageNode
15+
) : TabElement {
16+
override var hasUnsavedChanges: MutableState<Boolean> = mutableStateOf(false)
17+
18+
@Composable
19+
override fun tabName(): String = tabName
20+
override val tabIcon: ImageVector = Icons.Filled.Image
21+
}

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/theme/FileTypeMeta.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ fun getSuggestionTypeIcon(type: SuggestionType): ImageVector {
3737
}
3838
}
3939

40+
val readableImageFormats = listOf("png", "jpg", "jpeg", "bmp", "webp", "gif", "ico")
41+
4042
fun getFileExtensionMeta(fileName: String): FileTypeMeta {
4143
val ext = fileName.substringAfterLast('.', "").lowercase()
4244

4345
return when (ext) {
4446
"txt", "md", "log" -> FileTypeMeta(Icons.Outlined.Description)
45-
"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg" ->
47+
in readableImageFormats ->
4648
FileTypeMeta(Icons.Outlined.Image, ImagePurple)
4749
"mp3", "wav", "ogg", "flac", "aac" ->
4850
FileTypeMeta(Icons.Outlined.MusicNote, AudioTeal)

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipEntryFolderNode.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package me.lkl.dalvikus.tree.archive
22

33
import androidx.compose.material.icons.Icons
44
import androidx.compose.material.icons.filled.Folder
5+
import me.lkl.dalvikus.theme.readableImageFormats
56
import me.lkl.dalvikus.tree.ContainerNode
67
import me.lkl.dalvikus.tree.Node
78
import me.lkl.dalvikus.tree.backing.ZipBacking
@@ -27,9 +28,11 @@ class ZipEntryFolderNode(
2728
ZipEntryFolderNode(name, path, zipRoot, this)
2829
},
2930
onFile = { name, path, bytes ->
31+
// TODO this is code duplication with ZipNode, refactor to avoid it
3032
when {
3133
name.endsWith(".dex") -> DexFileNode(name, ZipBacking(path, zipRoot), this)
3234
name.endsWith(".xml") && zipRoot is ApkNode -> ApkEntryXmlNode(name, path, zipRoot, this)
35+
name.substringAfterLast(".").lowercase() in readableImageFormats -> ZipEntryImageNode(name, path, zipRoot, this)
3336
else -> ZipEntryFileNode(name, path, zipRoot, this)
3437
}
3538
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package me.lkl.dalvikus.tree.archive
2+
3+
import brut.androlib.res.decoder.AXmlResourceParser
4+
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
5+
import brut.androlib.res.decoder.AndroidManifestResourceParser
6+
import brut.androlib.res.decoder.ResStreamDecoder
7+
import brut.androlib.res.decoder.ResXmlPullStreamDecoder
8+
import me.lkl.dalvikus.tabs.CodeTab
9+
import me.lkl.dalvikus.tabs.TabElement
10+
import me.lkl.dalvikus.tree.ContainerNode
11+
import me.lkl.dalvikus.util.guessIfEditableTextually
12+
import me.lkl.dalvikus.util.newXmlSerializer
13+
import java.io.ByteArrayOutputStream
14+
import kotlin.properties.Delegates
15+
16+
17+
class ZipEntryImageNode(
18+
override val name: String,
19+
private val fullPath: String,
20+
private val zipRoot: ZipNode,
21+
override val parent: ContainerNode?
22+
) : ZipEntryFileNode(name, fullPath, zipRoot, parent) {
23+
24+
override fun isDisplayable(): Boolean = true
25+
override fun isEditable(): Boolean = false
26+
27+
override suspend fun createTab(): TabElement {
28+
return CodeTab(
29+
tabName = name,
30+
tabId = fullPath,
31+
tabIcon = icon,
32+
contentProvider = this
33+
)
34+
}
35+
}

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/tree/archive/ZipNode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import me.lkl.dalvikus.tree.backing.ZipBacking
66
import me.lkl.dalvikus.tree.buildChildNodes
77
import me.lkl.dalvikus.tree.dex.DexFileNode
88
import me.lkl.dalvikus.theme.getFileExtensionMeta
9+
import me.lkl.dalvikus.theme.readableImageFormats
910
import java.io.File
1011
import java.util.zip.ZipEntry
1112
import java.util.zip.ZipFile
@@ -46,6 +47,7 @@ open class ZipNode(
4647
when {
4748
name.endsWith(".dex") -> DexFileNode(name, ZipBacking(path, this), this)
4849
name.endsWith(".xml") && this is ApkNode -> ApkEntryXmlNode(name, path, this, this)
50+
name.substringAfterLast(".").lowercase() in readableImageFormats -> ZipEntryImageNode(name, path, this, this)
4951
else -> ZipEntryFileNode(name, path, this, this)
5052
}
5153
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package me.lkl.dalvikus.ui.image
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.gestures.detectTransformGestures
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.shape.RoundedCornerShape
10+
import androidx.compose.material3.CircularProgressIndicator
11+
import androidx.compose.material3.MaterialTheme
12+
import androidx.compose.material3.Text
13+
import androidx.compose.runtime.*
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.geometry.Offset
17+
import androidx.compose.ui.graphics.ImageBitmap
18+
import androidx.compose.ui.graphics.graphicsLayer
19+
import androidx.compose.ui.graphics.toComposeImageBitmap
20+
import androidx.compose.ui.input.pointer.pointerInput
21+
import androidx.compose.ui.unit.dp
22+
import androidx.compose.ui.unit.sp
23+
import kotlinx.coroutines.Dispatchers
24+
import kotlinx.coroutines.withContext
25+
import me.lkl.dalvikus.tabs.ImageTab
26+
import org.jetbrains.skia.Image as SkiaImage
27+
28+
@Composable
29+
fun ImageView(tab: ImageTab) {
30+
val imageByteArray by tab.contentProvider.contentFlow.collectAsState()
31+
var image by remember(imageByteArray) { mutableStateOf<ImageBitmap?>(null) }
32+
var scale by remember { mutableStateOf(1f) }
33+
var offset by remember { mutableStateOf(Offset.Zero) }
34+
35+
LaunchedEffect(tab) {
36+
tab.contentProvider.loadContent()
37+
}
38+
39+
LaunchedEffect(imageByteArray) {
40+
image = withContext(Dispatchers.Default) {
41+
runCatching {
42+
SkiaImage.makeFromEncoded(imageByteArray).toComposeImageBitmap()
43+
}.getOrNull()
44+
}
45+
scale = 1f
46+
offset = Offset.Zero
47+
}
48+
49+
val gestureModifier = Modifier.pointerInput(Unit) {
50+
detectTransformGestures { _, pan, zoom, _ ->
51+
scale = (scale * zoom).coerceIn(0.1f, 10f)
52+
offset += pan
53+
}
54+
}
55+
56+
val colors = MaterialTheme.colorScheme
57+
58+
Box(
59+
modifier = Modifier.fillMaxSize().background(colors.background)
60+
) {
61+
image?.let { img ->
62+
val imageWidth = img.width
63+
val imageHeight = img.height
64+
65+
Box(
66+
modifier = gestureModifier
67+
.graphicsLayer(
68+
scaleX = scale,
69+
scaleY = scale,
70+
translationX = offset.x,
71+
translationY = offset.y
72+
)
73+
.align(Alignment.Center)
74+
) {
75+
Image(
76+
bitmap = img,
77+
contentDescription = null,
78+
modifier = Modifier
79+
.align(Alignment.Center)
80+
.background(colors.surfaceVariant)
81+
)
82+
}
83+
84+
Text(
85+
text = "$imageWidth x $imageHeight",
86+
color = colors.onBackground,
87+
fontSize = 14.sp,
88+
modifier = Modifier
89+
.align(Alignment.BottomEnd)
90+
.padding(8.dp)
91+
.background(colors.surface.copy(alpha = 0.7f), shape = RoundedCornerShape(4.dp))
92+
.padding(horizontal = 8.dp, vertical = 4.dp)
93+
)
94+
} ?: run {
95+
CircularProgressIndicator(
96+
modifier = Modifier.align(Alignment.Center),
97+
color = colors.primary
98+
)
99+
}
100+
}
101+
}

composeApp/src/commonMain/kotlin/me/lkl/dalvikus/ui/tabs/TabContentRenderer.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import androidx.compose.runtime.Composable
88
import androidx.compose.ui.Modifier
99
import androidx.compose.ui.unit.dp
1010
import me.lkl.dalvikus.tabs.CodeTab
11+
import me.lkl.dalvikus.tabs.ImageTab
1112
import me.lkl.dalvikus.tabs.SmaliTab
1213
import me.lkl.dalvikus.tabs.TabElement
1314
import me.lkl.dalvikus.tabs.WelcomeTab
1415
import me.lkl.dalvikus.ui.editor.EditorView
16+
import me.lkl.dalvikus.ui.image.ImageView
1517

1618
@OptIn(ExperimentalMaterial3Api::class)
1719
@Composable
@@ -20,14 +22,8 @@ fun TabContentRenderer(tab: TabElement) {
2022
is WelcomeTab -> WelcomeView()
2123
is CodeTab -> EditorView(tab)
2224
is SmaliTab -> EditorView(tab)
23-
24-
else -> {
25-
Icon(
26-
imageVector = Icons.Default.BugReport,
27-
contentDescription = null,
28-
modifier = Modifier.fillMaxSize().padding(16.dp)
29-
)
30-
}
25+
is ImageTab -> ImageView(tab)
26+
else -> throw IllegalArgumentException("Unsupported tab type: ${tab::class.simpleName}")
3127
}
3228
}
3329

0 commit comments

Comments
 (0)