diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6374c06..771aa14 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,55 +4,78 @@ on: push: branches: - "master" - paths: - - "CHANGELOG.md" + paths-ignore: + - "*.md" + - "*.sh" + - "build.yaml" + # - "sync_frp.yaml" + workflow_dispatch: jobs: - build: + go: + runs-on: ubuntu-latest + strategy: + matrix: + GO_ARCH: [ "386", amd64, arm, arm64 ] + steps: + - uses: actions/checkout@v3 + + - name: Download AList Source Code + run: | + cd $GITHUB_WORKSPACE/alist-lib/scripts + chmod +x *.sh + ./init_alist_core.sh + ./init_alist_web.sh + + - uses: actions/setup-go@v4 + with: + go-version: 1.21.5 + cache-dependency-path: ${{ github.workspace }}/alist-lib/go.sum + + - name: Build + run: | + cd $GITHUB_WORKSPACE/alist-lib + + GOARCH=${{ matrix.GO_ARCH }} + + declare -A goarch2cc=( ["arm64"]="aarch64-linux-android32-clang" ["arm"]="armv7a-linux-androideabi32-clang" ["amd64"]="x86_64-linux-android32-clang" ["386"]="i686-linux-android32-clang") + export CC="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/${goarch2cc[$GOARCH]}" + + declare -A arch2lib=( ["arm64"]="arm64-v8a" ["arm"]="armeabi-v7a" ["amd64"]="x86_64" ["386"]="x86") + export LIB="${arch2lib[$GOARCH]}" + + ./scripts/install_alist.sh $GOARCH $LIB + + - name: Upload to Artifact + uses: actions/upload-artifact@v3 + with: + name: "app_libs" + path: "${{ github.workspace }}/app/libs" + + android: runs-on: ubuntu-latest + needs: [ go ] env: output: "${{ github.workspace }}/app/build/outputs/apk/release" steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-java@v3 with: distribution: temurin java-version: 17 - - uses: actions/setup-go@v4 - with: - go-version: 1.20.3 - cache: false - cache-dependency-path: ${{ github.workspace }}/alist-lib/go.sum + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 - - uses: actions/cache@v3 - name: Cache Go Modules + - name: Download Artifact + uses: actions/download-artifact@v3 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: go-${{ hashFiles('**/go.sum') }} - restore-keys: | - go- - - - name: Build Alist-lib - run: | - cd alist-lib/scripts - chmod +x *.sh - - ./install_alist.sh - ./install_web.sh - - go install golang.org/x/mobile/cmd/gomobile@latest - gomobile init - go get golang.org/x/mobile/bind - ./install_aar.sh all - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2.4.2 + name: "app_libs" + path: "${{ github.workspace }}/app/libs" - name: Init Signature run: | @@ -67,18 +90,40 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle + id: gradle run: ./gradlew assembleRelease -build-cache --parallel --daemon --warning-mode all + - name: Upload missing_rules.txt + if: failure() && steps.gradle.outcome != 'success' + uses: actions/upload-artifact@v3 + with: + name: "missing_rules" + path: "${{ github.workspace }}/app/build/outputs/mapping/release/missing_rules.txt" + - name: Init APP Version Name run: | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV - - name: Upload App To Artifact + - name: Upload App To Artifact arm64-v8a + if: success () || failure () + uses: actions/upload-artifact@v3 + with: + name: "AListAndroid-v${{ env.ver_name }}_arm64-v8a" + path: "${{ env.output }}/*-v8a.apk" + + - name: Upload App To Artifact arm-v7a + if: success () || failure () + uses: actions/upload-artifact@v3 + with: + name: "AListAndroid-v${{ env.ver_name }}_arm-v7a" + path: "${{ env.output }}/*-v7a.apk" + + - name: Upload App To Artifact x86 if: success () || failure () uses: actions/upload-artifact@v3 with: - name: "AlistAndroid_${{env.ver_name}}" - path: ${{ env.output }}/*.apk + name: "AListAndroid-v${{ env.ver_name }}_x86" + path: "${{ env.output }}/*_x86.apk" - uses: softprops/action-gh-release@v0.1.15 with: diff --git a/app/build.gradle b/app/build.gradle index 17eeb5e..1dab8e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -167,6 +167,7 @@ dependencies { def accompanistVersion = "0.31.3-beta" implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}") implementation("com.google.accompanist:accompanist-navigation-animation:${accompanistVersion}") + implementation("com.google.accompanist:accompanist-permissions:${accompanistVersion}") implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") diff --git a/app/src/main/java/com/github/jing332/alistandroid/model/ShortCuts.kt b/app/src/main/java/com/github/jing332/alistandroid/model/ShortCuts.kt index a51f9e7..0081501 100644 --- a/app/src/main/java/com/github/jing332/alistandroid/model/ShortCuts.kt +++ b/app/src/main/java/com/github/jing332/alistandroid/model/ShortCuts.kt @@ -2,7 +2,6 @@ package com.github.jing332.alistandroid.model import android.content.Context import android.content.Intent -import android.graphics.Color import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat @@ -21,8 +20,8 @@ object ShortCuts { private fun buildAlistSwitchShortCutInfo(context: Context): ShortcutInfoCompat { val msSwitchIntent = buildIntent(context) return ShortcutInfoCompat.Builder(context, "alist_switch") - .setShortLabel(context.getString(R.string.alist_switch)) - .setLongLabel(context.getString(R.string.alist_switch)) + .setShortLabel(context.getString(R.string.app_switch)) + .setLongLabel(context.getString(R.string.app_switch)) .setIcon(IconCompat.createWithResource(context, R.drawable.alist_switch)) .setIntent(msSwitchIntent) .build() diff --git a/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsScreen.kt b/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsScreen.kt index bf840c1..6fed9d1 100644 --- a/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsScreen.kt +++ b/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsScreen.kt @@ -1,53 +1,155 @@ package com.github.jing332.alistandroid.ui.nav.settings +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowCircleUp +import androidx.compose.material.icons.filled.ScreenLockPortrait +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect 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.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.github.jing332.alistandroid.R import com.github.jing332.alistandroid.config.AppConfig import com.github.jing332.alistandroid.ui.MyTools.isIgnoringBatteryOptimizations import com.github.jing332.alistandroid.ui.MyTools.killBattery +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +@OptIn(ExperimentalPermissionsApi::class) @Composable fun SettingsScreen() { val context = LocalContext.current Column(Modifier.statusBarsPadding()) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DividerPreference { + Text(stringResource(id = R.string.importent_settings)) + } + + AnimatedVisibility(visible = !context.isIgnoringBatteryOptimizations()) { + BasePreferenceWidget( + onClick = { context.killBattery() }, + title = { + Text( + stringResource(R.string.grant_battery_whiltelist), + color = MaterialTheme.colorScheme.error + ) + }, + subTitle = { Text(stringResource(R.string.grant_battery_whiltelist_desc)) }) { + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // A11 + var isGranted by remember { mutableStateOf(Environment.isExternalStorageManager()) } + val permissionCheckerObserver = remember { + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + isGranted = Environment.isExternalStorageManager() + } + } + } + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle, permissionCheckerObserver) { + lifecycle.addObserver(permissionCheckerObserver) + onDispose { lifecycle.removeObserver(permissionCheckerObserver) } + } + + BasePreferenceWidget( + onClick = { + context.startActivity(Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { + setData(Uri.parse("package:${context.packageName}")) + }) + }, + title = { + Text( + stringResource(id = R.string.all_files_manage_permission), + color = if (isGranted) Color.Companion.Unspecified else MaterialTheme.colorScheme.error, + ) + }, + subTitle = { Text(stringResource(id = R.string.files_permission_desc)) }, + content = { + Checkbox(enabled = false, checked = isGranted, onCheckedChange = {}) + }) + + } else { // < A11 + val readPermission = + rememberPermissionState(permission = Manifest.permission.READ_EXTERNAL_STORAGE) + AnimatedVisibility(visible = !readPermission.status.isGranted) { + BasePreferenceWidget( + onClick = { readPermission.launchPermissionRequest() }, + title = { + Text( + stringResource(id = R.string.read_external_storage_permission), + color = MaterialTheme.colorScheme.error + ) + }, + subTitle = { Text(stringResource(id = R.string.files_permission_desc)) } + ) + } + + val writePermission = + rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) + AnimatedVisibility(visible = !writePermission.status.isGranted) { + BasePreferenceWidget( + onClick = { writePermission.launchPermissionRequest() }, + title = { + Text( + stringResource(id = R.string.write_external_storage_permission), + color = MaterialTheme.colorScheme.error + ) + }, + subTitle = { Text(stringResource(id = R.string.files_permission_desc)) } + ) + } + } + + } + + DividerPreference { Text(text = stringResource(id = R.string.app_switch)) } + var checkUpdate by remember { AppConfig.isAutoCheckUpdate } - PreferenceSwitch( - title = { Text("自动检查更新") }, - subTitle = { Text("打开程序主界面时从Github检查更新") }, + SwitchPreference( + title = { Text(stringResource(R.string.auto_check_updates)) }, + subTitle = { Text(stringResource(R.string.auto_check_updates_desc)) }, checked = checkUpdate, onCheckedChange = { checkUpdate = it }, - icon = { - Icon(Icons.Default.ArrowCircleUp, contentDescription = null) - } + icon = { Icon(Icons.Default.ArrowCircleUp, contentDescription = null) } ) var enabledWakeLock by remember { AppConfig.enabledWakeLock } - PreferenceSwitch( - title = { Text("唤醒锁") }, - subTitle = { Text("打开可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台") }, + SwitchPreference( + title = { Text(stringResource(R.string.wake_lock)) }, + subTitle = { Text(stringResource(R.string.wake_lock_desc)) }, checked = enabledWakeLock, - onCheckedChange = { enabledWakeLock = it } + onCheckedChange = { enabledWakeLock = it }, + icon = { Icon(Icons.Default.ScreenLockPortrait, contentDescription = null) } ) - AnimatedVisibility(visible = !context.isIgnoringBatteryOptimizations()) { - BasePreferenceWidget( - onClick = { - context.killBattery() - }, - title = { Text("请求设置电池优化白名单") }, - subTitle = { Text("如果程序在后台运行时被系统杀死,可以尝试设置。") }) { - - } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsWidget.kt b/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsWidget.kt index 991eff7..7a64406 100644 --- a/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsWidget.kt +++ b/app/src/main/java/com/github/jing332/alistandroid/ui/nav/settings/SettingsWidget.kt @@ -6,16 +6,20 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -23,51 +27,81 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.github.jing332.text_searcher.ui.widgets.LabelSlider +import com.github.jing332.alistandroid.ui.widgets.AppDialog +import com.github.jing332.alistandroid.ui.widgets.LabelSlider @Composable -internal fun PreferenceSwitch( +internal fun DropdownPreference( modifier: Modifier = Modifier, - title: @Composable ColumnScope.() -> Unit, - subTitle: @Composable ColumnScope.() -> Unit, - icon: @Composable () -> Unit = {}, - - checked: Boolean, - onCheckedChange: (Boolean) -> Unit + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + icon: @Composable () -> Unit, + title: @Composable () -> Unit, + subTitle: @Composable () -> Unit, + actions: @Composable ColumnScope. () -> Unit = {} ) { - Row(modifier = modifier - .minimumInteractiveComponentSize() - .clip(MaterialTheme.shapes.extraSmall) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple() - ) { - onCheckedChange(!checked) - } - .padding(8.dp) - ) { - Column(Modifier.align(Alignment.CenterVertically)) { - icon(); + BasePreferenceWidget(modifier = modifier, icon = icon, onClick = { + onExpandedChange(true) + }, title = title, subTitle = subTitle) { + DropdownMenu( + modifier = Modifier.align(Alignment.Top), + expanded = expanded, + onDismissRequest = { onExpandedChange(false) }) { + actions() } + } +} - Column( +@Composable +internal fun DividerPreference(title: @Composable () -> Unit) { + Column(Modifier.padding(top = 4.dp)) { + HorizontalDivider(thickness = 0.5.dp) + Row( Modifier - .weight(1f) - .align(Alignment.CenterVertically) - .padding(start = 8.dp) + .padding(vertical = 8.dp) + .align(Alignment.CenterHorizontally) ) { - CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ), + ) { title() } - - CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleSmall) { - subTitle() - } } - Switch(checked = checked, onCheckedChange = onCheckedChange) } + +} + +@Composable +internal fun SwitchPreference( + modifier: Modifier = Modifier, + title: @Composable () -> Unit, + subTitle: @Composable () -> Unit, + icon: @Composable () -> Unit = {}, + + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + BasePreferenceWidget( + modifier = modifier, + onClick = { onCheckedChange(!checked) }, + title = title, + subTitle = subTitle, + icon = icon, + content = { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + Modifier.align(Alignment.CenterVertically) + ) + } + ) } @Composable @@ -75,12 +109,13 @@ internal fun BasePreferenceWidget( modifier: Modifier = Modifier, onClick: () -> Unit, title: @Composable () -> Unit, - subTitle: @Composable () -> Unit, + subTitle: @Composable () -> Unit = {}, icon: @Composable () -> Unit = {}, - content: @Composable RowScope.() -> Unit, + content: @Composable RowScope.() -> Unit = {}, ) { Row(modifier = modifier .minimumInteractiveComponentSize() + .defaultMinSize(minHeight = 64.dp) .clip(MaterialTheme.shapes.extraSmall) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -90,18 +125,23 @@ internal fun BasePreferenceWidget( } .padding(8.dp) ) { - icon() + Column( + Modifier.align(Alignment.CenterVertically) + ) { + icon() + } Column( Modifier .weight(1f) .align(Alignment.CenterVertically) + .padding(horizontal = 8.dp) ) { - CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleLarge) { + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) { title() } - CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleSmall) { + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) { subTitle() } } @@ -113,25 +153,34 @@ internal fun BasePreferenceWidget( } @Composable -internal fun PreferenceSlider( +internal fun SliderPreference( title: @Composable () -> Unit, subTitle: @Composable () -> Unit, + icon: @Composable () -> Unit = {}, value: Float, onValueChange: (Float) -> Unit, valueRange: ClosedFloatingPointRange = 0f..1f, steps: Int = 0, label: @Composable (title: @Composable () -> Unit) -> Unit, ) { + val view = LocalView.current + LaunchedEffect(value) { + view.announceForAccessibility(value.toString()) + } + PreferenceDialog(title = title, subTitle = subTitle, dialogContent = { LabelSlider( + modifier = Modifier.padding(vertical = 16.dp), value = value, onValueChange = onValueChange, valueRange = valueRange, - steps = steps + steps = steps, + buttonSteps = 1f, + buttonLongSteps = 2f ) { label(title) } - }, + }, icon = icon, endContent = { label(title) } @@ -143,28 +192,26 @@ internal fun PreferenceDialog( modifier: Modifier = Modifier, title: @Composable () -> Unit, subTitle: @Composable () -> Unit, + icon: @Composable () -> Unit, dialogContent: @Composable ColumnScope.() -> Unit, endContent: @Composable RowScope.() -> Unit = {}, ) { var showDialog by remember { mutableStateOf(false) } if (showDialog) { - Dialog(onDismissRequest = { showDialog = false }) { - Surface( - modifier = Modifier.fillMaxWidth(), - tonalElevation = 4.dp, - shape = MaterialTheme.shapes.small, - ) { - Column(Modifier.padding(8.dp)) { - title() + AppDialog( + title = title, + content = { + Column { dialogContent() } - } - } + }, + onDismissRequest = { showDialog = false } + ) } BasePreferenceWidget(modifier, onClick = { showDialog = true - }, title = title, subTitle = subTitle) { + }, title = title, icon = icon, subTitle = subTitle) { endContent() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/AppDialog.kt b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/AppDialog.kt new file mode 100644 index 0000000..67a5748 --- /dev/null +++ b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/AppDialog.kt @@ -0,0 +1,212 @@ +package com.github.jing332.alistandroid.ui.widgets + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.github.jing332.alistandroid.R +import kotlin.math.max + +@Preview +@Composable +fun PreviewAppDialog() { + var show by remember { mutableStateOf(true) } + if (show) { + AppDialog(title = { + Text("Title") + }, content = { + Column(Modifier.verticalScroll(rememberScrollState())) { + for (i in 0..50) { + Text("Content") + } + } + }, buttons = { + TextButton(onClick = { + show = false + }) { + Text("Cancel") + } + TextButton(onClick = { + show = false + }) { + Text("OK") + } + }, onDismissRequest = { + show = false + }) + } + +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppDialog( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, + properties: DialogProperties = DialogProperties(), + title: @Composable () -> Unit, + content: @Composable BoxScope.() -> Unit, + dialogContentPadding: PaddingValues = PaddingValues(12.dp), + buttons: @Composable BoxScope.() -> Unit = { + TextButton(onClick = onDismissRequest) { Text(stringResource(id = R.string.close)) } + }, +) = BasicAlertDialog(modifier = modifier, onDismissRequest = onDismissRequest, properties = properties) { + Surface( + tonalElevation = 8.dp, shadowElevation = 8.dp, shape = MaterialTheme.shapes.extraLarge + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(dialogContentPadding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleLarge) { + title() + } + } + + Box( + Modifier + .weight(weight = 1f, fill = false) + .align(Alignment.Start) + ) { + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) { + content() + } + } + + + Box(modifier = Modifier.align(Alignment.End)) { + AppDialogFlowRow( + mainAxisSpacing = ButtonsMainAxisSpacing, + crossAxisSpacing = ButtonsCrossAxisSpacing + ) { + buttons() + } + } + } + } +} + + +@Suppress("UnnecessaryVariable") +@Composable +internal fun AppDialogFlowRow( + mainAxisSpacing: Dp, crossAxisSpacing: Dp, content: @Composable () -> Unit +) { + Layout(content) { measurables, constraints -> + val sequences = mutableListOf>() + val crossAxisSizes = mutableListOf() + val crossAxisPositions = mutableListOf() + + var mainAxisSpace = 0 + var crossAxisSpace = 0 + + val currentSequence = mutableListOf() + var currentMainAxisSize = 0 + var currentCrossAxisSize = 0 + + // Return whether the placeable can be added to the current sequence. + fun canAddToCurrentSequence(placeable: Placeable) = + currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.width <= constraints.maxWidth + + // Store current sequence information and start a new sequence. + fun startNewSequence() { + if (sequences.isNotEmpty()) { + crossAxisSpace += crossAxisSpacing.roundToPx() + } + // Ensures that confirming actions appear above dismissive actions. + sequences.add(0, currentSequence.toList()) + crossAxisSizes += currentCrossAxisSize + crossAxisPositions += crossAxisSpace + + crossAxisSpace += currentCrossAxisSize + mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) + + currentSequence.clear() + currentMainAxisSize = 0 + currentCrossAxisSize = 0 + } + + for (measurable in measurables) { + // Ask the child for its preferred size. + val placeable = measurable.measure(constraints) + + // Start a new sequence if there is not enough space. + if (!canAddToCurrentSequence(placeable)) startNewSequence() + + // Add the child to the current sequence. + if (currentSequence.isNotEmpty()) { + currentMainAxisSize += mainAxisSpacing.roundToPx() + } + currentSequence.add(placeable) + currentMainAxisSize += placeable.width + currentCrossAxisSize = max(currentCrossAxisSize, placeable.height) + } + + if (currentSequence.isNotEmpty()) startNewSequence() + + val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth) + + val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight) + + val layoutWidth = mainAxisLayoutSize + + val layoutHeight = crossAxisLayoutSize + + layout(layoutWidth, layoutHeight) { + sequences.forEachIndexed { i, placeables -> + val childrenMainAxisSizes = IntArray(placeables.size) { j -> + placeables[j].width + if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 + } + val arrangement = Arrangement.End + val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } + with(arrangement) { + arrange( + mainAxisLayoutSize, + childrenMainAxisSizes, + layoutDirection, + mainAxisPositions + ) + } + placeables.forEachIndexed { j, placeable -> + placeable.place( + x = mainAxisPositions[j], y = crossAxisPositions[i] + ) + } + } + } + } +} + +private val ButtonsMainAxisSpacing = 8.dp +private val ButtonsCrossAxisSpacing = 12.dp \ No newline at end of file diff --git a/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LabelSlider.kt b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LabelSlider.kt index e02ef9a..a751690 100644 --- a/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LabelSlider.kt +++ b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LabelSlider.kt @@ -1,13 +1,26 @@ -package com.github.jing332.text_searcher.ui.widgets +package com.github.jing332.alistandroid.ui.widgets +import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout @@ -21,8 +34,22 @@ fun LabelSlider( valueRange: ClosedFloatingPointRange = 0f..1f, steps: Int = 0, onValueChangeFinished: (() -> Unit)? = null, - text: @Composable BoxScope.() -> Unit + + showButton: Boolean = true, + buttonSteps: Float = 0.01f, + buttonLongSteps: Float = 0.1f, + + onValueRemove: (Boolean) -> Unit = { + onValueChange(value - (if (it) buttonLongSteps else buttonSteps)) + }, + onValueAdd: (Boolean) -> Unit = { + onValueChange(value + if (it) buttonLongSteps else buttonSteps) + }, + + a11yDescription: String = "", + text: @Composable BoxScope.() -> Unit, ) { + val view = LocalView.current ConstraintLayout(modifier) { val (textRef, sliderRef) = createRefs() Box( @@ -33,30 +60,83 @@ fun LabelSlider( end.linkTo(parent.end) } .wrapContentHeight() - ) { text() } - Slider( - modifier = Modifier - .fillMaxWidth() - .constrainAs(sliderRef) { - start.linkTo(parent.start) - end.linkTo(parent.end) + ) { + text() + } + Row(Modifier.constrainAs(sliderRef) { + start.linkTo(parent.start) + end.linkTo(parent.end) + top.linkTo(textRef.bottom, margin = (-12).dp) + }) { + if (showButton) + LongClickIconButton( + onClick = { + Log.e("BUG大无语事件", value.toString()) + onValueRemove(false) + }, + onLongClick = { + Log.e("BUG大无语事件", value.toString()) + onValueRemove(true) + }, + enabled = value > valueRange.start, + modifier = Modifier + .semantics { + stateDescription = a11yDescription + contentDescription = a11yDescription + } + ) { + Icon(Icons.Default.Remove, "remove") + } + Slider( + modifier = Modifier + .weight(1f) + .semantics { + stateDescription = a11yDescription + contentDescription = a11yDescription + }, + value = value, + onValueChange = onValueChange, + enabled = enabled, + valueRange = valueRange, + steps = steps, + onValueChangeFinished = onValueChangeFinished + ) + if (showButton) + LongClickIconButton( + onClick = { + Log.e("BUG大无语事件", value.toString()) + onValueAdd(false) + }, + onLongClick = { + Log.e("BUG大无语事件", value.toString()) + onValueAdd(true) + }, + enabled = value < valueRange.endInclusive, + modifier = Modifier + .semantics { + stateDescription = a11yDescription + contentDescription = a11yDescription + } + ) { + Icon(Icons.Default.Add, "add") + } - top.linkTo(textRef.bottom, margin = (-16).dp) - }, - value = value, - onValueChange = onValueChange, - enabled = enabled, - valueRange = valueRange, - steps = steps, - onValueChangeFinished = onValueChangeFinished - ) + } } } @Preview @Composable fun PreviewSlider() { - LabelSlider(value = 0f, onValueChange = {}) { - Text("Hello World") + var value by remember { mutableFloatStateOf(0f) } + val str = "语速: $value" + LabelSlider( + value = value, + onValueChange = { value = it }, + valueRange = 0.1f..3.0f, + a11yDescription = str, + buttonSteps = 0.1f + ) { + Text(str) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LongClickIconButton.kt b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LongClickIconButton.kt new file mode 100644 index 0000000..a38dc88 --- /dev/null +++ b/app/src/main/java/com/github/jing332/alistandroid/ui/widgets/LongClickIconButton.kt @@ -0,0 +1,82 @@ +package com.github.jing332.alistandroid.ui.widgets + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.github.jing332.alistandroid.util.AndroidUtils.performLongPress + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LongClickIconButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + onLongClickLabel: String? = null, + enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit +) { + val stateLayerSize = 40.0.dp + val context = LocalContext.current + val view = LocalView.current + Box( + modifier = modifier + .minimumInteractiveComponentSize() + .size(stateLayerSize) + .clip(CircleShape) + .background(color = colors.mContainerColor(enabled).value) + .combinedClickable( + onClick = onClick, + onLongClick = { + view.performLongPress() + onLongClick() + }, + onLongClickLabel = onLongClickLabel, + enabled = enabled, + role = Role.Button, + interactionSource = interactionSource, + indication = rememberRipple( + bounded = false, + radius = stateLayerSize / 2 + ) + ), + contentAlignment = Alignment.Center + ) { + val contentColor = colors.mContentColor(enabled).value + CompositionLocalProvider(LocalContentColor provides contentColor, content = content) + content() + } +} + +@Composable +internal fun IconButtonColors.mContainerColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) +} + +@Composable +internal fun IconButtonColors.mContentColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/jing332/alistandroid/util/AndroidUtils.kt b/app/src/main/java/com/github/jing332/alistandroid/util/AndroidUtils.kt index 0c38c69..eb74d79 100644 --- a/app/src/main/java/com/github/jing332/alistandroid/util/AndroidUtils.kt +++ b/app/src/main/java/com/github/jing332/alistandroid/util/AndroidUtils.kt @@ -1,6 +1,9 @@ package com.github.jing332.alistandroid.util import android.os.Build +import android.view.View +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.core.view.HapticFeedbackConstantsCompat object AndroidUtils { const val ABI_ARM = "armeabi-v7a" @@ -12,4 +15,9 @@ object AndroidUtils { return Build.SUPPORTED_ABIS[0] } + fun View.performLongPress() { + isHapticFeedbackEnabled = true + performHapticFeedback(HapticFeedbackConstantsCompat.LONG_PRESS) + } + } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7bd850..e5e87ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,7 +25,7 @@ 密码 启动中 关闭中 - 开关 + 开关 更多选项 关于 监听地址 @@ -36,4 +36,16 @@ 路径已复制 检查更新 启动 + 所有文件访问权限 + 挂载本地存储时必须打开,否则无权限读写文件。 + 读取存储权限 + 写入存储权限 + 请求电池优化白名单 + 如果程序在后台运行时被系统杀死,可以尝试设置。 + 关闭 + 自动检查更新 + 打开程序主界面时从Github检查更新 + 唤醒锁 + 打开可防止锁屏后CPU休眠,但在部分系统可能会导致杀后台 + 重要设置