Add image preview
This commit is contained in:
@@ -8,14 +8,22 @@ import net.minecraft.text.Text;
|
|||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
@Mixin(ChatHud.class)
|
@Mixin(ChatHud.class)
|
||||||
public class MixinChatHud {
|
public class MixinChatHud {
|
||||||
@Inject(at = @At("HEAD"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", cancellable = true)
|
@ModifyArg(at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V")
|
||||||
public void onAddMessage(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) {
|
public Text onAddMessage(Text message) {
|
||||||
if (ClientChatLineReceivedEvent.Companion.publish(new ClientChatLineReceivedEvent(message)).getCancelled()) {
|
var event = new ClientChatLineReceivedEvent(message);
|
||||||
ci.cancel();
|
if (ClientChatLineReceivedEvent.Companion.publish(event).getCancelled()) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return event.getReplaceWith();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", at = @At("HEAD"), cancellable = true)
|
||||||
|
public void onAddMessage2(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) {
|
||||||
|
if (message == null) ci.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package moe.nea.firmament.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.screen.ChatScreen;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
|
||||||
|
@Mixin(ChatScreen.class)
|
||||||
|
public class MixinChatScreen {
|
||||||
|
}
|
||||||
@@ -20,19 +20,19 @@ package moe.nea.firmament
|
|||||||
|
|
||||||
import com.mojang.brigadier.CommandDispatcher
|
import com.mojang.brigadier.CommandDispatcher
|
||||||
import dev.architectury.event.events.client.ClientTickEvent
|
import dev.architectury.event.events.client.ClientTickEvent
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.plugins.UserAgent
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.cache.HttpCache
|
import io.ktor.client.plugins.cache.*
|
||||||
import io.ktor.client.plugins.compression.ContentEncoding
|
import io.ktor.client.plugins.compression.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.plugins.logging.LogLevel
|
import io.ktor.client.plugins.logging.*
|
||||||
import io.ktor.client.plugins.logging.Logging
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
|
||||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
|
||||||
|
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
|
||||||
import net.fabricmc.loader.api.FabricLoader
|
import net.fabricmc.loader.api.FabricLoader
|
||||||
import net.fabricmc.loader.api.Version
|
import net.fabricmc.loader.api.Version
|
||||||
import net.fabricmc.loader.api.metadata.ModMetadata
|
import net.fabricmc.loader.api.metadata.ModMetadata
|
||||||
@@ -51,6 +51,7 @@ import net.minecraft.command.CommandRegistryAccess
|
|||||||
import net.minecraft.util.Identifier
|
import net.minecraft.util.Identifier
|
||||||
import moe.nea.firmament.commands.registerFirmamentCommand
|
import moe.nea.firmament.commands.registerFirmamentCommand
|
||||||
import moe.nea.firmament.dbus.FirmamentDbusObject
|
import moe.nea.firmament.dbus.FirmamentDbusObject
|
||||||
|
import moe.nea.firmament.events.ScreenRenderPostEvent
|
||||||
import moe.nea.firmament.events.TickEvent
|
import moe.nea.firmament.events.TickEvent
|
||||||
import moe.nea.firmament.features.FeatureManager
|
import moe.nea.firmament.features.FeatureManager
|
||||||
import moe.nea.firmament.repo.HypixelStaticData
|
import moe.nea.firmament.repo.HypixelStaticData
|
||||||
@@ -134,7 +135,12 @@ object Firmament {
|
|||||||
globalJob.cancel()
|
globalJob.cancel()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight ->
|
||||||
|
ScreenEvents.afterRender(screen)
|
||||||
|
.register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta ->
|
||||||
|
ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext))
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun identifier(path: String) = Identifier(MOD_ID, path)
|
fun identifier(path: String) = Identifier(MOD_ID, path)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import moe.nea.firmament.util.unformattedString
|
|||||||
*/
|
*/
|
||||||
data class ClientChatLineReceivedEvent(val text: Text) : FirmamentEvent.Cancellable() {
|
data class ClientChatLineReceivedEvent(val text: Text) : FirmamentEvent.Cancellable() {
|
||||||
val unformattedString = text.unformattedString
|
val unformattedString = text.unformattedString
|
||||||
|
var replaceWith: Text = text
|
||||||
|
|
||||||
companion object : FirmamentEventBus<ClientChatLineReceivedEvent>()
|
companion object : FirmamentEventBus<ClientChatLineReceivedEvent>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package moe.nea.firmament.events
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext
|
||||||
|
import net.minecraft.client.gui.screen.Screen
|
||||||
|
|
||||||
|
data class ScreenRenderPostEvent(
|
||||||
|
val screen: Screen,
|
||||||
|
val mouseX: Int,
|
||||||
|
val mouseY: Int,
|
||||||
|
val tickDelta: Float,
|
||||||
|
val drawContext: DrawContext
|
||||||
|
) : FirmamentEvent() {
|
||||||
|
companion object : FirmamentEventBus<ScreenRenderPostEvent>()
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ package moe.nea.firmament.features
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import moe.nea.firmament.Firmament
|
import moe.nea.firmament.Firmament
|
||||||
|
import moe.nea.firmament.features.chat.ImagePreview
|
||||||
import moe.nea.firmament.features.debug.DebugView
|
import moe.nea.firmament.features.debug.DebugView
|
||||||
import moe.nea.firmament.features.debug.DeveloperFeatures
|
import moe.nea.firmament.features.debug.DeveloperFeatures
|
||||||
import moe.nea.firmament.features.fishing.FishingWarning
|
import moe.nea.firmament.features.fishing.FishingWarning
|
||||||
@@ -55,6 +56,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
|
|||||||
loadFeature(SlotLocking)
|
loadFeature(SlotLocking)
|
||||||
loadFeature(StorageOverlay)
|
loadFeature(StorageOverlay)
|
||||||
loadFeature(CraftingOverlay)
|
loadFeature(CraftingOverlay)
|
||||||
|
loadFeature(ImagePreview)
|
||||||
loadFeature(SaveCursorPosition)
|
loadFeature(SaveCursorPosition)
|
||||||
if (Firmament.DEBUG) {
|
if (Firmament.DEBUG) {
|
||||||
loadFeature(DeveloperFeatures)
|
loadFeature(DeveloperFeatures)
|
||||||
|
|||||||
150
src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt
Normal file
150
src/main/kotlin/moe/nea/firmament/features/chat/ImagePreview.kt
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package moe.nea.firmament.features.chat
|
||||||
|
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.utils.io.jvm.javaio.*
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.*
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlin.math.min
|
||||||
|
import net.minecraft.client.gui.screen.ChatScreen
|
||||||
|
import net.minecraft.client.texture.NativeImage
|
||||||
|
import net.minecraft.client.texture.NativeImageBackedTexture
|
||||||
|
import net.minecraft.text.HoverEvent
|
||||||
|
import net.minecraft.text.Style
|
||||||
|
import net.minecraft.text.Text
|
||||||
|
import net.minecraft.util.Formatting
|
||||||
|
import net.minecraft.util.Identifier
|
||||||
|
import moe.nea.firmament.Firmament
|
||||||
|
import moe.nea.firmament.events.ClientChatLineReceivedEvent
|
||||||
|
import moe.nea.firmament.events.ScreenRenderPostEvent
|
||||||
|
import moe.nea.firmament.features.FirmamentFeature
|
||||||
|
import moe.nea.firmament.gui.config.ManagedConfig
|
||||||
|
import moe.nea.firmament.util.MC
|
||||||
|
import moe.nea.firmament.util.transformEachRecursively
|
||||||
|
import moe.nea.firmament.util.unformattedString
|
||||||
|
|
||||||
|
object ImagePreview : FirmamentFeature {
|
||||||
|
override val identifier: String
|
||||||
|
get() = "image-preview"
|
||||||
|
|
||||||
|
object TConfig : ManagedConfig(identifier) {
|
||||||
|
val enabled by toggle("enabled") { true }
|
||||||
|
val allowAllHosts by toggle("allow-all-hosts") { false }
|
||||||
|
val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" }
|
||||||
|
val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() }
|
||||||
|
val screenPercentage by integer("percentage", 10, 100) { 50 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isHostAllowed(host: String) =
|
||||||
|
TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
|
||||||
|
|
||||||
|
fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
|
||||||
|
|
||||||
|
override val config get() = TConfig
|
||||||
|
val urlRegex = "https://[^. ]+\\.[^ ]+(\\.(png|gif|jpe?g))(\\?[^ ]*)?( |$)".toRegex()
|
||||||
|
|
||||||
|
data class Image(
|
||||||
|
val texture: Identifier,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
val imageCache: MutableMap<String, Deferred<Image?>> =
|
||||||
|
Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
|
||||||
|
|
||||||
|
fun tryCacheUrl(url: String) {
|
||||||
|
if (!isUrlAllowed(url)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (url in imageCache) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageCache[url] = Firmament.coroutineScope.async {
|
||||||
|
try {
|
||||||
|
val response = Firmament.httpClient.get(URL(url))
|
||||||
|
if (response.status.value == 200) {
|
||||||
|
val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob)
|
||||||
|
val image = NativeImage.read(inputStream)
|
||||||
|
val texture = MC.textureManager.registerDynamicTexture(
|
||||||
|
"dynamic_image_preview",
|
||||||
|
NativeImageBackedTexture(image)
|
||||||
|
)
|
||||||
|
Image(texture, image.width, image.height)
|
||||||
|
} else
|
||||||
|
null
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
exc.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoad() {
|
||||||
|
ClientChatLineReceivedEvent.subscribe {
|
||||||
|
it.replaceWith = it.text.transformEachRecursively { child ->
|
||||||
|
val text = child.string
|
||||||
|
if ("://" !in text) return@transformEachRecursively child
|
||||||
|
val s = Text.empty().setStyle(child.style)
|
||||||
|
var index = 0
|
||||||
|
while (index < text.length) {
|
||||||
|
val nextMatch = urlRegex.find(text, index)
|
||||||
|
if (nextMatch == null) {
|
||||||
|
s.append(Text.literal(text.substring(index, text.length)))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val range = nextMatch.groups[0]!!.range
|
||||||
|
val url = nextMatch.groupValues[0]
|
||||||
|
s.append(Text.literal(text.substring(index, range.first)))
|
||||||
|
s.append(
|
||||||
|
Text.literal(url).setStyle(
|
||||||
|
Style.EMPTY.withUnderline(true).withColor(
|
||||||
|
Formatting.AQUA
|
||||||
|
).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tryCacheUrl(url)
|
||||||
|
index = range.last + 1
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenRenderPostEvent.subscribe {
|
||||||
|
if (!TConfig.enabled) return@subscribe
|
||||||
|
if (it.screen !is ChatScreen) return@subscribe
|
||||||
|
val hoveredComponent =
|
||||||
|
MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return@subscribe
|
||||||
|
val hoverEvent = hoveredComponent.hoverEvent ?: return@subscribe
|
||||||
|
val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return@subscribe
|
||||||
|
val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return@subscribe
|
||||||
|
val imageFuture = imageCache[url] ?: return@subscribe
|
||||||
|
if (!imageFuture.isCompleted) return@subscribe
|
||||||
|
val image = imageFuture.getCompleted() ?: return@subscribe
|
||||||
|
val screen = MC.screen!!
|
||||||
|
val scale =
|
||||||
|
min(
|
||||||
|
1F,
|
||||||
|
min(
|
||||||
|
(TConfig.screenPercentage / 100F * screen.width.toFloat()) / image.width,
|
||||||
|
screen.height.toFloat() / image.height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
it.drawContext.matrices.push()
|
||||||
|
it.drawContext.matrices.scale(scale, scale, 1F)
|
||||||
|
it.drawContext.drawTexture(
|
||||||
|
image.texture,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1F,
|
||||||
|
1F,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
)
|
||||||
|
it.drawContext.matrices.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ object MC {
|
|||||||
player?.networkHandler?.sendCommand(command)
|
player?.networkHandler?.sendCommand(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline val textureManager get() = MinecraftClient.getInstance().textureManager
|
||||||
|
inline val inGameHud get() = MinecraftClient.getInstance().inGameHud
|
||||||
inline val font get() = MinecraftClient.getInstance().textRenderer
|
inline val font get() = MinecraftClient.getInstance().textRenderer
|
||||||
inline val soundManager get() = MinecraftClient.getInstance().soundManager
|
inline val soundManager get() = MinecraftClient.getInstance().soundManager
|
||||||
inline val player get() = MinecraftClient.getInstance().player
|
inline val player get() = MinecraftClient.getInstance().player
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package moe.nea.firmament.util
|
|||||||
import net.minecraft.text.LiteralTextContent
|
import net.minecraft.text.LiteralTextContent
|
||||||
import net.minecraft.text.Text
|
import net.minecraft.text.Text
|
||||||
import net.minecraft.text.TextContent
|
import net.minecraft.text.TextContent
|
||||||
|
import net.minecraft.text.TranslatableTextContent
|
||||||
import moe.nea.firmament.Firmament
|
import moe.nea.firmament.Firmament
|
||||||
|
|
||||||
|
|
||||||
@@ -86,3 +87,23 @@ class TextMatcher(text: Text) {
|
|||||||
val Text.unformattedString
|
val Text.unformattedString
|
||||||
get() = string.replace("§.".toRegex(), "")
|
get() = string.replace("§.".toRegex(), "")
|
||||||
|
|
||||||
|
|
||||||
|
fun Text.transformEachRecursively(function: (Text) -> Text): Text {
|
||||||
|
val c = this.content
|
||||||
|
if (c is TranslatableTextContent) {
|
||||||
|
return Text.translatableWithFallback(c.key, c.fallback, *c.args.map {
|
||||||
|
(if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function)
|
||||||
|
}.toTypedArray()).also { new ->
|
||||||
|
new.style = this.style
|
||||||
|
new.siblings.clear()
|
||||||
|
this.siblings.forEach { child ->
|
||||||
|
new.siblings.add(child.transformEachRecursively(function))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return function(this.copy().also { it.siblings.clear() }).also { tt ->
|
||||||
|
this.siblings.forEach {
|
||||||
|
tt.siblings.add(it.transformEachRecursively(function))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,5 +66,10 @@
|
|||||||
"firmament.config.storage-overlay.padding": "Padding",
|
"firmament.config.storage-overlay.padding": "Padding",
|
||||||
"firmament.config.storage-overlay.scroll-speed": "Scroll Speed",
|
"firmament.config.storage-overlay.scroll-speed": "Scroll Speed",
|
||||||
"firmament.config.storage-overlay.inverse-scroll": "Invert Scroll",
|
"firmament.config.storage-overlay.inverse-scroll": "Invert Scroll",
|
||||||
"firmament.config.storage-overlay.margin": "Margin"
|
"firmament.config.storage-overlay.margin": "Margin",
|
||||||
|
"firmament.config.image-preview": "Image Preview",
|
||||||
|
"firmament.config.image-preview.enabled": "Enable Image Preview",
|
||||||
|
"firmament.config.image-preview.allow-all-hosts": "Allow all Image Hosts",
|
||||||
|
"firmament.config.image-preview.allowed-hosts": "Allowed Image Hosts",
|
||||||
|
"firmament.config.image-preview.percentage": "Image Width (Percentage of screen)"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user