diff --git a/app/src/main/java/me/kbai/zhenxunui/api/ApiService.kt b/app/src/main/java/me/kbai/zhenxunui/api/ApiService.kt index b4b0b6f..addb03a 100644 --- a/app/src/main/java/me/kbai/zhenxunui/api/ApiService.kt +++ b/app/src/main/java/me/kbai/zhenxunui/api/ApiService.kt @@ -2,6 +2,7 @@ package me.kbai.zhenxunui.api import me.kbai.zhenxunui.model.BotBaseInfo import me.kbai.zhenxunui.model.BotMessageCount +import me.kbai.zhenxunui.model.ExecuteSql import me.kbai.zhenxunui.model.FriendListItem import me.kbai.zhenxunui.model.GroupInfo import me.kbai.zhenxunui.model.GroupListItem @@ -12,6 +13,9 @@ import me.kbai.zhenxunui.model.PluginInfo import me.kbai.zhenxunui.model.PluginSwitch import me.kbai.zhenxunui.model.RequestListResult import me.kbai.zhenxunui.model.SendMessage +import me.kbai.zhenxunui.model.SqlLog +import me.kbai.zhenxunui.model.TableColumn +import me.kbai.zhenxunui.model.TableListItem import me.kbai.zhenxunui.model.UpdateGroup import me.kbai.zhenxunui.model.UpdatePlugin import me.kbai.zhenxunui.model.UserDetail @@ -103,4 +107,16 @@ interface ApiService { @POST("manage/send_message") suspend fun sendMessage(@Body message: SendMessage): ApiResponse<*> + + @GET("database/get_table_list") + suspend fun getTableList(): ApiResponse> + + @GET("database/get_table_column") + suspend fun getTableColumn(@Query("table_name") table: String): ApiResponse> + + @GET("database/get_sql_log") + suspend fun getSqlLog(): ApiResponse> + + @POST("database/exec_sql") + suspend fun executeSql(@Body sql: ExecuteSql): RawApiResponse } \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/model/ExecuteSql.kt b/app/src/main/java/me/kbai/zhenxunui/model/ExecuteSql.kt new file mode 100644 index 0000000..f69736d --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/model/ExecuteSql.kt @@ -0,0 +1,5 @@ +package me.kbai.zhenxunui.model + +data class ExecuteSql( + val sql: String +) diff --git a/app/src/main/java/me/kbai/zhenxunui/model/SqlLog.kt b/app/src/main/java/me/kbai/zhenxunui/model/SqlLog.kt new file mode 100644 index 0000000..fe0b91e --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/model/SqlLog.kt @@ -0,0 +1,17 @@ +package me.kbai.zhenxunui.model + +import com.google.gson.annotations.SerializedName + +data class SqlLog( + val id: Int, + /** + * The time for executed sql. sample: `2024-03-06T06:54:48.829937+00:00` + */ + @SerializedName("create_time") + val createTime: String, + @SerializedName("is_suc") + val success: Boolean, + val ip: String, + val result: String, + val sql: String +) \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/model/TableColumn.kt b/app/src/main/java/me/kbai/zhenxunui/model/TableColumn.kt new file mode 100644 index 0000000..844261d --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/model/TableColumn.kt @@ -0,0 +1,14 @@ +package me.kbai.zhenxunui.model + +import com.google.gson.annotations.SerializedName + +data class TableColumn( + @SerializedName("column_name") + val name: String, + @SerializedName("data_type") + val type: String, + @SerializedName("max_length") + val maxLength: Int?, + @SerializedName("is_nullable") + val nullable: String +) \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/model/TableListItem.kt b/app/src/main/java/me/kbai/zhenxunui/model/TableListItem.kt new file mode 100644 index 0000000..fcc0248 --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/model/TableListItem.kt @@ -0,0 +1,6 @@ +package me.kbai.zhenxunui.model + +data class TableListItem( + val name: String, + val desc: String? +) diff --git a/app/src/main/java/me/kbai/zhenxunui/repository/ApiRepository.kt b/app/src/main/java/me/kbai/zhenxunui/repository/ApiRepository.kt index c869819..f5fa0b7 100644 --- a/app/src/main/java/me/kbai/zhenxunui/repository/ApiRepository.kt +++ b/app/src/main/java/me/kbai/zhenxunui/repository/ApiRepository.kt @@ -114,6 +114,10 @@ object ApiRepository { } } + fun getTableList() = networkFlow { BotApi.service.getTableList() } + + fun getTableColumn(table: String) = networkFlow { BotApi.service.getTableColumn(table) } + private fun networkFlow( tempData: T? = null, f: suspend () -> ApiResponse diff --git a/app/src/main/java/me/kbai/zhenxunui/ui/MainActivity.kt b/app/src/main/java/me/kbai/zhenxunui/ui/MainActivity.kt index 2235bf8..d397044 100644 --- a/app/src/main/java/me/kbai/zhenxunui/ui/MainActivity.kt +++ b/app/src/main/java/me/kbai/zhenxunui/ui/MainActivity.kt @@ -41,6 +41,7 @@ class MainActivity : BaseActivity() { R.id.nav_plugin, R.id.nav_friend_list, R.id.nav_request, + R.id.nav_db_manage, // R.id.nav_info ), viewBinding.drawerLayout ) diff --git a/app/src/main/java/me/kbai/zhenxunui/ui/db/DbManageFragment.kt b/app/src/main/java/me/kbai/zhenxunui/ui/db/DbManageFragment.kt new file mode 100644 index 0000000..9dec973 --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/ui/db/DbManageFragment.kt @@ -0,0 +1,43 @@ +package me.kbai.zhenxunui.ui.db + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import me.kbai.zhenxunui.base.BaseFragment +import me.kbai.zhenxunui.databinding.FragmentDbManageBinding +import me.kbai.zhenxunui.extends.setOnDebounceClickListener +import me.kbai.zhenxunui.extends.viewLifecycleScope +import me.kbai.zhenxunui.viewmodel.DbManageViewModel + +/** + * @author Sean on 2023/5/30 + */ +class DbManageFragment : BaseFragment() { + + private val mViewModel by viewModels() + + override fun getViewBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): FragmentDbManageBinding = FragmentDbManageBinding.inflate(inflater, container, false) + + override fun initView() = viewBinding.run { + + srlRefresh.setOnRefreshListener { + mViewModel.requestTableList() + } + + btnSql.setOnDebounceClickListener { + //TODO + } + } + + override fun initData() { + mViewModel.tables.observe(viewLifecycleOwner) { + viewBinding.srlRefresh.isRefreshing = false + viewBinding.rvTables.adapter = DbTableAdapter(it, mViewModel, viewLifecycleScope) + } + + mViewModel.requestTableList() + } +} \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/ui/db/DbTableAdapter.kt b/app/src/main/java/me/kbai/zhenxunui/ui/db/DbTableAdapter.kt new file mode 100644 index 0000000..6e4348d --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/ui/db/DbTableAdapter.kt @@ -0,0 +1,69 @@ +package me.kbai.zhenxunui.ui.db + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.view.size +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import me.kbai.zhenxunui.databinding.ItemDbManageTableBinding +import me.kbai.zhenxunui.extends.setOnDebounceClickListener +import me.kbai.zhenxunui.model.TableListItem +import me.kbai.zhenxunui.viewmodel.DbManageViewModel + +class DbTableAdapter( + private val data: List, + private val viewModel: DbManageViewModel, + private val coroutineScope: CoroutineScope +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemDbManageTableBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun getItemCount() = data.size + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = data[position] + + holder.binding.run { + tvTableName.text = item.name + tvDescription.text = item.desc + + root.setOnDebounceClickListener { + elFields.toggleExpand() + + if (rvFields.adapter != null) { + return@setOnDebounceClickListener + } + coroutineScope.launch { + viewModel.getColumns(item.name) + .collect { + if (!holder.recycled) { + pbWaiting.isVisible = false + rvFields.adapter = TableFieldAdapter(it) + rvFields.post { elFields.expand() } + } + } + } + } + + rvFields.adapter = null + elFields.collapse(false) + pbWaiting.isVisible = true + holder.recycled = false + } + } + + override fun onViewRecycled(holder: ItemViewHolder) { + holder.recycled = true + } + + class ItemViewHolder(val binding: ItemDbManageTableBinding) : + RecyclerView.ViewHolder(binding.root) { + var recycled: Boolean = false + } +} \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/ui/db/TableFieldAdapter.kt b/app/src/main/java/me/kbai/zhenxunui/ui/db/TableFieldAdapter.kt new file mode 100644 index 0000000..1edee76 --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/ui/db/TableFieldAdapter.kt @@ -0,0 +1,37 @@ +package me.kbai.zhenxunui.ui.db + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import me.kbai.zhenxunui.R +import me.kbai.zhenxunui.databinding.ItemDbManageFieldBinding +import me.kbai.zhenxunui.model.TableColumn + +class TableFieldAdapter( + private val data: List +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( + ItemDbManageFieldBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + + override fun getItemCount() = data.size + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) = holder.binding.run { + val item = data[position] + + tvName.text = item.name + tvType.text = item.type + tvMaxLength.text = + tvMaxLength.context.getString( + R.string.max_length_format, + item.maxLength?.toString().orEmpty() + ) + tvNullable.text = tvNullable.context.getString(R.string.nullable_format, item.nullable) + } + + class ItemViewHolder(val binding: ItemDbManageFieldBinding) : + RecyclerView.ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/ui/home/HomeFragment.kt b/app/src/main/java/me/kbai/zhenxunui/ui/home/HomeFragment.kt deleted file mode 100644 index 4d6d47f..0000000 --- a/app/src/main/java/me/kbai/zhenxunui/ui/home/HomeFragment.kt +++ /dev/null @@ -1,19 +0,0 @@ -package me.kbai.zhenxunui.ui.home - -import android.view.LayoutInflater -import android.view.ViewGroup -import me.kbai.zhenxunui.base.BaseFragment -import me.kbai.zhenxunui.databinding.FragmentHomeBinding - -/** - * @author Sean on 2023/5/30 - */ -class HomeFragment : BaseFragment() { - override fun getViewBinding( - inflater: LayoutInflater, - container: ViewGroup? - ): FragmentHomeBinding = FragmentHomeBinding.inflate(inflater, container, false) - - override fun initView() { - } -} \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/viewmodel/DbManageViewModel.kt b/app/src/main/java/me/kbai/zhenxunui/viewmodel/DbManageViewModel.kt new file mode 100644 index 0000000..67035e1 --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/viewmodel/DbManageViewModel.kt @@ -0,0 +1,43 @@ +package me.kbai.zhenxunui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.webkit.internal.ApiFeature.M +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import me.kbai.zhenxunui.extends.apiCollect +import me.kbai.zhenxunui.model.TableColumn +import me.kbai.zhenxunui.model.TableListItem +import me.kbai.zhenxunui.repository.ApiRepository + +class DbManageViewModel : ViewModel() { + + private val _tables: MutableLiveData> = MutableLiveData() + val tables: LiveData> = _tables + + private val _columnMap: MutableMap> = HashMap() + + fun requestTableList() = viewModelScope.launch { + ApiRepository.getTableList().apiCollect { + if (it.success()) { + _tables.value = it.data ?: return@apiCollect + _columnMap.clear() + } + } + } + + fun getColumns(table: String) = flow { + val data = _columnMap[table] + if (data != null) { + emit(data) + return@flow + } + val res = ApiRepository.getTableColumn(table).apiCollect() + if (res.success() && res.data != null) { + _columnMap[table] = res.data + emit(res.data) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/kbai/zhenxunui/widget/ExpandLayout.java b/app/src/main/java/me/kbai/zhenxunui/widget/ExpandLayout.java new file mode 100644 index 0000000..6dfddea --- /dev/null +++ b/app/src/main/java/me/kbai/zhenxunui/widget/ExpandLayout.java @@ -0,0 +1,130 @@ +package me.kbai.zhenxunui.widget; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import me.kbai.zhenxunui.R; + +public class ExpandLayout extends LinearLayout { + + private boolean isExpand; + private long animationDuration; + private boolean lock; + + private int maxHeight; + + public ExpandLayout(Context context) { + this(context, null); + } + + public ExpandLayout(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ExpandLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + readAttr(context, attrs); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!isExpand) setViewHeight(this, 0); + } + + public static void setViewHeight(View view, int height) { + final ViewGroup.LayoutParams params = view.getLayoutParams(); + params.height = height; + view.requestLayout(); + } + + public boolean isExpand() { + return isExpand; + } + + /** + * 折叠view + */ + public void collapse(boolean animator) { + isExpand = false; + if (animator) { + animateToggle(animationDuration); + } else { + setViewHeight(this, 0); + } + } + + /** + * 展开view + */ + public void expand() { + isExpand = true; + animateToggle(animationDuration); + } + + public void toggleExpand() { + if (lock) { + return; + } + if (isExpand) { + collapse(true); + } else { + expand(); + } + } + + private void readAttr(Context context, AttributeSet attrs) { + TypedArray array = null; + try { + //noinspection resource + array = context.obtainStyledAttributes(attrs, R.styleable.ExpandLayout); + isExpand = array.getBoolean(R.styleable.ExpandLayout_expanded, true); + maxHeight = array.getDimensionPixelSize(R.styleable.ExpandLayout_maxHeight, 0); + animationDuration = array.getInteger(R.styleable.ExpandLayout_animationDuration, 300); + } catch (Throwable e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } finally { + if (array != null) array.recycle(); + } + } + + /** + * 切换动画实现 + */ + private void animateToggle(long animationDuration) { + //展开时重新计算高度 + if (isExpand) { + measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ); + } + int viewHeight = maxHeight > 0 ? Math.min(getMeasuredHeight(), maxHeight) : getMeasuredHeight(); + + ValueAnimator heightAnimation = isExpand ? + ValueAnimator.ofFloat(0f, viewHeight) : ValueAnimator.ofFloat(viewHeight, 0f); + heightAnimation.setDuration(animationDuration / 2); + heightAnimation.setStartDelay(animationDuration / 2); + + heightAnimation.addUpdateListener(animation -> { + int value = (int) (float) animation.getAnimatedValue(); + setViewHeight(this, value); + if (value == viewHeight || value == 0) { + lock = false; + } + }); + + heightAnimation.start(); + lock = true; + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ced75eb..34291b2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index 8992f42..0000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_db_manage_field.xml b/app/src/main/res/layout/item_db_manage_field.xml new file mode 100644 index 0000000..c83fbf3 --- /dev/null +++ b/app/src/main/res/layout/item_db_manage_field.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_db_manage_table.xml b/app/src/main/res/layout/item_db_manage_table.xml new file mode 100644 index 0000000..c5e1d7a --- /dev/null +++ b/app/src/main/res/layout/item_db_manage_table.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_navigation_items.xml b/app/src/main/res/menu/main_navigation_items.xml index 59b5f6d..c577668 100644 --- a/app/src/main/res/menu/main_navigation_items.xml +++ b/app/src/main/res/menu/main_navigation_items.xml @@ -22,9 +22,13 @@ + + android:id="@+id/nav_db_manage" + android:title="@string/nav_database_manage" /> + + + diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 7190cf3..e8b25c2 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -39,11 +39,17 @@ android:label="@string/nav_request" tools:layout="@layout/fragment_request" /> - - - - - + + + + + + + 插件列表 请求管理 好友/群组 + 数据库管理 系统信息 " " 请输入用户名 @@ -123,4 +124,8 @@ 封禁 切换账号 用户信息 + 类型: %s + 最大长度: %s + 允许空值: %s + 执行SQL \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f00bb85..9bbcec5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -36,4 +36,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dff78f..7f7c1f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Plugin Friend/Group Request + Database Manage SysInfo Username Please input username @@ -127,4 +128,8 @@ Banned Select Account User Information + Type: %s + Max Length: %s + Nullable: %s + EXEC SQL \ No newline at end of file