refactor(templates): move config templates from a list card to a top-bar action (#561)

The templates card sat at the top of the scrolling config list, pushing
the actual settings down. Templates apply to the config as a whole, so
they belong with the editor's fixed actions: replace the card with a
top-bar icon button (WtaScreen actions) that opens a dropdown — save as
template, tap-to-apply list (scrollable when long), and manage. Dialogs
and store semantics unchanged; docs wording updated to point at the
top-bar button.

Co-authored-by: shiaho <222622008+shiaho777@users.noreply.github.com>
This commit is contained in:
shiaho
2026-08-16 00:09:01 +08:00
committed by GitHub
parent 84eb80db85
commit 8e79238b54
4 changed files with 72 additions and 79 deletions
@@ -3,13 +3,15 @@ package com.webtoapp.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.DriveFileRenameOutline
import androidx.compose.material.icons.outlined.EditNote
import androidx.compose.material.icons.outlined.LibraryAddCheck
import androidx.compose.material3.*
import androidx.compose.runtime.*
import kotlinx.coroutines.launch
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -18,98 +20,89 @@ import com.webtoapp.core.i18n.Strings
import com.webtoapp.data.model.WebViewConfig
import com.webtoapp.data.repository.ConfigTemplateStore
import com.webtoapp.ui.components.PremiumTextField
import com.webtoapp.ui.design.*
/**
* Save / apply named snapshots of the common config (WebViewConfig). Applying replaces
* the whole WebViewConfig a template is a full snapshot, so the result is exactly
* what was saved, never a mix of old and new values.
* Top-bar action for common-config templates: save the current WebViewConfig as a named
* snapshot, apply one to the app (a template is a full snapshot applying replaces the
* whole WebViewConfig), and manage saved templates. Lives in the editor's fixed top bar
* instead of the scrolling config list, since it applies to the config as a whole.
*/
@Composable
fun ConfigTemplateCard(
fun ConfigTemplateMenuButton(
config: WebViewConfig,
snackbarHostState: SnackbarHostState,
onApplyConfig: (WebViewConfig) -> Unit
) {
val context = LocalContext.current
var templates by remember { mutableStateOf(ConfigTemplateStore.list(context)) }
var refresh by remember { mutableIntStateOf(0) }
LaunchedEffect(refresh) {
LaunchedEffect(refresh, templates.isEmpty()) {
// reload after any mutation; also opportunistically refresh when the menu opens
if (refresh > 0) templates = ConfigTemplateStore.list(context)
}
var menuOpen by remember { mutableStateOf(false) }
var saveDialogOpen by remember { mutableStateOf(false) }
var manageOpen by remember { mutableStateOf(false) }
var renameTarget by remember { mutableStateOf<ConfigTemplateStore.ConfigTemplate?>(null) }
var renameText by remember { mutableStateOf("") }
var toast by remember { mutableStateOf<String?>(null) }
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(toast) {
toast?.let {
snackbarHostState.showSnackbar(it)
toast = null
}
val scope = rememberCoroutineScope()
fun notify(message: String) {
scope.launch { snackbarHostState.showSnackbar(message) }
}
Box {
WtaSettingCard {
Column {
WtaChoiceRow(
title = Strings.configTemplates,
subtitle = Strings.configTemplatesDesc,
icon = Icons.Outlined.LibraryAddCheck,
value = "",
isExpanded = templates.isNotEmpty(),
onClick = { manageOpen = true }
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = WtaSpacing.RowHorizontal, vertical = WtaSpacing.ContentGap),
horizontalArrangement = Arrangement.spacedBy(WtaSpacing.ContentGap)
) {
WtaButton(
onClick = { saveDialogOpen = true },
text = Strings.templateSaveAs,
modifier = Modifier.weight(1f),
variant = WtaButtonVariant.Outlined,
leadingIcon = Icons.Outlined.EditNote
)
IconButton(onClick = {
templates = ConfigTemplateStore.list(context)
menuOpen = true
}) {
Icon(
Icons.Outlined.LibraryAddCheck,
contentDescription = Strings.configTemplates
)
}
DropdownMenu(
expanded = menuOpen,
onDismissRequest = { menuOpen = false }
) {
DropdownMenuItem(
text = { Text(Strings.templateSaveAs) },
onClick = {
menuOpen = false
saveDialogOpen = true
}
if (templates.isNotEmpty()) {
Column(modifier = Modifier.padding(horizontal = WtaSpacing.RowHorizontal)) {
Text(
Strings.templateApplyHint,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(WtaSpacing.ContentGap))
templates.forEach { template ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = {
onApplyConfig(template.webViewConfig)
toast = Strings.templateApplied(template.name)
}) {
Text(template.name, style = MaterialTheme.typography.bodyMedium)
}
)
if (templates.isNotEmpty()) {
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
Text(
Strings.templateApplyHint,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp)
)
Column(modifier = Modifier.heightIn(max = 280.dp).verticalScroll(rememberScrollState())) {
templates.forEach { template ->
DropdownMenuItem(
text = { Text(template.name) },
onClick = {
menuOpen = false
onApplyConfig(template.webViewConfig)
notify(Strings.templateApplied(template.name))
}
}
Spacer(modifier = Modifier.height(WtaSpacing.ContentGap))
TextButton(onClick = { manageOpen = true }) {
Text(Strings.templateManage)
}
)
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem(
text = { Text(Strings.templateManage) },
onClick = {
menuOpen = false
manageOpen = true
}
)
}
}
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
if (saveDialogOpen) {
@@ -131,7 +124,7 @@ fun ConfigTemplateCard(
onClick = {
if (ConfigTemplateStore.save(context, name, config)) {
refresh++
toast = Strings.templateSaved(name.trim())
notify(Strings.templateSaved(name.trim()))
}
saveDialogOpen = false
}
@@ -148,6 +148,15 @@ fun CreateAppScreen(
onBack = {
if (hasUnsavedChanges) showDiscardDialog = true else onBack()
},
actions = {
ConfigTemplateMenuButton(
config = editState.webViewConfig,
snackbarHostState = snackbarHostState,
onApplyConfig = { templateConfig ->
viewModel.updateEditState { copy(webViewConfig = templateConfig) }
}
)
},
bottomBar = {
CreateAppBottomBar(
canPreview = canPreview,
@@ -189,15 +198,6 @@ fun CreateAppScreen(
)
}
item {
ConfigTemplateCard(
config = editState.webViewConfig,
onApplyConfig = { templateConfig ->
viewModel.updateEditState { copy(webViewConfig = templateConfig) }
}
)
}
item {
if (editState.appType == AppType.WEB) {
PwaAnalysisSection(
@@ -2,7 +2,7 @@
Save the whole common config as a named template once, then apply it to any app in one tap — so a heavily customized browser setup never has to be re-configured app by app.
**Where:** the **Config Templates** card at the top of the [Edit Common Config](/guide/app-actions/edit-common-config/) editor.
**Where:** the **Config Templates** button (library icon) in the top bar of the [Edit Common Config](/guide/app-actions/edit-common-config/) editor.
## What a template contains
@@ -2,7 +2,7 @@
把整套通用配置保存为命名模板,之后任何应用一键套用 —— 精心调好的浏览器配置不用再逐个应用重新设置。
**位置:**[编辑通用配置](/zh/guide/app-actions/edit-common-config/)编辑器顶的**配置模板**卡片
**位置:**[编辑通用配置](/zh/guide/app-actions/edit-common-config/)编辑器顶的**配置模板**按钮(书库图标)
## 模板包含什么