Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object Authors {
val Nyako = Author("nyakowint", 118437263754395652L)
val Juby210 = Author("Juby210", 324622488644616195L)
val LampDelivery = Author("LampDelivery", 650805815623680030L)
val miaaaa0a = Author("miaaaa0a", 435750383491481602L)
}

/**
Expand Down
20 changes: 20 additions & 0 deletions plugins/ImageToClipboard/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import com.aliucord.gradle.repo.Authors
import com.aliucord.gradle.repo.author

plugins {
id("com.aliucord.plugins-repo")
}

version = "1.2.0"
description = "add an option to copy images to your clipboard"

aliucord {
author(Authors.miaaaa0a)

changelog.set(
"""
# 1.2.0
* completely rewrite mime type detection
""".trimIndent(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ing.frolick.imagetoclipboard

import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider

import com.aliucord.Logger
import com.aliucord.Utils
import com.aliucord.Utils.appContext
import com.aliucord.annotations.AliucordPlugin
import com.aliucord.entities.Plugin
import com.aliucord.patcher.Hook
import com.aliucord.utils.ViewUtils.findViewById
import com.discord.app.AppFragment
import com.discord.databinding.WidgetMediaBinding
import com.discord.utilities.color.ColorCompat
import com.discord.widgets.media.WidgetMedia

import com.google.android.material.appbar.AppBarLayout
import com.lytefast.flexinput.R
import java.io.File
import java.io.InputStream
import java.net.URL

@SuppressWarnings("unused")
@AliucordPlugin
class ImageToClipboard : Plugin() {
// https://stackoverflow.com/questions/14473180/regex-to-get-a-filename-from-a-url#comment115606945_26253039
var urlRegex = Regex("[^/\\\\&?#]+\\.\\w{3,4}(?=([?&#].*$|$))")

@SuppressLint("RestrictedApi")
override fun start(context: Context) {
patcher.patch(
WidgetMedia::class.java, "onViewBoundOrOnResume", arrayOf(),
Hook {
val widgetMedia = it.thisObject as WidgetMedia
val binding = WidgetMedia.`access$getBinding$p`(widgetMedia)
val field = WidgetMediaBinding::class.java.getDeclaredField("b").apply { isAccessible = true }
val appBarLayout = field.get(binding) as AppBarLayout
val menuMediaGroup = appBarLayout.findViewById<TextView>("menu_media_download").parent as ViewGroup

val imageUrl = (widgetMedia as AppFragment).mostRecentIntent
.getStringExtra("INTENT_MEDIA_URL") ?: return@Hook
val fileName = urlRegex.find(imageUrl, 0)?.value
val fileExt = fileName?.split('.')?.last()
var mimeType = MimeType.OTHER
Utils.threadPool.submit {
mimeType = MimeTypeGetter.get(imageUrl)
}.get()
//logger.info("MIME TYPE: ${mimeType.type}")

//Logger().info("CHILDREN COUNT: ${actionBar.childCount}")
//val menuMediaGroup = actionBar.getChildAt(0) as ViewGroup

val ctx = menuMediaGroup.context
val copyBtn = TextView(ctx, null, 0, R.i.UiKit_ImageButton).apply {
setCompoundDrawablesWithIntrinsicBounds(
ContextCompat
.getDrawable(ctx, R.e.ic_copy_24dp)!!
.mutate()
.apply {
setTint(ColorCompat.getThemedColor(ctx, R.b.colorTextNormal))
},
null,
null,
null,
)
setOnClickListener {
Utils.showToast("copying...", false)

Utils.threadPool.execute {
try {
val stream = URL(imageUrl).openStream()

// context.filesDir is /data/data/com.aliucord/files, we are saving the image there
// in theory it'd be better to use context.cacheDir, but discord doesn't expose that
// in file_paths.xml, so it's not usable
val file = File(context.filesDir, "clipboard_tmp.${mimeType.ext}")
file.outputStream().use { out -> stream.copyTo(out) }
val uri = FileProvider.getUriForFile(
appContext,
"${context.packageName}.file-provider",
file
)
val clip = ClipData(
ClipDescription("image", arrayOf(mimeType.type)),
ClipData.Item(uri)
)
(appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
.setPrimaryClip(clip)

Utils.mainThread.post { Utils.showToast("copied the image!", false) }
} catch (e: Exception) {
logger.error("itc error: ${e.message}", e)
Utils.mainThread.post { Utils.showToast("failed to copy >.<", false) }
}
}
}
}
if (appBarLayout.findViewWithTag<View>("copy_btn") == null) {
// non image begone!!!
if (fileExt == "png" ||
fileExt == "jpg" ||
fileExt == "jpeg" ||
fileExt == "webp" ||
fileExt == "gif" ||
fileExt == "apng") {
copyBtn.tag = "copy_btn"
menuMediaGroup.addView(copyBtn)
}
}
},
)
}

override fun stop(context: Context) {
patcher.unpatchAll()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ing.frolick.imagetoclipboard

import android.os.Build
import androidx.annotation.RequiresApi
import java.io.File
import java.io.InputStream
import java.net.URL
import java.nio.file.Files
import java.util.Arrays.asList
import com.aliucord.Logger
import java.net.HttpURLConnection

class MimeTypeGetter {
companion object {
private fun fromMagicNumber(url: String): MimeType {
val stream = URL(url).openStream()
val header = ByteArray(12).also { buf ->
var pos = 0
while (pos < buf.size) {
val n = stream.read(buf, pos, buf.size - pos)
if (n == -1) break
pos += n
}
}
stream.close()
fun at(vararg bytes: Int, offset: Int = 0) =
bytes.indices.all { header[offset + it].toInt() and 0xFF == bytes[it] }

// TODO: FIX APNG DETECTION AND MAKE IT OPTIMAL, THIS ALGO IS REALLY SLOW AND BROKEN
// only real way to detect APNG is to look for the animation control chunk (acTL)
// that comes later in the file, since the format has the same file magic as PNG
/*if (at(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)) {
val buf = ArrayDeque<Int>()
// read first 4 bytes for sliding window
while (buf.size <= 4) buf.addLast(stream.read().also { if (it == -1) return MimeType.PNG })
Logger("piglin").info("CHUNK: ${ArrayDeque(listOf(0x49, 0x44, 0x41, 0x54)).joinToString(" ") { "%02x".format(it.toByte()) }}")

while (buf.intersect(listOf(0x61, 0x63, 0x54, 0x4C)).isNotEmpty()) {
// acTL always comes before the IDAT chunk. IDAT chunk before acTL == PNG
if (buf.intersect(listOf(0x49, 0x44, 0x41, 0x54)).isEmpty()) return MimeType.PNG

buf.removeFirst()
val n = stream.read()
//Logger("piglin").info("CHUNK: ${buf.joinToString(" ") { "%02x".format(it.toByte()) }}")
if (n == -1) return MimeType.PNG
buf.addLast(n)
}
return MimeType.APNG
}*/

return when {
at(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) -> MimeType.PNG
at(0xFF, 0xD8, 0xFF, 0xDB) -> MimeType.JPEG
at(0xFF, 0xD8, 0xFF, 0xE0) -> MimeType.JPEG
at(0xFF, 0xD8, 0xFF, 0xEE) -> MimeType.JPEG
at(0xFF, 0xD8, 0xFF, 0xE1) -> MimeType.JPEG
at(0x47, 0x49, 0x46, 0x38, 0x37, 0x61) -> MimeType.GIF
at(0x47, 0x49, 0x46, 0x38, 0x39, 0x61) -> MimeType.GIF
at(0x42, 0x4D) -> MimeType.BMP
// webp: RIFF????WEBP
at(0x52, 0x49, 0x46, 0x46) &&
at(0x57, 0x45, 0x42, 0x50, offset = 8) -> MimeType.WEBP
else -> MimeType.OTHER
}
}

private fun fromHeaders(url: String): MimeType? {
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
this.instanceFollowRedirects = true
this.requestMethod = "HEAD"
}
connection.connect()
connection.headerFields["Content-Type"]?.let {
return MimeType.entries.firstOrNull { e -> e.type == it.first() } ?: MimeType.OTHER
}
return null
}

fun get(url: String): MimeType {
return fromHeaders(url) ?: fromMagicNumber(url)
}
}
}

enum class MimeType(val type: String, val ext: String) {
PNG("image/png", "png"),
APNG("image/apng", "png"),
JPEG("image/jpeg", "jpg"),
GIF("image/gif", "gif"),
BMP("image/x-ms-bmp", "bmp"),
WEBP("image/webp", "webp"),
OTHER("", "bin")
}
20 changes: 20 additions & 0 deletions plugins/NativeFriendNicknames/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import com.aliucord.gradle.repo.Authors
import com.aliucord.gradle.repo.author

plugins {
id("com.aliucord.plugins-repo")
}

version = "1.1.6"
description = "a plugin that backports the friend nickname feature"

aliucord {
author(Authors.miaaaa0a)

changelog.set(
"""
# 1.1.6
* don't display friend nickname in servers
""".trimIndent(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ing.frolick.nativefriendnicknames

import android.view.View

import com.aliucord.Http
import com.aliucord.Utils
import com.aliucord.fragments.InputDialog
import com.aliucord.wrappers.users.globalName
import com.discord.models.user.User
import com.discord.stores.StoreStream
import com.discord.stores.updates.ObservationDeck

data class UpdateRelationshipRequest(
val nickname: String?
)

class EditNicknameDialog(private val user: User) : InputDialog() {
override fun onViewBound(view: View) {
setTitle("Edit Friend Nickname")
setDescription("Enter a new nickname for your friend:")
setPlaceholderText("Nickname")

setOnDialogShownListener {
// only replace input if a friend nickname already exists
if (Stores.friendNicknames.getNickname(user.id) != null)
inputLayout.editText?.setText(user.globalName)
}

setOnOkListener {
val inputName = input.trim()

if (inputName != user.globalName) {
Utils.threadPool.execute {
val req = UpdateRelationshipRequest(inputName)
val res = Http.Request
.newDiscordRNRequest("/users/@me/relationships/${user.id}", "PATCH")
.executeWithJson(req)
// why does this not throw the usual 200 success code dieee discord
if (res.statusCode == 204) {
// this is supposed to instantly update everything that uses User.globalName
// but it doesn't :(
/*StoreStream.`access$getDispatcher$p`(StoreStream.getPresences().stream).schedule {
val storeUsers = StoreStream.Companion!!.users
val getUsersUpdate =
storeUsers::class.java.getDeclaredMethod("access\$getUsersUpdate\$cp").apply {
isAccessible = true
}
val usersUpdate = getUsersUpdate.invoke(null)
storeUsers.markChanged(usersUpdate as ObservationDeck.UpdateSource?)
}*/
} else {
Utils.mainThread.post {
Utils.showToast("Error ${res.statusCode} while changing ${user.username}'s nickname!", true)
}
}
}
}

dismiss()
}

super.onViewBound(view)
}
}
Loading