Skip to content

Commit e46ea62

Browse files
committed
feat: customNotifTitle
1 parent ae1e2f4 commit e46ea62

File tree

4 files changed

+244
-41
lines changed

4 files changed

+244
-41
lines changed

app/src/main/kotlin/li/songe/gkd/service/StatusService.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.combine
88
import kotlinx.coroutines.flow.debounce
99
import kotlinx.coroutines.flow.stateIn
1010
import kotlinx.coroutines.launch
11+
import li.songe.gkd.META
1112
import li.songe.gkd.notif.abNotif
1213
import li.songe.gkd.permission.foregroundServiceSpecialUseState
1314
import li.songe.gkd.permission.notificationState
@@ -38,19 +39,30 @@ class StatusService : Service(), OnSimpleLife {
3839
ruleSummaryFlow,
3940
actionCountFlow,
4041
) { abRunning, store, ruleSummary, count ->
41-
if (!abRunning) return@combine "无障碍未授权"
42-
if (!store.enableMatch) return@combine "暂停规则匹配"
43-
if (store.useCustomNotifText) {
44-
return@combine store.customNotifText
45-
.replace("\${i}", ruleSummary.globalGroups.size.toString())
46-
.replace("\${k}", ruleSummary.appSize.toString())
47-
.replace("\${u}", ruleSummary.appGroupSize.toString())
48-
.replace("\${n}", count.toString())
42+
if (!abRunning) {
43+
META.appName to "无障碍未授权"
44+
} else if (!store.enableMatch) {
45+
META.appName to "暂停规则匹配"
46+
} else if (store.useCustomNotifText) {
47+
listOf(store.customNotifTitle, store.customNotifText).map {
48+
it.replace("\${i}", ruleSummary.globalGroups.size.toString())
49+
.replace("\${k}", ruleSummary.appSize.toString())
50+
.replace("\${u}", ruleSummary.appGroupSize.toString())
51+
.replace("\${n}", count.toString())
52+
}.run {
53+
first() to last()
54+
}
55+
} else {
56+
META.appName to getSubsStatus(ruleSummary, count)
57+
}
58+
}.debounce(1000L)
59+
.stateIn(scope, SharingStarted.Eagerly, "" to "")
60+
.collect { (title, text) ->
61+
abNotif.copy(
62+
title = title,
63+
text = text
64+
).notifyService()
4965
}
50-
return@combine getSubsStatus(ruleSummary, count)
51-
}.debounce(500L).stateIn(scope, SharingStarted.Eagerly, "").collect { text ->
52-
abNotif.copy(text = text).notifyService()
53-
}
5466
}
5567
}
5668
}

app/src/main/kotlin/li/songe/gkd/store/SettingsStore.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ data class SettingsStore(
2626
val showSaveSnapshotToast: Boolean = true,
2727
val useSystemToast: Boolean = false,
2828
val useCustomNotifText: Boolean = false,
29+
val customNotifTitle: String = META.appName,
2930
val customNotifText: String = "\${i}全局/\${k}应用/\${u}规则组/\${n}触发",
3031
val enableActivityLog: Boolean = false,
3132
val updateChannel: Int = if (META.versionName.contains("beta")) UpdateChannelOption.Beta.value else UpdateChannelOption.Stable.value,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package li.songe.gkd.ui.component
2+
3+
import androidx.compose.foundation.interaction.MutableInteractionSource
4+
import androidx.compose.foundation.interaction.collectIsFocusedAsState
5+
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.defaultMinSize
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.text.BasicTextField
9+
import androidx.compose.foundation.text.KeyboardActions
10+
import androidx.compose.foundation.text.KeyboardOptions
11+
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
12+
import androidx.compose.material3.LocalTextStyle
13+
import androidx.compose.material3.OutlinedTextFieldDefaults
14+
import androidx.compose.material3.TextFieldColors
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.CompositionLocalProvider
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.graphics.Shape
20+
import androidx.compose.ui.graphics.SolidColor
21+
import androidx.compose.ui.graphics.takeOrElse
22+
import androidx.compose.ui.platform.LocalDensity
23+
import androidx.compose.ui.semantics.semantics
24+
import androidx.compose.ui.text.TextStyle
25+
import androidx.compose.ui.text.input.VisualTransformation
26+
import androidx.compose.ui.unit.sp
27+
28+
// copy from androidx/compose/material3/OutlinedTextField.kt
29+
30+
@Composable
31+
fun CustomOutlinedTextField(
32+
value: String,
33+
onValueChange: (String) -> Unit,
34+
modifier: Modifier = Modifier,
35+
enabled: Boolean = true,
36+
readOnly: Boolean = false,
37+
textStyle: TextStyle = LocalTextStyle.current,
38+
label: @Composable (() -> Unit)? = null,
39+
placeholder: @Composable (() -> Unit)? = null,
40+
leadingIcon: @Composable (() -> Unit)? = null,
41+
trailingIcon: @Composable (() -> Unit)? = null,
42+
prefix: @Composable (() -> Unit)? = null,
43+
suffix: @Composable (() -> Unit)? = null,
44+
supportingText: @Composable (() -> Unit)? = null,
45+
isError: Boolean = false,
46+
visualTransformation: VisualTransformation = VisualTransformation.None,
47+
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
48+
keyboardActions: KeyboardActions = KeyboardActions.Default,
49+
singleLine: Boolean = false,
50+
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
51+
minLines: Int = 1,
52+
interactionSource: MutableInteractionSource? = null,
53+
shape: Shape = OutlinedTextFieldDefaults.shape,
54+
colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
55+
contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding()
56+
) {
57+
@Suppress("NAME_SHADOWING")
58+
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
59+
// If color is not provided via the text style, use content color as a default
60+
val textColor =
61+
textStyle.color.takeOrElse {
62+
val focused = interactionSource.collectIsFocusedAsState().value
63+
colors.run {
64+
when {
65+
!enabled -> disabledTextColor
66+
isError -> errorTextColor
67+
focused -> focusedTextColor
68+
else -> unfocusedTextColor
69+
}
70+
}
71+
}
72+
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
73+
74+
val density = LocalDensity.current
75+
76+
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
77+
BasicTextField(
78+
value = value,
79+
modifier =
80+
modifier
81+
.then(
82+
if (label != null) {
83+
Modifier
84+
// Merge semantics at the beginning of the modifier chain to ensure
85+
// padding is considered part of the text field.
86+
.semantics(mergeDescendants = true) {}
87+
.padding(top = with(density) { 8.sp.toDp() })
88+
} else {
89+
Modifier
90+
}
91+
)
92+
// .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
93+
.defaultMinSize(
94+
minWidth = OutlinedTextFieldDefaults.MinWidth,
95+
minHeight = OutlinedTextFieldDefaults.MinHeight
96+
),
97+
onValueChange = onValueChange,
98+
enabled = enabled,
99+
readOnly = readOnly,
100+
textStyle = mergedTextStyle,
101+
cursorBrush = SolidColor(colors.run { if (isError) errorCursorColor else cursorColor }),
102+
visualTransformation = visualTransformation,
103+
keyboardOptions = keyboardOptions,
104+
keyboardActions = keyboardActions,
105+
interactionSource = interactionSource,
106+
singleLine = singleLine,
107+
maxLines = maxLines,
108+
minLines = minLines,
109+
decorationBox =
110+
@Composable { innerTextField ->
111+
OutlinedTextFieldDefaults.DecorationBox(
112+
value = value,
113+
visualTransformation = visualTransformation,
114+
innerTextField = innerTextField,
115+
placeholder = placeholder,
116+
label = label,
117+
leadingIcon = leadingIcon,
118+
trailingIcon = trailingIcon,
119+
prefix = prefix,
120+
suffix = suffix,
121+
supportingText = supportingText,
122+
singleLine = singleLine,
123+
enabled = enabled,
124+
isError = isError,
125+
interactionSource = interactionSource,
126+
colors = colors,
127+
container = {
128+
OutlinedTextFieldDefaults.Container(
129+
enabled = enabled,
130+
isError = isError,
131+
interactionSource = interactionSource,
132+
colors = colors,
133+
shape = shape,
134+
)
135+
},
136+
contentPadding = contentPadding,
137+
)
138+
}
139+
)
140+
}
141+
}

app/src/main/kotlin/li/songe/gkd/ui/home/SettingsPage.kt

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package li.songe.gkd.ui.home
22

3+
import androidx.activity.compose.LocalActivity
34
import androidx.compose.animation.AnimatedVisibility
45
import androidx.compose.foundation.clickable
56
import androidx.compose.foundation.layout.Arrangement
67
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.PaddingValues
79
import androidx.compose.foundation.layout.Row
810
import androidx.compose.foundation.layout.Spacer
911
import androidx.compose.foundation.layout.fillMaxWidth
@@ -32,12 +34,15 @@ import androidx.compose.ui.Alignment
3234
import androidx.compose.ui.Modifier
3335
import androidx.compose.ui.input.nestedscroll.nestedScroll
3436
import androidx.compose.ui.text.style.TextAlign
37+
import androidx.compose.ui.unit.dp
3538
import androidx.compose.ui.window.DialogProperties
3639
import androidx.lifecycle.viewmodel.compose.viewModel
40+
import com.blankj.utilcode.util.KeyboardUtils
3741
import com.ramcosta.composedestinations.generated.destinations.AboutPageDestination
3842
import com.ramcosta.composedestinations.generated.destinations.AdvancedPageDestination
3943
import kotlinx.coroutines.flow.update
4044
import li.songe.gkd.store.storeFlow
45+
import li.songe.gkd.ui.component.CustomOutlinedTextField
4146
import li.songe.gkd.ui.component.SettingItem
4247
import li.songe.gkd.ui.component.TextMenu
4348
import li.songe.gkd.ui.component.TextSwitch
@@ -50,10 +55,12 @@ import li.songe.gkd.ui.theme.supportDynamicColor
5055
import li.songe.gkd.util.DarkThemeOption
5156
import li.songe.gkd.util.findOption
5257
import li.songe.gkd.util.throttle
58+
import li.songe.gkd.util.toast
5359

5460
@Composable
5561
fun useSettingsPage(): ScaffoldExt {
5662
val mainVm = LocalMainViewModel.current
63+
val activity = LocalActivity.current
5764
val store by storeFlow.collectAsState()
5865
val vm = viewModel<HomeVm>()
5966

@@ -64,7 +71,6 @@ fun useSettingsPage(): ScaffoldExt {
6471
mutableStateOf(false)
6572
}
6673

67-
6874
if (showToastInputDlg) {
6975
var value by remember {
7076
mutableStateOf(store.clickToast)
@@ -116,9 +122,8 @@ fun useSettingsPage(): ScaffoldExt {
116122
)
117123
}
118124
if (showNotifTextInputDlg) {
119-
var value by remember {
120-
mutableStateOf(store.customNotifText)
121-
}
125+
var titleValue by remember { mutableStateOf(store.customNotifTitle) }
126+
var textValue by remember { mutableStateOf(store.customNotifText) }
122127
AlertDialog(
123128
properties = DialogProperties(dismissOnClickOutside = false),
124129
title = {
@@ -129,9 +134,17 @@ fun useSettingsPage(): ScaffoldExt {
129134
) {
130135
Text(text = "通知文案")
131136
IconButton(onClick = throttle {
137+
KeyboardUtils.hideSoftInput(activity)
138+
showNotifTextInputDlg = false
139+
val confirmAction = {
140+
mainVm.dialogFlow.value = null
141+
showNotifTextInputDlg = true
142+
}
132143
mainVm.dialogFlow.updateDialogOptions(
133144
title = "文案规则",
134-
text = "通知文案支持变量替换,规则如下\n\${i} 全局规则数\n\${k} 应用数\n\${u} 应用规则组数\n\${n} 触发次数\n\n示例模板\n\${i}全局/\${k}应用/\${u}规则组/\${n}触发\n\n替换结果\n0全局/1应用/2规则组/3触发",
145+
text = "通知文案支持变量替换,规则如下\n\${i} 全局规则数\n\${k} 应用数\n\${u} 应用规则组数\n\${n} 触发次数\n\n示例模板\n\${i}全局/\${k}应用/\${u}规则组/\${n}触发\n\n替换结果\n0全局/1应用/2规则组/3触发",
146+
confirmAction = confirmAction,
147+
onDismissRequest = confirmAction,
135148
)
136149
}) {
137150
Icon(
@@ -142,35 +155,67 @@ fun useSettingsPage(): ScaffoldExt {
142155
}
143156
},
144157
text = {
145-
val maxCharLen = 64
146-
OutlinedTextField(
147-
value = value,
148-
placeholder = {
149-
Text(text = "请输入文案内容,支持变量替换")
150-
},
151-
onValueChange = {
152-
value = if (it.length > maxCharLen) it.take(maxCharLen) else it
153-
},
154-
maxLines = 4,
155-
supportingText = {
156-
Text(
157-
text = "${value.length} / $maxCharLen",
158-
modifier = Modifier.fillMaxWidth(),
159-
textAlign = TextAlign.End,
160-
)
161-
},
162-
modifier = Modifier
163-
.fillMaxWidth()
164-
.autoFocus()
165-
)
158+
val titleMaxLen = 32
159+
val textMaxLen = 64
160+
Column(
161+
modifier = Modifier.fillMaxWidth(),
162+
) {
163+
CustomOutlinedTextField(
164+
label = { Text("主标题") },
165+
value = titleValue,
166+
placeholder = { Text(text = "请输入内容,支持变量替换") },
167+
onValueChange = {
168+
titleValue = (if (it.length > titleMaxLen) it.take(titleMaxLen) else it)
169+
.filter { c -> c !in "\n\r" }
170+
},
171+
supportingText = {
172+
Text(
173+
text = "${titleValue.length} / $titleMaxLen",
174+
modifier = Modifier.fillMaxWidth(),
175+
textAlign = TextAlign.End,
176+
)
177+
},
178+
singleLine = true,
179+
modifier = Modifier.fillMaxWidth(),
180+
contentPadding = PaddingValues(12.dp),
181+
)
182+
Spacer(modifier = Modifier.height(4.dp))
183+
CustomOutlinedTextField(
184+
label = { Text("副标题") },
185+
value = textValue,
186+
placeholder = { Text(text = "请输入内容,支持变量替换") },
187+
onValueChange = {
188+
textValue = if (it.length > textMaxLen) it.take(textMaxLen) else it
189+
},
190+
supportingText = {
191+
Text(
192+
text = "${textValue.length} / $textMaxLen",
193+
modifier = Modifier.fillMaxWidth(),
194+
textAlign = TextAlign.End,
195+
)
196+
},
197+
maxLines = 4,
198+
modifier = Modifier
199+
.fillMaxWidth()
200+
.autoFocus(),
201+
contentPadding = PaddingValues(12.dp),
202+
)
203+
}
166204
},
167205
onDismissRequest = {
168206
showNotifTextInputDlg = false
169207
},
170208
confirmButton = {
171-
TextButton(enabled = value.isNotEmpty(), onClick = {
172-
storeFlow.update { it.copy(customNotifText = value) }
209+
TextButton(onClick = {
210+
KeyboardUtils.hideSoftInput(activity)
211+
storeFlow.update {
212+
it.copy(
213+
customNotifTitle = titleValue,
214+
customNotifText = textValue
215+
)
216+
}
173217
showNotifTextInputDlg = false
218+
toast("更新成功")
174219
}) {
175220
Text(
176221
text = "确认",
@@ -247,7 +292,11 @@ fun useSettingsPage(): ScaffoldExt {
247292
val subsStatus by vm.subsStatusFlow.collectAsState()
248293
TextSwitch(
249294
title = "通知文案",
250-
subtitle = if (store.useCustomNotifText) store.customNotifText else subsStatus,
295+
subtitle = if (store.useCustomNotifText) {
296+
store.customNotifTitle + " / " + store.customNotifText
297+
} else {
298+
subsStatus
299+
},
251300
checked = store.useCustomNotifText,
252301
modifier = Modifier.clickable {
253302
showNotifTextInputDlg = true

0 commit comments

Comments
 (0)