feat: 插件系统(BETA) & 查询发言插件
This commit is contained in:
parent
0e48bfe809
commit
c306846e7e
|
|
@ -252,3 +252,5 @@
|
|||
-keep class androidx.recyclerview.widget.RecyclerView$LayoutParams { *; }
|
||||
-keep class androidx.recyclerview.widget.RecyclerView$ViewHolder { *; }
|
||||
-keep class androidx.recyclerview.widget.RecyclerView$LayoutManager { *; }
|
||||
|
||||
-keep class com.huanchengfly.tieba.post.plugins.** { *; }
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"id": "CommentLookup",
|
||||
"name": "发言查询",
|
||||
"desc": "使用第三方工具箱查询用户过往发言",
|
||||
"version": "1.0",
|
||||
"mainClass": "com.huanchengfly.tieba.post.plugins.PluginCommentLookup"
|
||||
}
|
||||
]
|
||||
|
|
@ -19,6 +19,8 @@ import android.widget.TextView
|
|||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.flurry.android.FlurryAgent
|
||||
import com.huanchengfly.tieba.post.api.interfaces.CommonCallback
|
||||
import com.huanchengfly.tieba.post.plugins.PluginManager
|
||||
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
|
||||
import com.huanchengfly.tieba.post.ui.theme.interfaces.ThemeSwitcher
|
||||
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
|
||||
import com.huanchengfly.tieba.post.utils.*
|
||||
|
|
@ -32,13 +34,14 @@ import org.litepal.LitePal
|
|||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class BaseApplication : Application() {
|
||||
class BaseApplication : Application(), IApp {
|
||||
private val mActivityList: MutableList<Activity> = mutableListOf()
|
||||
|
||||
override fun onCreate() {
|
||||
instance = this
|
||||
super.onCreate()
|
||||
ThemeUtils.init(ThemeDelegate)
|
||||
PluginManager.init(this)
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
LitePal.initialize(this)
|
||||
FlurryAgent.Builder()
|
||||
|
|
@ -441,13 +444,33 @@ class BaseApplication : Application() {
|
|||
R.color.default_color_shadow -> return getColorByAttr(context, R.attr.shadow_color)
|
||||
R.color.default_color_unselected -> return getColorByAttr(context, R.attr.colorUnselected)
|
||||
R.color.default_color_text -> return getColorByAttr(context, R.attr.colorText)
|
||||
R.color.default_color_text_on_primary -> return getColorByAttr(context, R.attr.colorTextOnPrimary)
|
||||
R.color.default_color_text_secondary -> return getColorByAttr(context, R.attr.colorTextSecondary)
|
||||
R.color.default_color_text_disabled -> return getColorByAttr(context, R.attr.color_text_disabled)
|
||||
R.color.default_color_text_on_primary -> return getColorByAttr(
|
||||
context,
|
||||
R.attr.colorTextOnPrimary
|
||||
)
|
||||
R.color.default_color_text_secondary -> return getColorByAttr(
|
||||
context,
|
||||
R.attr.colorTextSecondary
|
||||
)
|
||||
R.color.default_color_text_disabled -> return getColorByAttr(
|
||||
context,
|
||||
R.attr.color_text_disabled
|
||||
)
|
||||
R.color.default_color_divider -> return getColorByAttr(context, R.attr.colorDivider)
|
||||
R.color.default_color_swipe_refresh_view_background -> return getColorByAttr(context, R.attr.color_swipe_refresh_layout_background)
|
||||
R.color.default_color_swipe_refresh_view_background -> return getColorByAttr(
|
||||
context,
|
||||
R.attr.color_swipe_refresh_layout_background
|
||||
)
|
||||
}
|
||||
return context.getColorCompat(colorId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAppContext(): Context {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun launchUrl(url: String) {
|
||||
launchUrl(this, url)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ 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;
|
||||
|
|
@ -34,6 +35,6 @@ public class MyURLSpan extends ClickableSpan {
|
|||
|
||||
@Override
|
||||
public void onClick(@NonNull View view) {
|
||||
navigationHelper.navigationByData(NavigationHelper.ACTION_URL, this.url);
|
||||
UtilsKt.launchUrl(context, url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.huanchengfly.tieba.post.plugins
|
||||
|
||||
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
|
||||
|
||||
abstract class IPlugin(
|
||||
val app: IApp,
|
||||
val manifest: PluginManifest
|
||||
) {
|
||||
val context: Context
|
||||
get() = app.getAppContext()
|
||||
|
||||
open fun onCreate() {}
|
||||
|
||||
open fun onEnable() {}
|
||||
|
||||
open fun onDisable() {}
|
||||
|
||||
open fun onDestroy() {}
|
||||
}
|
||||
|
||||
inline fun <reified Data> IPlugin.registerMenuItem(
|
||||
id: String,
|
||||
title: String,
|
||||
callback: ClickCallback<Data>? = null
|
||||
) {
|
||||
val menu = getMenuByData(Data::class)
|
||||
PluginManager.registerMenuItem(this, PluginMenuItem(id, menu, title, callback))
|
||||
}
|
||||
|
||||
inline fun <reified Data> IPlugin.registerMenuItem(
|
||||
id: String,
|
||||
title: String,
|
||||
crossinline callback: (Data) -> Unit
|
||||
) {
|
||||
registerMenuItem(id, title, object : ClickCallback<Data> {
|
||||
override fun onClick(data: Data) {
|
||||
callback.invoke(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.huanchengfly.tieba.post.plugins
|
||||
|
||||
import com.huanchengfly.tieba.post.R
|
||||
import com.huanchengfly.tieba.post.api.models.ProfileBean
|
||||
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
|
||||
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
|
||||
|
||||
class PluginCommentLookup(app: IApp, manifest: PluginManifest) : IPlugin(app, manifest) {
|
||||
override fun onEnable() {
|
||||
super.onEnable()
|
||||
registerMenuItem<ProfileBean>(
|
||||
"lookup_comment",
|
||||
context.getString(R.string.plugin_comment_lookup_menu)
|
||||
) {
|
||||
app.launchUrl("https://www.82cat.com/tieba/reply/${it.user?.name}/1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package com.huanchengfly.tieba.post.plugins
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.huanchengfly.tieba.post.api.models.ProfileBean
|
||||
import com.huanchengfly.tieba.post.fromJson
|
||||
import com.huanchengfly.tieba.post.plugins.interfaces.IApp
|
||||
import com.huanchengfly.tieba.post.plugins.models.PluginManifest
|
||||
import com.huanchengfly.tieba.post.utils.AssetUtil
|
||||
import com.huanchengfly.tieba.post.utils.SharedPreferencesUtil
|
||||
import com.huanchengfly.tieba.post.utils.SharedPreferencesUtil.SP_PLUGINS
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object PluginManager {
|
||||
const val MENU_USER_ACTIVITY = "user_activity"
|
||||
const val MENU_NONE = "none"
|
||||
|
||||
lateinit var appInstance: IApp
|
||||
val pluginManifests: MutableList<PluginManifest> = mutableListOf()
|
||||
val pluginInstances: MutableList<IPlugin> = mutableListOf()
|
||||
|
||||
val registeredPluginMenuItems: MutableMap<String, MutableMap<String, PluginMenuItem<*>>> =
|
||||
mutableMapOf()
|
||||
|
||||
val context: Context
|
||||
get() = appInstance.getAppContext()
|
||||
val preferences: SharedPreferences
|
||||
get() = SharedPreferencesUtil.get(SP_PLUGINS)
|
||||
|
||||
init {
|
||||
listOf(
|
||||
MENU_USER_ACTIVITY,
|
||||
MENU_NONE
|
||||
).forEach {
|
||||
registeredPluginMenuItems[it] = mutableMapOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun <Data> registerMenuItem(pluginInstance: IPlugin, menuItem: PluginMenuItem<Data>) {
|
||||
registeredPluginMenuItems[menuItem.menuId]!!["${pluginInstance.manifest.id}_${menuItem.id}"] =
|
||||
menuItem
|
||||
}
|
||||
|
||||
fun init(app: IApp) {
|
||||
appInstance = app
|
||||
reloadPlugins()
|
||||
}
|
||||
|
||||
fun enablePlugin(id: String) {
|
||||
pluginInstances.filter { it.manifest.id == id }.forEach { enablePlugin(it) }
|
||||
}
|
||||
|
||||
fun disablePlugin(id: String) {
|
||||
pluginInstances.filter { it.manifest.id == id }.forEach { disablePlugin(it) }
|
||||
}
|
||||
|
||||
private fun createPlugin(pluginManifest: PluginManifest): IPlugin? {
|
||||
try {
|
||||
val mainClazz = Class.forName(pluginManifest.mainClass)
|
||||
val constructor =
|
||||
mainClazz.getDeclaredConstructor(IApp::class.java, PluginManifest::class.java)
|
||||
constructor.isAccessible = true
|
||||
return constructor.newInstance(appInstance, pluginManifest) as IPlugin
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun enablePlugin(pluginInstance: IPlugin) {
|
||||
try {
|
||||
pluginInstance.onEnable()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun disablePlugin(pluginInstance: IPlugin) {
|
||||
try {
|
||||
pluginInstance.onDisable()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyPlugin(pluginInstance: IPlugin) {
|
||||
try {
|
||||
pluginInstance.onDestroy()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPluginManifests() {
|
||||
pluginManifests.clear()
|
||||
pluginManifests.addAll(
|
||||
AssetUtil.getStringFromAsset(context, "plugins.json").fromJson<List<PluginManifest>>()
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadPlugins() {
|
||||
pluginInstances.forEach {
|
||||
disablePlugin(it)
|
||||
destroyPlugin(it)
|
||||
}
|
||||
pluginInstances.clear()
|
||||
reloadPluginManifests()
|
||||
pluginManifests.forEach {
|
||||
try {
|
||||
if (it.pluginCreated && preferences.getBoolean("${it.id}_enabled", true)) {
|
||||
val pluginInstance = createPlugin(it)
|
||||
if (pluginInstance != null) {
|
||||
pluginInstances.add(pluginInstance)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
pluginInstances.forEach {
|
||||
enablePlugin(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val PluginManifest.pluginCreated: Boolean
|
||||
get() {
|
||||
return pluginInstances.firstOrNull { it.manifest.id == id } != null
|
||||
}
|
||||
}
|
||||
|
||||
class PluginMenuItem<Data>(
|
||||
val id: String,
|
||||
val menuId: String,
|
||||
val title: String,
|
||||
val callback: ClickCallback<Data>? = null
|
||||
) {
|
||||
interface ClickCallback<Data> {
|
||||
fun onClick(data: Data)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PluginMenuItem<*>) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (menuId != other.menuId) return false
|
||||
if (title != other.title) return false
|
||||
if (callback != other.callback) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + menuId.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
else -> "none"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.huanchengfly.tieba.post.plugins.interfaces
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface IApp {
|
||||
fun getAppContext(): Context
|
||||
|
||||
fun launchUrl(url: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.huanchengfly.tieba.post.plugins.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PluginManifest(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val desc: String,
|
||||
val version: String,
|
||||
@SerializedName("main_class")
|
||||
val mainClass: String
|
||||
)
|
||||
|
|
@ -17,6 +17,7 @@ public class SharedPreferencesUtil {
|
|||
public static final String SP_PERMISSION = "permission";
|
||||
public static final String SP_IGNORE_VERSIONS = "ignore_version";
|
||||
public static final String SP_WEBVIEW_INFO = "webview_info";
|
||||
public static final String SP_PLUGINS = "plugins";
|
||||
|
||||
public static SharedPreferences get(@Preferences String name) {
|
||||
return get(BaseApplication.getInstance(), name);
|
||||
|
|
@ -50,7 +51,7 @@ public class SharedPreferencesUtil {
|
|||
return put(get(context, preference), key, value);
|
||||
}
|
||||
|
||||
@StringDef({SP_APP_DATA, SP_IGNORE_VERSIONS, SP_PERMISSION, SP_SETTINGS, SP_WEBVIEW_INFO, SP_DRAFT})
|
||||
@StringDef({SP_APP_DATA, SP_IGNORE_VERSIONS, SP_PERMISSION, SP_SETTINGS, SP_WEBVIEW_INFO, SP_DRAFT, SP_PLUGINS})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Preferences {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
package com.huanchengfly.tieba.post.utils
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import com.huanchengfly.tieba.post.BaseApplication
|
||||
import com.huanchengfly.tieba.post.R
|
||||
import com.huanchengfly.tieba.post.activities.WebViewActivity
|
||||
import com.huanchengfly.tieba.post.dpToPxFloat
|
||||
import com.huanchengfly.tieba.post.ui.theme.utils.ColorStateListUtils
|
||||
import com.huanchengfly.tieba.post.ui.theme.utils.ThemeUtils
|
||||
|
||||
@JvmOverloads
|
||||
fun getItemBackgroundDrawable(
|
||||
|
|
@ -101,13 +108,54 @@ fun getIntermixedColorBackground(
|
|||
context,
|
||||
position,
|
||||
itemCount,
|
||||
positionOffset,
|
||||
radius,
|
||||
if (context.appPreferences.listItemsBackgroundIntermixed) {
|
||||
colors
|
||||
} else {
|
||||
intArrayOf(colors[0])
|
||||
},
|
||||
ripple
|
||||
positionOffset,
|
||||
radius,
|
||||
if (context.appPreferences.listItemsBackgroundIntermixed) {
|
||||
colors
|
||||
} else {
|
||||
intArrayOf(colors[0])
|
||||
},
|
||||
ripple
|
||||
)
|
||||
}
|
||||
|
||||
fun launchUrl(context: Context, url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
val host = uri.host
|
||||
val path = uri.path
|
||||
val scheme = uri.scheme
|
||||
if (host == null || scheme == null || path == null) {
|
||||
return
|
||||
}
|
||||
if (!path.contains("android_asset")) {
|
||||
val isTiebaLink =
|
||||
host.contains("tieba.baidu.com") || host.contains("wappass.baidu.com") || host.contains(
|
||||
"ufosdk.baidu.com"
|
||||
) || host.contains("m.help.baidu.com")
|
||||
if (isTiebaLink || context.appPreferences.useWebView) {
|
||||
WebViewActivity.launch(context, url)
|
||||
} else {
|
||||
if (context.appPreferences.useCustomTabs) {
|
||||
val intentBuilder = CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ThemeUtils.getColorByAttr(
|
||||
context,
|
||||
R.attr.colorToolbar
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
try {
|
||||
intentBuilder.build().launchUrl(context, uri)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
}
|
||||
} else {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,4 +465,5 @@
|
|||
<string name="bubble_not_completed">功能未完成开发,敬请期待</string>
|
||||
<string name="title_theme_translucent">透明主题</string>
|
||||
<string name="title_theme_custom">自定义</string>
|
||||
<string name="plugin_comment_lookup_menu">查询发言</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue