feat: 枝网查重(插件)

This commit is contained in:
HuanChengFly 2021-08-19 16:59:13 +08:00
parent ec65fbdb08
commit 9a26537f12
18 changed files with 438 additions and 30 deletions

View File

@ -7,6 +7,14 @@
"author": "huanchengfly",
"version": "1.0",
"main_class": "com.huanchengfly.tieba.post.plugins.PluginCommentLookup"
},
{
"id": "AsoulCnki",
"name": "枝网查重",
"desc": "调用枝网查重 API 查询重复小作文",
"author": "huanchengfly",
"version": "1.0",
"main_class": "com.huanchengfly.tieba.post.plugins.asoulcnki.PluginAsoulCnki"
}
]
}

View File

@ -77,8 +77,8 @@ class BaseApplication : Application(), IApp {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
LitePal.initialize(this)
FlurryAgent.Builder()
.withCaptureUncaughtExceptions(true)
.build(this, "ZMRX6W76WNF95ZHT857X")
.withCaptureUncaughtExceptions(true)
.build(this, "ZMRX6W76WNF95ZHT857X")
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
private var clipBoardHash: Int = 0
private fun updateClipBoardHashCode() {
@ -517,7 +517,33 @@ class BaseApplication : Application(), IApp {
return this
}
override fun getCurrentContext(): Context {
return mActivityList.lastOrNull() ?: this
}
override fun launchUrl(url: String) {
launchUrl(mActivityList.lastOrNull() ?: this, url)
launchUrl(getCurrentContext(), url)
}
override fun showLoadingDialog(): Dialog {
return LoadingDialog(getCurrentContext()).apply { show() }
}
override fun toastShort(text: String) {
getCurrentContext().toastShort(text)
}
override fun showAlertDialog(builder: AlertDialog.Builder.() -> Unit): AlertDialog {
val dialog = AlertDialog.Builder(getCurrentContext())
.apply(builder)
.create()
if (getCurrentContext() !is BaseActivity || (getCurrentContext() as BaseActivity).isActivityRunning) {
dialog.show()
}
return dialog
}
override fun copyText(text: String) {
TiebaUtil.copyText(getCurrentContext(), text)
}
}

View File

@ -5,16 +5,19 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.ColorRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.huanchengfly.tieba.post.utils.GsonUtil
import com.huanchengfly.tieba.post.utils.MD5Util
fun Float.dpToPx(): Int =
dpToPxFloat().toInt()
@ -38,26 +41,21 @@ fun Int.pxToDp(): Int = this.toFloat().pxToDp()
fun Int.pxToSp(): Int = this.toFloat().pxToSp()
inline fun <reified Data> String.fromJson() = GsonUtil.getGson().fromJson<Data>(this, Data::class.java)
inline fun <reified Data> String.fromJson(): Data {
val type = object : TypeToken<Data>() {}.type
return GsonUtil.getGson().fromJson(this, type)
}
fun Any.toJson(): String = Gson().toJson(this)
fun String.toMD5(): String = MD5Util.toMd5(this)
fun Context.getColorCompat(@ColorRes id: Int): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
resources.getColor(id, theme)
} else {
resources.getColor(id)
}
return ContextCompat.getColor(this, id)
}
fun Context.getColorStateListCompat(id: Int): ColorStateList {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
resources.getColorStateList(id, theme)
} else {
resources.getColorStateList(id)
}
return AppCompatResources.getColorStateList(this, id)
}
inline fun <reified T : Activity> Context.goToActivity() {

View File

@ -508,7 +508,6 @@ class ThreadActivity : BaseActivity(), View.OnClickListener, IThreadMenuFragment
totalPage = Integer.valueOf(threadContentBean.page.totalPage!!)
refresh(pid)
}
})
}

View File

@ -32,6 +32,7 @@ import com.huanchengfly.tieba.post.fragments.ConfirmDialogFragment;
import com.huanchengfly.tieba.post.fragments.MenuDialogFragment;
import com.huanchengfly.tieba.post.models.PhotoViewBean;
import com.huanchengfly.tieba.post.models.ReplyInfoBean;
import com.huanchengfly.tieba.post.plugins.PluginManager;
import com.huanchengfly.tieba.post.utils.AccountUtil;
import com.huanchengfly.tieba.post.utils.BilibiliUtil;
import com.huanchengfly.tieba.post.utils.DateTimeUtils;
@ -178,9 +179,10 @@ public class RecyclerFloorAdapter extends CommonBaseAdapter<SubFloorListBean.Pos
}
return true;
}
return false;
return PluginManager.INSTANCE.performPluginMenuClick(PluginManager.MENU_SUB_POST_ITEM, item.getItemId(), postInfo);
})
.setInitMenuCallback(menu -> {
PluginManager.INSTANCE.initPluginMenu(menu, PluginManager.MENU_SUB_POST_ITEM);
if (TextUtils.equals(AccountUtil.getLoginInfo(mContext).getUid(), postInfo.getAuthor().getId())) {
menu.findItem(R.id.menu_delete).setVisible(true);
}

View File

@ -20,6 +20,7 @@ import com.huanchengfly.tieba.post.api.models.ThreadContentBean
import com.huanchengfly.tieba.post.components.MyViewHolder
import com.huanchengfly.tieba.post.fragments.MenuDialogFragment
import com.huanchengfly.tieba.post.models.ReplyInfoBean
import com.huanchengfly.tieba.post.plugins.PluginManager
import com.huanchengfly.tieba.post.utils.*
import com.huanchengfly.tieba.post.utils.NavigationHelper
import com.huanchengfly.tieba.post.utils.TiebaUtil.reportPost
@ -120,13 +121,22 @@ class ThreadMainPostAdapter(
stringBuilder.append(contentBean.text)
}
}
Util.showCopyDialog(context as BaseActivity?, stringBuilder.toString(), threadPostBean!!.id)
Util.showCopyDialog(
context as BaseActivity?,
stringBuilder.toString(),
threadPostBean!!.id
)
true
}
else -> false
else -> PluginManager.performPluginMenuClick(
PluginManager.MENU_POST_ITEM,
item.itemId,
threadPostBean!!
)
}
}
.setInitMenuCallback { menu: Menu ->
PluginManager.initPluginMenu(menu, PluginManager.MENU_POST_ITEM)
menu.findItem(R.id.menu_delete).isVisible = false
}
.show((context as BaseActivity).supportFragmentManager, threadPostBean!!.id + "_Menu")

View File

@ -34,6 +34,7 @@ import com.huanchengfly.tieba.post.fragments.ConfirmDialogFragment
import com.huanchengfly.tieba.post.fragments.FloorFragment.Companion.newInstance
import com.huanchengfly.tieba.post.fragments.MenuDialogFragment
import com.huanchengfly.tieba.post.models.ReplyInfoBean
import com.huanchengfly.tieba.post.plugins.PluginManager
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
import com.huanchengfly.tieba.post.utils.*
import com.huanchengfly.tieba.post.utils.BilibiliUtil.replaceVideoNumberSpan
@ -314,9 +315,14 @@ class ThreadReplyAdapter(context: Context) : BaseSingleTypeDelegateAdapter<PostL
return@setOnNavigationItemSelectedListener true
}
}
false
PluginManager.performPluginMenuClick(
PluginManager.MENU_SUB_POST_ITEM,
item.itemId,
subPostListItemBean
)
}
.setInitMenuCallback { menu: Menu ->
PluginManager.initPluginMenu(menu, PluginManager.MENU_SUB_POST_ITEM)
menu.findItem(R.id.menu_report).isVisible = false
if (TextUtils.equals(AccountUtil.getUid(context), subPostListItemBean.authorId)) {
menu.findItem(R.id.menu_delete).isVisible = true
@ -384,9 +390,14 @@ class ThreadReplyAdapter(context: Context) : BaseSingleTypeDelegateAdapter<PostL
return@setOnNavigationItemSelectedListener true
}
}
false
PluginManager.performPluginMenuClick(
PluginManager.MENU_POST_ITEM,
item.itemId,
postListItemBean
)
}
.setInitMenuCallback { menu: Menu ->
PluginManager.initPluginMenu(menu, PluginManager.MENU_POST_ITEM)
if (TextUtils.equals(dataBean!!.user!!.id, postListItemBean.authorId) || TextUtils.equals(dataBean!!.user!!.id, dataBean!!.thread!!.author!!.id)) {
menu.findItem(R.id.menu_delete).isVisible = true
}

View File

@ -3,27 +3,22 @@ package com.huanchengfly.tieba.post.components.spans;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import com.huanchengfly.tieba.post.R;
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils;
import com.huanchengfly.tieba.post.utils.NavigationHelper;
import com.huanchengfly.tieba.post.utils.UtilsKt;
public class MyURLSpan extends ClickableSpan {
public String url;
private Context context;
private NavigationHelper navigationHelper;
public MyURLSpan(Context context, String url) {
super();
Log.i("MyURLSpan", "MyURLSpan: " + url);
this.url = url;
this.context = context;
this.navigationHelper = NavigationHelper.newInstance(context);
}
@Override

View File

@ -4,19 +4,29 @@ import android.content.Context
import com.huanchengfly.tieba.post.plugins.PluginMenuItem.ClickCallback
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class IPlugin(
val app: IApp,
val manifest: PluginManifest
) {
) : CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
val context: Context
get() = app.getAppContext()
get() = app.getCurrentContext()
open fun onCreate() {}
open fun onEnable() {}
open fun onDisable() {}
open fun onDisable() {
job.cancel()
}
open fun onDestroy() {}
}

View File

@ -4,6 +4,8 @@ import android.content.Context
import android.content.SharedPreferences
import android.view.Menu
import com.huanchengfly.tieba.post.api.models.ProfileBean
import com.huanchengfly.tieba.post.api.models.SubFloorListBean
import com.huanchengfly.tieba.post.api.models.ThreadContentBean
import com.huanchengfly.tieba.post.fromJson
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.BuiltInPlugins
@ -15,6 +17,8 @@ import kotlin.reflect.KClass
object PluginManager {
const val MENU_USER_ACTIVITY = "user_activity"
const val MENU_POST_ITEM = "post_item"
const val MENU_SUB_POST_ITEM = "sub_post_item"
const val MENU_NONE = "none"
lateinit var appInstance: IApp
@ -37,6 +41,8 @@ object PluginManager {
registeredPluginMenuItems.clear()
listOf(
MENU_USER_ACTIVITY,
MENU_POST_ITEM,
MENU_SUB_POST_ITEM,
MENU_NONE
).forEach {
registeredPluginMenuItems[it] = mutableMapOf()
@ -56,7 +62,7 @@ object PluginManager {
fun initPluginMenu(menu: Menu, menuId: String) {
val menuItems = registeredPluginMenuItems[menuId]!!
menuItems.forEach {
menu.add(0, it.key, 0, it.value.title)
menu.add(0, it.key, 100, it.value.title)
}
}
@ -196,6 +202,8 @@ fun getMenuByData(dataClass: KClass<*>): String = getMenuByData(dataClass.java)
fun getMenuByData(dataClass: Class<*>): String {
return when (dataClass.canonicalName) {
ProfileBean::class.java.canonicalName -> PluginManager.MENU_USER_ACTIVITY
ThreadContentBean.PostListItemBean::class.java.canonicalName -> PluginManager.MENU_POST_ITEM
SubFloorListBean.PostInfo::class.java.canonicalName -> PluginManager.MENU_SUB_POST_ITEM
else -> "none"
}
}

View File

@ -0,0 +1,36 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
import com.huanchengfly.tieba.post.R;
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils;
import com.huanchengfly.tieba.post.utils.UtilsKt;
public class MyURLSpan extends ClickableSpan {
public String url;
private Context context;
public MyURLSpan(Context context, String url) {
super();
this.url = url;
this.context = context;
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(ThemeUtils.getColorByAttr(this.context, R.attr.colorAccent));
ds.setUnderlineText(false);
}
@Override
public void onClick(@NonNull View view) {
UtilsKt.launchUrl(context, url);
//context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
}

View File

@ -0,0 +1,175 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki
import android.graphics.Bitmap
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.ThreadContentBean
import com.huanchengfly.tieba.post.components.LinkTouchMovementMethod
import com.huanchengfly.tieba.post.components.spans.MyImageSpan
import com.huanchengfly.tieba.post.plugins.IPlugin
import com.huanchengfly.tieba.post.plugins.asoulcnki.api.CheckApi
import com.huanchengfly.tieba.post.plugins.asoulcnki.models.CheckApiBody
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
import com.huanchengfly.tieba.post.plugins.registerMenuItem
import com.huanchengfly.tieba.post.toJson
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
import com.huanchengfly.tieba.post.utils.DisplayUtil
import com.huanchengfly.tieba.post.utils.Util
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
class PluginAsoulCnki(app: IApp, manifest: PluginManifest) : IPlugin(app, manifest) {
override fun onEnable() {
super.onEnable()
registerMenuItem<ThreadContentBean.PostListItemBean>(
"asoul_cnki_check",
context.getString(R.string.plugin_asoul_cnki_check)
) { data ->
val dialog = app.showLoadingDialog()
val body = CheckApiBody(getPostTextContent(data)).toJson()
launch(Dispatchers.IO + job) {
val result =
CheckApi.instance.checkAsync(body.toRequestBody("application/json, charset=utf-8".toMediaTypeOrNull()))
.await()
launch(Dispatchers.Main + job) {
dialog.cancel()
if (result.code == 0) {
val numberFormatter = NumberFormat.getNumberInstance().apply {
maximumFractionDigits = 2
minimumFractionDigits = 2
}
app.showAlertDialog {
setTitle("查重结果")
val percent = "${numberFormatter.format(result.data.rate * 100.0)}%"
val resultForCopy = context.getString(
R.string.plugin_asoul_cnki_result,
formatDateTime("yyyy-MM-dd HH:mm:ss"),
percent,
if (result.data.related.isNotEmpty()) {
context.getString(
R.string.plugin_asoul_cnki_related,
result.data.related[0].replyUrl,
result.data.related[0].reply.mName,
formatDateTime(
"yyyy-MM-dd HH:mm",
result.data.related[0].reply.ctime * 1000L
)
)
} else {
""
}
)
val view = View.inflate(
context,
R.layout.plugin_asoul_cnki_dialog_check_result,
null
)
val percentView = view.findViewById<TextView>(R.id.check_result_percent)
val progress =
view.findViewById<ProgressBar>(R.id.check_result_progress)
val relatedView = view.findViewById<View>(R.id.check_result_related)
val relatedTitle =
view.findViewById<TextView>(R.id.check_result_related_title)
val relatedContent =
view.findViewById<TextView>(R.id.check_result_related_content)
percentView.text = context.getString(
R.string.plugin_asoul_cnki_check_result_percent,
percent
)
progress.progress = (result.data.rate * 10000).toInt()
if (result.data.related.isNullOrEmpty()) {
relatedView.visibility = View.GONE
} else {
relatedView.visibility = View.VISIBLE
relatedTitle.text = context.getString(
R.string.plugin_asoul_cnki_check_result_related,
result.data.related.size
)
}
val relatedContentText = SpannableStringBuilder()
result.data.related.forEach {
relatedContentText.appendLink("${it.reply.mName} 的评论", it.replyUrl)
.append("\n")
}
relatedContent.apply {
text = relatedContentText
movementMethod = LinkTouchMovementMethod.getInstance()
}
setView(view)
setPositiveButton(R.string.btn_copy_check_result) { _, _ ->
app.copyText(resultForCopy)
}
setNegativeButton(R.string.btn_close, null)
}
} else {
app.toastShort("查重失败 ${result.code}")
}
}
}
}
}
private fun SpannableStringBuilder.appendLink(
text: CharSequence,
url: String
): SpannableStringBuilder {
val spannableStringBuilder = SpannableStringBuilder()
val size = DisplayUtil.sp2px(context, 14f)
val bitmap = Util.tintBitmap(
Bitmap.createScaledBitmap(
Util.getBitmapFromVectorDrawable(
context,
R.drawable.ic_link
),
size,
size,
true
),
ThemeUtils.getColorByAttr(context, R.attr.colorAccent)
)
spannableStringBuilder.append(
"[链接]",
MyImageSpan(context, bitmap),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannableStringBuilder.append(" ")
spannableStringBuilder.append(
text,
MyURLSpan(context, url),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
return append(spannableStringBuilder)
}
fun getPostTextContent(item: ThreadContentBean.PostListItemBean): String {
val stringBuilder = StringBuilder()
for (contentBean in item.content!!) {
when (contentBean.type) {
"2" -> contentBean.setText("#(" + contentBean.c + ")")
"3", "20" -> contentBean.setText("[图片]\n")
"10" -> contentBean.setText("[语音]\n")
}
if (contentBean.text != null) {
stringBuilder.append(contentBean.text)
}
}
return stringBuilder.toString()
}
private fun formatDateTime(
pattern: String,
timestamp: Long = System.currentTimeMillis()
): String {
return SimpleDateFormat(pattern, Locale.getDefault()).format(Date(timestamp))
}
}

View File

@ -0,0 +1,25 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki.api
import com.huanchengfly.tieba.post.api.retrofit.NullOnEmptyConverterFactory
import com.huanchengfly.tieba.post.api.retrofit.adapter.DeferredCallAdapterFactory
import com.huanchengfly.tieba.post.api.retrofit.converter.gson.GsonConverterFactory
import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
import retrofit2.Retrofit
object CheckApi {
private val connectionPool = ConnectionPool()
val instance: ICheckApi by lazy {
Retrofit.Builder()
.baseUrl("https://asoulcnki.asia/")
.addCallAdapterFactory(DeferredCallAdapterFactory())
.addConverterFactory(NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(OkHttpClient.Builder().apply {
connectionPool(connectionPool)
}.build())
.build()
.create(ICheckApi::class.java)
}
}

View File

@ -0,0 +1,19 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki.api
import com.huanchengfly.tieba.post.api.ParamExpression
import com.huanchengfly.tieba.post.api.forEachNonNull
import okhttp3.Interceptor
import okhttp3.Response
class CommonHeaderInterceptor(private vararg val additionHeaders: ParamExpression) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val headers = request.headers
return chain.proceed(request.newBuilder().apply {
additionHeaders.forEachNonNull { name, value ->
if (headers[name] == null) addHeader(name, value)
}
}.build())
}
}

View File

@ -0,0 +1,16 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki.api
import com.huanchengfly.tieba.post.plugins.asoulcnki.models.CheckResult
import kotlinx.coroutines.Deferred
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface ICheckApi {
@POST("/v1/api/check")
@Headers("content-type: application/json;charset=UTF-8")
fun checkAsync(
@Body requestBody: RequestBody
): Deferred<CheckResult>
}

View File

@ -0,0 +1,5 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki.models
data class CheckApiBody(
val text: String
)

View File

@ -0,0 +1,53 @@
package com.huanchengfly.tieba.post.plugins.asoulcnki.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class CheckResult(
val code: Int, // 0
val `data`: Data,
val message: String // success
) {
@Keep
data class Data(
@SerializedName("end_time")
val endTime: Int, // 1629010807
val rate: Double, // 1.0
val related: List<Related>,
@SerializedName("start_time")
val startTime: Int // 1606137506
) {
@Keep
data class Related(
val rate: Double, // 1.0
val reply: Reply,
@SerializedName("reply_url")
val replyUrl: String // https://www.bilibili.com/video/av377092608/#reply5051494613
) {
@Keep
data class Reply(
val content: String, // 曾几何时我也想像asoul的beeeeeeeela一样做幸福滤镜下的事至少在这层滤镜下beeeeeeeela的一举一动都是随心所欲且浪漫真实的当我看到beeeeeeeela能像个二次元一样和弹幕大谈特谈50音当我看到beeeeeeeela能够笑着在夜里唱着不知道练了多少遍的云烟成雨当我看到她可以在失落后得到安抚和拥抱…以往的笑意消散殆尽剩下的只有我对beeeeeeeela浪漫的感动和一种无中生有的失意了。我也想像她一样。但这是虚假的每次在烂醉酩酊起来后依然会痛苦每次在浪费时间的时候都能意识到你不能感受到我感受到的东西。但就算是这样没了你我可能就会完蛋了吧。因为我们需要一个梦。
val ctime: Int, // 1627881576
@SerializedName("dynamic_id")
val dynamicId: String, // 553473662133564230
@SerializedName("like_num")
val likeNum: Int, // 9
@SerializedName("m_name")
val mName: String, // 走出童年
val mid: Int, // 671239951
val oid: String, // 377092608
@SerializedName("origin_rpid")
val originRpid: String, // -1
val rpid: String, // 5051494613
@SerializedName("similar_count")
val similarCount: Int, // 1
@SerializedName("similar_like_sum")
val similarLikeSum: Int, // 424
@SerializedName("type_id")
val typeId: Int, // 1
val uid: Int // 672346917
)
}
}
}

View File

@ -1,9 +1,21 @@
package com.huanchengfly.tieba.post.plugins.interfaces
import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
interface IApp {
fun getAppContext(): Context
fun getCurrentContext(): Context
fun launchUrl(url: String)
fun showLoadingDialog(): Dialog
fun toastShort(text: String)
fun copyText(text: String)
fun showAlertDialog(builder: AlertDialog.Builder.() -> Unit): AlertDialog
}