初步用Compose重构关于界面
增加了以后会使用的一些自定义Compose控件 尝试让机器人自动发布预发布版本 修复透明模式下搜索框"删除所有"文字不明显 #354 忙忙忙咩咩咩咩
This commit is contained in:
+152
-58
@@ -33,23 +33,67 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !startsWith(github.event.head_commit.message, 'Merge pull request') }}
|
||||
outputs:
|
||||
version: ${{ steps.set-ver.outputs.version }}
|
||||
versionL: ${{ steps.set-ver.outputs.versionL }}
|
||||
lanzou: ${{ steps.check.outputs.lanzou }}
|
||||
telegram: ${{ steps.check.outputs.telegram }}
|
||||
# versionL: 完整版本号,例如 3.26.3-beta.1
|
||||
versionL: ${{ steps.set-ver.outputs.full_version_name }}
|
||||
# version: 基础版本号,例如 3.26.3
|
||||
version: ${{ steps.set-ver.outputs.current_version }}
|
||||
# build_number: 增量构建号,例如 1
|
||||
build_number: ${{ steps.set-ver.outputs.build_number }}
|
||||
# latest_tag: 最近一个用于版本递增的 tag,例如 v3.26.2-beta.5
|
||||
latest_tag: ${{ steps.set-ver.outputs.last_tag }}
|
||||
steps:
|
||||
- id: set-ver
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# 确保拉取所有标签,用于版本号计算
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 计算版本号和构建号
|
||||
id: set-ver
|
||||
run: |
|
||||
echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT
|
||||
echo "versionL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)" >> $GITHUB_OUTPUT
|
||||
- id: check
|
||||
run: |
|
||||
if [ ${{ secrets.LANZOU_ID }} ]; then
|
||||
echo "lanzou=yes" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ ${{ secrets.BOT_TOKEN }} ]; then
|
||||
echo "telegram=yes" >> $GITHUB_OUTPUT
|
||||
# 1. 读取 version.properties 文件获取基础版本号
|
||||
MAJOR=$(grep 'VERSION_MAJOR' version.properties | cut -d'=' -f2)
|
||||
MINOR=$(grep 'VERSION_MINOR' version.properties | cut -d'=' -f2)
|
||||
PATCH=$(grep 'VERSION_PATCH' version.properties | cut -d'=' -f2)
|
||||
CURRENT_VERSION="$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
# 2. 尝试获取最近一次成功的构建号 (beta build number)
|
||||
# 查找最近的、格式为 vX.Y.Z-beta.N 的标签,且 X.Y.Z 部分与当前版本匹配
|
||||
# 只有在推送到 main 分支时才进行自增计算
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
LAST_TAG=$(git tag --list "v*.*.*-beta.*" --sort=-committerdate | head -n 1)
|
||||
|
||||
# 查找匹配当前 CURRENT_VERSION 的上一个 TAG
|
||||
LAST_MATCHING_TAG=$(git tag --list "v$CURRENT_VERSION-beta.*" --sort=-committerdate | head -n 1)
|
||||
|
||||
if [[ $LAST_MATCHING_TAG =~ v[0-9]+\.[0-9]+\.[0-9]+-beta\.([0-9]+) ]]; then
|
||||
# 如果找到匹配当前 PATCH 版本的 beta 标签,则递增
|
||||
LAST_BUILD_NUM="${BASH_REMATCH[1]}"
|
||||
NEW_BUILD_NUM=$((LAST_BUILD_NUM + 1))
|
||||
LAST_TAG_FOR_LOG="$LAST_MATCHING_TAG"
|
||||
echo "版本 $CURRENT_VERSION 未变更,构建号自增:$LAST_BUILD_NUM -> $NEW_BUILD_NUM"
|
||||
else
|
||||
# 如果找不到匹配的标签 (可能是 PATCH 刚修改,或首次构建),则重置为 1
|
||||
NEW_BUILD_NUM=1
|
||||
# 如果是 PATCH 首次构建,则使用仓库中最近的那个 tag 作为日志起点
|
||||
LAST_TAG_FOR_LOG="$LAST_TAG"
|
||||
echo "版本 $CURRENT_VERSION 为新版本或首次构建,构建号重置为 $NEW_BUILD_NUM"
|
||||
fi
|
||||
else
|
||||
# 对于 PR 或其他分支,使用一个固定的构建号 0
|
||||
NEW_BUILD_NUM=0
|
||||
LAST_TAG_FOR_LOG=$(git tag --sort=-committerdate | head -n 1)
|
||||
echo "非 main 分支推送,构建号设置为 $NEW_BUILD_NUM"
|
||||
fi
|
||||
|
||||
# 3. 构造最终版本信息
|
||||
VERSION_SUFFIX_NAME="-beta.$NEW_BUILD_NUM"
|
||||
FULL_VERSION_NAME="${CURRENT_VERSION}${VERSION_SUFFIX_NAME}"
|
||||
|
||||
# 4. 设置输出供后续作业使用
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "full_version_name=$FULL_VERSION_NAME" >> $GITHUB_OUTPUT
|
||||
echo "build_number=$NEW_BUILD_NUM" >> $GITHUB_OUTPUT
|
||||
echo "last_tag=$LAST_TAG_FOR_LOG" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
@@ -62,8 +106,10 @@ jobs:
|
||||
env:
|
||||
product: ${{ matrix.product }}
|
||||
type: ${{ matrix.type }}
|
||||
# 使用 prepare job 的输出
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
VERSIONL: ${{ needs.prepare.outputs.versionL }}
|
||||
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -88,12 +134,24 @@ jobs:
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: 统一版本号
|
||||
run: |
|
||||
# 1. 临时修改 version.properties 中的 VERSION_SUFFIX,供 build.gradle 读取 versionName
|
||||
echo "VERSION_SUFFIX=-beta.${{ env.BUILD_NUMBER }}" >> $GITHUB_WORKSPACE/version.properties
|
||||
|
||||
# 2. 覆盖 build.gradle 中复杂的 versionCode 计算逻辑
|
||||
# 将 versionCode 设为 10000 + BUILD_NUMBER,忽略 gitCommits
|
||||
# 替换 app/build.gradle 中 versionCode 所在行
|
||||
sed -i "s/versionCode 10000 + gitCommits/versionCode 10000 + ${{ env.BUILD_NUMBER }}/" $GITHUB_WORKSPACE/app/build.gradle
|
||||
|
||||
echo "versionName 已设置为 ${{ env.VERSIONL }}"
|
||||
echo "versionCode 已设置为 10000 + ${{ env.BUILD_NUMBER }}"
|
||||
|
||||
- name: Build With Gradle
|
||||
run: |
|
||||
echo "统一版本号"
|
||||
sed -i "s/ext.versionName = .*/ext.versionName = \"${{ env.VERSION }}\"/" $GITHUB_WORKSPACE/app/build.gradle
|
||||
echo "开始${{ env.product }}${{ env.type }}构建"
|
||||
chmod +x gradlew
|
||||
# Gradle 现在会读取修改后的 version.properties 来获取 versionName
|
||||
./gradlew assemble${{ env.product }}Release
|
||||
|
||||
- name: Move Missing Rules Files
|
||||
@@ -145,31 +203,6 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
path: ${{ github.workspace }}/mapping/mapping.txt
|
||||
|
||||
lanzou:
|
||||
needs: [ prepare, build ]
|
||||
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# 登录蓝奏云后在控制台运行document.cookie
|
||||
ylogin: ${{ secrets.LANZOU_ID }}
|
||||
phpdisk_info: ${{ secrets.LANZOU_PSD }}
|
||||
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
|
||||
LANZOU_FOLDER_ID: ${{ secrets.LANZOU_FOLDER_ID }}
|
||||
#蓝奏云分享链接
|
||||
LANZOU_URL: ${{ secrets.LANZOU_URL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: apk/
|
||||
- working-directory: apk/
|
||||
run: mv */*.apk . ;rm -rf */
|
||||
- name: Upload To Lanzou
|
||||
continue-on-error: true
|
||||
run: |
|
||||
path="$GITHUB_WORKSPACE/apk/"
|
||||
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
|
||||
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: $LANZOU_URL"
|
||||
|
||||
test_Branch:
|
||||
needs: [ prepare, build ]
|
||||
@@ -194,26 +227,87 @@ jobs:
|
||||
git commit -m "${{ needs.prepare.outputs.versionL }}"
|
||||
git push -f -u origin test
|
||||
|
||||
telegram:
|
||||
create_pre_release:
|
||||
needs: [ prepare, build ]
|
||||
if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.telegram == 'yes' }}
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
github.ref == 'refs/heads/main' &&
|
||||
needs.prepare.outputs.build_number > 0 &&
|
||||
success()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: apk/
|
||||
- working-directory: apk/
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 获取所有 Commit 消息作为 Release Body
|
||||
id: get_commits
|
||||
run: |
|
||||
for file in `ls */*.apk`; do
|
||||
mv "$file" "$(echo "$file"|sed -e 's#.*\/##g' -e "s/_/ /g" -e 's/legado/阅读/')"
|
||||
done
|
||||
rm -rf */
|
||||
- name: Post to channel
|
||||
uses: xireiki/channel-post@v1
|
||||
# 获取从上一个 tag 到当前 commit 之间的所有 commit 消息
|
||||
# 如果 latest_tag 为空,则从第一个 commit 开始获取
|
||||
|
||||
LATEST_TAG="${{ needs.prepare.outputs.latest_tag }}"
|
||||
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
# 如果没有找到任何 tag,则从第一个 commit 开始
|
||||
COMMIT_LOG=$(git log --pretty=format:'* %s (%an)' --reverse)
|
||||
else
|
||||
# 从最近的 tag 到当前 HEAD
|
||||
# 使用 ^$LATEST_TAG 来排除 LATEST_TAG 本身的 commit
|
||||
COMMIT_LOG=$(git log $LATEST_TAG..HEAD --pretty=format:'* %s (%an)')
|
||||
fi
|
||||
|
||||
# 检查是否获取到日志
|
||||
if [ -z "$COMMIT_LOG" ]; then
|
||||
RELEASE_BODY="本次发布无新增 Commit。"
|
||||
else
|
||||
RELEASE_BODY="# 本次更新内容\n\n$COMMIT_LOG"
|
||||
fi
|
||||
|
||||
# 输出给下一步使用
|
||||
echo "release_body<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$RELEASE_BODY" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name:
|
||||
创建 Pre-Release Tag: v${{ needs.prepare.outputs.versionL }}
|
||||
# 创建一个 Tag,供下一轮计算版本号时使用
|
||||
run: |
|
||||
git tag v${{ needs.prepare.outputs.versionL }}
|
||||
git push origin v${{ needs.prepare.outputs.versionL }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 创建 Pre-Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
chat_id: ${{ secrets.CHANNEL_ID }}
|
||||
bot_token: ${{ secrets.BOT_TOKEN }}
|
||||
context: "#阅读 #Legado #Beta ${{ needs.prepare.outputs.versionL }}"
|
||||
path: apk/*
|
||||
method: sendFile
|
||||
# Tag 名称:3.26.3-beta.1
|
||||
tag_name: ${{ needs.prepare.outputs.versionL }}
|
||||
# Release 标题:3.26.3-beta.1
|
||||
release_name: ${{ needs.prepare.outputs.versionL }}
|
||||
# Release 内容为所有 Commit 消息
|
||||
body: ${{ steps.get_commits.outputs.release_body }}
|
||||
draft: false
|
||||
# 标记为预发布版本
|
||||
prerelease: true
|
||||
|
||||
- name: 下载所有 APK Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: '*-apk'
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: 上传 Release Assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: artifacts/*.apk
|
||||
# 重新命名上传的 APK 文件,使其包含版本号
|
||||
asset_name: legado-${{ needs.prepare.outputs.versionL }}.apk
|
||||
asset_content_type: application/zip
|
||||
+11
-5
@@ -356,9 +356,6 @@ dependencies {
|
||||
// Activity
|
||||
implementation libs.androidx.activity.compose
|
||||
|
||||
// Navigation(用于弹框或未来扩展)
|
||||
implementation libs.navigation.compose
|
||||
|
||||
// Coil
|
||||
implementation libs.coil.compose
|
||||
|
||||
@@ -366,15 +363,24 @@ dependencies {
|
||||
implementation libs.accompanist.webview
|
||||
|
||||
// Markdown 渲染(Compose MD)
|
||||
implementation libs.markdown.compose
|
||||
// implementation libs.markdown.compose
|
||||
|
||||
// 协程
|
||||
implementation libs.kotlinx.coroutines.android
|
||||
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||
implementation libs.androidx.lifecycle.runtime.compose
|
||||
implementation libs.androidx.lifecycle.viewmodel.compose
|
||||
implementation libs.androidx.compose.animation
|
||||
implementation libs.androidx.compose.foundation
|
||||
implementation libs.androidx.constraintlayout.compose
|
||||
implementation libs.androidx.compose.ui.viewbinding
|
||||
implementation libs.androidx.navigation.compose
|
||||
implementation libs.androidx.compose.material
|
||||
implementation libs.compose.materialIcons
|
||||
|
||||
implementation(platform(libs.koin.bom))
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.utils.disableAutoFill
|
||||
import io.legado.app.utils.fullScreen
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.setStatusBarColorAuto
|
||||
import io.legado.app.utils.themeColor
|
||||
import io.legado.app.utils.windowSize
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseComposeActivity(
|
||||
val fullScreen: Boolean = true,
|
||||
private val toolBarTheme: Theme = Theme.Auto,
|
||||
private val transparent: Boolean = false,
|
||||
private val imageBg: Boolean = true
|
||||
) : AppCompatActivity() {
|
||||
|
||||
@Composable
|
||||
protected abstract fun Content()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initTheme()
|
||||
window.decorView.disableAutoFill()
|
||||
AppContextWrapper.applyLocaleAndFont(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Compose 入口
|
||||
setContent {
|
||||
AppTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSystemBar()
|
||||
|
||||
if (imageBg) {
|
||||
upBackgroundImage()
|
||||
}
|
||||
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
open fun setupSystemBar() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (fullScreen) fullScreen()
|
||||
|
||||
setStatusBarColorAuto(
|
||||
themeColor(com.google.android.material.R.attr.colorSurface),
|
||||
true,
|
||||
fullScreen
|
||||
)
|
||||
}
|
||||
|
||||
open fun upBackgroundImage() {
|
||||
try {
|
||||
ThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
||||
window.setBackgroundDrawable(it.toDrawable(resources))
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
observeEvent<String>(EventBus.RECREATE) {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
|
||||
open fun initTheme() {
|
||||
when (getPrefString("app_theme", "0")) {
|
||||
"0" -> {
|
||||
DynamicColors.applyToActivitiesIfAvailable(application)
|
||||
}
|
||||
"1" -> setTheme(R.style.Theme_Base_GR)
|
||||
"2" -> setTheme(R.style.Theme_Base_Lemon)
|
||||
"3" -> setTheme(R.style.Theme_Base_WH)
|
||||
"4" -> setTheme(R.style.Theme_Base_Elink)
|
||||
"5" -> setTheme(R.style.Theme_Base_Sora)
|
||||
"6" -> setTheme(R.style.Theme_Base_August)
|
||||
"7" -> setTheme(R.style.Theme_Base_Carlotta)
|
||||
"8" -> setTheme(R.style.Theme_Base_Koharu)
|
||||
"9" -> setTheme(R.style.Theme_Base_Yuuka)
|
||||
"10" -> setTheme(R.style.Theme_Base_Phoebe)
|
||||
"11" -> setTheme(R.style.Theme_Base_Mujika)
|
||||
"12" -> {
|
||||
if (AppConfig.customMode == "accent")
|
||||
setTheme(R.style.ThemeOverlay_WhiteBackground)
|
||||
|
||||
val colorImagePath = getPrefString(PreferKey.colorImage)
|
||||
if (!colorImagePath.isNullOrBlank()) {
|
||||
val file = File(colorImagePath)
|
||||
if (file.exists()) {
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
if (bitmap != null) {
|
||||
val colorAccuracy = true
|
||||
val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
|
||||
val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
|
||||
val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
|
||||
|
||||
val options = DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(scaledBitmap)
|
||||
.build()
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(application, options)
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
}else{
|
||||
DynamicColors.applyToActivitiesIfAvailable(
|
||||
application,
|
||||
DynamicColorsOptions.Builder()
|
||||
.setContentBasedSource(application.primaryColor)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"13" -> setTheme(R.style.AppTheme_Transparent)
|
||||
}
|
||||
|
||||
if (AppConfig.pureBlack)
|
||||
setTheme(R.style.ThemeOverlay_PureBlack)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val colorScheme = when {
|
||||
dynamicColor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ->
|
||||
if (darkTheme) dynamicDarkColorScheme(context)
|
||||
else dynamicLightColorScheme(context)
|
||||
|
||||
darkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
shapes = AppShapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
// Material3 Typography
|
||||
val AppTypography = Typography()
|
||||
|
||||
// Material3 Shapes
|
||||
val AppShapes = Shapes()
|
||||
@@ -1,7 +1,5 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.filletBackground
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -9,21 +7,16 @@ import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.core.app.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseActivity
|
||||
import io.legado.app.constant.AppConst.appInfo
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.databinding.ActivityAboutBinding
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.update.AppUpdate
|
||||
import io.legado.app.help.update.AppUpdateGitHub
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.FileDoc
|
||||
@@ -40,18 +33,24 @@ import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.share
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
class AboutActivity : AppCompatActivity() {
|
||||
class AboutActivity : BaseComposeActivity() {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
AboutScreen(
|
||||
onCheckUpdate = { checkUpdate() },
|
||||
onOpenUrl = { openUrl(it) },
|
||||
onShowMdFile = { title, file -> showMdFile(title, file) },
|
||||
onSaveLog = { saveLog() },
|
||||
onCreateHeapDump = { createHeapDump() },
|
||||
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() },
|
||||
)
|
||||
}
|
||||
|
||||
private val waitDialog by lazy {
|
||||
WaitDialog(this).setText(R.string.checking_update)
|
||||
@@ -60,18 +59,6 @@ class AboutActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
AboutScreen(
|
||||
onCheckUpdate = { checkUpdate() },
|
||||
onOpenUrl = { openUrl(it) },
|
||||
onShowMdFile = { title, file -> showMdFile(title, file) },
|
||||
onSaveLog = { saveLog() },
|
||||
onCreateHeapDump = { createHeapDump() },
|
||||
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
@@ -100,9 +87,9 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun checkUpdate() {
|
||||
waitDialog.show()
|
||||
AppUpdate.gitHubUpdate?.run {
|
||||
kotlin.check(lifecycleScope)
|
||||
check(lifecycleScope)
|
||||
.onSuccess {
|
||||
showDialogFragment(UpdateDialog(it))
|
||||
showDialogFragment(UpdateDialog(it, UpdateDialog.Mode.UPDATE))
|
||||
}.onError {
|
||||
appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}")
|
||||
}.onFinally {
|
||||
@@ -119,17 +106,17 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun saveLog() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
appCtx.toastOnUi("未设置备份目录")
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordLog) {
|
||||
appCtx.toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
|
||||
toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
|
||||
delay(3000)
|
||||
}
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
copyLogs(doc)
|
||||
copyHeapDump(doc)
|
||||
appCtx.toastOnUi("已保存至备份目录")
|
||||
toastOnUi("已保存至备份目录")
|
||||
}.onError {
|
||||
AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
|
||||
}
|
||||
@@ -138,21 +125,21 @@ class AboutActivity : AppCompatActivity() {
|
||||
private fun createHeapDump() {
|
||||
Coroutine.async {
|
||||
val backupPath = AppConfig.backupPath ?: let {
|
||||
appCtx.toastOnUi("未设置备份目录")
|
||||
toastOnUi("未设置备份目录")
|
||||
return@async
|
||||
}
|
||||
if (!AppConfig.recordHeapDump) {
|
||||
appCtx.toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
|
||||
toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
|
||||
delay(3000)
|
||||
}
|
||||
appCtx.toastOnUi("开始创建堆转储")
|
||||
toastOnUi("开始创建堆转储")
|
||||
System.gc()
|
||||
CrashHandler.doHeapDump(true)
|
||||
val doc = FileDoc.fromUri(backupPath.toUri(), true)
|
||||
if (!copyHeapDump(doc)) {
|
||||
appCtx.toastOnUi("未找到堆转储文件")
|
||||
toastOnUi("未找到堆转储文件")
|
||||
} else {
|
||||
appCtx.toastOnUi("已保存至备份目录")
|
||||
toastOnUi("已保存至备份目录")
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
|
||||
@@ -160,7 +147,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun copyLogs(doc: FileDoc) {
|
||||
val cacheDir = appCtx.externalCache
|
||||
val cacheDir = externalCache
|
||||
val logFiles = File(cacheDir, "logs")
|
||||
val crashFiles = File(cacheDir, "crash")
|
||||
val logcatFile = File(cacheDir, "logcat.txt")
|
||||
@@ -182,7 +169,7 @@ class AboutActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun copyHeapDump(doc: FileDoc): Boolean {
|
||||
val heapFile = FileDoc.fromFile(File(appCtx.externalCache, "heapDump")).list()
|
||||
val heapFile = FileDoc.fromFile(File(externalCache, "heapDump")).list()
|
||||
?.firstOrNull() ?: return false
|
||||
doc.find("heapDump")?.delete()
|
||||
val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
|
||||
|
||||
@@ -1,47 +1,147 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ButtonGroup
|
||||
import androidx.compose.material3.ButtonGroupDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppTheme
|
||||
import io.legado.app.constant.AppConst.appInfo
|
||||
import io.legado.app.ui.widget.compose.SettingItem
|
||||
import io.legado.app.ui.widget.compose.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.compose.TextCard
|
||||
|
||||
@Preview(showBackground = true, name = "Light Mode")
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
|
||||
@Composable
|
||||
fun PreviewAboutScreen() {
|
||||
AppTheme {
|
||||
AboutScreen(
|
||||
versionName = "1.0",
|
||||
onCheckUpdate = {},
|
||||
onOpenUrl = {},
|
||||
onShowMdFile = { _, _ -> },
|
||||
onSaveLog = {},
|
||||
onCreateHeapDump = {},
|
||||
onShowCrashLogs = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun ButtonGroup(
|
||||
onOpenUrl: (String) -> Unit,
|
||||
onCheckUpdate: () -> Unit
|
||||
) {
|
||||
|
||||
ButtonGroup(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
overflowIndicator = { menuState ->
|
||||
ButtonGroupDefaults.OverflowIndicator(menuState = menuState)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
clickableItem(
|
||||
onClick = { onOpenUrl("https://example.com") },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_web_outline),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
|
||||
clickableItem(
|
||||
onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_github),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
|
||||
clickableItem(
|
||||
onClick = { onCheckUpdate() },
|
||||
weight = 1f,
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_import),
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AboutScreen(
|
||||
versionName: String = appInfo.versionName,
|
||||
onNavigateBack: () -> Unit = {},
|
||||
onCheckUpdate: () -> Unit = {},
|
||||
onOpenUrl: (String) -> Unit = {},
|
||||
onShowMdFile: (String, String) -> Unit = { _, _ -> },
|
||||
onSaveLog: () -> Unit = {},
|
||||
onCreateHeapDump: () -> Unit = {},
|
||||
onShowCrashLogs: () -> Unit = {},
|
||||
versionName: String = "1.0.0"
|
||||
onShowCrashLogs: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val version = remember { appInfo.versionName }
|
||||
LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.about)) }
|
||||
title = { Text(stringResource(R.string.about)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
@@ -51,57 +151,109 @@ fun AboutScreen(
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(text = "版本:$version", style = MaterialTheme.typography.bodyLarge)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = onCheckUpdate) {
|
||||
Text(stringResource(R.string.check_update))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = { onOpenUrl(context.getString(R.string.github_url)) }) {
|
||||
Text(stringResource(R.string.github_url))
|
||||
}
|
||||
|
||||
Button(onClick = { onOpenUrl(context.getString(R.string.legado_url)) }) {
|
||||
Text(stringResource(R.string.web_service))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
Modifier.padding(vertical = 8.dp),
|
||||
DividerDefaults.Thickness,
|
||||
DividerDefaults.color
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(text = "文档与政策", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.privacy_policy), "privacyPolicy.md") }) {
|
||||
Text(stringResource(R.string.privacy_policy))
|
||||
}
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.license), "LICENSE.md") }) {
|
||||
Text(stringResource(R.string.license))
|
||||
}
|
||||
TextButton(onClick = { onShowMdFile(context.getString(R.string.disclaimer), "disclaimer.md") }) {
|
||||
Text(stringResource(R.string.disclaimer))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
Modifier.padding(vertical = 8.dp),
|
||||
DividerDefaults.Thickness,
|
||||
DividerDefaults.color
|
||||
Text(
|
||||
text = stringResource(R.string.app_name), style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally))
|
||||
TextCard(
|
||||
text = versionName,
|
||||
backgroundColor = colorScheme.tertiaryContainer,
|
||||
contentColor = colorScheme.onTertiaryContainer,
|
||||
cornerRadius = 8.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_description), style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally))
|
||||
Row (modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center){
|
||||
FilledTonalIconButton ( onClick = { onOpenUrl("https://example.com") } ) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_web_outline),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
Text(text = "开发工具", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(onClick = onShowCrashLogs) {
|
||||
Text(stringResource(R.string.crash_log))
|
||||
}
|
||||
TextButton(onClick = onSaveLog) {
|
||||
Text(stringResource(R.string.save_log))
|
||||
}
|
||||
TextButton(onClick = onCreateHeapDump) {
|
||||
Text(stringResource(R.string.create_heap_dump))
|
||||
FilledTonalIconButton (onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_github),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
FilledTonalIconButton (onClick = { onCheckUpdate }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_import),
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SplicedColumnGroup(
|
||||
title = stringResource(R.string.about),
|
||||
content = listOf(
|
||||
{
|
||||
SettingItem(
|
||||
modifier = Modifier.background(colorScheme.surfaceBright),
|
||||
title = stringResource(R.string.contributors),
|
||||
description = "",
|
||||
onClick = {
|
||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.license),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("许可证", "LICENSE.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.disclaimer),
|
||||
description = "",
|
||||
onClick = {
|
||||
onShowMdFile("免责声明", "disclaimer.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.crash_log),
|
||||
description = "",
|
||||
onClick = onShowCrashLogs
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.save_log),
|
||||
description = "",
|
||||
onClick = onSaveLog
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.create_heap_dump),
|
||||
description = "",
|
||||
onClick = onCreateHeapDump
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
@Composable
|
||||
fun AnimatedText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
for (i in text.indices) {
|
||||
val char = text[i]
|
||||
Box {
|
||||
AnimatedContent(
|
||||
targetState = char,
|
||||
transitionSpec = {
|
||||
(slideInVertically(initialOffsetY = { it })).togetherWith(
|
||||
slideOutVertically(targetOffsetY = { -it })
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
) {
|
||||
Text(
|
||||
style = style,
|
||||
color = color,
|
||||
softWrap = softWrap,
|
||||
text = it.toString(),
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
overflow = overflow,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画文本控件
|
||||
*
|
||||
* 区别于 AnimatedText, 该控件在文本变化时,提供整行的滑动动画效果
|
||||
*/
|
||||
@Composable
|
||||
fun AnimatedTextLine(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
var currentText by remember { mutableStateOf(text) }
|
||||
SideEffect { currentText = text }
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = currentText,
|
||||
transitionSpec = {
|
||||
(slideInVertically(initialOffsetY = { it })).togetherWith(
|
||||
slideOutVertically(targetOffsetY = { -it })
|
||||
)
|
||||
},
|
||||
label = ""
|
||||
) { text ->
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
style = style,
|
||||
color = color,
|
||||
softWrap = softWrap,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
overflow = overflow,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
onTextLayout = onTextLayout
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.base.AppTypography
|
||||
|
||||
@Composable
|
||||
fun SettingItem(
|
||||
modifier: Modifier = Modifier,
|
||||
painter: Painter? = null,
|
||||
title: String,
|
||||
option: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
description: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
painter?.let {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 8.dp).size(24.dp),
|
||||
painter = it,
|
||||
tint = colorScheme.onSurfaceVariant,
|
||||
contentDescription = "Icon"
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = colorScheme.onSurface,
|
||||
style = AppTypography.titleMedium,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
style = AppTypography.labelMedium
|
||||
)
|
||||
option?.let {
|
||||
AnimatedText(
|
||||
text = it,
|
||||
style = AppTypography.labelMedium,
|
||||
color = colorScheme.primary,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trailingContent?.let { composable ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight()
|
||||
.width(55.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
composable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
||||
*
|
||||
* @param title the title of the group
|
||||
* @param content a list of composable that will be displayed in the group
|
||||
* @param modifier Modifier
|
||||
*/
|
||||
@Composable
|
||||
fun SplicedColumnGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = "",
|
||||
content: List<@Composable () -> Unit>,
|
||||
) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
val cornerRadius = 16.dp
|
||||
val connectionRadius = 5.dp
|
||||
// Define shapes for different positions.
|
||||
val topShape = RoundedCornerShape(
|
||||
topStart = cornerRadius,
|
||||
topEnd = cornerRadius,
|
||||
bottomStart = connectionRadius,
|
||||
bottomEnd = connectionRadius
|
||||
)
|
||||
val middleShape = RoundedCornerShape(connectionRadius)
|
||||
val bottomShape = RoundedCornerShape(
|
||||
topStart = connectionRadius,
|
||||
topEnd = connectionRadius,
|
||||
bottomStart = cornerRadius,
|
||||
bottomEnd = cornerRadius
|
||||
)
|
||||
val singleShape = RoundedCornerShape(cornerRadius)
|
||||
|
||||
Column(modifier = modifier.padding(vertical = 8.dp)) {
|
||||
// Group title
|
||||
if (title != "")
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
// The container for setting items.
|
||||
Column(
|
||||
modifier = Modifier.clip(
|
||||
// Clip the whole column to ensure content stays within the rounded bounds.
|
||||
if (content.size == 1) singleShape else RoundedCornerShape(cornerRadius)
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
content.forEachIndexed { index, itemContent ->
|
||||
// Determine the shape based on the pkg's position.
|
||||
val shape = when {
|
||||
content.size == 1 -> singleShape
|
||||
index == 0 -> topShape
|
||||
index == content.size - 1 -> bottomShape
|
||||
else -> middleShape
|
||||
}
|
||||
|
||||
// Apply background with the correct shape to the pkg.
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceBright, shape)
|
||||
) {
|
||||
itemContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package io.legado.app.ui.widget.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewTextCard() {
|
||||
MaterialTheme {
|
||||
TextCard(
|
||||
text = "v1.0.0"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextCard(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = colorScheme.tertiaryContainer,
|
||||
contentColor: Color = colorScheme.onTertiaryContainer,
|
||||
cornerRadius: Dp = 12.dp,
|
||||
paddingHorizontal: Dp = 8.dp,
|
||||
paddingVertical: Dp = 0.dp,
|
||||
textSize: TextUnit = 10.sp,
|
||||
bold: Boolean = true,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (onClick != null) Modifier.clickable { onClick() }
|
||||
else Modifier
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = backgroundColor,
|
||||
contentColor = contentColor
|
||||
),
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = paddingHorizontal,
|
||||
vertical = paddingVertical
|
||||
),
|
||||
fontSize = textSize,
|
||||
fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -191,13 +191,13 @@
|
||||
android:id="@+id/tv_clear_history"
|
||||
style="@style/Widget.Material3Expressive.Button"
|
||||
android:text="@string/delete_all"
|
||||
app:backgroundTint="?attr/colorTertiary"
|
||||
app:iconTint="?attr/colorOnTertiary"
|
||||
android:textColor="?attr/colorOnTertiary"
|
||||
app:backgroundTint="?attr/colorSecondaryContainer"
|
||||
app:iconTint="?attr/colorOnSecondaryContainer"
|
||||
android:textColor="?attr/colorOnSecondaryContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_clear_all"
|
||||
app:strokeColor="?attr/colorTertiary"
|
||||
app:strokeColor="?attr/colorSecondaryContainer"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VERSION_MAJOR=3
|
||||
VERSION_MINOR=26
|
||||
VERSION_PATCH=3
|
||||
VERSION_PATCH=4
|
||||
VERSION_SUFFIX=
|
||||
+33
-10
@@ -1,13 +1,16 @@
|
||||
[versions]
|
||||
|
||||
accompanistWebview = "0.36.0"
|
||||
animation = "1.9.4"
|
||||
coilCompose = "2.7.0"
|
||||
composeBom = "2025.11.00"
|
||||
coreSplashscreen = "1.2.0-rc01"
|
||||
composeBom = "2025.11.01"
|
||||
constraintlayoutCompose = "1.1.1"
|
||||
coreSplashscreen = "1.2.0"
|
||||
datastorePreferences = "1.1.7"
|
||||
kotlin = "2.2.20"
|
||||
kotlinxCoroutinesAndroid = "1.8.1"
|
||||
ksp = "2.2.20-2.0.4"
|
||||
foundation = "1.9.4"
|
||||
kotlin = "2.2.21"
|
||||
kotlinxCoroutinesAndroid = "1.10.2"
|
||||
ksp = "2.2.21-2.0.4"
|
||||
agp = "8.13.1"
|
||||
appcompat = "1.7.1"
|
||||
colorpicker = "1.1.0"
|
||||
@@ -22,7 +25,7 @@ fragment = "1.8.9"
|
||||
hutool = "5.8.22"
|
||||
|
||||
libarchive = "1.1.6"
|
||||
lifecycle = "2.9.4"
|
||||
lifecycle = "2.10.0"
|
||||
glide = "5.0.5"
|
||||
gson = "2.13.2"
|
||||
jsonPath = "2.9.0"
|
||||
@@ -31,13 +34,18 @@ jsonPath = "2.9.0"
|
||||
jsoup = "1.16.2"
|
||||
jsoupxpath = "2.5.3"
|
||||
coroutines = "1.10.2"
|
||||
lifecycleViewmodelCompose = "2.10.0"
|
||||
liveeventbus = "1.8.14"
|
||||
markdownCompose = "0.5.0"
|
||||
markwon = "4.6.2"
|
||||
material = "1.14.0-alpha06"
|
||||
material = "1.14.0-alpha07"
|
||||
material3 = "1.5.0-alpha09"
|
||||
material3IconsExtended = "1.2.0"
|
||||
materialIconsExtended = "1.7.8"
|
||||
media = "1.7.1"
|
||||
media3 = "1.8.0"
|
||||
nanoHttpd = "2.3.1"
|
||||
navigationCompose = "2.9.6"
|
||||
okhttp = "5.2.0"
|
||||
palette = "1.0.0"
|
||||
preference = "1.2.1"
|
||||
@@ -55,13 +63,14 @@ swiperefreshlayout = "1.1.0"
|
||||
#noinspection GradleDependency
|
||||
recyclerview = "1.4.0"
|
||||
#noinspection GradleDependency
|
||||
uiViewbinding = "1.9.4"
|
||||
viewpager2 = "1.1.0"
|
||||
webkit = "1.14.0"
|
||||
collection = "1.5.0"
|
||||
|
||||
zxingLite = "3.3.0"
|
||||
|
||||
|
||||
koin-bom = "4.1.1"
|
||||
[libraries]
|
||||
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
|
||||
@@ -74,19 +83,33 @@ androidx-annotation = { group = "androidx.annotation", name = "annotation", vers
|
||||
#androidx-annotation-experimental = { group = "androidx.annotation", name = "annotation-experimental", version = "1.3.1" }
|
||||
|
||||
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
|
||||
androidx-compose-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
|
||||
androidx-compose-material = { module = "androidx.compose.material:material" }
|
||||
compose-materialIcons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.7.8" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
androidx-compose-material3-icons-extended = { module = "androidx.compose.material3:material3-icons-extended", version.ref = "material3IconsExtended" }
|
||||
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding", version.ref = "uiViewbinding" }
|
||||
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose" }
|
||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-palette = { module = "androidx.palette:palette", version.ref = "palette" }
|
||||
androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" }
|
||||
appcompat-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||
|
||||
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
|
||||
koin-core = { module = "io.insert-koin:koin-core" }
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android" }
|
||||
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose" }
|
||||
koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel" }
|
||||
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
||||
core-core = { module = "androidx.core:core", version.ref = "core" }
|
||||
@@ -194,7 +217,7 @@ coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
|
||||
androidTest = ["androidx-espresso-core", "androidx-junit", "androidx-runner"]
|
||||
|
||||
[plugins]
|
||||
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
android-test = { id = "com.android.test", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user