feat: 迁移至material3支持深色模式 (#9)

This commit is contained in:
lisonge 2023-10-19 16:17:59 +08:00
parent 9244742169
commit 265c9f2cc0
48 changed files with 1729 additions and 1432 deletions

View File

@ -122,7 +122,8 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.compose.ui)
implementation(libs.compose.material)
// implementation(libs.compose.material)
implementation(libs.compose.material3)
implementation(libs.compose.preview)
debugImplementation(libs.compose.tooling)
androidTestImplementation(libs.compose.junit4)

View File

@ -46,11 +46,6 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
<activity
android:name="li.songe.gkd.MainActivity"
android:configChanges="uiMode|screenSize|orientation|keyboardHidden|touchscreen|smallestScreenSize|screenLayout|navigation|mnc|mcc|locale|layoutDirection|keyboard|fontWeightAdjustment|fontScale|density|colorMode"

View File

@ -0,0 +1,120 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
import androidx.compose.ui.unit.Velocity
/**
* A nested scroll modifier that provides scroll events to [state].
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param state The [PullRefreshState] associated with this pull-to-refresh component.
* The state will be updated by this modifier.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored.
*/
// TODO(b/244423199): Move pullRefresh into its own material library similar to material-ripple.
fun Modifier.pullRefresh(
state: PullRefreshState,
enabled: Boolean = true,
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefresh"
properties["state"] = state
properties["enabled"] = enabled
},
) {
Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
}
/**
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
* pull refresh components.
*
* Note that this modifier must be added above a scrolling container, such as a lazy column, in
* order to receive scroll events. For example:
*
* @sample androidx.compose.material.samples.CustomPullRefreshSample
*
* @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument.
* Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling
* down despite being at the top of a scrollable component), whereas negative delta (swiping up) is
* dispatched first (in case it is needed to push the indicator back up), and then the unconsumed
* delta is passed on to the child. The callback returns how much delta was consumed.
* @param onRelease Callback for when drag is released, takes float flingVelocity as argument.
* The callback returns how much velocity was consumed - in most cases this should only consume
* velocity if pull refresh has been dragged already and the velocity is positive (the fling is
* downwards), as an upwards fling should typically still scroll a scrollable component beneath the
* pullRefresh. This is invoked before any remaining velocity is passed to the child.
* @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither
* [onPull] nor [onRelease] will be invoked.
*/
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Float,
enabled: Boolean = true,
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefresh"
properties["onPull"] = onPull
properties["onRelease"] = onRelease
properties["enabled"] = enabled
},
) {
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
}
private class PullRefreshNestedScrollConnection(
private val onPull: (pullDelta: Float) -> Float,
private val onRelease: suspend (flingVelocity: Float) -> Float,
private val enabled: Boolean,
) : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource,
): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
else -> Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity(0f, onRelease(available.y))
}
}

View File

@ -0,0 +1,242 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
/**
* The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout.
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param refreshing A boolean representing whether a refresh is occurring.
* @param state The [PullRefreshState] which controls where and how the indicator will be drawn.
* @param modifier Modifiers for the indicator.
* @param backgroundColor The color of the indicator's background.
* @param contentColor The color of the indicator's arc and arrow.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
*/
// TODO(b/244423199): Consider whether the state parameter should be replaced with lambdas to
// enable people to use this indicator with custom pull-to-refresh components.
@Composable
fun PullRefreshIndicator(
refreshing: Boolean,
state: PullRefreshState,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = contentColorFor(backgroundColor),
scale: Boolean = false,
) {
val showElevation by remember(refreshing, state) {
derivedStateOf { refreshing || state.position > 0.5f }
}
Box(
modifier = modifier
.size(IndicatorSize)
.pullRefreshIndicatorTransform(state, scale)
.shadow(if (showElevation) Elevation else 0.dp, SpinnerShape, clip = true)
.background(
color = surfaceColorAtElevation(
color = backgroundColor, elevation = Elevation
), shape = SpinnerShape
),
) {
Crossfade(
targetState = refreshing,
animationSpec = tween(durationMillis = CrossfadeDurationMs),
) { refreshing ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
if (refreshing) {
CircularProgressIndicator(
color = contentColor,
strokeWidth = StrokeWidth,
modifier = Modifier.size(spinnerSize),
)
} else {
CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize))
}
}
}
}
}
/**
* Modifier.size MUST be specified.
*/
@Composable
private fun CircularArrowIndicator(
state: PullRefreshState,
color: Color,
modifier: Modifier,
) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
val targetAlpha by remember(state) {
derivedStateOf {
if (state.progress >= 1f) MaxAlpha else MinAlpha
}
}
val alphaState = animateFloatAsState(targetValue = targetAlpha, animationSpec = AlphaTween)
// Empty semantics for tests
Canvas(modifier.semantics {}) {
val values = ArrowValues(state.progress)
val alpha = alphaState.value
rotate(degrees = values.rotation) {
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
val arcBounds = Rect(
size.center.x - arcRadius,
size.center.y - arcRadius,
size.center.x + arcRadius,
size.center.y + arcRadius,
)
drawArc(
color = color,
alpha = alpha,
startAngle = values.startAngle,
sweepAngle = values.endAngle - values.startAngle,
useCenter = false,
topLeft = arcBounds.topLeft,
size = arcBounds.size,
style = Stroke(
width = StrokeWidth.toPx(),
cap = StrokeCap.Square,
),
)
drawArrow(path, arcBounds, color, alpha, values)
}
}
}
@Immutable
private class ArrowValues(
val rotation: Float,
val startAngle: Float,
val endAngle: Float,
val scale: Float,
)
private fun ArrowValues(progress: Float): ArrowValues {
// Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%.
val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// Calculations based on SwipeRefreshLayout specification.
val endTrim = adjustedPercent * MaxProgressArc
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
val startAngle = rotation * 360
val endAngle = (rotation + endTrim) * 360
val scale = min(1f, adjustedPercent)
return ArrowValues(rotation, startAngle, endAngle, scale)
}
private fun DrawScope.drawArrow(
arrow: Path,
bounds: Rect,
color: Color,
alpha: Float,
values: ArrowValues,
) {
arrow.reset()
arrow.moveTo(0f, 0f) // Move to left corner
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
// Line to tip of arrow
arrow.lineTo(
x = ArrowWidth.toPx() * values.scale / 2,
y = ArrowHeight.toPx() * values.scale,
)
val radius = min(bounds.width, bounds.height) / 2f
val inset = ArrowWidth.toPx() * values.scale / 2f
arrow.translate(
Offset(
x = radius + bounds.center.x - inset,
y = bounds.center.y + StrokeWidth.toPx() / 2f,
),
)
arrow.close()
rotate(degrees = values.endAngle) {
drawPath(path = arrow, color = color, alpha = alpha)
}
}
private const val CrossfadeDurationMs = 100
private const val MaxProgressArc = 0.8f
private val IndicatorSize = 40.dp
private val SpinnerShape = CircleShape
private val ArcRadius = 7.5.dp
private val StrokeWidth = 2.5.dp
private val ArrowWidth = 10.dp
private val ArrowHeight = 5.dp
private val Elevation = 6.dp
// Values taken from SwipeRefreshLayout
private const val MinAlpha = 0.3f
private const val MaxAlpha = 1f
private val AlphaTween = tween<Float>(300, easing = LinearEasing)

View File

@ -0,0 +1,13 @@
package androidx.compose.material3.pullrefresh
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
@Composable
internal fun surfaceColorAtElevation(color: Color, elevation: Dp): Color = when (color) {
MaterialTheme.colorScheme.surface -> MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
else -> color
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.inspectable
/**
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
* based on the given [PullRefreshState].
*
* @sample androidx.compose.material.samples.PullRefreshIndicatorTransformSample
*
* @param state The [PullRefreshState] which determines the position of the indicator.
* @param scale A boolean controlling whether the indicator's size scales with pull progress or not.
*/
// TODO: Consider whether the state parameter should be replaced with lambdas.
fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState,
scale: Boolean = false,
) = inspectable(
inspectorInfo = debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
},
) {
Modifier
// Essentially we only want to clip the at the top, so the indicator will not appear when
// the position is 0. It is preferable to clip the indicator as opposed to the layout that
// contains the indicator, as this would also end up clipping shadows drawn by items in a
// list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
// for the other dimensions to allow for more room for elevation / arbitrary indicators - we
// only ever really want to clip at the top edge.
.drawWithContent {
clipRect(
top = 0f,
left = -Float.MAX_VALUE,
right = Float.MAX_VALUE,
bottom = Float.MAX_VALUE,
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer {
translationY = state.position - size.height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
.transform(state.position / state.threshold)
.coerceIn(0f, 1f)
scaleX = scaleFraction
scaleY = scaleFraction
}
}
}

View File

@ -0,0 +1,229 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.pullrefresh
import androidx.compose.animation.core.animate
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
/**
* Creates a [PullRefreshState] that is remembered across compositions.
*
* Changes to [refreshing] will result in [PullRefreshState] being updated.
*
* @sample androidx.compose.material.samples.PullRefreshSample
*
* @param refreshing A boolean representing whether a refresh is currently occurring.
* @param onRefresh The function to be called to trigger a refresh.
* @param refreshThreshold The threshold below which, if a release
* occurs, [onRefresh] will be called.
* @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This
* offset corresponds to the position of the bottom of the indicator.
*/
@Composable
fun rememberPullRefreshState(
refreshing: Boolean,
onRefresh: () -> Unit,
refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
): PullRefreshState {
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
val scope = rememberCoroutineScope()
val onRefreshState = rememberUpdatedState(onRefresh)
val thresholdPx: Float
val refreshingOffsetPx: Float
with(LocalDensity.current) {
thresholdPx = refreshThreshold.toPx()
refreshingOffsetPx = refreshingOffset.toPx()
}
val state = remember(scope) {
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
}
SideEffect {
state.setRefreshing(refreshing)
state.setThreshold(thresholdPx)
state.setRefreshingOffset(refreshingOffsetPx)
}
return state
}
/**
* A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh
* behaviour to a scroll component. Based on Android's SwipeRefreshLayout.
*
* Provides [progress], a float representing how far the user has pulled as a percentage of the
* refreshThreshold. Values of one or less indicate that the user has not yet pulled past the
* threshold. Values greater than one indicate how far past the threshold the user has pulled.
*
* Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like
* pull-to-refresh behaviour with a custom indicator.
*
* Should be created using [rememberPullRefreshState].
*/
class PullRefreshState internal constructor(
private val animationScope: CoroutineScope,
private val onRefreshState: State<() -> Unit>,
refreshingOffset: Float,
threshold: Float,
) {
/**
* A float representing how far the user has pulled as a percentage of the refreshThreshold.
*
* If the component has not been pulled at all, progress is zero. If the pull has reached
* halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has
* gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to
* two times the refreshThreshold.
*/
val progress get() = adjustedDistancePulled / threshold
internal val refreshing get() = _refreshing
internal val position get() = _position
internal val threshold get() = _threshold
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier }
private var _refreshing by mutableStateOf(false)
private var _position by mutableFloatStateOf(0f)
private var distancePulled by mutableFloatStateOf(0f)
private var _threshold by mutableFloatStateOf(threshold)
private var _refreshingOffset by mutableFloatStateOf(refreshingOffset)
internal fun onPull(pullDelta: Float): Float {
if (_refreshing) return 0f // Already refreshing, do nothing.
val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f)
val dragConsumed = newOffset - distancePulled
distancePulled = newOffset
_position = calculateIndicatorPosition()
return dragConsumed
}
internal fun onRelease(velocity: Float): Float {
if (refreshing) return 0f // Already refreshing, do nothing
if (adjustedDistancePulled > threshold) {
onRefreshState.value()
}
animateIndicatorTo(0f)
val consumed = when {
// We are flinging without having dragged the pull refresh (for example a fling inside
// a list) - don't consume
distancePulled == 0f -> 0f
// If the velocity is negative, the fling is upwards, and we don't want to prevent the
// the list from scrolling
velocity < 0f -> 0f
// We are showing the indicator, and the fling is downwards - consume everything
else -> velocity
}
distancePulled = 0f
return consumed
}
internal fun setRefreshing(refreshing: Boolean) {
if (_refreshing != refreshing) {
_refreshing = refreshing
distancePulled = 0f
animateIndicatorTo(if (refreshing) _refreshingOffset else 0f)
}
}
internal fun setThreshold(threshold: Float) {
_threshold = threshold
}
internal fun setRefreshingOffset(refreshingOffset: Float) {
if (_refreshingOffset != refreshingOffset) {
_refreshingOffset = refreshingOffset
if (refreshing) animateIndicatorTo(refreshingOffset)
}
}
// Make sure to cancel any existing animations when we launch a new one. We use this instead of
// Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
// overhead of running through the animation pipeline instead of directly mutating the state.
private val mutatorMutex = MutatorMutex()
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
mutatorMutex.mutate {
animate(initialValue = _position, targetValue = offset) { value, _ ->
_position = value
}
}
}
private fun calculateIndicatorPosition(): Float = when {
// If drag hasn't gone past the threshold, the position is the adjustedDistancePulled.
adjustedDistancePulled <= threshold -> adjustedDistancePulled
else -> {
// How far beyond the threshold pull has gone, as a percentage of the threshold.
val overshootPercent = abs(progress) - 1.0f
// Limit the overshoot to 200%. Linear between 0 and 200.
val linearTension = overshootPercent.coerceIn(0f, 2f)
// Non-linear tension. Increases with linearTension, but at a decreasing rate.
val tensionPercent = linearTension - linearTension.pow(2) / 4
// The additional offset beyond the threshold.
val extraOffset = threshold * tensionPercent
threshold + extraOffset
}
}
}
/**
* Default parameter values for [rememberPullRefreshState].
*/
object PullRefreshDefaults {
/**
* If the indicator is below this threshold offset when it is released, a refresh
* will be triggered.
*/
val RefreshThreshold = 80.dp
/**
* The offset at which the indicator should be rendered whilst a refresh is occurring.
*/
val RefreshingOffset = 56.dp
}
/**
* The distance pulled is multiplied by this value to give us the adjusted distance pulled, which
* is used in calculating the indicator position (when the adjusted distance pulled is less than
* the refresh threshold, it is the indicator position, otherwise the indicator position is
* derived from the progress).
*/
private const val DragMultiplier = 0.5f

View File

@ -47,9 +47,9 @@ class MainActivity : CompositionActivity({
}
setContent {
UpgradeDialog()
val navController = rememberNavController()
AppTheme(false) {
AppTheme {
UpgradeDialog()
CompositionLocalProvider(
LocalLauncher provides launcher,
LocalPickContentLauncher provides pickContentLauncher,

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent
import android.view.ViewConfiguration
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material3.Icon
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource

View File

@ -1,24 +0,0 @@
package li.songe.gkd.icon
import androidx.compose.material.Icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.tooling.preview.Preview
// @DslMarker
// https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/jsMain/kotlin/react/ChildrenBuilder.kt
val AddIcon = materialIcon(name = "AddIcon") {
addPath(
pathData = addPathNodes("M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"),
fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
)
}
@Preview
@Composable
fun PreviewAddIcon() {
Icon(imageVector = AddIcon, contentDescription = null)
}

View File

@ -1,22 +0,0 @@
package li.songe.gkd.icon
import androidx.compose.material.Icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.tooling.preview.Preview
val ArrowIcon = materialIcon(name = "ArrowIcon") {
addPath(
pathData = addPathNodes("M6.23 20.23L8 22l10-10L8 2L6.23 3.77L14.46 12z"),
fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
)
}
@Preview
@Composable
fun PreviewArrowIcon() {
Icon(imageVector = ArrowIcon, contentDescription = null)
}

View File

@ -1,22 +0,0 @@
package li.songe.gkd.icon
import androidx.compose.material.Icon
import androidx.compose.material.icons.materialIcon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.tooling.preview.Preview
val HomeIcon = materialIcon(name = "ArrowIcon") {
addPath(
pathData = addPathNodes("M16.612 2.214a1.01 1.01 0 0 0-1.242 0L1 13.419l1.243 1.572L4 13.621V26a2.004 2.004 0 0 0 2 2h20a2.004 2.004 0 0 0 2-2V13.63L29.757 15L31 13.428zM18 26h-4v-8h4zm2 0v-8a2.002 2.002 0 0 0-2-2h-4a2.002 2.002 0 0 0-2 2v8H6V12.062l10-7.79l10 7.8V26z"),
fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
)
}
@Preview
@Composable
fun PreviewHomeIcon() {
Icon(imageVector = HomeIcon, contentDescription = null)
}

View File

@ -1,31 +0,0 @@
package li.songe.gkd.icon
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.google.accompanist.drawablepainter.rememberDrawablePainter
@Preview
@Composable
fun PreviewTestDsl() {
val vectorString = """
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z" />
</vector>
""".trim()
val drawable = Drawable.createFromStream(vectorString.byteInputStream(), "ic_back")
if (drawable != null) {
Image(painter = rememberDrawablePainter(drawable = drawable), contentDescription = null)
} else {
Text(text = "null drawable")
}
}

View File

@ -16,7 +16,7 @@ val abNotif by lazy {
Notif(
id = 100,
icon = SafeR.ic_launcher,
title = "搞快点",
title = "GKD",
text = "无障碍正在运行",
ongoing = true,
autoCancel = false
@ -26,7 +26,7 @@ val screenshotNotif by lazy {
Notif(
id = 101,
icon = SafeR.ic_launcher,
title = "搞快点",
title = "GKD",
text = "截屏服务正在运行",
ongoing = true,
autoCancel = false
@ -37,7 +37,7 @@ val floatingNotif by lazy {
Notif(
id = 102,
icon = SafeR.ic_launcher,
title = "搞快点",
title = "GKD",
text = "悬浮窗按钮正在显示",
ongoing = true,
autoCancel = false
@ -47,7 +47,7 @@ val httpNotif by lazy {
Notif(
id = 103,
icon = SafeR.ic_launcher,
title = "搞快点",
title = "GKD",
text = "HTTP服务正在运行",
ongoing = true,
autoCancel = false

View File

@ -10,7 +10,7 @@ data class NotifChannel(
val defaultChannel by lazy {
NotifChannel(
id = "default", name = "搞快点", desc = "显示服务运行状态"
id = "default", name = "GKD", desc = "显示服务运行状态"
)
}

View File

@ -9,7 +9,6 @@ import android.graphics.Bitmap
import android.os.Build
import android.view.Display
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.NetworkUtils
import com.blankj.utilcode.util.ScreenUtils

View File

@ -1,16 +1,12 @@
package li.songe.gkd.ui
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
@ -23,18 +19,6 @@ import li.songe.gkd.util.ProfileTransitions
@Destination(style = ProfileTransitions::class)
@Composable
fun AboutPage() {
// val systemUiController = rememberSystemUiController()
// val context = LocalContext.current as ComponentActivity
// DisposableEffect(systemUiController) {
// val oldVisible = systemUiController.isStatusBarVisible
// systemUiController.isStatusBarVisible = false
// WindowCompat.setDecorFitsSystemWindows(context.window, false)
// onDispose {
// systemUiController.isStatusBarVisible = oldVisible
// WindowCompat.setDecorFitsSystemWindows(context.window, true)
// }
// }
val context = LocalContext.current
val navController = LocalNavController.current
Scaffold(topBar = {
SimpleTopAppBar(onClickIcon = { navController.popBackStack() }, title = "关于")

View File

@ -16,18 +16,21 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.AlertDialog
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -88,7 +91,7 @@ fun AppItemPage(
)
}
val editable = subsItemId < 0
val editable = subsItem != null && subsItemId < 0
var showAddDlg by remember { mutableStateOf(false) }
@ -115,172 +118,172 @@ fun AppItemPage(
)
}
}
},
content = { contentPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
item {
Spacer(modifier = Modifier.height(10.dp))
}
appRaw?.groups?.let { groupsVal ->
itemsIndexed(groupsVal, { i, g -> i.toString() + g.key }) { _, group ->
Row(
}, content = { contentPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(contentPadding)
) {
item {
Spacer(modifier = Modifier.height(10.dp))
}
appRaw?.groups?.let { groupsVal ->
itemsIndexed(groupsVal, { i, g -> i.toString() + g.key }) { _, group ->
Row(
modifier = Modifier
.background(
if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent
)
.clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp)
.fillMaxWidth()
.height(45.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier
.background(
if (group.key == focusGroupKey) Color(0x500a95ff) else Color.Transparent
)
.clickable { setShowGroupItem(group) }
.padding(10.dp, 6.dp)
.fillMaxWidth()
.height(45.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = group.name,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
)
if (group.valid) {
Text(
text = group.name,
text = group.desc ?: "-",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
)
} else {
Text(
text = "规则组损坏",
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.width(10.dp))
if (editable) {
IconButton(onClick = {
setMenuGroupRaw(group)
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "more",
modifier = Modifier.size(30.dp)
)
if (group.valid) {
Text(
text = group.desc ?: "-",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
)
} else {
Text(
text = "规则组损坏",
color = Color.Red,
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.width(10.dp))
if (editable) {
IconButton(onClick = {
setMenuGroupRaw(group)
}) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "more",
modifier = Modifier.size(30.dp)
)
}
Spacer(modifier = Modifier.width(10.dp))
}
val subsConfig = subsConfigs.find { it.groupKey == group.key }
Switch(checked = (subsConfig?.enable ?: group.enable) ?: true,
modifier = Modifier,
onCheckedChange = scope.launchAsFn { enable ->
val newItem = (subsConfig?.copy(enable = enable) ?: SubsConfig(
type = SubsConfig.GroupType,
subsItemId = subsItemId,
appId = appId,
groupKey = group.key,
enable = enable
))
DbSet.subsConfigDao.insert(newItem)
})
}
val subsConfig = subsConfigs.find { it.groupKey == group.key }
Switch(checked = (subsConfig?.enable ?: group.enable) ?: true,
modifier = Modifier,
onCheckedChange = scope.launchAsFn { enable ->
val newItem = (subsConfig?.copy(enable = enable) ?: SubsConfig(
type = SubsConfig.GroupType,
subsItemId = subsItemId,
appId = appId,
groupKey = group.key,
enable = enable
))
DbSet.subsConfigDao.insert(newItem)
})
}
}
item {
Spacer(modifier = Modifier.height(20.dp))
}
}
})
item {
Spacer(modifier = Modifier.height(20.dp))
}
}
})
showGroupItem?.let { showGroupItemVal ->
AlertDialog(modifier = Modifier.defaultMinSize(300.dp),
onDismissRequest = { setShowGroupItem(null) },
title = {
Text(text = showGroupItemVal.name)
},
text = {
Column {
if (showGroupItemVal.enable == false) {
Text(text = "该规则组默认不启用", color = Color.Blue)
Spacer(modifier = Modifier.height(10.dp))
}
Text(text = showGroupItemVal.desc ?: "-")
}
},
confirmButton = {
TextButton(onClick = {
val groupAppText = Singleton.json.encodeToString(
appRaw?.copy(
groups = listOf(showGroupItemVal)
)
)
ClipboardUtils.copyText(groupAppText)
ToastUtils.showShort("复制成功")
}) {
Text(text = "复制规则组")
}
})
onDismissRequest = { setShowGroupItem(null) },
title = {
Text(text = showGroupItemVal.name)
},
text = {
Column {
if (showGroupItemVal.enable == false) {
Text(text = "该规则组默认不启用")
Spacer(modifier = Modifier.height(10.dp))
}
Text(text = showGroupItemVal.desc ?: "-")
}
},
confirmButton = {
TextButton(onClick = {
val groupAppText = Singleton.json.encodeToString(
appRaw?.copy(
groups = listOf(showGroupItemVal)
)
)
ClipboardUtils.copyText(groupAppText)
ToastUtils.showShort("复制成功")
}) {
Text(text = "复制规则组")
}
})
}
if (menuGroupRaw != null && appRawVal != null && subsItemVal != null) {
Dialog(onDismissRequest = { setMenuGroupRaw(null) }) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
Card(
modifier = Modifier
.padding(10.dp)
.width(200.dp)
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Text(text = "编辑", modifier = Modifier
.clickable {
setEditGroupRaw(menuGroupRaw)
setMenuGroupRaw(null)
}
.padding(10.dp)
.fillMaxWidth())
Text(text = "删除", color = Color.Red, modifier = Modifier
.clickable {
vm.viewModelScope.launchTry(Dispatchers.IO) {
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@launchTry
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps
.toMutableList()
.apply {
set(indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key })
)
})
subsItemVal.subsFile.writeText(
Singleton.json.encodeToString(
newSubsRaw
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
DbSet.subsConfigDao.delete(
subsItemVal.id, appRawVal.id, menuGroupRaw.key
)
ToastUtils.showShort("删除成功")
Column {
Text(text = "编辑", modifier = Modifier
.clickable {
setEditGroupRaw(menuGroupRaw)
setMenuGroupRaw(null)
}
}
.padding(10.dp)
.fillMaxWidth())
.padding(16.dp)
.fillMaxWidth())
Text(text = "删除", modifier = Modifier
.clickable {
vm.viewModelScope.launchTry(Dispatchers.IO) {
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@launchTry
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps
.toMutableList()
.apply {
set(
indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key })
)
})
subsItemVal.subsFile.writeText(
Singleton.json.encodeToString(
newSubsRaw
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
DbSet.subsConfigDao.delete(
subsItemVal.id, appRawVal.id, menuGroupRaw.key
)
ToastUtils.showShort("删除成功")
setMenuGroupRaw(null)
}
}
.padding(16.dp)
.fillMaxWidth(), color = MaterialTheme.colorScheme.error)
}
}
}
}
@ -289,153 +292,146 @@ fun AppItemPage(
var source by remember {
mutableStateOf(Singleton.json.encodeToString(editGroupRaw))
}
Dialog(onDismissRequest = { setEditGroupRaw(null) }) {
Column(
modifier = Modifier.defaultMinSize(minWidth = 300.dp),
) {
Text(text = "编辑规则组", fontSize = 18.sp, modifier = Modifier.padding(10.dp))
val oldSource = remember { source }
AlertDialog(
title = { Text(text = "编辑规则组") },
text = {
OutlinedTextField(
value = source,
onValueChange = { source = it },
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入规则组") },
maxLines = 8,
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth()
) {
TextButton(onClick = {
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
if (newGroupRaw.key != editGroupRaw.key) {
ToastUtils.showShort("不能更改规则组的key")
return@TextButton
}
if (!newGroupRaw.valid) {
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
setEditGroupRaw(null)
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
set(indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups.toMutableList().apply {
set(
indexOfFirst { g -> g.key == newGroupRaw.key }, newGroupRaw
)
})
)
})
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(
Singleton.json.encodeToString(
newSubsRaw
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
ToastUtils.showShort("更新成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "更新")
}
},
onDismissRequest = { setEditGroupRaw(null) },
dismissButton = {
TextButton(onClick = { setEditGroupRaw(null) }) {
Text(text = "取消")
}
}
}
},
confirmButton = {
TextButton(onClick = {
if (oldSource == source) {
ToastUtils.showShort("规则无变动")
return@TextButton
}
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
if (newGroupRaw.key != editGroupRaw.key) {
ToastUtils.showShort("不能更改规则组的key")
return@TextButton
}
if (!newGroupRaw.valid) {
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
setEditGroupRaw(null)
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
set(indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups.toMutableList().apply {
set(
indexOfFirst { g -> g.key == newGroupRaw.key }, newGroupRaw
)
})
)
})
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(
Singleton.json.encodeToString(
newSubsRaw
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
ToastUtils.showShort("更新成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "更新")
}
},
)
}
if (showAddDlg && appRawVal != null && subsItemVal != null) {
var source by remember {
mutableStateOf("")
}
Dialog(onDismissRequest = { showAddDlg = false }) {
Column(
modifier = Modifier.defaultMinSize(minWidth = 300.dp),
) {
Text(text = "添加规则组", fontSize = 18.sp, modifier = Modifier.padding(10.dp))
OutlinedTextField(
value = source,
onValueChange = { source = it },
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
placeholder = { Text(text = "请输入规则组\n可以是APP规则\n也可以是单个规则组") },
maxLines = 8,
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth()
) {
TextButton(onClick = {
val newAppRaw = try {
SubscriptionRaw.parseAppRaw(source)
} catch (_: Exception) {
null
}
val tempGroups = if (newAppRaw == null) {
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
listOf(newGroupRaw)
} else {
if (newAppRaw.id != appRawVal.id) {
ToastUtils.showShort("id不一致,无法添加")
return@TextButton
}
if (newAppRaw.groups.isEmpty()) {
ToastUtils.showShort("不能添加空规则组")
return@TextButton
}
newAppRaw.groups
}
if (!tempGroups.all { g -> g.valid }) {
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
tempGroups.forEach { g ->
if (appRawVal.groups.any { g2 -> g2.name == g.name }) {
ToastUtils.showShort("存在同名规则[${g.name}]")
return@TextButton
}
}
val newKey = appRawVal.groups.maxBy { g -> g.key }.key + 1
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
set(indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups + tempGroups.mapIndexed { i, g ->
g.copy(
key = newKey + i
)
})
)
})
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(Singleton.json.encodeToString(newSubsRaw))
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
showAddDlg = false
ToastUtils.showShort("添加成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
AlertDialog(title = { Text(text = "添加规则组") }, text = {
OutlinedTextField(
value = source,
onValueChange = { source = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入规则组\n可以是APP规则\n也可以是单个规则组") },
maxLines = 8,
)
}, onDismissRequest = { showAddDlg = false }, confirmButton = {
TextButton(onClick = {
val newAppRaw = try {
SubscriptionRaw.parseAppRaw(source)
} catch (_: Exception) {
null
}
val tempGroups = if (newAppRaw == null) {
val newGroupRaw = try {
SubscriptionRaw.parseGroupRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则:${e.message}")
return@TextButton
}
listOf(newGroupRaw)
} else {
if (newAppRaw.id != appRawVal.id) {
ToastUtils.showShort("id不一致,无法添加")
return@TextButton
}
if (newAppRaw.groups.isEmpty()) {
ToastUtils.showShort("不能添加空规则组")
return@TextButton
}
newAppRaw.groups
}
if (!tempGroups.all { g -> g.valid }) {
ToastUtils.showShort("非法规则:存在非法选择器")
return@TextButton
}
tempGroups.forEach { g ->
if (appRawVal.groups.any { g2 -> g2.name == g.name }) {
ToastUtils.showShort("存在同名规则[${g.name}]")
return@TextButton
}
}
val newKey = appRawVal.groups.maxBy { g -> g.key }.key + 1
val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
set(indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups + tempGroups.mapIndexed { i, g ->
g.copy(
key = newKey + i
)
})
)
})
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(Singleton.json.encodeToString(newSubsRaw))
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
showAddDlg = false
ToastUtils.showShort("添加成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
}
}
}, dismissButton = {
TextButton(onClick = { showAddDlg = false }) {
Text(text = "取消")
}
})
}
}

View File

@ -1,10 +1,6 @@
package li.songe.gkd.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -16,14 +12,18 @@ 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.material.AlertDialog
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -33,7 +33,6 @@ import androidx.compose.runtime.rememberCoroutineScope
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.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@ -87,27 +86,22 @@ fun ClickLogPage() {
})
}, content = { contentPadding ->
if (clickLogs.isNotEmpty()) {
LazyColumn(
modifier = Modifier
.padding(10.dp, 0.dp, 10.dp, 0.dp)
.padding(contentPadding),
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(contentPadding),
) {
item {
Spacer(modifier = Modifier.height(5.dp))
}
items(clickLogs, { triggerLog -> triggerLog.id }) { triggerLog ->
Column(modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(1.dp, Color.Black))
.clickable {
previewClickLog = triggerLog
}) {
}
.fillMaxWidth()
.padding(10.dp)) {
Row {
Text(
text = triggerLog.id.format("yyyy-MM-dd HH:mm:ss"),
text = triggerLog.id.format("MM-dd HH:mm:ss"),
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.width(10.dp))
@ -132,6 +126,7 @@ fun ClickLogPage() {
Text(text = rule.name)
}
}
Divider()
}
item {
Spacer(modifier = Modifier.height(10.dp))
@ -151,35 +146,41 @@ fun ClickLogPage() {
previewClickLog?.let { previewTriggerLogVal ->
Dialog(onDismissRequest = { previewClickLog = null }) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
Card(
modifier = Modifier
.width(250.dp)
.background(Color.White)
.padding(8.dp)
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Text(text = "查看规则组", modifier = Modifier
.clickable {
previewTriggerLogVal.appId ?: return@clickable
navController.navigate(
AppItemPageDestination(
previewTriggerLogVal.subsId,
previewTriggerLogVal.appId,
previewTriggerLogVal.groupKey
Column {
Text(text = "查看规则组", modifier = Modifier
.clickable {
previewTriggerLogVal.appId ?: return@clickable
navController.navigate(
AppItemPageDestination(
previewTriggerLogVal.subsId,
previewTriggerLogVal.appId,
previewTriggerLogVal.groupKey
)
)
)
previewClickLog = null
}
.fillMaxWidth()
.padding(10.dp))
Text(text = "删除", modifier = Modifier
.clickable(onClick = scope.launchAsFn {
previewClickLog = null
DbSet.clickLogDao.delete(previewTriggerLogVal)
})
.fillMaxWidth()
.padding(10.dp))
previewClickLog = null
}
.fillMaxWidth()
.padding(16.dp))
Text(
text = "删除",
modifier = Modifier
.clickable(onClick = scope.launchAsFn {
previewClickLog = null
DbSet.clickLogDao.delete(previewTriggerLogVal)
})
.fillMaxWidth()
.padding(16.dp),
color = MaterialTheme.colorScheme.error
)
}
}
}
}
@ -191,7 +192,7 @@ fun ClickLogPage() {
showDeleteDlg = false
DbSet.clickLogDao.deleteAll()
}) {
Text(text = "", color = Color.Red)
Text(text = "", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {

View File

@ -11,19 +11,19 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.unit.dp
import androidx.compose.ui.unit.sp
@ -31,7 +31,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.hilt.navigation.compose.hiltViewModel
import com.blankj.utilcode.util.ToastUtils
import kotlinx.coroutines.Dispatchers
import li.songe.gkd.util.navigate
import li.songe.gkd.MainActivity
import li.songe.gkd.appScope
import li.songe.gkd.service.GkdAbService
@ -41,12 +40,14 @@ import li.songe.gkd.ui.destinations.ClickLogPageDestination
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.SafeR
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.updateStorage
import li.songe.gkd.util.usePollState
val controlNav = BottomNavItem(label = "主页", icon = SafeR.ic_home, route = "settings")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ControlPage() {
val context = LocalContext.current as MainActivity
@ -58,9 +59,9 @@ fun ControlPage() {
Scaffold(
topBar = {
TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
TopAppBar(title = {
Text(
text = "搞快点", color = Color.Black
text = "GKD"
)
})
},
@ -78,35 +79,33 @@ fun ControlPage() {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
if (!notifEnabled) {
AuthCard(title = "通知权限",
desc = "用于启动后台服务,展示服务运行状态",
onAuthClick = {
val intent = Intent()
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)
context.startActivity(intent)
})
AuthCard(title = "通知权限", desc = "用于启动后台服务,展示服务运行状态", onAuthClick = {
val intent = Intent()
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)
context.startActivity(intent)
})
Divider()
}
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
if (!gkdAccessRunning) {
AuthCard(title = "无障碍权限",
desc = "用于获取屏幕信息,点击屏幕上的控件",
onAuthClick = {
if (notifEnabled) {
appScope.launchTry(Dispatchers.IO) {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
// android.content.ActivityNotFoundException
// https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/113010?pid=1
context.startActivity(intent)
}
} else {
ToastUtils.showShort("必须先开启[通知权限]")
}
})
desc = "用于获取屏幕信息,点击屏幕上的控件",
onAuthClick = {
if (notifEnabled) {
appScope.launchTry(Dispatchers.IO) {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
// android.content.ActivityNotFoundException
// https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/113010?pid=1
context.startActivity(intent)
}
} else {
ToastUtils.showShort("必须先开启[通知权限]")
}
})
Divider()
}
@ -114,29 +113,29 @@ fun ControlPage() {
val canDrawOverlays by usePollState { Settings.canDrawOverlays(context) }
if (!canDrawOverlays) {
AuthCard(title = "悬浮窗权限",
desc = "用于后台提示,显示保存快照按钮等功能",
onAuthClick = {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
})
desc = "用于后台提示,显示保存快照按钮等功能",
onAuthClick = {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
})
Divider()
}
if (gkdAccessRunning) {
TextSwitch(name = "服务开启",
desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
checked = store.enableService,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
enableService = it
)
)
})
desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
checked = store.enableService,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
enableService = it
)
)
})
Divider()
}

View File

@ -7,24 +7,20 @@ import android.media.projection.MediaProjectionManager
import android.provider.Settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -263,46 +259,41 @@ fun DebugPage() {
var value by remember {
mutableStateOf(store.httpServerPort.toString())
}
Column(
modifier = Modifier.padding(10.dp)
) {
Text(text = "请输入新端口", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(10.dp))
AlertDialog(title = { Text(text = "请输入新端口") }, text = {
OutlinedTextField(
value = value,
onValueChange = { value = it.trim() },
onValueChange = {
value = it.trim().let { s -> if (s.length > 5) s.substring(0..4) else s }
},
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Row(
horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = { showPortDlg = false }) {
Text(
text = "取消", modifier = Modifier
)
}, onDismissRequest = { showPortDlg = false }, confirmButton = {
TextButton(onClick = {
val newPort = value.toIntOrNull()
if (newPort == null || !(5000 <= newPort && newPort <= 65535)) {
ToastUtils.showShort("请输入在 5000~65535 的任意数字")
return@TextButton
}
Spacer(modifier = Modifier.width(5.dp))
TextButton(onClick = {
val newPort = value.toIntOrNull()
if (newPort == null || !(5000 <= newPort && newPort <= 65535)) {
ToastUtils.showShort("请输入在 5000~65535 的任意数字")
return@TextButton
}
updateStorage(
storeFlow, store.copy(
httpServerPort = newPort
)
updateStorage(
storeFlow, store.copy(
httpServerPort = newPort
)
showPortDlg = false
}) {
Text(
text = "确认", modifier = Modifier
)
}
)
showPortDlg = false
}) {
Text(
text = "确认", modifier = Modifier
)
}
}
}, dismissButton = {
TextButton(onClick = { showPortDlg = false }) {
Text(
text = "取消"
)
}
})
}
}
}

View File

@ -1,29 +1,22 @@
package li.songe.gkd.ui
import android.app.Activity
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.getImportUrl
val BottomNavItems = listOf(
subsNav, controlNav, settingsNav
@ -39,39 +32,22 @@ data class BottomNavItem(
@Destination(style = ProfileTransitions::class)
@Composable
fun HomePage() {
val context = LocalContext.current as Activity
val vm = hiltViewModel<HomePageVm>()
val tab by vm.tabFlow.collectAsState()
val intent by vm.intentFlow.collectAsState()
LaunchedEffect(key1 = Unit, block = {
vm.intentFlow.value = context.intent
})
LaunchedEffect(intent, block = {
if (getImportUrl(intent) != null) {
vm.tabFlow.value = subsNav
}
})
Scaffold(bottomBar = {
BottomNavigation(
backgroundColor = Color.Transparent, elevation = 0.dp
) {
NavigationBar {
BottomNavItems.forEach { navItem ->
BottomNavigationItem(selected = tab == navItem,
modifier = Modifier.background(Color.Transparent),
onClick = {
vm.tabFlow.value = navItem
},
icon = {
Icon(
painter = painterResource(id = navItem.icon),
contentDescription = navItem.label,
modifier = Modifier.padding(2.dp)
)
},
label = {
Text(text = navItem.label)
})
NavigationBarItem(selected = tab == navItem, modifier = Modifier, onClick = {
vm.tabFlow.value = navItem
}, icon = {
Icon(
painter = painterResource(id = navItem.icon),
contentDescription = navItem.label
)
}, label = {
Text(text = navItem.label)
})
}
}
}, content = { padding ->

View File

@ -1,6 +1,5 @@
package li.songe.gkd.ui
import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.LogUtils
@ -35,7 +34,6 @@ import javax.inject.Inject
@HiltViewModel
class HomePageVm @Inject constructor() : ViewModel() {
val tabFlow = MutableStateFlow(controlNav)
val intentFlow = MutableStateFlow<Intent?>(null)
init {
appScope.launchTry(Dispatchers.IO) {

View File

@ -3,10 +3,8 @@ package li.songe.gkd.ui
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.background
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.Spacer
@ -16,20 +14,22 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AlertDialog
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.RadioButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
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.collectAsState
import androidx.compose.runtime.getValue
@ -38,7 +38,6 @@ 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.unit.dp
import androidx.compose.ui.unit.sp
@ -74,6 +73,7 @@ val settingsNav = BottomNavItem(
label = "设置", icon = SafeR.ic_cog, route = "settings"
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsPage() {
val context = LocalContext.current as MainActivity
@ -85,6 +85,9 @@ fun SettingsPage() {
var showSubsIntervalDlg by remember {
mutableStateOf(false)
}
var showDarkThemeDlg by remember {
mutableStateOf(false)
}
var showToastInputDlg by remember {
mutableStateOf(false)
}
@ -96,9 +99,9 @@ fun SettingsPage() {
val checkUpdating by checkUpdatingFlow.collectAsState()
Scaffold(topBar = {
TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
TopAppBar(title = {
Text(
text = "设置", color = Color.Black
text = "设置"
)
})
}, content = { contentPadding ->
@ -112,33 +115,33 @@ fun SettingsPage() {
) {
TextSwitch(name = "后台隐藏",
desc = "在[最近任务]界面中隐藏本应用",
checked = store.excludeFromRecents,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
excludeFromRecents = it
)
)
})
desc = "在[最近任务]界面中隐藏本应用",
checked = store.excludeFromRecents,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
excludeFromRecents = it
)
)
})
Divider()
TextSwitch(name = "点击提示",
desc = "触发点击时提示:[${store.clickToast}]",
checked = store.toastWhenClick,
modifier = Modifier.clickable {
showToastInputDlg = true
},
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
toastWhenClick = it
)
)
if (!Settings.canDrawOverlays(context)) {
ToastUtils.showShort("需要悬浮窗权限")
}
})
desc = "触发点击时提示:[${store.clickToast}]",
checked = store.toastWhenClick,
modifier = Modifier.clickable {
showToastInputDlg = true
},
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
toastWhenClick = it
)
)
if (it && !Settings.canDrawOverlays(context)) {
ToastUtils.showShort("需要悬浮窗权限")
}
})
Divider()
Row(modifier = Modifier
@ -153,7 +156,7 @@ fun SettingsPage() {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = radioOptions.find { it.second == store.updateSubsInterval }?.first
text = updateTimeRadioOptions.find { it.second == store.updateSubsInterval }?.first
?: store.updateSubsInterval.toString(), fontSize = 14.sp
)
Icon(
@ -161,18 +164,18 @@ fun SettingsPage() {
)
}
}
Divider()
TextSwitch(name = "自动更新应用",
desc = "打开应用时自动检测是否存在新版本",
checked = store.autoCheckAppUpdate,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
autoCheckAppUpdate = it
)
)
})
desc = "打开应用时自动检测是否存在新版本",
checked = store.autoCheckAppUpdate,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
autoCheckAppUpdate = it
)
)
})
Divider()
SettingItem(title = if (checkUpdating) "检查更新ing" else "检查更新", onClick = {
@ -185,6 +188,29 @@ fun SettingsPage() {
}
})
Divider()
Row(modifier = Modifier
.clickable {
showDarkThemeDlg = true
}
.padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
modifier = Modifier.weight(1f), text = "深色模式", fontSize = 18.sp
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = darkThemeRadioOptions.find { it.second == store.enableDarkTheme }?.first
?: store.enableDarkTheme.toString(), fontSize = 14.sp
)
Icon(
imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more"
)
}
}
Divider()
SettingItem(title = "问题反馈", onClick = {
appScope.launchTry(Dispatchers.IO) {
// ActivityNotFoundException
@ -197,28 +223,30 @@ fun SettingsPage() {
}
})
Divider()
TextSwitch(name = "保存日志",
desc = "保存最近7天的日志",
checked = store.log2FileSwitch,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
log2FileSwitch = it
)
)
if (!it) {
appScope.launchTry(Dispatchers.IO) {
val logFiles = LogUtils.getLogFiles()
if (logFiles.isNotEmpty()) {
logFiles.forEach { f ->
f.delete()
}
ToastUtils.showShort("已删除全部日志")
}
}
}
})
desc = "保存最近7天的日志",
checked = store.log2FileSwitch,
onCheckedChange = {
updateStorage(
storeFlow, store.copy(
log2FileSwitch = it
)
)
if (!it) {
appScope.launchTry(Dispatchers.IO) {
val logFiles = LogUtils.getLogFiles()
if (logFiles.isNotEmpty()) {
logFiles.forEach { f ->
f.delete()
}
ToastUtils.showShort("已删除全部日志")
}
}
}
})
Divider()
SettingItem(title = "分享日志", onClick = {
vm.viewModelScope.launchTry(Dispatchers.IO) {
val logFiles = LogUtils.getLogFiles()
@ -230,6 +258,7 @@ fun SettingsPage() {
}
})
Divider()
SettingItem(title = "高级模式", onClick = {
navController.navigate(DebugPageDestination)
})
@ -245,36 +274,81 @@ fun SettingsPage() {
if (showSubsIntervalDlg) {
Dialog(onDismissRequest = { showSubsIntervalDlg = false }) {
Column(
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
radioOptions.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(selected = (option.second == store.updateSubsInterval),
Column {
updateTimeRadioOptions.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(selected = (option.second == store.updateSubsInterval),
onClick = {
updateStorage(
storeFlow,
storeFlow.value.copy(updateSubsInterval = option.second)
)
})
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.second == store.updateSubsInterval),
onClick = {
updateStorage(
storeFlow,
storeFlow.value.copy(updateSubsInterval = option.second)
)
})
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.second == store.updateSubsInterval),
onClick = {
updateStorage(
storeFlow,
storeFlow.value.copy(updateSubsInterval = option.second)
)
})
Text(
text = option.first,
style = MaterialTheme.typography.body1.merge(),
modifier = Modifier.padding(start = 16.dp)
)
Text(
text = option.first, modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
}
if (showDarkThemeDlg) {
Dialog(onDismissRequest = { showDarkThemeDlg = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column {
darkThemeRadioOptions.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = (option.second == store.enableDarkTheme),
onClick = {
updateStorage(
storeFlow,
storeFlow.value.copy(enableDarkTheme = option.second)
)
})
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (option.second == store.enableDarkTheme),
onClick = {
updateStorage(
storeFlow,
storeFlow.value.copy(enableDarkTheme = option.second)
)
})
Text(
text = option.first, modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
@ -286,37 +360,46 @@ fun SettingsPage() {
var value by remember {
mutableStateOf(store.clickToast)
}
Column(
modifier = Modifier.padding(10.dp)
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Text(text = "请输入提示文字", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = value,
onValueChange = { value = it },
singleLine = true,
modifier = Modifier,
)
Row(
horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()
Column(
modifier = Modifier.padding(10.dp)
) {
TextButton(onClick = { showToastInputDlg = false }) {
Text(
text = "取消", modifier = Modifier
)
}
Spacer(modifier = Modifier.width(5.dp))
TextButton(onClick = {
updateStorage(
storeFlow, store.copy(
clickToast = value
Text(text = "请输入提示文字")
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = value,
onValueChange = { value = it },
singleLine = true,
modifier = Modifier,
)
Row(
horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = { showToastInputDlg = false }) {
Text(
text = "取消", modifier = Modifier
)
)
showToastInputDlg = false
}) {
Text(
text = "确认", modifier = Modifier
)
}
Spacer(modifier = Modifier.width(5.dp))
TextButton(onClick = {
updateStorage(
storeFlow, store.copy(
clickToast = value
)
)
showToastInputDlg = false
}) {
Text(
text = "确认", modifier = Modifier
)
}
}
}
}
@ -325,16 +408,16 @@ fun SettingsPage() {
if (showShareLogDlg) {
Dialog(onDismissRequest = { showShareLogDlg = false }) {
Box(
Modifier
.width(200.dp)
.background(Color.White)
.padding(8.dp)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
val modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(16.dp)
Text(
text = "调用系统分享", modifier = Modifier
.clickable(onClick = {
@ -399,35 +482,21 @@ fun SettingsPage() {
}
is LoadStatus.Loading -> {
Dialog(onDismissRequest = { }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "上传文件中,请稍等",
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Spacer(modifier = Modifier.height(15.dp))
AlertDialog(
title = { Text(text = "上传文件中") },
text = {
LinearProgressIndicator(progress = uploadStatusVal.progress)
Spacer(modifier = Modifier.height(5.dp))
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
vm.uploadJob?.cancel(CancellationException("终止上传"))
vm.uploadJob = null
}) {
Text(text = "终止上传", color = Color.Red)
}
},
onDismissRequest = { },
confirmButton = {
TextButton(onClick = {
vm.uploadJob?.cancel(CancellationException("终止上传"))
vm.uploadJob = null
}) {
Text(text = "终止上传")
}
}
}
},
)
}
is LoadStatus.Success -> {
@ -454,10 +523,16 @@ fun SettingsPage() {
}
}
val radioOptions = listOf(
private val updateTimeRadioOptions = listOf(
"暂停" to -1L,
"每小时" to 60 * 60_000L,
"每6小时" to 6 * 60 * 60_000L,
"每12小时" to 12 * 60 * 60_000L,
"每天" to 24 * 60 * 60_000L
)
private val darkThemeRadioOptions = listOf(
"跟随系统" to null,
"启用" to true,
"关闭" to false,
)

View File

@ -5,12 +5,7 @@ import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.Spacer
@ -22,16 +17,19 @@ 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.rememberLazyListState
import androidx.compose.material.AlertDialog
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -41,11 +39,9 @@ import androidx.compose.runtime.rememberCoroutineScope
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.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.content.FileProvider
import androidx.hilt.navigation.compose.hiltViewModel
@ -85,6 +81,7 @@ fun SnapshotPage() {
val scope = rememberCoroutineScope()
val context = LocalContext.current as ComponentActivity
val navController = LocalNavController.current
val colorScheme = MaterialTheme.colorScheme
val pickContentLauncher = LocalPickContentLauncher.current
val requestPermissionLauncher = LocalRequestPermissionLauncher.current
@ -101,7 +98,6 @@ fun SnapshotPage() {
var showDeleteDlg by remember {
mutableStateOf(false)
}
val scrollState = rememberLazyListState()
Scaffold(topBar = {
SimpleTopAppBar(onClickIcon = { navController.popBackStack() },
@ -120,22 +116,18 @@ fun SnapshotPage() {
}, content = { contentPadding ->
if (snapshots.isNotEmpty()) {
LazyColumn(
modifier = Modifier
.padding(10.dp, 0.dp, 10.dp, 0.dp)
.padding(contentPadding),
state = scrollState,
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(contentPadding),
) {
items(snapshots, { it.id }) { snapshot ->
Column(modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(1.dp, Color.Black))
.clickable {
selectedSnapshot = snapshot
}) {
}
.padding(10.dp)) {
Row {
Text(
text = snapshot.id.format("yyyy-MM-dd HH:mm:ss"),
text = snapshot.id.format("MM-dd HH:mm:ss"),
fontFamily = FontFamily.Monospace
)
Spacer(modifier = Modifier.width(10.dp))
@ -144,6 +136,7 @@ fun SnapshotPage() {
Spacer(modifier = Modifier.width(10.dp))
Text(text = snapshot.activityId ?: "")
}
Divider()
}
item {
Spacer(modifier = Modifier.height(10.dp))
@ -162,16 +155,16 @@ fun SnapshotPage() {
selectedSnapshot?.let { snapshotVal ->
Dialog(onDismissRequest = { selectedSnapshot = null }) {
Box(
Modifier
.width(200.dp)
.background(Color.White)
.padding(8.dp)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
val modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.padding(16.dp)
Text(
text = "查看", modifier = Modifier
.clickable(onClick = scope.launchAsFn {
@ -184,6 +177,7 @@ fun SnapshotPage() {
})
.then(modifier)
)
Divider()
Text(
text = "分享",
modifier = Modifier
@ -199,11 +193,16 @@ fun SnapshotPage() {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(Intent.createChooser(intent, "分享快照文件"))
context.startActivity(
Intent.createChooser(
intent, "分享快照文件"
)
)
selectedSnapshot = null
})
.then(modifier)
)
Divider()
if (recordStore.snapshotIdMap.containsKey(snapshotVal.id)) {
Text(
text = "复制链接", modifier = Modifier
@ -224,6 +223,7 @@ fun SnapshotPage() {
.then(modifier)
)
}
Divider()
Text(
text = "保存截图到相册",
@ -247,6 +247,7 @@ fun SnapshotPage() {
})
.then(modifier)
)
Divider()
Text(
text = "替换截图(去除隐私)",
modifier = Modifier
@ -281,6 +282,7 @@ fun SnapshotPage() {
})
.then(modifier)
)
Divider()
Text(
text = "删除", modifier = Modifier
.clickable(onClick = scope.launchAsFn {
@ -300,7 +302,7 @@ fun SnapshotPage() {
}
selectedSnapshot = null
})
.then(modifier)
.then(modifier), color = colorScheme.error
)
}
}
@ -328,35 +330,21 @@ fun SnapshotPage() {
}
is LoadStatus.Loading -> {
Dialog(onDismissRequest = { }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "上传文件中,请稍等",
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Spacer(modifier = Modifier.height(15.dp))
AlertDialog(
title = { Text(text = "上传文件中") },
text = {
LinearProgressIndicator(progress = uploadStatusVal.progress)
Spacer(modifier = Modifier.height(5.dp))
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
vm.uploadJob?.cancel(CancellationException("终止上传"))
vm.uploadJob = null
}) {
Text(text = "终止上传", color = Color.Red)
}
},
onDismissRequest = { },
confirmButton = {
TextButton(onClick = {
vm.uploadJob?.cancel(CancellationException("终止上传"))
vm.uploadJob = null
}) {
Text(text = "终止上传")
}
}
}
},
)
}
is LoadStatus.Success -> {
@ -386,17 +374,19 @@ fun SnapshotPage() {
AlertDialog(onDismissRequest = { showDeleteDlg = false },
title = { Text(text = "是否删除全部快照记录?") },
confirmButton = {
TextButton(onClick = scope.launchAsFn(Dispatchers.IO) {
showDeleteDlg = false
snapshots.forEach { s ->
SnapshotExt.removeAssets(s.id)
}
DbSet.snapshotDao.deleteAll()
updateStorage(
recordStoreFlow, recordStoreFlow.value.copy(snapshotIdMap = emptyMap())
)
}) {
Text(text = "", color = Color.Red)
TextButton(
onClick = scope.launchAsFn(Dispatchers.IO) {
showDeleteDlg = false
snapshots.forEach { s ->
SnapshotExt.removeAssets(s.id)
}
DbSet.snapshotDao.deleteAll()
updateStorage(
recordStoreFlow, recordStoreFlow.value.copy(snapshotIdMap = emptyMap())
)
},
) {
Text(text = "", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {

View File

@ -2,42 +2,36 @@ package li.songe.gkd.ui
import android.webkit.URLUtil
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -49,7 +43,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
@ -66,7 +59,6 @@ import li.songe.gkd.ui.destinations.SubsPageDestination
import li.songe.gkd.util.DEFAULT_SUBS_UPDATE_URL
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.SafeR
import li.songe.gkd.util.getImportUrl
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.navigate
import li.songe.gkd.util.subsIdToRawFlow
@ -80,26 +72,16 @@ val subsNav = BottomNavItem(
label = "订阅", icon = SafeR.ic_link, route = "subscription"
)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun SubsManagePage() {
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
val vm = hiltViewModel<SubsManageVm>()
val homeVm = hiltViewModel<HomePageVm>()
val subItems by subsItemsFlow.collectAsState()
val subsIdToRaw by subsIdToRawFlow.collectAsState()
val intent by homeVm.intentFlow.collectAsState()
LaunchedEffect(key1 = intent, block = {
val importUrl = getImportUrl(intent)
if (importUrl != null) {
homeVm.intentFlow.value = null
}
})
val orderSubItems = remember {
mutableStateOf(subItems)
}
@ -141,9 +123,9 @@ fun SubsManagePage() {
Scaffold(
topBar = {
TopAppBar(backgroundColor = Color(0xfff8f9f9), title = {
TopAppBar(title = {
Text(
text = "订阅", color = Color.Black
text = "订阅",
)
})
},
@ -190,8 +172,6 @@ fun SubsManagePage() {
.clickable {
navController.navigate(SubsPageDestination(subItem.id))
},
elevation = 0.dp,
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
shape = RoundedCornerShape(8.dp),
) {
SubsItemCard(
@ -217,38 +197,34 @@ fun SubsManagePage() {
}
}
menuSubItem?.let { menuSubItemVal ->
Dialog(onDismissRequest = { menuSubItem = null }) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
Card(
modifier = Modifier
.width(200.dp)
.wrapContentHeight()
.background(Color.White)
.padding(8.dp)
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
if (menuSubItemVal.updateUrl != null) {
Text(text = "复制链接", modifier = Modifier
Column {
if (menuSubItemVal.updateUrl != null) {
Text(text = "复制链接", modifier = Modifier
.clickable {
menuSubItem = null
ClipboardUtils.copyText(menuSubItemVal.updateUrl)
ToastUtils.showShort("复制成功")
}
.fillMaxWidth()
.padding(16.dp))
}
Text(text = "删除订阅", modifier = Modifier
.clickable {
deleteSubItem = menuSubItemVal
menuSubItem = null
ClipboardUtils.copyText(menuSubItemVal.updateUrl)
ToastUtils.showShort("复制成功")
}
.fillMaxWidth()
.padding(8.dp))
.padding(16.dp), color = MaterialTheme.colorScheme.error)
}
Text(text = "删除订阅", color = Color.Red, modifier = Modifier
.clickable {
deleteSubItem = menuSubItemVal
menuSubItem = null
}
.fillMaxWidth()
.padding(8.dp))
}
}
}
@ -262,7 +238,7 @@ fun SubsManagePage() {
deleteSubItem = null
deleteSubItemVal.removeAssets()
}) {
Text(text = "", color = Color.Red)
Text(text = "", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
@ -276,28 +252,29 @@ fun SubsManagePage() {
if (showAddDialog) {
Dialog(onDismissRequest = { showAddDialog = false }) {
Column(
Card(
modifier = Modifier
.width(250.dp)
.background(Color.White)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Text(text = "导入默认订阅", modifier = Modifier
.clickable {
showAddDialog = false
vm.addSubsFromUrl(DEFAULT_SUBS_UPDATE_URL)
}
.fillMaxWidth()
.padding(8.dp))
Column {
Text(text = "导入默认订阅", modifier = Modifier
.clickable {
showAddDialog = false
vm.addSubsFromUrl(DEFAULT_SUBS_UPDATE_URL)
}
.fillMaxWidth()
.padding(16.dp))
Text(text = "导入其它订阅", modifier = Modifier
.clickable {
showAddDialog = false
showAddLinkDialog = true
}
.fillMaxWidth()
.padding(8.dp))
Text(text = "导入其它订阅", modifier = Modifier
.clickable {
showAddDialog = false
showAddLinkDialog = true
}
.fillMaxWidth()
.padding(16.dp))
}
}
}
}
@ -310,42 +287,29 @@ fun SubsManagePage() {
}
}
if (showAddLinkDialog) {
Dialog(onDismissRequest = { showAddLinkDialog = false }) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.width(300.dp)
.background(Color.White)
.padding(10.dp)
) {
Text(text = "请输入订阅链接", fontSize = 20.sp)
Spacer(modifier = Modifier.height(2.dp))
OutlinedTextField(
value = link,
onValueChange = { link = it.trim() },
maxLines = 2,
textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
modifier = Modifier.fillMaxWidth()
)
Row(
horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = {
if (!URLUtil.isNetworkUrl(link)) {
ToastUtils.showShort("非法链接")
return@TextButton
}
if (subItems.any { s -> s.updateUrl == link }) {
ToastUtils.showShort("链接已存在")
return@TextButton
}
showAddLinkDialog = false
vm.addSubsFromUrl(url = link)
}) {
Text(text = "添加")
}
AlertDialog(title = { Text(text = "请输入订阅链接") }, text = {
OutlinedTextField(
value = link,
onValueChange = { link = it.trim() },
maxLines = 2,
textStyle = LocalTextStyle.current.copy(fontSize = 14.sp),
modifier = Modifier.fillMaxWidth()
)
}, onDismissRequest = { showAddLinkDialog = false }, confirmButton = {
TextButton(onClick = {
if (!URLUtil.isNetworkUrl(link)) {
ToastUtils.showShort("非法链接")
return@TextButton
}
if (subItems.any { s -> s.updateUrl == link }) {
ToastUtils.showShort("链接已存在")
return@TextButton
}
showAddLinkDialog = false
vm.addSubsFromUrl(url = link)
}) {
Text(text = "添加")
}
}
})
}
}

View File

@ -26,7 +26,7 @@ import javax.inject.Inject
@HiltViewModel
class SubsManageVm @Inject constructor() : ViewModel() {
fun addSubsFromUrl(url: String) = viewModelScope.launchTry {
fun addSubsFromUrl(url: String) = viewModelScope.launchTry(Dispatchers.IO) {
if (refreshingFlow.value) return@launchTry
if (!URLUtil.isNetworkUrl(url)) {

View File

@ -5,26 +5,26 @@ import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.itemsIndexed
import androidx.compose.material.AlertDialog
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -34,11 +34,9 @@ import androidx.compose.runtime.rememberCoroutineScope
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.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
@ -217,83 +215,74 @@ fun SubsPage(
var source by remember {
mutableStateOf("")
}
Dialog(onDismissRequest = { showAddDlg = false }) {
Column(
modifier = Modifier.defaultMinSize(minWidth = 300.dp),
) {
Text(text = "添加APP规则", fontSize = 18.sp, modifier = Modifier.padding(10.dp))
OutlinedTextField(
value = source,
onValueChange = { source = it },
maxLines = 8,
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
placeholder = { Text(text = "请输入规则\n若APP规则已经存在则追加") },
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth()
) {
TextButton(onClick = {
val newAppRaw = try {
SubscriptionRaw.parseAppRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
AlertDialog(title = { Text(text = "添加APP规则") }, text = {
OutlinedTextField(
value = source,
onValueChange = { source = it },
maxLines = 8,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入规则\n若APP规则已经存在则追加") },
)
}, onDismissRequest = { showAddDlg = false }, confirmButton = {
TextButton(onClick = {
val newAppRaw = try {
SubscriptionRaw.parseAppRaw(source)
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
return@TextButton
}
if (newAppRaw.groups.any { s -> s.name.isBlank() }) {
ToastUtils.showShort("不允许添加空白名规则组,请先命名")
return@TextButton
}
val oldAppRawIndex = subsRaw.apps.indexOfFirst { a -> a.id == newAppRaw.id }
val oldAppRaw = subsRaw.apps.getOrNull(oldAppRawIndex)
if (oldAppRaw != null) {
// check same group name
newAppRaw.groups.forEach { g ->
if (oldAppRaw.groups.any { g0 -> g0.name == g.name }) {
ToastUtils.showShort("已经存在同名规则[${g.name}]\n请修改名称后再添加")
return@TextButton
}
if (newAppRaw.groups.any { s -> s.name.isBlank() }) {
ToastUtils.showShort("不允许添加空白名规则组,请先命名")
return@TextButton
}
val oldAppRawIndex = subsRaw.apps.indexOfFirst { a -> a.id == newAppRaw.id }
val oldAppRaw = subsRaw.apps.getOrNull(oldAppRawIndex)
if (oldAppRaw != null) {
// check same group name
newAppRaw.groups.forEach { g ->
if (oldAppRaw.groups.any { g0 -> g0.name == g.name }) {
ToastUtils.showShort("已经存在同名规则[${g.name}]\n请修改名称后再添加")
return@TextButton
}
}
}
val newApps = if (oldAppRaw != null) {
val oldKey = oldAppRaw.groups.maxBy { g -> g.key }.key + 1
val finalAppRaw =
newAppRaw.copy(groups = oldAppRaw.groups + newAppRaw.groups.mapIndexed { i, g ->
g.copy(
key = oldKey + i
)
})
subsRaw.apps.toMutableList().apply {
set(oldAppRawIndex, finalAppRaw)
}
} else {
subsRaw.apps.toMutableList().apply {
add(newAppRaw)
}
}
vm.viewModelScope.launchTry {
subsItemVal.subsFile.writeText(
SubscriptionRaw.stringify(
subsRaw.copy(
apps = newApps, version = subsRaw.version + 1
)
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
showAddDlg = false
ToastUtils.showShort("添加成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
}
}
val newApps = if (oldAppRaw != null) {
val oldKey = oldAppRaw.groups.maxBy { g -> g.key }.key + 1
val finalAppRaw =
newAppRaw.copy(groups = oldAppRaw.groups + newAppRaw.groups.mapIndexed { i, g ->
g.copy(
key = oldKey + i
)
})
subsRaw.apps.toMutableList().apply {
set(oldAppRawIndex, finalAppRaw)
}
} else {
subsRaw.apps.toMutableList().apply {
add(newAppRaw)
}
}
vm.viewModelScope.launchTry {
subsItemVal.subsFile.writeText(
SubscriptionRaw.stringify(
subsRaw.copy(
apps = newApps, version = subsRaw.version + 1
)
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
showAddDlg = false
ToastUtils.showShort("添加成功")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
}
}
}, dismissButton = {
TextButton(onClick = { showAddDlg = false }) {
Text(text = "取消")
}
})
}
val editAppRawVal = editAppRaw
@ -301,100 +290,92 @@ fun SubsPage(
var source by remember {
mutableStateOf(Singleton.json.encodeToString(editAppRawVal))
}
Dialog(onDismissRequest = { editAppRaw = null }) {
Column(
modifier = Modifier.defaultMinSize(minWidth = 300.dp),
) {
Text(text = "编辑本地APP规则", fontSize = 18.sp, modifier = Modifier.padding(10.dp))
OutlinedTextField(
value = source,
onValueChange = { source = it },
maxLines = 8,
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
placeholder = { Text(text = "请输入规则") },
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.padding(start = 10.dp, end = 10.dp)
.fillMaxWidth()
) {
TextButton(onClick = {
try {
val newAppRaw = SubscriptionRaw.parseAppRaw(source)
if (newAppRaw.id != editAppRawVal.id) {
ToastUtils.showShort("不允许修改规则id")
return@TextButton
}
val oldAppRawIndex =
subsRaw.apps.indexOfFirst { a -> a.id == editAppRawVal.id }
vm.viewModelScope.launchTry {
subsItemVal.subsFile.writeText(
SubscriptionRaw.stringify(
subsRaw.copy(
apps = subsRaw.apps.toMutableList().apply {
set(oldAppRawIndex, newAppRaw)
}, version = subsRaw.version + 1
)
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
editAppRaw = null
ToastUtils.showShort("更新成功")
}
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
AlertDialog(title = { Text(text = "编辑本地APP规则") }, text = {
OutlinedTextField(
value = source,
onValueChange = { source = it },
maxLines = 8,
modifier = Modifier.fillMaxWidth(),
placeholder = { Text(text = "请输入规则") },
)
}, onDismissRequest = { editAppRaw = null }, confirmButton = {
TextButton(onClick = {
try {
val newAppRaw = SubscriptionRaw.parseAppRaw(source)
if (newAppRaw.id != editAppRawVal.id) {
ToastUtils.showShort("不允许修改规则id")
return@TextButton
}
val oldAppRawIndex = subsRaw.apps.indexOfFirst { a -> a.id == editAppRawVal.id }
vm.viewModelScope.launchTry {
subsItemVal.subsFile.writeText(
SubscriptionRaw.stringify(
subsRaw.copy(
apps = subsRaw.apps.toMutableList().apply {
set(oldAppRawIndex, newAppRaw)
}, version = subsRaw.version + 1
)
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
editAppRaw = null
ToastUtils.showShort("更新成功")
}
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("非法规则${e.message}")
}
}, enabled = source.isNotEmpty()) {
Text(text = "添加")
}
}
}, dismissButton = {
TextButton(onClick = { editAppRaw = null }) {
Text(text = "取消")
}
})
}
val menuAppRawVal = menuAppRaw
if (menuAppRawVal != null && subsItemVal != null && subsRaw != null) {
Dialog(onDismissRequest = { menuAppRaw = null }) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
Card(
modifier = Modifier
.padding(10.dp)
.width(200.dp)
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Text(text = "复制", modifier = Modifier
.clickable {
ClipboardUtils.copyText(
Singleton.json.encodeToString(
menuAppRawVal
)
)
ToastUtils.showShort("复制成功")
menuAppRaw = null
}
.padding(10.dp)
.fillMaxWidth())
Text(text = "删除", color = Color.Red, modifier = Modifier
.clickable {
// 也许需要二次确认
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(
Column {
Text(text = "复制", modifier = Modifier
.clickable {
ClipboardUtils.copyText(
Singleton.json.encodeToString(
subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != menuAppRawVal.id })
menuAppRawVal
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
DbSet.subsConfigDao.delete(subsItemVal.id, menuAppRawVal.id)
ToastUtils.showShort("删除成功")
ToastUtils.showShort("复制成功")
menuAppRaw = null
}
menuAppRaw = null
}
.padding(10.dp)
.fillMaxWidth())
.fillMaxWidth()
.padding(16.dp))
Text(text = "删除", modifier = Modifier
.clickable {
// 也许需要二次确认
vm.viewModelScope.launchTry(Dispatchers.IO) {
subsItemVal.subsFile.writeText(
Singleton.json.encodeToString(
subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != menuAppRawVal.id })
)
)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
DbSet.subsConfigDao.delete(subsItemVal.id, menuAppRawVal.id)
ToastUtils.showShort("删除成功")
}
menuAppRaw = null
}
.fillMaxWidth()
.padding(16.dp), color = MaterialTheme.colorScheme.error)
}
}
}
}

View File

@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View File

@ -2,23 +2,20 @@ package li.songe.gkd.ui.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import li.songe.gkd.icon.ArrowIcon
@Composable
fun SettingItem(
@ -40,10 +37,4 @@ fun SettingItem(
Text(text = title, fontSize = 18.sp)
Icon(imageVector = imageVector, contentDescription = title)
}
}
@Preview
@Composable
fun PreviewSettingItem() {
SettingItem(title = "你好", onClick = {})
}

View File

@ -1,49 +1,34 @@
package li.songe.gkd.ui.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import li.songe.gkd.util.SafeR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleTopAppBar(
@DrawableRes iconId: Int = SafeR.ic_back,
onClickIcon: (() -> Unit)? = null,
actions: @Composable() (RowScope.() -> Unit) = {},
actions: @Composable (RowScope.() -> Unit) = {},
title: String,
) {
TopAppBar(backgroundColor = Color(0xfff8f9f9), navigationIcon = {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(onClick = {
onClickIcon?.invoke()
}) {
Icon(
painter = painterResource(id = iconId),
contentDescription = null,
modifier = Modifier.size(30.dp)
)
}
TopAppBar(navigationIcon = {
IconButton(onClick = {
onClickIcon?.invoke()
}) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
modifier = Modifier.size(30.dp),
)
}
}, title = { Text(text = title) }, actions = actions
)

View File

@ -1,32 +0,0 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun SnapshotCard() {
Row {
Text(text = "06-02 20:47:48")
Spacer(modifier = Modifier.size(10.dp))
Text(text = "酷安")
Spacer(modifier = Modifier.size(10.dp))
Text(text = "查看")
Spacer(modifier = Modifier.size(10.dp))
Text(text = "分享")
Spacer(modifier = Modifier.size(10.dp))
Text(text = "删除")
}
}
@Preview
@Composable
fun PreviewSnapshotCard() {
SnapshotCard()
}

View File

@ -1,23 +1,20 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import com.blankj.utilcode.util.BarUtils
import com.blankj.utilcode.util.ConvertUtils
@Composable
fun StatusBar(color: Color = Color.Transparent) {
fun StatusBar() {
Spacer(
modifier = Modifier
.height(statusBarHeight)
.fillMaxWidth()
.background(color)
)
}

View File

@ -13,12 +13,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

View File

@ -10,31 +10,23 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubscriptionRaw
import li.songe.gkd.util.formatTimeAgo
import li.songe.gkd.util.safeRemoteBaseUrls
val safeRemoteBaseUrls = arrayOf(
"https://registry.npmmirror.com/@gkd-kit/",
"https://cdn.jsdelivr.net/npm/@gkd-kit/",
"https://unpkg.com/@gkd-kit/",
"https://github.com/gkd-kit/",
"https://raw.githubusercontent.com/gkd-kit/"
)
@Composable
fun SubsItemCard(
@ -133,17 +125,3 @@ fun SubsItemCard(
}
}
}
@Preview
@Composable
fun PreviewSubscriptionItemCard() {
Surface(modifier = Modifier.width(400.dp)) {
SubsItemCard(
SubsItem(
id = 0,
order = 1,
updateUrl = "https://registry.npmmirror.com/@gkd-kit/subscription/latest/files",
), subscriptionRaw = null, index = 1
)
}
}

View File

@ -6,12 +6,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Switch
import androidx.compose.material.Text
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -42,11 +41,3 @@ fun TextSwitch(
)
}
}
@Preview
@Composable
fun PreviewTextSwitch() {
TextSwitch(
name = "隐藏后台", desc = "在最近任务列表中隐藏", checked = true
)
}

View File

@ -1,11 +0,0 @@
package li.songe.gkd.ui.component
import androidx.compose.runtime.Composable
@Composable
fun UnDialog(
title: String? = null,
content: @Composable () -> Unit,
) {
}

View File

@ -1,8 +0,0 @@
package li.songe.gkd.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFf8f9f9)
val Purple500 = Color(0xFFf2f3f4)
val Purple700 = Color(0xFFe5e7e9)
val Teal200 = Color(0xFF03DAC5)

View File

@ -1,11 +0,0 @@
package li.songe.gkd.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View File

@ -1,24 +1,32 @@
package li.songe.gkd.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import li.songe.gkd.util.map
import li.songe.gkd.util.storeFlow
private val darkColorPalette = darkColors()
private val lightColorPalette = lightColors()
val LightColorScheme = lightColorScheme()
val DarkColorScheme = darkColorScheme()
@Composable
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
darkColorPalette
fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val scope = rememberCoroutineScope()
val enableDarkTheme by storeFlow.map(scope) { s -> s.enableDarkTheme }.collectAsState()
val colorScheme = if (enableDarkTheme ?: useDarkTheme) {
DarkColorScheme
} else {
lightColorPalette
LightColorScheme
}
MaterialTheme(
colors = colors, typography = Typography, shapes = Shapes, content = content
colorScheme = colorScheme, content = content
)
}

View File

@ -1,28 +0,0 @@
package li.songe.gkd.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View File

@ -6,3 +6,11 @@ const val FILE_UPLOAD_URL = "https://github-upload-assets.lisonge.workers.dev/"
const val DEFAULT_SUBS_UPDATE_URL =
"https://registry.npmmirror.com/@gkd-kit/subscription/latest/files"
val safeRemoteBaseUrls = arrayOf(
"https://registry.npmmirror.com/@gkd-kit/",
"https://cdn.jsdelivr.net/npm/@gkd-kit/",
"https://unpkg.com/@gkd-kit/",
"https://github.com/gkd-kit/",
"https://raw.githubusercontent.com/gkd-kit/"
)

View File

@ -1,78 +0,0 @@
package li.songe.gkd.util
import android.graphics.Path
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import androidx.compose.material.icons.materialIcon
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.addPathNodes
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
fun createDrawable(block: () -> Unit) {
val s = materialIcon(name = "xx") {
addPath(addPathNodes(""))
}
}
fun createVectorDrawable(block: () -> Unit) {
val path = Path().apply {
addPathNodes("").forEach {
it.isQuad
}
}
val shapeDrawable = ShapeDrawable(RectShape())
shapeDrawable.apply {
addPathNodes("")[0].isCurve
}
// val r = Resources()
// Drawable.createFromXml()
}
val x = createDrawable {
// val p =
vector {
width = 24.dp
height = 24.dp
viewportWidth = 24F
viewportHeight = 24F
path {
width
fillColor = Color(0xFF000000)
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
}
}
}
interface VectorType {
var width: Dp
var height: Dp
var viewportWidth: Float
var viewportHeight: Float
}
fun vector(block: VectorType.() -> Unit) {}
interface PathType {
var fillColor: Color
var pathData: String
}
fun path(block: PathType.() -> Unit) {}
fun testDrawable() {
val s2 = vector {
width = 24.dp
height = 24.dp
viewportWidth = 24F
viewportHeight = 24F
path {
width
fillColor = Color(0xFF000000)
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
}
}
}

View File

@ -86,6 +86,7 @@ data class Store(
val hideSnapshotStatusBar: Boolean = false,
val enableShizuku: Boolean = false,
val log2FileSwitch: Boolean = true,
val enableDarkTheme: Boolean? = null,
)
val storeFlow by lazy {

View File

@ -2,26 +2,19 @@ package li.songe.gkd.util
import android.os.Parcelable
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.AlertDialog
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.blankj.utilcode.util.AppUtils
import io.ktor.client.call.body
import io.ktor.client.plugins.onDownload
@ -116,7 +109,14 @@ fun UpgradeDialog() {
AlertDialog(title = {
Text(text = "检测到新版本")
}, text = {
Text(text = "v${BuildConfig.VERSION_NAME} -> v${newVersionVal.versionName}\n\n${newVersionVal.changelog}".trimEnd())
Text(
text = "v${BuildConfig.VERSION_NAME} -> v${newVersionVal.versionName}\n\n${newVersionVal.changelog}\n${newVersionVal.changelog}".trimEnd(),
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
)
}, onDismissRequest = { }, confirmButton = {
TextButton(onClick = {
newVersionFlow.value = null
@ -135,42 +135,27 @@ fun UpgradeDialog() {
downloadStatus?.let { downloadStatusVal ->
when (downloadStatusVal) {
is LoadStatus.Loading -> {
Dialog(onDismissRequest = { }) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "下载新版本中,稍等片刻",
fontSize = 16.sp,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Spacer(modifier = Modifier.height(15.dp))
AlertDialog(
title = { Text(text = "下载新版本中") },
text = {
LinearProgressIndicator(progress = downloadStatusVal.progress)
Spacer(modifier = Modifier.height(5.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
downloadStatusFlow.value = LoadStatus.Failure(
Exception("终止下载")
)
}) {
Text(text = "终止下载", color = Color.Red)
}
},
onDismissRequest = {},
confirmButton = {
TextButton(onClick = {
downloadStatusFlow.value = LoadStatus.Failure(
Exception("终止下载")
)
}) {
Text(text = "终止下载")
}
}
}
},
)
}
is LoadStatus.Failure -> {
AlertDialog(
title = { Text(text = "安装包下载失败") },
title = { Text(text = "新版本下载失败") },
text = {
Text(text = downloadStatusVal.exception.let {
it.message ?: it.toString()

View File

@ -1,18 +1,14 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="android:Theme.Material.NoActionBar"></style>
<style name="AppTheme" parent="Theme.AppCompat">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:background">@android:color/white</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="SplashScreenTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">
@drawable/ic_launcher_circle
</item>
<item name="windowSplashScreenBackground">@android:color/white</item>
<item name="windowSplashScreenAnimationDuration">1000</item>
<!-- postSplashScreenTheme must invoke installSplashScreen -->
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>

View File

@ -1,17 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:background">@android:color/white</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar"></style>
<style name="SplashScreenTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">
@drawable/ic_launcher_circle
</item>
<item name="windowSplashScreenBackground">@android:color/white</item>
<item name="windowSplashScreenAnimationDuration">1000</item>
<!-- postSplashScreenTheme must invoke installSplashScreen -->
<item name="postSplashScreenTheme">@style/AppTheme</item>

View File

@ -57,6 +57,7 @@ dependencyResolutionManagement {
version("compose.compilerVersion", "1.5.3")
library("compose.ui", "androidx.compose.ui:ui:1.5.1")
library("compose.material", "androidx.compose.material:material:1.5.1")
library("compose.material3", "androidx.compose.material3:material3:1.1.2")
library("compose.preview", "androidx.compose.ui:ui-tooling-preview:1.5.1")
library("compose.tooling", "androidx.compose.ui:ui-tooling:1.5.1")
library("compose.junit4", "androidx.compose.ui:ui-test-junit4:1.5.1")