Skip to content

Commit

Permalink
Added all the client-side implementations, with no support for image …
Browse files Browse the repository at this point in the history
…avatars yet
  • Loading branch information
Mnemotechnician committed Nov 26, 2023
1 parent b361470 commit 8e8b9bc
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 68 deletions.
44 changes: 0 additions & 44 deletions minchat-client/src/main/kotlin/io/minchat/client/Minchat.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.minchat.client

import arc.Events
import arc.math.Mathf
import arc.scene.ui.Label
import com.github.mnemotechnician.mkui.delegates.setting
import com.github.mnemotechnician.mkui.extensions.dsl.*
Expand All @@ -16,7 +15,6 @@ import io.minchat.client.plugin.MinchatPluginHandler
import io.minchat.client.plugin.impl.AccountSaverPlugin
import io.minchat.client.ui.*
import io.minchat.client.ui.chat.ChatFragment
import io.minchat.client.ui.dialog.AbstractModalDialog
import io.minchat.client.ui.managers.*
import io.minchat.common.MINCHAT_VERSION
import io.minchat.rest.*
Expand All @@ -26,8 +24,6 @@ import mindustry.Vars
import mindustry.game.EventType
import mindustry.mod.Mod
import mindustry.ui.Styles
import java.io.File
import java.net.URL
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.concurrent.thread
Expand Down Expand Up @@ -146,46 +142,6 @@ class MinchatMod : Mod(), CoroutineScope {
dontShowInfoAgain = it
}.row()
}.show()

// TODO REMOVE AFTER TESTING
object : AbstractModalDialog() {
init {
header.addLabel("AsyncImage test").row()

val images = listOf(
"https://1000logos.net/wp-content/uploads/2021/05/Google-logo.png",
"file:///home/fox/Downloads/866054901817409567.png",
"file:///home/fox/Downloads/images.png",
"invalid image"
).map {
it to AsyncImage(this@MinchatMod).also {
body.add(it)
.minSize(48f)
.maxSize(512f)
.row()
}
}

action("refresh") {
images.forEach { (url, element) ->
if (url.startsWith("file://")) {
element.setFileAsync {
val data = File(url.substringAfter("//")).readBytes()
client.fileCache.getFileOrPut(url, "png") {
delay(Mathf.random(2000L))
data
}
}
} else {
element.setFileAsync {
val stream = URL(url).openStream()
client.fileCache.getFileOrPut(url, "png") { stream.readBytes() }
}
}
}
}
}
}.show()
}

MinchatKeybinds.registerDefaultKeybinds()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import kotlin.math.*
*
* Currently only supports PNG images.
*/
class AsyncImage(parentScope: CoroutineScope) : Image(), CoroutineScope {
open class AsyncImage(parentScope: CoroutineScope) : Image(), CoroutineScope {
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler(::reportException)

private var fetcherJob: Job? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package io.minchat.client.ui.chat

import arc.Core
import arc.graphics.Color
import arc.scene.event.Touchable
import arc.scene.ui.Label
import arc.util.*
import com.github.mnemotechnician.mkui.extensions.dsl.*
import com.github.mnemotechnician.mkui.extensions.elements.*
import com.github.mnemotechnician.mkui.extensions.elements.content
import com.github.mnemotechnician.mkui.extensions.runUi
import io.minchat.client.Minchat
import io.minchat.client.config.MinchatSettings
import io.minchat.client.misc.*
import io.minchat.client.ui.AsyncImage
import io.minchat.client.ui.MinchatStyle.layoutMargin
import io.minchat.client.ui.MinchatStyle.layoutPad
import io.minchat.client.ui.dialog.*
import io.minchat.common.entity.User
import io.minchat.rest.entity.MinchatMessage
import kotlinx.coroutines.*
import io.minchat.client.ui.MinchatStyle as Style
Expand Down Expand Up @@ -90,17 +87,9 @@ class NormalMessageElement(
}
val nameColorTag = "[#$nameColor]"

val avatar = message.author.avatar ?: User.Avatar.defaultAvatar
if (avatar is User.Avatar.IconAvatar) {
addImage(Core.atlas.find(avatar.iconName), scaling = Scaling.fill)
.size(48f)
} else {
add(AsyncImage(this@NormalMessageElement).apply {
setFileAsync {
message.author.getImageAvatar(false)!!
}
}).size(48f).scaleImage(Scaling.fill)
}
add(UserAvatarElement(message.authorId, message.author.avatar, false))
.maxSize(40f)
.get().clicked(::showUserDialog)

addSpace(width = 5f)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.minchat.client.ui.chat

import arc.Core
import arc.scene.style.TextureRegionDrawable
import io.minchat.client.Minchat
import io.minchat.client.ui.AsyncImage
import io.minchat.common.entity.User
import kotlinx.coroutines.CoroutineScope

class UserAvatarElement(
userId: Long,
avatar: User.Avatar?,
val full: Boolean,
parentScope: CoroutineScope
) : AsyncImage(parentScope) {
var userId = userId
set(value) {
field = value
updateAvatar()
}
var avatar = avatar
set(value) {
field = value
updateAvatar()
}

init {
updateAvatar()
}

fun updateAvatar() {
val avatar = avatar ?: User.Avatar.defaultAvatar
when (avatar) {
is User.Avatar.IconAvatar -> {
setDrawableAsync(avatar.iconName) {
val region = Core.atlas.find(avatar.iconName).takeIf { Core.atlas.isFound(it) }
?: error("No region found for icon avatar ${avatar.iconName}!")

TextureRegionDrawable(region)
}
}

is User.Avatar.ImageAvatar, is User.Avatar.LocalAvatar -> {
setFileAsync {
Minchat.client.getCacheableAvatar(userId, avatar, full) {}
?: error("No avatar found for user ${userId} and avatar ${avatar}")
}
}
}
}
}

fun CoroutineScope.UserAvatarElement(
userId: Long,
avatar: User.Avatar?,
full: Boolean
) = UserAvatarElement(userId, avatar, full, this)
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ object Dialogs : CoroutineScope {
val cancellable: Boolean,
val onSelect: (Int) -> Unit
) : AbstractModalDialog() {
override val addCloseAction get() = cancellable
override val closeButtonText get() = "Cancel"
override val addCloseAction get() = false

init {
if (message.isNotBlank()) header.addTable(Style.surfaceBackground) {
Expand All @@ -248,10 +247,18 @@ object Dialogs : CoroutineScope {
.minWidth(300f)
}

for (i in choices.indices) {
if (cancellable) {
nextActionRow()
action("Cancel") {
hide()
}
}

for (i in choices.indices.reversed()) {
nextActionRow()
action(choices[i]) {
onSelect(i)
hide()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package io.minchat.client.ui.dialog

import arc.Core
import arc.graphics.Color
import arc.scene.style.TextureRegionDrawable
import arc.scene.ui.Label
import arc.util.Align
import arc.scene.ui.layout.Table
import arc.util.*
import com.github.mnemotechnician.mkui.extensions.dsl.*
import com.github.mnemotechnician.mkui.extensions.elements.*
import io.ktor.utils.io.jvm.javaio.*
import io.minchat.client.Minchat
import io.minchat.client.misc.*
import io.minchat.client.ui.MinchatStyle.buttonMargin
import io.minchat.client.ui.MinchatStyle.layoutMargin
import io.minchat.client.ui.MinchatStyle.layoutPad
import io.minchat.client.ui.chat.UserAvatarElement
import io.minchat.common.entity.*
import io.minchat.rest.entity.MinchatUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.*
import mindustry.Vars
import java.time.Instant
import kotlin.random.Random
import kotlin.reflect.KMutableProperty0
Expand Down Expand Up @@ -119,24 +125,140 @@ abstract class UserDialog(

inner class UserEditDialog : AbstractModalDialog() {
val user = this@UserDialog.user!!
var newAvatar = user.avatar
set(value) {
field = value
avatarElement.avatar = value
}

lateinit var avatarElement: UserAvatarElement

init {
header.addLabel("Editing user ${user.tag} (${user.displayName}).", align = Align.left, wrap = true)
.fillX().row()

// Avatar preview & change button
body.addTable(Style.surfaceBackground) {
margin(layoutMargin)

add(UserAvatarElement(user.id, user.avatar, true))
.maxSize(128f)
.apply { avatarElement = get() }

// Actions - to the left of the avatar
addTable {
top()
textButton("Change", Style.InnerActionButton) {
Dialogs.choices(
"What to change your avatar to?",
"Mindustry icon" to { IconAvatarChangeDialog().show() },
"Image" to { Dialogs.TODO() },
"Nothing" to {
Dialogs.confirm("Are you sure you want to reset your avatar?") {
newAvatar = null
}
},
cancellable = true
)
}.margin(buttonMargin).growX()
}.growX()
}.pad(layoutPad).fillX()
.colspan(2)
.row()

val usernameField = inputField("New nickname", default = user.nickname ?: user.username) {
it.length in 3..40
}

action("Confirm") {
hide()
launchSafeWithStatus("Editing user ${user.username}...") {
this@UserDialog.user = user.edit(
newNickname = usernameField.content
)
if (usernameField.content != user.nickname) {
this@UserDialog.user = user.edit(
newNickname = usernameField.content
)
}

if (newAvatar != user.avatar) when (val avatar = newAvatar) {
// If an icon avatar, call the icon avatar route.
// If a local one, call the image avatar route. Otherwise, it's an error.
null -> user.setIconAvatar(null)
is User.Avatar.IconAvatar -> user.setIconAvatar(avatar.iconName)
is User.Avatar.LocalAvatar -> {
Vars.ui.loadfrag.apply {
show("Uploading avatar...")
setButton { cancel(CancellationException("Avatar upload was cancelled.")) }

user.uploadAvatar(avatar.file.inputStream().toByteReadChannel()) {
setProgress(it)
}

hide()
}
}
is User.Avatar.ImageAvatar -> {
error("Cannot set avatar to a non-local image avatar!")
}
}
}
}.disabled { !usernameField.isValid }
}

inner class IconAvatarChangeDialog : AbstractModalDialog() {
lateinit var iconsTable: Table

init {
header.addLabel("Choose an icon.").fillX().row()

// A search bar
body.textField("", Style.TextInput) {
rebuildWithCriteria(it.trim().replace(' ', '-').takeIf { it.isNotEmpty() })
}.fillX()
.pad(layoutPad)
.apply { get().hint = "Type to search" }
.row()

body.addTable(Style.surfaceBackground) {
// The icon list
limitedScrollPane(limitW = false, limitH = true) {
margin(layoutMargin)
iconsTable = this
}.pad(layoutPad)
.fillX()
.height(Core.graphics.height / 3f)
.row()
}.margin(layoutMargin).pad(layoutPad)
.row()

rebuildWithCriteria(null)
}

fun rebuildWithCriteria(queryString: String?) {
iconsTable.clear()

val icons = Core.atlas.regions.toList().filter {
(it.name.endsWith("-full") || it.name.endsWith("-ui"))
&& (queryString == null || it.name.contains(queryString, true))
}.distinctBy {
// Prevent duplicates
it.name.removeSuffix("-ui").removeSuffix("-full")
}

val iconsPerRow = (Core.graphics.width / 64).coerceAtLeast(2);
for (icon in icons) {
val image = iconsTable.addImage(TextureRegionDrawable(icon), scaling = Scaling.fill)
.size(48f)
.pad(layoutPad)
.rowPer(iconsPerRow)
.get()

image.clicked {
newAvatar = User.Avatar.IconAvatar(icon.name)
hide()
}
}
}
}
}

inner class UserDeleteConfirmDialog : AbstractModalDialog() {
Expand Down

0 comments on commit 8e8b9bc

Please sign in to comment.