代码优化
This commit is contained in:
@@ -64,7 +64,10 @@
|
||||
"PowerShell(npm *)",
|
||||
"PowerShell(gcc *)",
|
||||
"Bash(gh repo *)",
|
||||
"Bash(Test-Path *)"
|
||||
"Bash(Test-Path *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(Select-String -Pattern \"FAILED|error:\")",
|
||||
"Bash(Select-Object -First 5)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Test Build
|
||||
name: Auto Release
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
echo "build_number=$NEW_BUILD_NUM" >> $GITHUB_OUTPUT
|
||||
echo "commit_number=$COMMIT_COUNT" >> $GITHUB_OUTPUT
|
||||
echo "last_tag=$LAST_TAG_FOR_LOG" >> $GITHUB_OUTPUT
|
||||
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT # 新的输出标志
|
||||
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT # 新s的输出标志
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
@@ -1,140 +0,0 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# paths:
|
||||
# - 'CHANGELOG.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master'
|
||||
outputs:
|
||||
version: ${{ steps.set-ver.outputs.version }}
|
||||
play: ${{ steps.check.outputs.play }}
|
||||
sign: ${{ steps.check.outputs.sign }}
|
||||
steps:
|
||||
- id: set-ver
|
||||
run: |
|
||||
echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT
|
||||
- id: check
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.RELEASE_KEY_STORE }}" ]; then
|
||||
echo "sign=yes" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ ! -z "${{ secrets.SERVICE_ACCOUNT_JSON }}" ]; then
|
||||
echo "play=yes" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
if: ${{ needs.prepare.outputs.sign }}
|
||||
strategy:
|
||||
matrix:
|
||||
product: [ app, google ]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
product: ${{ matrix.product }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
play: ${{ needs.prepare.outputs.play }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
- name: Release Apk Sign
|
||||
run: |
|
||||
# not use this output
|
||||
# echo "KeyStore=yes" >> $GITHUB_OUTPUT
|
||||
echo -e "\n" >> gradle.properties
|
||||
echo RELEASE_KEY_ALIAS='${{ secrets.RELEASE_KEY_ALIAS }}' >> gradle.properties
|
||||
echo RELEASE_KEY_PASSWORD='${{ secrets.RELEASE_KEY_PASSWORD }}' >> gradle.properties
|
||||
echo RELEASE_STORE_PASSWORD='${{ secrets.RELEASE_STORE_PASSWORD }}' >> gradle.properties
|
||||
echo RELEASE_STORE_FILE='./key.jks' >> gradle.properties
|
||||
echo ${{ secrets.RELEASE_KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks
|
||||
|
||||
- name: Unify Version Name
|
||||
run: |
|
||||
echo "统一版本号"
|
||||
sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Build With Gradle
|
||||
run: |
|
||||
echo "开始进行${{ env.product }}构建"
|
||||
chmod +x gradlew
|
||||
./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all
|
||||
|
||||
- name: Organize the Files
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/apk/
|
||||
cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
|
||||
|
||||
- name: Upload App To Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: legado_${{ env.product }}
|
||||
path: ${{ github.workspace }}/apk/*.apk
|
||||
|
||||
- name: Release
|
||||
if: ${{ env.product == 'app' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: legado_app_${{ env.VERSION }}
|
||||
tag_name: ${{ env.VERSION }}
|
||||
body_path: ${{ github.workspace }}/CHANGELOG.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: ${{ github.workspace }}/apk/legado_app_*.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare For GooglePlay
|
||||
if: env.product == 'google' && env.play == 'yes'
|
||||
run: |
|
||||
mkdir -p ReleaseNotes
|
||||
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-en-US
|
||||
ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-zh-CN
|
||||
|
||||
- name: Release To GooglePlay
|
||||
if: env.product == 'google' && env.play == 'yes'
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.legado.play
|
||||
releaseFiles: ${{ github.workspace }}/apk/legado_google_*.apk
|
||||
track: production
|
||||
whatsNewDirectory: ${{ github.workspace }}/ReleaseNotes
|
||||
|
||||
- name: Push Assets To "release" Branch
|
||||
if: ${{ github.actor == 'gedoor' }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/apk/
|
||||
git init
|
||||
git checkout -b release
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release"
|
||||
git add *.apk
|
||||
git commit -m "${{ env.VERSION }}"
|
||||
git push -f -u origin release
|
||||
|
||||
- name: Purge Jsdelivr Cache
|
||||
if: ${{ github.actor == 'gedoor' }}
|
||||
run: |
|
||||
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.actor }}/release@release/)
|
||||
if echo $result |grep -q 'success.*true'; then
|
||||
echo "jsdelivr缓存更新成功"
|
||||
else
|
||||
echo $result
|
||||
fi
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import splitties.init.appCtx
|
||||
import splitties.systemservices.notificationManager
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -69,7 +70,7 @@ class CacheBookService : BaseService() {
|
||||
private val downloadJobLock = Any()
|
||||
private var downloadJob: Job? = null
|
||||
private var notificationContent = appCtx.getString(R.string.service_starting)
|
||||
private var mutex = Mutex()
|
||||
private val bookLocks = ConcurrentHashMap<String, Mutex>()
|
||||
private var lastDiagnosticsLogTime = 0L
|
||||
private val notificationBuilder by lazy {
|
||||
val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
|
||||
@@ -297,7 +298,8 @@ class CacheBookService : BaseService() {
|
||||
|
||||
if (chapterCount == 0) {
|
||||
cacheBook.setLoading()
|
||||
mutex.withLock {
|
||||
val bookMutex = bookLocks.getOrPut(request.bookUrl) { Mutex() }
|
||||
bookMutex.withLock {
|
||||
val name = book.name
|
||||
if (!admission.isCurrent()) {
|
||||
CacheBook.removeModelFromService(request.bookUrl, cacheBook)
|
||||
@@ -371,8 +373,8 @@ class CacheBookService : BaseService() {
|
||||
}
|
||||
|
||||
private fun admittedBookUrls(): Set<String> {
|
||||
return CacheBook.cacheBookMap.keys.toHashSet().apply {
|
||||
synchronized(admissionLock) {
|
||||
return synchronized(admissionLock) {
|
||||
CacheBook.cacheBookMap.keys.toHashSet().apply {
|
||||
addAll(admittingBookUrls)
|
||||
}
|
||||
}
|
||||
@@ -431,21 +433,24 @@ class CacheBookService : BaseService() {
|
||||
}
|
||||
|
||||
private fun finishAdmissionJob(bookUrl: String) {
|
||||
var restart = false
|
||||
val waiters = synchronized(admissionLock) {
|
||||
val buffer = admissionBuffers[bookUrl]
|
||||
if (buffer != null && buffer.isNotEmpty()) {
|
||||
restart = true
|
||||
emptyList()
|
||||
} else {
|
||||
admissionBuffers.remove(bookUrl)
|
||||
admittingBookUrls.remove(bookUrl)
|
||||
admissionIdleWaiters.remove(bookUrl).orEmpty()
|
||||
// New requests arrived while job was finishing — restart immediately.
|
||||
startAdmissionJob(bookUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (restart) {
|
||||
startAdmissionJob(bookUrl)
|
||||
return
|
||||
admissionBuffers.remove(bookUrl)
|
||||
admittingBookUrls.remove(bookUrl)
|
||||
// Re-check: a concurrent submitDownloadRequest may have added a request
|
||||
// between the buffer empty-check and the admittingBookUrls removal.
|
||||
val recheck = admissionBuffers[bookUrl]
|
||||
if (recheck != null && recheck.isNotEmpty()) {
|
||||
admittingBookUrls.add(bookUrl)
|
||||
startAdmissionJob(bookUrl)
|
||||
return
|
||||
}
|
||||
admissionIdleWaiters.remove(bookUrl).orEmpty()
|
||||
}
|
||||
waiters.forEach { it.complete(Unit) }
|
||||
}
|
||||
@@ -480,26 +485,23 @@ class CacheBookService : BaseService() {
|
||||
|
||||
private suspend fun runDownloadLoop() {
|
||||
try {
|
||||
var lastActiveTime = System.currentTimeMillis()
|
||||
var idleSince = -1L
|
||||
while (currentCoroutineContext().isActive) {
|
||||
drainPendingDownloadRequests()
|
||||
if (CacheBook.isGloballyPaused) {
|
||||
break
|
||||
}
|
||||
val isActiveNow = CacheBook.isRun
|
||||
|| hasPendingDownloadRequests()
|
||||
|| hasAdmittingRequests()
|
||||
if (isActiveNow) {
|
||||
lastActiveTime = System.currentTimeMillis()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if (!CacheBook.isRun) {
|
||||
if (System.currentTimeMillis() - lastActiveTime > MAX_IDLE_SPIN_MS) break
|
||||
delay(200)
|
||||
if (CacheBook.isRun) {
|
||||
idleSince = -1L
|
||||
CacheBook.startProcessJob(cachePool)
|
||||
continue
|
||||
}
|
||||
CacheBook.startProcessJob(cachePool)
|
||||
// !isRun — only keep looping while there is pending/admitting work.
|
||||
if (!hasPendingDownloadRequests() && !hasAdmittingRequests()) break
|
||||
val now = System.currentTimeMillis()
|
||||
if (idleSince < 0) idleSince = now
|
||||
if (now - idleSince > MAX_IDLE_SPIN_MS) break
|
||||
delay(200)
|
||||
}
|
||||
} finally {
|
||||
val finishedJob = currentCoroutineContext()[Job]
|
||||
|
||||
@@ -136,7 +136,9 @@ fun BookInfoScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
)
|
||||
@Composable
|
||||
private fun BookInfoScreenContent(
|
||||
state: BookInfoUiState,
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.config.themeManage
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -29,6 +30,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.config.ThemeExportData
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -74,7 +76,8 @@ fun EditThemeSheet(
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 32.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Name
|
||||
AppTextField(
|
||||
@@ -84,7 +87,6 @@ fun EditThemeSheet(
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -5,18 +5,21 @@ import android.os.Looper
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
@@ -119,14 +122,17 @@ fun ThemeManageScreen(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp
|
||||
)
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
SplicedColumnGroup {
|
||||
ClickableSettingItem(
|
||||
title = "保存当前设置",
|
||||
@@ -155,12 +161,12 @@ fun ThemeManageScreen(
|
||||
}
|
||||
|
||||
if (savedThemes.isNotEmpty()) {
|
||||
item {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
AppText(
|
||||
text = "已保存的主题",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp)
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -288,79 +294,112 @@ private fun SavedThemeItem(
|
||||
) {
|
||||
GlassCard(
|
||||
onClick = onApply,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
cornerRadius = 20.dp
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val previewColor = if (theme.data.themeColor != 0) {
|
||||
Color(theme.data.themeColor)
|
||||
} else if (theme.data.cPrimary != 0) {
|
||||
Color(theme.data.cPrimary)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
val lightPrimary = if (theme.data.themeColor != 0) Color(theme.data.themeColor)
|
||||
else if (theme.data.cPrimary != 0) Color(theme.data.cPrimary)
|
||||
else MaterialTheme.colorScheme.primary
|
||||
|
||||
val darkPrimary = if (theme.data.cNPrimary != 0) Color(theme.data.cNPrimary)
|
||||
else lightPrimary
|
||||
|
||||
val lightBg = if (theme.data.themeBackgroundColor != 0) Color(theme.data.themeBackgroundColor)
|
||||
else Color(0xFFF7F2FA)
|
||||
|
||||
val darkBg = if (theme.data.isPureBlack) Color.Black else Color(0xFF1C1B1F)
|
||||
|
||||
// 预览区域
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.2f)
|
||||
) {
|
||||
// 日间行
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
0.5f to lightBg,
|
||||
0.8f to lightPrimary
|
||||
)
|
||||
)
|
||||
) {
|
||||
AppText(
|
||||
text = "日间",
|
||||
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||
color = if (theme.data.primaryTextColor != 0) Color(theme.data.primaryTextColor).copy(alpha = 0.6f)
|
||||
else Color.Black.copy(alpha = 0.5f),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 夜间行
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
0.5f to darkBg,
|
||||
0.8f to darkPrimary
|
||||
)
|
||||
)
|
||||
) {
|
||||
AppText(
|
||||
text = "夜间",
|
||||
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(previewColor)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = theme.name,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1
|
||||
)
|
||||
val features = mutableListOf<String>()
|
||||
features.add(
|
||||
when (theme.data.appTheme) {
|
||||
"0" -> "动态取色"
|
||||
"12" -> "自定义颜色"
|
||||
else -> "预设主题"
|
||||
}
|
||||
)
|
||||
if (theme.data.enableBlur) features.add("模糊效果")
|
||||
if (theme.data.useFloatingBottomBar) features.add("浮动底栏")
|
||||
AppText(
|
||||
text = features.joinToString(" · "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onEdit) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "编辑",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onExport) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "导出",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "删除",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onEdit, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "编辑",
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onExport, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "导出",
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "删除",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -240,6 +241,7 @@ fun MainScreen(
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
WideNavigationRailItem(
|
||||
modifier = Modifier.testTag("nav_${destination.route}"),
|
||||
railExpanded = navState.targetValue == WideNavigationRailValue.Expanded,
|
||||
selected = selected,
|
||||
onClick = {
|
||||
@@ -308,6 +310,7 @@ fun MainScreen(
|
||||
MainDestination.My -> ThemeConfig.navIconMy
|
||||
}
|
||||
AppNavigationBarItem(
|
||||
modifier = Modifier.testTag("nav_${destination.route}"),
|
||||
selected = selected,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.ReportDrawnWhen
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
@@ -81,6 +82,7 @@ import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
@@ -145,6 +147,8 @@ fun BookshelfScreen(
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ReportDrawnWhen { uiState.groups.isNotEmpty() }
|
||||
|
||||
val activeOverlay = uiState.activeOverlay
|
||||
val showGroupMenu = activeOverlay == BookshelfOverlay.GroupMenu
|
||||
val isEditMode = uiState.isEditMode
|
||||
@@ -1215,6 +1219,7 @@ fun BookshelfPage(
|
||||
state = gridState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag("bookshelf_list")
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
@@ -20,7 +18,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
targetSdk = 37
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -31,24 +29,13 @@ android {
|
||||
productFlavors {
|
||||
create("app") { dimension = "mode" }
|
||||
}
|
||||
|
||||
// This code creates the gradle managed device used to generate baseline profiles.
|
||||
// To use GMD please invoke generation through the command line:
|
||||
// ./gradlew :app:generateBaselineProfile
|
||||
testOptions.managedDevices.allDevices {
|
||||
create<ManagedVirtualDevice>("pixel6Api34") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 34
|
||||
systemImageSource = "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the configuration block for the Baseline Profile plugin.
|
||||
// You can specify to run the generators on a managed devices or connected devices.
|
||||
baselineProfile {
|
||||
managedDevices += "pixel6Api34"
|
||||
useConnectedDevices = false
|
||||
// managedDevices += "pixel6Api34"
|
||||
useConnectedDevices = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -59,6 +46,11 @@ dependencies {
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { v ->
|
||||
if (v.buildType == "noR8" || v.buildType == "debug") {
|
||||
v.enable = false
|
||||
}
|
||||
}
|
||||
onVariants { v ->
|
||||
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
|
||||
v.instrumentationRunnerArguments.put(
|
||||
|
||||
+33
-36
@@ -4,32 +4,15 @@ import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Direction
|
||||
import androidx.test.uiautomator.Until
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* This test class generates a basic startup baseline profile for the target package.
|
||||
*
|
||||
* We recommend you start with this but add important user flows to the profile to improve their performance.
|
||||
* Refer to the [baseline profile documentation](https://d.android.com/topic/performance/baselineprofiles)
|
||||
* for more information.
|
||||
*
|
||||
* You can run the generator with the "Generate Baseline Profile" run configuration in Android Studio or
|
||||
* the equivalent `generateBaselineProfile` gradle task:
|
||||
* ```
|
||||
* ./gradlew :app:generateReleaseBaselineProfile
|
||||
* ```
|
||||
* The run configuration runs the Gradle task and applies filtering to run only the generators.
|
||||
*
|
||||
* Check [documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args)
|
||||
* for more information about available instrumentation arguments.
|
||||
*
|
||||
* After you run the generator, you can verify the improvements running the [StartupBenchmarks] benchmark.
|
||||
*
|
||||
* When using this class to generate a baseline profile, only API 33+ or rooted API 28+ are supported.
|
||||
*
|
||||
* The minimum required version of androidx.benchmark to generate a baseline profile is 1.2.0.
|
||||
**/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
@@ -40,29 +23,43 @@ class BaselineProfileGenerator {
|
||||
|
||||
@Test
|
||||
fun generate() {
|
||||
// The application id for the running build variant is read from the instrumentation arguments.
|
||||
rule.collect(
|
||||
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
|
||||
val packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: "io.legado.app"
|
||||
|
||||
// See: https://d.android.com/topic/performance/baselineprofiles/dex-layout-optimizations
|
||||
rule.collect(
|
||||
packageName = packageName,
|
||||
includeInStartupProfile = true
|
||||
) {
|
||||
// This block defines the app's critical user journey. Here we are interested in
|
||||
// optimizing for app startup. But you can also navigate and scroll through your most important UI.
|
||||
|
||||
// Start default activity for your app
|
||||
// 1. 启动应用
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
|
||||
// TODO Write more interactions to optimize advanced journeys of your app.
|
||||
// For example:
|
||||
// 1. Wait until the content is asynchronously loaded
|
||||
// 2. Scroll the feed content
|
||||
// 3. Navigate to detail screen
|
||||
// 2. 等待书架加载 (我们在 BookshelfScreen 中添加了 ReportDrawnWhen)
|
||||
// 等待书架列表 testTag 出现
|
||||
device.wait(Until.hasObject(By.res(packageName, "bookshelf_list")), 10000)
|
||||
|
||||
// 3. 模拟滑动书架 (优化列表渲染性能)
|
||||
val bookshelf = device.findObject(By.res(packageName, "bookshelf_list"))
|
||||
bookshelf?.apply {
|
||||
setGestureMargin(device.displayWidth / 10)
|
||||
fling(Direction.DOWN)
|
||||
device.waitForIdle()
|
||||
fling(Direction.UP)
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
// 4. 遍历主要 Tab (优化 Compose 导航和各页面初始化)
|
||||
val tabs = listOf("nav_explore", "nav_rss", "nav_my", "nav_bookshelf")
|
||||
for (tabTag in tabs) {
|
||||
val tab = device.findObject(By.res(packageName, tabTag))
|
||||
if (tab != null) {
|
||||
tab.click()
|
||||
device.waitForIdle()
|
||||
// 稍微等待页面加载
|
||||
Thread.sleep(500)
|
||||
}
|
||||
}
|
||||
|
||||
// Check UiAutomator documentation for more information how to interact with the app.
|
||||
// https://d.android.com/training/testing/other-components/ui-automator
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,14 @@ import androidx.benchmark.macro.BaselineProfileMode
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.FrameTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Direction
|
||||
import androidx.test.uiautomator.Until
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -49,27 +53,32 @@ class StartupBenchmarks {
|
||||
|
||||
private fun benchmark(compilationMode: CompilationMode) {
|
||||
// The application id for the running build variant is read from the instrumentation arguments.
|
||||
val packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: "io.legado.app"
|
||||
|
||||
rule.measureRepeated(
|
||||
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
packageName = packageName,
|
||||
metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
startupMode = StartupMode.COLD,
|
||||
iterations = 10,
|
||||
iterations = 3,
|
||||
setupBlock = {
|
||||
pressHome()
|
||||
},
|
||||
measureBlock = {
|
||||
startActivityAndWait()
|
||||
|
||||
// TODO Add interactions to wait for when your app is fully drawn.
|
||||
// The app is fully drawn when Activity.reportFullyDrawn is called.
|
||||
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
|
||||
// from the AndroidX Activity library.
|
||||
// 等待书架列表加载完成
|
||||
// 我们在 BookshelfScreen 中添加了 testTag("bookshelf_list")
|
||||
device.wait(Until.hasObject(By.res(packageName, "bookshelf_list")), 10000)
|
||||
|
||||
// Check the UiAutomator documentation for more information on how to
|
||||
// interact with the app.
|
||||
// https://d.android.com/training/testing/other-components/ui-automator
|
||||
// 模拟交互:在书架上进行滑动,以确保 Profile 捕获到列表渲染和图片加载的路径
|
||||
val bookshelfList = device.findObject(By.res(packageName, "bookshelf_list"))
|
||||
bookshelfList?.apply {
|
||||
setGestureMargin(device.displayWidth / 10)
|
||||
fling(Direction.DOWN)
|
||||
device.waitForIdle()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ plugins {
|
||||
alias(libs.plugins.google.services) apply false
|
||||
alias(libs.plugins.room) apply false
|
||||
alias(libs.plugins.download) apply false
|
||||
alias(libs.plugins.android.test) apply false
|
||||
alias(libs.plugins.baselineprofile) apply false
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
|
||||
+2
-2
@@ -45,5 +45,5 @@ android.uniquePackageNames=false
|
||||
android.dependency.useConstraints=true
|
||||
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false
|
||||
android.r8.strictFullModeForKeepRules=false
|
||||
|
||||
|
||||
org.gradle.configuration-cache=true
|
||||
android.newDsl=false
|
||||
+26
-18
@@ -1,27 +1,27 @@
|
||||
[versions]
|
||||
|
||||
accompanistWebview = "0.36.0"
|
||||
adaptive = "1.3.0-alpha10"
|
||||
adaptiveLayout = "1.3.0-alpha10"
|
||||
adaptiveNavigation = "1.3.0-alpha10"
|
||||
animation = "1.10.6"
|
||||
adaptive = "1.3.0-beta01"
|
||||
adaptiveLayout = "1.3.0-beta01"
|
||||
adaptiveNavigation = "1.3.0-beta01"
|
||||
animation = "1.11.1"
|
||||
biometric = "1.4.0-alpha06"
|
||||
coilCompose = "2.7.0"
|
||||
composeBom = "2026.03.01"
|
||||
composeBom = "2026.05.00"
|
||||
constraintlayoutCompose = "1.1.1"
|
||||
coreSplashscreen = "1.2.0"
|
||||
datastorePreferences = "1.2.1"
|
||||
foundation = "1.10.6"
|
||||
foundation = "1.11.1"
|
||||
kotlin = "2.3.10"
|
||||
kotlinxSerialization = "1.10.0"
|
||||
kotlinxSerialization = "1.11.0"
|
||||
kotlinxCoroutinesAndroid = "1.10.2"
|
||||
kotlinxSerializationJson = "1.10.0"
|
||||
kotlinxSerializationJson = "1.11.0"
|
||||
kotlinxCollectionsImmutable = "0.4.0"
|
||||
ksp = "2.3.6"
|
||||
agp = "9.1.1"
|
||||
agp = "9.2.1"
|
||||
appcompat = "1.7.1"
|
||||
colorpicker = "1.1.0"
|
||||
colorpicker-compose = "1.1.3"
|
||||
colorpicker-compose = "1.1.4"
|
||||
commonsText = "1.15.0"
|
||||
constraintlayout = "2.2.1"
|
||||
core = "1.18.0"
|
||||
@@ -34,7 +34,7 @@ hutool = "5.8.22"
|
||||
|
||||
libarchive = "1.1.6"
|
||||
lifecycle = "2.10.0"
|
||||
glide = "5.0.5"
|
||||
glide = "5.0.7"
|
||||
gson = "2.13.2"
|
||||
jsonPath = "2.10.0"
|
||||
# issue #3811,不要更新版本,新版引入了一个破坏性变更(详见https://github.com/jhy/jsoup/pull/2017)
|
||||
@@ -44,17 +44,17 @@ jsoupxpath = "2.5.3"
|
||||
coroutines = "1.10.2"
|
||||
lifecycleViewmodelCompose = "2.10.0"
|
||||
liveeventbus = "1.8.14"
|
||||
navigation3 = "1.0.1"
|
||||
navigation3 = "1.1.1"
|
||||
markdownCompose = "0.5.0"
|
||||
markwon = "4.6.2"
|
||||
material = "1.14.0-beta01"
|
||||
material3 = "1.5.0-alpha17"
|
||||
material = "1.14.0-rc01"
|
||||
material3 = "1.5.0-alpha19"
|
||||
material3IconsExtended = "1.7.8"
|
||||
materialKolor = "4.1.1"
|
||||
media = "1.7.1"
|
||||
media3 = "1.8.0"
|
||||
nanoHttpd = "2.3.1"
|
||||
navigationCompose = "2.9.7"
|
||||
navigationCompose = "2.9.8"
|
||||
okhttp = "5.3.2"
|
||||
palette = "1.0.0"
|
||||
preference = "1.2.1"
|
||||
@@ -74,21 +74,25 @@ recyclerview = "1.4.0"
|
||||
#noinspection GradleDependency
|
||||
uiViewbinding = "1.9.4"
|
||||
viewpager2 = "1.1.0"
|
||||
webkit = "1.15.0"
|
||||
webkit = "1.16.0"
|
||||
collection = "1.6.0"
|
||||
|
||||
zxingLite = "3.3.0"
|
||||
|
||||
koin-bom = "4.2.0"
|
||||
koin-bom = "4.2.1"
|
||||
biometricKtx = "1.1.0"
|
||||
|
||||
reorderable = "3.0.0"
|
||||
reorderable = "3.1.0"
|
||||
haze = "1.7.2"
|
||||
timber = "5.0.1"
|
||||
miuix = "0.9.0"
|
||||
backdrop = "1.0.6"
|
||||
capsule = "2.1.3"
|
||||
lyricViewx = "1.3.2"
|
||||
uiautomator = "2.3.0"
|
||||
benchmarkMacroJunit4 = "1.4.1"
|
||||
baselineprofile = "1.4.1"
|
||||
profileinstaller = "1.4.1"
|
||||
[libraries]
|
||||
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
|
||||
@@ -260,6 +264,9 @@ miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.
|
||||
capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
|
||||
backdrop = { module = "io.github.kyant0:backdrop", version.ref = "backdrop" }
|
||||
lyricViewx = { module = "com.github.Moriafly:LyricViewX", version.ref = "lyricViewx" }
|
||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||
|
||||
[bundles]
|
||||
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
|
||||
@@ -291,3 +298,4 @@ room = { id = "androidx.room", version.ref = "room" }
|
||||
#protobuf = { id = "com.google.protobuf", version = "0.9.4" }
|
||||
|
||||
download = { id = "de.undercouch.download", version = "5.6.0" }
|
||||
baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -53,3 +53,4 @@ rootProject.name = 'legado'
|
||||
include ':app'
|
||||
include ':modules:book'
|
||||
include ':modules:rhino'
|
||||
include ':baselineprofile'
|
||||
|
||||
Reference in New Issue
Block a user