From 58893de5c21abc8a0c6ceaab0ed856300126fc78 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Sat, 22 Nov 2025 02:35:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E7=94=A8Compose=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=85=B3=E4=BA=8E=E7=95=8C=E9=9D=A2=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E4=BB=A5=E5=90=8E=E4=BC=9A=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E4=BA=9B=E8=87=AA=E5=AE=9A=E4=B9=89Compose?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6=20=E5=B0=9D=E8=AF=95=E8=AE=A9=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E4=BA=BA=E8=87=AA=E5=8A=A8=E5=8F=91=E5=B8=83=E9=A2=84?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E7=89=88=E6=9C=AC=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=80=8F=E6=98=8E=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E6=A1=86"=E5=88=A0=E9=99=A4=E6=89=80=E6=9C=89"=E6=96=87?= =?UTF-8?q?=E5=AD=97=E4=B8=8D=E6=98=8E=E6=98=BE=20#354=20=E5=BF=99?= =?UTF-8?q?=E5=BF=99=E5=BF=99=E5=92=A9=E5=92=A9=E5=92=A9=E5=92=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 210 ++++++++++---- app/build.gradle | 16 +- .../io/legado/app/base/BaseComposeActivity.kt | 148 ++++++++++ app/src/main/java/io/legado/app/base/Theme.kt | 37 +++ .../io/legado/app/ui/about/AboutActivity.kt | 67 ++--- .../io/legado/app/ui/about/AboutScreen.kt | 268 ++++++++++++++---- .../app/ui/widget/compose/AnimatedText.kt | 152 ++++++++++ .../app/ui/widget/compose/SettingItem.kt | 92 ++++++ .../ui/widget/compose/SplicedColumnGroup.kt | 84 ++++++ .../legado/app/ui/widget/compose/TextCard.kt | 67 +++++ .../main/res/layout/activity_book_search.xml | 8 +- app/version.properties | 2 +- gradle/libs.versions.toml | 43 ++- 13 files changed, 1018 insertions(+), 176 deletions(-) create mode 100644 app/src/main/java/io/legado/app/base/BaseComposeActivity.kt create mode 100644 app/src/main/java/io/legado/app/base/Theme.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/compose/AnimatedText.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/compose/SettingItem.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/compose/SplicedColumnGroup.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/compose/TextCard.kt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a402f23ca..ad72eb011 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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<> $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 \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d0eb3d665..3738bed9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) } diff --git a/app/src/main/java/io/legado/app/base/BaseComposeActivity.kt b/app/src/main/java/io/legado/app/base/BaseComposeActivity.kt new file mode 100644 index 000000000..a7d5b799c --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseComposeActivity.kt @@ -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(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) + } + +} diff --git a/app/src/main/java/io/legado/app/base/Theme.kt b/app/src/main/java/io/legado/app/base/Theme.kt new file mode 100644 index 000000000..cedbd785a --- /dev/null +++ b/app/src/main/java/io/legado/app/base/Theme.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt index 724ff870d..43302e568 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt @@ -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() }, + ) + } 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() } - ) - } - } } 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") diff --git a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt index 9f4e1e9b7..d14da4e22 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt @@ -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 + ) + } + ) + ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/compose/AnimatedText.kt b/app/src/main/java/io/legado/app/ui/widget/compose/AnimatedText.kt new file mode 100644 index 000000000..315f64ada --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/compose/AnimatedText.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/compose/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/compose/SettingItem.kt new file mode 100644 index 000000000..44c8388c6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/compose/SettingItem.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/compose/SplicedColumnGroup.kt b/app/src/main/java/io/legado/app/ui/widget/compose/SplicedColumnGroup.kt new file mode 100644 index 000000000..f41c86c57 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/compose/SplicedColumnGroup.kt @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/compose/TextCard.kt b/app/src/main/java/io/legado/app/ui/widget/compose/TextCard.kt new file mode 100644 index 000000000..c67d9a7bc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/compose/TextCard.kt @@ -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 + ) + } +} diff --git a/app/src/main/res/layout/activity_book_search.xml b/app/src/main/res/layout/activity_book_search.xml index 8bd430d67..45281a987 100644 --- a/app/src/main/res/layout/activity_book_search.xml +++ b/app/src/main/res/layout/activity_book_search.xml @@ -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" /> diff --git a/app/version.properties b/app/version.properties index 0041ddb65..ea0ba9329 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,4 +1,4 @@ VERSION_MAJOR=3 VERSION_MINOR=26 -VERSION_PATCH=3 +VERSION_PATCH=4 VERSION_SUFFIX= \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2236bdb5..90af8e6c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }