diff --git a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt index cab4c36..cc656bc 100644 --- a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt +++ b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt @@ -44,6 +44,7 @@ object Authors { val Nyako = Author("nyakowint", 118437263754395652L) val Juby210 = Author("Juby210", 324622488644616195L) val LampDelivery = Author("LampDelivery", 650805815623680030L) + val Ty = Author("Ty", 487443883127472129L) val miaaaa0a = Author("miaaaa0a", 435750383491481602L) } diff --git a/plugins/CallTime/build.gradle.kts b/plugins/CallTime/build.gradle.kts new file mode 100644 index 0000000..d4d930d --- /dev/null +++ b/plugins/CallTime/build.gradle.kts @@ -0,0 +1,25 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.3" +description = "A plugin that shows how long you have been in a VC for." + +aliucord { + changelog.set(""" + # Version 1.0.3 + * Updated for new discord/aliucord version + # Version 1.0.2 + * Fix a crash + * Fix seconds bugging out with long times + # Version 1.0.1 + * Fix ending timer on mute/deafen + # Version 1.0.0 + * Initial release + """.trimIndent()) + + author(Authors.Ty) +} \ No newline at end of file diff --git a/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt b/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt new file mode 100644 index 0000000..ecf01f7 --- /dev/null +++ b/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt @@ -0,0 +1,83 @@ +package tech.tyman.plugins.calltime + +import android.content.Context +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.PatcherAPI +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.discord.gateway.io.OutgoingPayload +import com.discord.rtcconnection.RtcConnection +import com.discord.stores.StoreRtcConnection +import com.discord.utilities.streams.StreamContext +import com.discord.utilities.time.ClockFactory +import com.discord.utilities.voice.VoiceViewUtils +import com.discord.widgets.status.WidgetGlobalStatusIndicator +import com.discord.widgets.status.WidgetGlobalStatusIndicatorViewModel +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.concurrent.schedule + +@AliucordPlugin +class CallTime : Plugin() { + private var vcConnectedTime: Long? = null + private var cachedVoiceState: OutgoingPayload.VoiceStateUpdate? = null + private val timer = Timer() + private var timerTask: TimerTask? = null + private val clock = ClockFactory.get() + private val elapsedTime: String + get() = run { + val vcConnectedTime = vcConnectedTime ?: return "00:00:00" + val duration = clock.currentTimeMillis() - vcConnectedTime + return String.format( + "%02d:%02d:%02d", + TimeUnit.MILLISECONDS.toHours(duration), + TimeUnit.MILLISECONDS.toMinutes(duration) % TimeUnit.HOURS.toMinutes(1), + TimeUnit.MILLISECONDS.toSeconds(duration) % TimeUnit.MINUTES.toSeconds(1) + ) + } + + override fun start(ctx: Context) { + patcher.patchConnectedText() + } + + private fun PatcherAPI.patchConnectedText() { + val currentVoiceStateField = StoreRtcConnection::class.java.getDeclaredField("currentVoiceState").apply { isAccessible = true } + val setupIndicatorStatusMethod = WidgetGlobalStatusIndicator::class.java.getDeclaredMethod("setupIndicatorStatus", WidgetGlobalStatusIndicatorViewModel.ViewState.CallOngoing::class.java).apply { isAccessible = true } + val getConnectedTextMethod = VoiceViewUtils::class.java.getDeclaredMethod("getConnectedText", Context::class.java, RtcConnection.State::class.java, StreamContext::class.java, Boolean::class.javaPrimitiveType) + + patch(setupIndicatorStatusMethod, Hook { + val widget = it.thisObject as WidgetGlobalStatusIndicator + timerTask?.cancel() + timerTask = timer.schedule(1000) { + widget.activity?.runOnUiThread { + widget.view ?: return@runOnUiThread + setupIndicatorStatusMethod.invoke(widget, it.args[0]) + } + } + }) + + patch(StoreRtcConnection::class.java.getDeclaredMethod("onVoiceStateUpdated"), Hook { + val store = it.thisObject as StoreRtcConnection + val currentVoiceState = currentVoiceStateField[store] as OutgoingPayload.VoiceStateUpdate? + ?: return@Hook + if (currentVoiceState.channelId != null && cachedVoiceState?.channelId != currentVoiceState.channelId) { + vcConnectedTime = clock.currentTimeMillis() + } else if (currentVoiceState.channelId == null && cachedVoiceState?.channelId != null) { + timerTask?.cancel() + timerTask = null + if (vcConnectedTime != null) Utils.showToast("Call ended. Elapsed time: $elapsedTime", true) + vcConnectedTime = null + } + cachedVoiceState = currentVoiceState + }) + patch(getConnectedTextMethod, Hook { + it.result = "${it.result} ($elapsedTime)" + }) + + } + + override fun stop(context: Context) { + patcher.unpatchAll() + } +} \ No newline at end of file diff --git a/plugins/ReverseImageSearch/build.gradle.kts b/plugins/ReverseImageSearch/build.gradle.kts new file mode 100644 index 0000000..9785116 --- /dev/null +++ b/plugins/ReverseImageSearch/build.gradle.kts @@ -0,0 +1,26 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.4" +description = "A plugin that adds a button for reverse image searching a sent image." + +aliucord { + changelog.set(""" + # Version 1.0.4 + * Fix reverse searching not working on expired discord urls and images inside embeds (thanks Delphox) + # Version 1.0.3 + * Replace google images search with google lens, as the old images URL no longer works (thanks Delphox) + # Version 1.0.2 + * Fixed google images search (i love it when google) + # Version 1.0.1 + * Fixed a bug where some images would not work when using google + # Version 1.0.0 + * Initial release + """.trimIndent()) + + author(Authors.Ty) +} \ No newline at end of file diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt new file mode 100644 index 0000000..4d0538b --- /dev/null +++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt @@ -0,0 +1,40 @@ +package tech.tyman.plugins.reverseimagesearch + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.widgets.BottomSheet +import com.discord.widgets.media.WidgetMedia +import com.lytefast.flexinput.R + +class EngineSheet(val url: String?) : BottomSheet() { + @SuppressLint("SetTextI18n") + override fun onViewCreated(view: View, bundle: Bundle?) { + super.onViewCreated(view, bundle) + + val context = view.context + val textViews = Engine.values().mapNotNull { + if (it == Engine.ASK) return@mapNotNull null + TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + val iconDrawable = ResourcesCompat + .getDrawable(resources, R.e.ic_open_in_browser_white_24dp, null) + text = "Open in ${it.niceName}" + setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null) + setOnClickListener { _ -> + val url = this@EngineSheet.url ?: return@setOnClickListener + val intent = Intent(Intent.ACTION_VIEW, it.getUrl(url)) + startActivity(intent) + } + } + } + textViews.forEach { + linearLayout.addView(it) + } + } +} \ No newline at end of file diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt new file mode 100644 index 0000000..5427d6d --- /dev/null +++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt @@ -0,0 +1,89 @@ +package tech.tyman.plugins.reverseimagesearch + +import android.content.Context +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import androidx.appcompat.view.menu.ActionMenuItemView +import com.aliucord.Utils +import com.discord.widgets.media.WidgetMedia +import com.aliucord.patcher.after +import android.net.Uri +import android.content.Intent +import android.graphics.drawable.Drawable +import androidx.core.content.res.ResourcesCompat +import com.aliucord.Logger +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@AliucordPlugin +class ReverseImageSearch : Plugin() { + lateinit var pluginIcon: Drawable + + init { + settingsTab = SettingsTab(SearchSettings::class.java).withArgs(settings) + needsResources = true + } + + override fun load(ctx: Context) { + val res = resources!! + + pluginIcon = ResourcesCompat.getDrawable( + res, + res.getIdentifier("ic_baseline_image_search_24", "drawable", "com.aliucord.plugins"), + null + )!! + } + + override fun start(context: Context) { + val isVideoMethod = WidgetMedia::class.java.getDeclaredMethod("isVideo").apply { + isAccessible = true + } + patcher.after("onViewBoundOrOnResume") { + if (isVideoMethod.invoke(this) as Boolean) return@after + val btnId = Utils.getResId("menu_media_browser", "id") + val btn = Utils.appActivity.findViewById(btnId) + btn.setOnLongClickListener { + val url = mostRecentIntent.getStringExtra("INTENT_IMAGE_URL") ?: return@setOnLongClickListener false + if (settings.engine == Engine.ASK) { + EngineSheet(url) + .show(Utils.appActivity.supportFragmentManager, "Reverse Image Search") + } else { + val intent = Intent(Intent.ACTION_VIEW, settings.engine.getUrl(url)) + startActivity(intent) + } + true + } + } + } + + override fun stop(context: Context) { + patcher.unpatchAll() + } +} + +@Suppress("unused") +enum class Engine(val urlTemplate: String?, val niceName: String) { + ASK(null, "Ask every time"), + GOOGLE("https://lens.google.com/uploadbyurl?url=%s", "Google Lens"), + TIN_EYE("https://www.tineye.com/search?url=%s", "TinEye"), + YANDEX("https://yandex.com/images/search?url=%s&rpt=imageview", "Yandex"), + BING("https://www.bing.com/images/search?q=imgurl:%s&view=detailv2&iss=sbi&FORM=IRSBIQ", "Bing"), + IQDB("https://iqdb.org/?url=%s", "IQDB"), + SAUCE_NAO("https://saucenao.com/search.php?url=%s", "SauceNAO"); + + fun getUrl(url: String): Uri { + val uri = Uri.parse( + urlTemplate?.format( + URLEncoder.encode(url, StandardCharsets.UTF_8.toString()) + ) + ) + + return uri + } + + companion object { + fun fromOrdinal(ordinal: Int): Engine? { + return values().find { it.ordinal == ordinal } + } + } +} diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt new file mode 100644 index 0000000..064f96a --- /dev/null +++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt @@ -0,0 +1,52 @@ +package tech.tyman.plugins.reverseimagesearch + +import android.annotation.SuppressLint +import android.view.View +import android.widget.RadioGroup +import android.widget.TextView +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.fragments.SettingsPage +import com.discord.utilities.color.ColorCompat +import com.discord.views.CheckedSetting +import com.discord.views.RadioManager +import com.lytefast.flexinput.R + +var SettingsAPI.engine: Engine + get() = Engine.fromOrdinal(this.getInt("engine", Engine.ASK.ordinal)) ?: Engine.ASK + set(value) = this.setInt("engine", value.ordinal) + +@SuppressLint("SetTextI18n") +class SearchSettings(private val settings: SettingsAPI) : SettingsPage() { + override fun onViewBound(view: View?) { + super.onViewBound(view) + setActionBarTitle("ReverseImageSearch") + val ctx = requireContext() + + val radios = Engine.values().mapNotNull { + Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.RADIO, it.niceName, null) + } + val radioManager = RadioManager(radios) + val radioGroup = RadioGroup(ctx) + + radios.forEachIndexed { i, radio -> + radio.e { + settings.engine = Engine.fromOrdinal(i) ?: return@e + radioManager.a(radio) + } + radioGroup.addView(radio) + if (settings.engine.ordinal == i) radioManager.a(radio) + } + + val helpText = TextView(ctx) + helpText.linksClickable = true + helpText.text = + "To reverse image search an image, open it and long press the \"Open in browser\" icon. " + + "Then, a reverse image search using then engine of your choice (you can choose this above)" + + "will open in a browser." + helpText.setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorOnPrimary)) + + addView(radioGroup) + addView(helpText) + } +} \ No newline at end of file diff --git a/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml b/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml new file mode 100644 index 0000000..83058ae --- /dev/null +++ b/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/plugins/RoleColorContrast/build.gradle.kts b/plugins/RoleColorContrast/build.gradle.kts new file mode 100644 index 0000000..1baf777 --- /dev/null +++ b/plugins/RoleColorContrast/build.gradle.kts @@ -0,0 +1,23 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.1.0" +description = "A plugin to improve contrast between role colors and the background" + +aliucord { + changelog.set(""" + # Version 1.1.0 + * Now supports names in the member list + * Fixed a possible issue when themer was not installed + * Contrast adjustments are now faster + + # Version 1.0.0 + * Initial release, supporting only names in chat + """.trimIndent()) + + author(Authors.Ty) +} \ No newline at end of file diff --git a/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt new file mode 100644 index 0000000..16b273f --- /dev/null +++ b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt @@ -0,0 +1,131 @@ +@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS") + +package xyz.myriation.plugins.rolecolorcontrast + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import com.aliucord.PluginManager +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.after +import com.discord.models.member.GuildMember +import com.discord.stores.StoreStream +import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListAdapter +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage +import com.lytefast.flexinput.R +import kotlin.math.pow + +enum class ContrastLevel { + Failing, AA, AAA +} + +@AliucordPlugin +class RoleColorContrast : Plugin() { + lateinit var pluginIcon: Drawable + + init { + settingsTab = SettingsTab(PluginSettings::class.java).withArgs(settings) + } + + @get:ColorInt + val themerBackgroundColor by lazy { + val classLoader = (PluginManager.plugins["Themer"] ?: return@lazy null)::class.java.classLoader!! + val resourceManager = + classLoader.loadClass("dev.vendicated.aliucordplugs.themer.ResourceManager").getDeclaredField("INSTANCE") + .get(null) + resourceManager::class.java.getDeclaredMethod("getColorForName", String::class.java) + .invoke(resourceManager!!, "primary_dark_600") as Int? + } + + override fun load(context: Context) { + pluginIcon = ContextCompat.getDrawable(context, R.e.ic_accessibility_24dp)!! + } + + @SuppressLint("SetTextI18n") + override fun start(context: Context) { + // Patch names in chat + patcher.after("getAuthorTextColor", GuildMember::class.java) { + it.result = coerceToContrast(it.result as Int, getBackgroundColor() ?: return@after) + } + // Patch names in guild member list + patcher.after("getColor") { + it.result = coerceToContrast(it.result as Int, getBackgroundColor() ?: return@after) + } + } + + override fun stop(context: Context) { + patcher.unpatchAll() + } + + private fun getBackgroundColor(): Int? { + val theme = StoreStream.getUserSettingsSystem().theme + return when (theme) { + "pureEvil" -> ContextCompat.getColor(Utils.appContext, R.c.black) + "dark" -> themerBackgroundColor ?: ContextCompat.getColor(Utils.appContext, R.c.primary_dark_600) + "light" -> ContextCompat.getColor(Utils.appContext, R.c.white) + else -> return null + } + } + + private fun coerceToContrast(text: Int, background: Int): Int { + val hsl = FloatArray(3) + ColorUtils.colorToHSL(text, hsl) + val factor = if (calculateRelativeLuminance(background) < 0.5) 0.03f else -0.03f + val comparator = if (settings.getBool("useAAA", false)) ContrastLevel.AAA else ContrastLevel.AA + while (calculateContrast( + ColorUtils.setAlphaComponent(ColorUtils.HSLToColor(hsl), 255), background + ) < comparator + ) { + hsl[2] += factor + // If we can't create full contrast, settle for the best we can + if (hsl[2] > 1 || hsl[2] < 0) { + hsl[2] = hsl[2].coerceIn(0f..1f) + break + } + } + return ColorUtils.HSLToColor(hsl) + } + + private fun calculateContrast(text: Int, background: Int): ContrastLevel { + val textLuminance = calculateRelativeLuminance(text) + val backgroundLuminance = calculateRelativeLuminance(background) + val (luminanceOne, luminanceTwo) = if (textLuminance >= backgroundLuminance) { + Pair(textLuminance, backgroundLuminance) + } else { + Pair(backgroundLuminance, textLuminance) + } + + val ratio = (luminanceOne + 0.05) / (luminanceTwo + 0.05) + return when { + ratio >= 7 -> ContrastLevel.AAA + ratio >= 4.5 -> ContrastLevel.AA + else -> ContrastLevel.Failing + } + } + + private fun calculateRelativeLuminance(color: Int): Double { + val r = Color.red(color) / 255.0 + val g = Color.green(color) / 255.0 + val b = Color.blue(color) / 255.0 + + return 0.2126 * if (r <= 0.03928) { + r / 12.92 + } else { + ((r + 0.055) / 1.055).pow(2.4) + } + 0.7152 * if (g <= 0.03928) { + g / 12.92 + } else { + ((g + 0.055) / 1.055).pow(2.4) + } + 0.0722 * if (b <= 0.03928) { + b / 12.92 + } else { + ((b + 0.055) / 1.055).pow(2.4) + } + } +} \ No newline at end of file diff --git a/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt new file mode 100644 index 0000000..748663d --- /dev/null +++ b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt @@ -0,0 +1,29 @@ +@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS") +package xyz.myriation.plugins.rolecolorcontrast + +import android.view.View +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.fragments.SettingsPage +import com.discord.views.CheckedSetting + +// TODO: Add example text +class PluginSettings(private val settings: SettingsAPI) : SettingsPage() { + override fun onViewBound(view: View?) { + super.onViewBound(view) + val ctx = requireContext() + + setActionBarTitle("RoleColorContrast") + addView( + Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Use AAA requirements instead of AA requirements for text contrast", + "AAA requirements are stricter, and require more contrast than the AA ones" + ).apply { + isChecked = settings.getBool("useAAA", false) + setOnCheckedListener { settings.setBool("useAAA", it) } + } + ) + } +} \ No newline at end of file diff --git a/plugins/Texter/build.gradle.kts b/plugins/Texter/build.gradle.kts new file mode 100644 index 0000000..6b52f2a --- /dev/null +++ b/plugins/Texter/build.gradle.kts @@ -0,0 +1,25 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.3" +description = "A plugin to modify text formatting inspired by plugins like BetterFormattingRedux (for BetterDiscord) and texter (for Powercord)" + +aliucord { + changelog.set(""" + # Version 1.0.3 + * Updated for new discord/aliucord version + # Version 1.0.2 + * Converted code to use kotlin + # Version 1.0.1 + * Refactored code + * Resolve older android compatibility issues + # Version 1.0.0 + * Initial release + """.trimIndent()) + + author(Authors.Ty) +} \ No newline at end of file diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt new file mode 100644 index 0000000..2b82134 --- /dev/null +++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt @@ -0,0 +1,348 @@ +package tech.tyman.plugins.texter + +import java.lang.StringBuilder + +object Maps { + fun getMappedString(map: Map, string: String): String { + val splitText = string.chunked(1) + val newString = StringBuilder() + for (character in splitText) { + newString.append( + map.getOrDefault(character, character) + ) + } + return newString.toString() + } + + val smallLetters = mapOf( + "q" to "ǫ", + "w" to "ᴡ", + "e" to "ᴇ", + "r" to "ʀ", + "t" to "ᴛ", + "y" to "ʏ", + "u" to "ᴜ", + "i" to "ɪ", + "o" to "ᴏ", + "p" to "ᴘ", + "a" to "ᴀ", + "s" to "s", + "d" to "ᴅ", + "f" to "ғ", + "g" to "ɢ", + "h" to "ʜ", + "j" to "ᴊ", + "k" to "ᴋ", + "l" to "ʟ", + "z" to "ᴢ", + "x" to "x", + "c" to "ᴄ", + "v" to "ᴠ", + "b" to "ʙ", + "n" to "ɴ", + "m" to "ᴍ", + "Q" to "ǫ", + "W" to "ᴡ", + "E" to "ᴇ", + "R" to "ʀ", + "T" to "ᴛ", + "Y" to "ʏ", + "U" to "ᴜ", + "I" to "ɪ", + "O" to "ᴏ", + "P" to "ᴘ", + "A" to "ᴀ", + "S" to "s", + "D" to "ᴅ", + "F" to "ғ", + "G" to "ɢ", + "H" to "ʜ", + "J" to "ᴊ", + "K" to "ᴋ", + "L" to "ʟ", + "Z" to "ᴢ", + "X" to "x", + "C" to "ᴄ", + "V" to "ᴠ", + "B" to "ʙ", + "N" to "ɴ", + "M" to "ᴍ" + ) + val smallerLetters = mapOf( + "q" to "ᑫ", + "w" to "ʷ", + "e" to "ᵉ", + "r" to "ʳ", + "t" to "ᵗ", + "y" to "ʸ", + "u" to "ᵘ", + "i" to "ᶦ", + "o" to "ᵒ", + "p" to "ᵖ", + "a" to "ᵃ", + "s" to "ˢ", + "d" to "ᵈ", + "f" to "ᶠ", + "g" to "ᵍ", + "h" to "ʰ", + "j" to "ʲ", + "k" to "ᵏ", + "l" to "ˡ", + "z" to "ᶻ", + "x" to "ˣ", + "c" to "ᶜ", + "v" to "ᵛ", + "b" to "ᵇ", + "n" to "ⁿ", + "m" to "ᵐ", + "Q" to "ᑫ", + "W" to "ʷ", + "E" to "ᵉ", + "R" to "ʳ", + "T" to "ᵗ", + "Y" to "ʸ", + "U" to "ᵘ", + "I" to "ᶦ", + "O" to "ᵒ", + "P" to "ᵖ", + "A" to "ᵃ", + "S" to "ˢ", + "D" to "ᵈ", + "F" to "ᶠ", + "G" to "ᵍ", + "H" to "ʰ", + "J" to "ʲ", + "K" to "ᵏ", + "L" to "ˡ", + "Z" to "ᶻ", + "X" to "ˣ", + "C" to "ᶜ", + "V" to "ᵛ", + "B" to "ᵇ", + "N" to "ⁿ", + "M" to "ᵐ", + "1" to "¹", + "2" to "²", + "3" to "³", + "4" to "⁴", + "5" to "⁵", + "6" to "⁶", + "7" to "⁷", + "8" to "⁸", + "9" to "⁹", + "0" to "⁰" + ) + val fullWidthLetters = mapOf( + "q" to "q", + "w" to "w", + "e" to "e", + "r" to "r", + "t" to "t", + "y" to "y", + "u" to "u", + "i" to "i", + "o" to "o", + "p" to "p", + "a" to "a", + "s" to "s", + "d" to "d", + "f" to "f", + "g" to "g", + "h" to "h", + "j" to "j", + "k" to "k", + "l" to "l", + "z" to "z", + "x" to "x", + "c" to "c", + "v" to "v", + "b" to "b", + "n" to "n", + "m" to "m", + "Q" to "Q", + "W" to "W", + "E" to "E", + "R" to "R", + "T" to "T", + "Y" to "Y", + "U" to "U", + "I" to "I", + "O" to "O", + "P" to "P", + "A" to "A", + "S" to "S", + "D" to "D", + "F" to "F", + "G" to "G", + "H" to "H", + "J" to "J", + "K" to "K", + "L" to "L", + "Z" to "Z", + "X" to "X", + "C" to "C", + "V" to "V", + "B" to "B", + "N" to "N", + "M" to "M", + "1" to "1", + "2" to "2", + "3" to "3", + "4" to "4", + "5" to "5", + "6" to "6", + "7" to "7", + "8" to "8", + "9" to "9", + "0" to "0" + ) + val emojiLetters = mapOf( + "q" to ":regional_indicator_q:", + "w" to ":regional_indicator_w:", + "e" to ":regional_indicator_e:", + "r" to ":regional_indicator_r:", + "t" to ":regional_indicator_t:", + "y" to ":regional_indicator_y:", + "u" to ":regional_indicator_u:", + "i" to ":regional_indicator_i:", + "o" to ":regional_indicator_o:", + "p" to ":regional_indicator_p:", + "a" to ":regional_indicator_a:", + "s" to ":regional_indicator_s:", + "d" to ":regional_indicator_d:", + "f" to ":regional_indicator_f:", + "g" to ":regional_indicator_g:", + "h" to ":regional_indicator_h:", + "j" to ":regional_indicator_j:", + "k" to ":regional_indicator_k:", + "l" to ":regional_indicator_l:", + "z" to ":regional_indicator_z:", + "x" to ":regional_indicator_x:", + "c" to ":regional_indicator_c:", + "v" to ":regional_indicator_v:", + "b" to ":regional_indicator_b:", + "n" to ":regional_indicator_n:", + "m" to ":regional_indicator_m:", + "Q" to ":regional_indicator_q:", + "W" to ":regional_indicator_w:", + "E" to ":regional_indicator_e:", + "R" to ":regional_indicator_r:", + "T" to ":regional_indicator_t:", + "Y" to ":regional_indicator_y:", + "U" to ":regional_indicator_u:", + "I" to ":regional_indicator_i:", + "O" to ":regional_indicator_o:", + "P" to ":regional_indicator_p:", + "A" to ":regional_indicator_a:", + "S" to ":regional_indicator_s:", + "D" to ":regional_indicator_d:", + "F" to ":regional_indicator_f:", + "G" to ":regional_indicator_g:", + "H" to ":regional_indicator_h:", + "J" to ":regional_indicator_j:", + "K" to ":regional_indicator_k:", + "L" to ":regional_indicator_l:", + "Z" to ":regional_indicator_z:", + "X" to ":regional_indicator_x:", + "C" to ":regional_indicator_c:", + "V" to ":regional_indicator_v:", + "B" to ":regional_indicator_b:", + "N" to ":regional_indicator_n:", + "M" to ":regional_indicator_m:", + "1" to ":one:", + "2" to ":two:", + "3" to ":three:", + "4" to ":four:", + "5" to ":five:", + "6" to ":six:", + "7" to ":seven:", + "8" to ":eight:", + "9" to ":nine:", + "0" to ":zero:" + ) + val flippedLetters = mapOf( + "q" to "b", + "w" to "ʍ", + "e" to "ǝ", + "r" to "ɹ", + "t" to "ʇ", + "y" to "ʎ", + "u" to "n", + "i" to "ᴉ", + "p" to "d", + "a" to "ɐ", + "s" to "s", + "d" to "p", + "f" to "ɟ", + "g" to "ɓ", + "h" to "ɥ", + "j" to "ſ", + "k" to "ʞ", + "l" to "ๅ", + "z" to "z", + "x" to "x", + "c" to "ɔ", + "v" to "ʌ", + "b" to "q", + "n" to "u", + "m" to "ɯ", + "Q" to "Ὸ", + "W" to "M", + "E" to "Ǝ", + "R" to "ꓤ", + "T" to "ꓕ", + "Y" to "⅄", + "U" to "ꓵ", + "I" to "I", + "O" to "O", + "P" to "ꓒ", + "A" to "Ɐ", + "S" to "S", + "D" to "ꓷ", + "F" to "ꓞ", + "G" to "ꓨ", + "H" to "H", + "J" to "ſ", + "K" to "ꓘ", + "L" to "ꓶ", + "Z" to "Z", + "X" to "X", + "C" to "ꓛ", + "V" to "ꓥ", + "B" to "ꓭ", + "N" to "N", + "M" to "W", + "1" to "Ɩ", + "2" to "Շ", + "3" to "Ɛ", + "4" to "h", + "5" to "૬", + "6" to "9", + "7" to "L", + "8" to "8", + "9" to "6", + "0" to "0" + ) + val leetLetters = mapOf( + "e" to "3", + "r" to "Я", + "t" to "7", + "o" to "0", + "a" to "4", + "s" to "5", + "h" to "#", + "l" to "1", + "z" to "2", + "b" to "8", + "E" to "3", + "R" to "Я", + "T" to "7", + "O" to "0", + "A" to "4", + "S" to "5", + "H" to "#", + "L" to "1", + "Z" to "2", + "B" to "8" + ) +} \ No newline at end of file diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt new file mode 100644 index 0000000..9af0026 --- /dev/null +++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt @@ -0,0 +1,103 @@ +package tech.tyman.plugins.texter + +import android.content.Context +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.CommandsAPI +import com.aliucord.entities.CommandContext +import com.aliucord.entities.Plugin +import com.discord.api.commands.ApplicationCommandType + +@AliucordPlugin +class Texter : Plugin() { + override fun start(context: Context) { + registerConverterCommand( + "small", + "Turns your text into small letters", emptyList() + ) { ctx: CommandContext -> getResult(Maps.smallLetters, ctx.getString("text")!!) } + registerConverterCommand( + "smaller", + "Turns your text into smaller letters", emptyList() + ) { ctx: CommandContext -> getResult(Maps.smallerLetters, ctx.getString("text")!!) } + registerConverterCommand( + "fullwidth", + "Turns your text into full width letters", listOf( + "fw" + ) + ) { ctx: CommandContext -> getResult(Maps.fullWidthLetters, ctx.getString("text")!!) } + registerConverterCommand( + "emoji", + "Turns your text into emoji letters", + listOf( + "emojify", + "blockify" + ) + ) { ctx: CommandContext -> getResult(Maps.emojiLetters, ctx.getString("text")!!) } + registerConverterCommand( + "flip", + "Flips your text upside down" + ) { ctx: CommandContext -> getResult(Maps.flippedLetters, ctx.getString("text")!!.reverse()) } + registerConverterCommand( + "clap", + "Adds clapping icons to your text" + ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.clapify()) } + registerConverterCommand( + "reverse", + "Reverses your text" + ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.reverse()) } + registerConverterCommand( + "space", + "Spaces out your text" + ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.chunked(1).joinToString(" ")) } + registerConverterCommand( + "mock", + "Capitalizes random parts of your text" + ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.mock()) } + registerConverterCommand( + "leet", + "Makes your text 1337 style", listOf( + "leetify" + ) + ) { ctx: CommandContext -> getResult(Maps.leetLetters, ctx.getString("text")!!) } + } + + private fun registerConverterCommand(name: String, description: String, execute: (CommandContext) -> CommandsAPI.CommandResult) { + commands.registerCommand( + name, + description, listOf( + Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change") + ), + execute + ) + } + private fun registerConverterCommand(name: String, description: String, aliases: List, execute: (CommandContext) -> CommandsAPI.CommandResult) { + commands.registerCommand( + name, + description, listOf( + Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change") + ), + execute + ) + for (alias in aliases) { + commands.registerCommand( + alias, + description, listOf( + Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change") + ), + execute + ) + } + } + + override fun stop(context: Context) { + commands.unregisterAll() + } + + private fun getResult(map: Map, textToMap: String): CommandsAPI.CommandResult { + return CommandsAPI.CommandResult(Maps.getMappedString(map, textToMap), null, true) + } + + private fun getResult(textToSend: String): CommandsAPI.CommandResult { + return CommandsAPI.CommandResult(textToSend, null, true) + } +} \ No newline at end of file diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt new file mode 100644 index 0000000..ccf4f51 --- /dev/null +++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt @@ -0,0 +1,16 @@ +package tech.tyman.plugins.texter + +import java.lang.StringBuilder +import java.util.* + +fun String.clapify() = ":clap:" + this.replace(" ", ":clap:") + ":clap:" +fun String.reverse() = StringBuilder(this).reverse().toString() + +fun String.mock(): String { + val builder = StringBuilder() + val random = Random() + for (character in this.chunked(1)) { + builder.append(if (random.nextBoolean()) character.lowercase() else character.uppercase()) + } + return builder.toString() +} \ No newline at end of file diff --git a/plugins/Translate/build.gradle.kts b/plugins/Translate/build.gradle.kts new file mode 100644 index 0000000..e6a3700 --- /dev/null +++ b/plugins/Translate/build.gradle.kts @@ -0,0 +1,39 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.3.1" +description = "Adds an option to translate messages." +aliucord { + changelog.set(""" + # Version 1.3.1 + * Fix translating multiple lines of text (finally) + # Version 1.3.0 + * Added "show original functionality", will show the original, untranslated message, but still keeps it cached so you can instantly re-translate it. + # Version 1.2.4 + * Fixed translate settings crash + # Version 1.2.3 + * Updated to support discord 101.3 + # Version 1.2.2 + * Updated for new discord/aliucord version + # Version 1.2.1 + * Improved/added language code validation + * Added a list of language codes to plugin settings + # Version 1.2.0 + * Edits the message with the translated text instead of creating a new local message + # Version 1.1.2 + * Fixed message links not having /channels/ + * Moved classes to com.aliucord.plugins.translate + # Version 1.1.1 + * Added settings to modify the default translation language + # Version 1.1.0 + * Add /translate to translate text from one language to another, and send it in chat by default. + # Version 1.0.0 + * Initial release + """.trimIndent()) + + author(Authors.Ty) +} \ No newline at end of file diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt new file mode 100644 index 0000000..c42ef4e --- /dev/null +++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt @@ -0,0 +1,55 @@ +package tech.tyman.plugins.translate + +import android.text.Editable +import android.text.util.Linkify +import android.view.View +import android.widget.TextView +import com.aliucord.views.TextInput +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.fragments.SettingsPage +import com.aliucord.views.Button +import com.discord.utilities.color.ColorCompat +import com.discord.utilities.view.text.TextWatcher +import com.lytefast.flexinput.R + + +class PluginSettings(private val settings: SettingsAPI) : SettingsPage() { + override fun onViewBound(view: View?) { + super.onViewBound(view) + setActionBarTitle("Translate") + val ctx = requireContext() + val input = TextInput(ctx, "Language to translate messages to by default") + val editText = input.editText + val button = Button(ctx) + button.text = "Save" + button.setOnClickListener { + settings.setString("defaultLanguage", editText.text.toString()) + Utils.showToast("Saved translate settings!") + close() + } + editText.maxLines = 1 + editText.setText(settings.getString("defaultLanguage", "en")) + editText.addTextChangedListener(object : TextWatcher() { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + if (!languageCodes.containsValue(s.toString())) { + button.alpha = 0.5f + button.isClickable = false + } else { + button.alpha = 1f + button.isClickable = true + } + } + }) + val helpText = TextView(ctx) + helpText.linksClickable = true + helpText.text = "Supported language codes:\n\n${languageCodes.map { "${it.key} -> ${it.value}" }.joinToString("\n")}" + Linkify.addLinks(helpText, Linkify.WEB_URLS) + helpText.setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorOnPrimary)) + addView(input) + addView(button) + addView(helpText) + } +} diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt new file mode 100644 index 0000000..c1ea364 --- /dev/null +++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt @@ -0,0 +1,234 @@ +package tech.tyman.plugins.translate + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.text.Spanned +import android.text.style.RelativeSizeSpan +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.widget.NestedScrollView +import com.aliucord.CollectionUtils +import com.aliucord.Http +import com.aliucord.Utils +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.api.CommandsAPI +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.discord.api.commands.ApplicationCommandType +import com.discord.databinding.WidgetChatListActionsBinding +import com.discord.utilities.textprocessing.node.EditedMessageNode +import com.discord.utilities.view.text.SimpleDraweeSpanTextView +import com.discord.widgets.chat.list.WidgetChatList +import com.discord.widgets.chat.list.actions.WidgetChatListActions +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage +import com.discord.widgets.chat.list.entries.MessageEntry +import com.facebook.drawee.span.DraweeSpanStringBuilder +import com.lytefast.flexinput.R +import org.json.JSONArray +import java.lang.reflect.Field +import java.util.regex.Pattern + +@AliucordPlugin +class Translate : Plugin() { + lateinit var pluginIcon: Drawable + private val translatedMessages = mutableMapOf() + private var chatList: WidgetChatList? = null + private val messageLoggerEditedRegex = Pattern.compile("(?:.+ \\(.+: .+\\)\\n)+(.+)\$") + + init { + settingsTab = SettingsTab(PluginSettings::class.java).withArgs(settings) + } + + override fun load(ctx: Context) { + pluginIcon = ContextCompat.getDrawable(ctx, R.e.ic_locale_24dp)!! + } + + override fun start(context: Context) { + patchMessageContextMenu() + patchProcessMessageText() + commands.registerCommand( + "translate", + "Translates text from one language to another, sends by default", + listOf( + Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to translate"), + Utils.createCommandOption(ApplicationCommandType.STRING, "to", "The language to translate to (default en, must be a language code described in plugin settings)", choices = languageCodeChoices), + Utils.createCommandOption(ApplicationCommandType.STRING, "from", "The language to translate from (default auto, must be a language code described in plugin settings)", choices = languageCodeChoices), + Utils.createCommandOption(ApplicationCommandType.BOOLEAN, "send", "Whether or not to send the message in chat (default true)") + ) + ) { ctx -> + val translateData = translateMessage( + ctx.getRequiredString("text"), + ctx.getString("from"), + ctx.getString("to") + ) + if (translateData !is TranslateSuccessData) { + with(translateData as TranslateErrorData) { + return@registerCommand CommandsAPI.CommandResult( + "$errorText ($errorCode)", + null, + false + ) + } + } + return@registerCommand CommandsAPI.CommandResult( + translateData.translatedText, + null, + ctx.getBoolOrDefault("send", true) + ) + } + } + + private fun DraweeSpanStringBuilder.setTranslated(translateData: TranslateSuccessData, context: Context) { + val contentStartIndex = messageLoggerEditedRegex.matcher(this.toString()).let { + if (it.find()) { + it.start(1) + } else 0 + } + this.replace(contentStartIndex, contentStartIndex + translateData.sourceText.length, translateData.translatedText) + val textEnd = this.length + this.append(" (translated: ${translateData.sourceLanguage} -> ${translateData.translatedLanguage})") + this.setSpan(RelativeSizeSpan(0.75f), textEnd, this.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + if (textEnd != this.length) { + this.setSpan(EditedMessageNode.Companion.`access$getForegroundColorSpan`(EditedMessageNode.Companion, context), + textEnd, this.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + + private fun patchProcessMessageText() { + patcher.patch(WidgetChatList::class.java.getDeclaredConstructor(), Hook { + chatList = it.thisObject as WidgetChatList + }) + + val mDraweeStringBuilder: Field = SimpleDraweeSpanTextView::class.java.getDeclaredField("mDraweeStringBuilder") + mDraweeStringBuilder.isAccessible = true + patcher.patch(WidgetChatListAdapterItemMessage::class.java, "processMessageText", arrayOf(SimpleDraweeSpanTextView::class.java, MessageEntry::class.java), Hook { + val messageEntry = it.args[1] as MessageEntry + val message = messageEntry.message ?: return@Hook + val id = message.id + val translateData = translatedMessages[id] ?: return@Hook + if (translateData.showingOriginal) return@Hook + if (translateData.sourceText != message.content) { + translatedMessages.remove(id) + return@Hook + } + val textView = it.args[0] as SimpleDraweeSpanTextView + val builder = mDraweeStringBuilder[textView] as DraweeSpanStringBuilder? + ?: return@Hook + val context = textView.context + builder.setTranslated(translateData, context) + textView.setDraweeSpanStringBuilder(builder) + }) + } + + private fun patchMessageContextMenu() { + val viewId = View.generateViewId() + val messageContextMenu = WidgetChatListActions::class.java + val getBinding = messageContextMenu.getDeclaredMethod("getBinding").apply { isAccessible = true } + + patcher.patch(messageContextMenu.getDeclaredMethod("configureUI", WidgetChatListActions.Model::class.java), Hook { + val menu = it.thisObject as WidgetChatListActions + val binding = getBinding.invoke(menu) as WidgetChatListActionsBinding + val translateButton = binding.a.findViewById(viewId) + translateButton.setOnClickListener { _ -> + val message = (it.args[0] as WidgetChatListActions.Model).message + val translationEntry = translatedMessages[message.id] + + if (translationEntry == null) { + // If not translated yet, fetch and cache the translation, then rerender the message + Utils.threadPool.execute { + val response = translateMessage(message.content) + if (response !is TranslateSuccessData) { + with (response as TranslateErrorData) { + Utils.showToast("$errorText ($errorCode)", true) + return@execute + } + } + translatedMessages[message.id] = response + chatList?.rerenderMessage(message.id) + Utils.showToast("Translated message") + menu.dismiss() + } + } else { + // If translated, then no need to translate anything, so just flip the showingOriginal property and rerender + translationEntry.showingOriginal = !translationEntry.showingOriginal + chatList?.rerenderMessage(message.id) + menu.dismiss() + } + } + }) + + patcher.patch(messageContextMenu, "onViewCreated", arrayOf(View::class.java, Bundle::class.java), Hook { + val linearLayout = (it.args[0] as NestedScrollView).getChildAt(0) as LinearLayout + val context = linearLayout.context + val messageId = WidgetChatListActions.`access$getMessageId$p`(it.thisObject as WidgetChatListActions) + linearLayout.addView(TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + val translationEntry = translatedMessages[messageId] + + id = viewId + text = if (translationEntry == null || translationEntry.showingOriginal) { + // If not translated yet, or original is currently shown + "Translate message" + } else { + // Otherwise, it must be translated and original is not currently being shown + "Show original" + } + setCompoundDrawablesRelativeWithIntrinsicBounds(pluginIcon, null, null, null) + }) + }) + } + + override fun stop(context: Context) = patcher.unpatchAll() + + private fun translateMessage(text: String, from: String? = null, to: String? = null): TranslateData { + val toLang = to ?: settings.getString("defaultLanguage", "en") + val fromLang = from ?: "auto" + val queryBuilder = Http.QueryBuilder("https://translate.googleapis.com/translate_a/single").run { + append("client", "gtx") + append("sl", fromLang) + append("tl", toLang) + append("dt", "t") + append("q", text) + } + val translatedJsonReq = Http.Request(queryBuilder.toString(), "GET").apply { + setHeader("Content-Type", "application/json") + setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4592.0 Safari/537.36") + }.execute() + + if (!translatedJsonReq.ok()) { + return when (translatedJsonReq.statusCode) { + 429 -> TranslateErrorData( + errorCode = 429, + errorText = "Translate API ratelimit reached. Please try again later." + ) + else -> TranslateErrorData( + errorCode = translatedJsonReq.statusCode, + errorText = "An unknown error occurred. Please report this to the developer of Translate." + ) + } + } + val parsedJson = JSONArray(translatedJsonReq.text()) + + val translatedSections = parsedJson.getJSONArray(0) + + val translatedText = buildString { + for (i in 0 until translatedSections.length()) { + append(translatedSections.getJSONArray(i).getString(0)) + } + } + + return TranslateSuccessData( + sourceLanguage = parsedJson.getString(2), + translatedLanguage = toLang, + sourceText = text, + translatedText = translatedText + ) + } + +// private fun Message.guildId(): Long? { +// val channel = ChannelWrapper(StoreStream.getChannels().getChannel(this.channelId)) +// return if (channel.isGuild()) channel.guildId else null +// } +} diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt new file mode 100644 index 0000000..046ff6f --- /dev/null +++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt @@ -0,0 +1,171 @@ +package tech.tyman.plugins.translate + +import com.aliucord.CollectionUtils +import com.discord.api.commands.CommandChoice +import com.discord.widgets.chat.list.WidgetChatList +import com.discord.widgets.chat.list.entries.MessageEntry + +fun WidgetChatList.rerenderMessage(messageId: Long) { + val adapter = WidgetChatList.`access$getAdapter$p`(this) + val data = adapter.internalData + val i = CollectionUtils.findIndex(data) { m -> + m is MessageEntry && m.message.id == messageId + } + if (i != -1) adapter.notifyItemChanged(i) +} + +open class TranslateData + +data class TranslateSuccessData( + val sourceLanguage: String, + val translatedLanguage: String, + val sourceText: String, + val translatedText: String, + var showingOriginal: Boolean = false +) : TranslateData() + +data class TranslateErrorData( + val errorCode: Number, + val errorText: String +) : TranslateData() + +val languageCodes = mapOf( + "Afrikaans" to "af", + "Albanian" to "sq", + "Amharic" to "am", + "Arabic" to "ar", + "Armenian" to "hy", + "Assamese" to "as", + "Azerbaijani" to "az", + "Bangla" to "bn", + "Basque" to "eu", + "Belarusian" to "be", + "Bosnian" to "bs", + "Bulgarian" to "bg", + "Burmese" to "my", + "Cantonese" to "zh-yue", + "Catalan" to "ca", + "Cebuano" to "ceb", + "Central Kurdish" to "ckb", + "Chakma" to "ccp", + "Cherokee" to "chr", + "Chinese (Hong Kong)" to "zh-HK", + "Chinese (Simplified, China)" to "zh-Hans", + "Chinese (Simplified)" to "zh-CN", + "Chinese (Traditional, Taiwan)" to "zh-Hant", + "Chinese (Traditional)" to "zh-TW", + "Corsican" to "co", + "Croatian" to "hr", + "Czech" to "cs", + "Danish" to "da", + "Dutch" to "nl", + "Dzongkha" to "dz", + "English" to "en", + "Esperanto" to "eo", + "Estonian" to "et", + "Filipino" to "fil", + "Filipino" to "tl", + "Finnish" to "fi", + "French" to "fr", + "Fulah" to "ff", + "Galician" to "gl", + "Georgian" to "ka", + "German" to "de", + "Greek" to "el", + "Gujarati" to "gu", + "Haitian Creole" to "ht", + "Hawaiian" to "haw", + "Hebrew" to "he", + "Hindi" to "hi", + "Hmong" to "hmn", + "Hungarian" to "hu", + "Icelandic" to "is", + "Indonesian" to "id", + "Inuktitut" to "iu", + "Irish" to "ga", + "Italian" to "it", + "Japanese" to "ja", + "Javanese" to "jv", + "Javanese" to "jw", + "Kannada" to "kn", + "Kazakh" to "kk", + "Khmer" to "km", + "Korean" to "ko", + "Kurdish" to "ku", + "Kyrgyz" to "ky", + "Lao" to "lo", + "Latin" to "la", + "Latvian" to "lv", + "Lisu" to "lis", + "Lithuanian" to "lt", + "Luxembourgish" to "lb", + "Macedonian" to "mk", + "Makah" to "myh", + "Malagasy" to "mg", + "Malay" to "ms", + "Malayalam" to "ml", + "Maltese" to "mt", + "Manipuri (Meitei Mayek)" to "mni-Mtei", + "Maori" to "mi", + "Marathi" to "mr", + "Menominee" to "mez", + "Mongolian" to "mn", + "Navajo" to "nv", + "Nepali" to "ne", + "Norwegian Nynorsk" to "nn", + "Norwegian" to "nb", + "Norwegian" to "no", + "Nyanja" to "ny", + "Odia" to "or", + "Ojibwa" to "oj", + "Oneida" to "one", + "Osage" to "osa", + "Pashto" to "ps", + "Persian" to "fa", + "Plains Cree" to "crk", + "Polish" to "pl", + "Portuguese (Brazil)" to "pt-BR", + "Portuguese (Portugal)" to "pt-PT", + "Punjabi" to "pa", + "Rohingya" to "rhg", + "Romanian" to "ro", + "Romany" to "rom", + "Russian" to "ru", + "Samoan" to "sm", + "Sanskrit" to "sa", + "Scottish Gaelic" to "gd", + "Seneca" to "see", + "Serbian" to "sr", + "Shona" to "sn", + "Sindhi" to "sd", + "Sinhala" to "si", + "Slovak" to "sk", + "Slovenian" to "sl", + "Somali" to "so", + "Southern Uzbek" to "uzs", + "Spanish" to "es", + "Sundanese" to "su", + "Swahili" to "sw", + "Swedish" to "sv", + "Tajik" to "tg", + "Tamil" to "ta", + "Tatar" to "tt", + "Telugu" to "te", + "Thai" to "th", + "Tibetan" to "bo", + "Tigrinya" to "ti", + "Turkish" to "tr", + "Ukrainian" to "uk", + "Urdu" to "ur", + "Uyghur" to "ug", + "Uzbek" to "uz", + "Vietnamese" to "vi", + "Welsh" to "cy", + "Western Frisian" to "fy", + "Xhosa" to "xh", + "Yiddish" to "yi", + "Yoruba" to "yo", + "Zulu" to "zu" +) + +val languageCodeChoices = languageCodes.map {CommandChoice(it.key, it.value)} \ No newline at end of file