dev global custom config

This commit is contained in:
armv9 2025-09-03 19:16:29 +09:00
parent 7c46729c83
commit 835fcca7b9
15 changed files with 134 additions and 63 deletions

View File

@ -16,6 +16,8 @@ object Key {
const val MODE_VPN = "vpn"
const val MODE_PROXY = "proxy"
const val GLOBAL_CUSTOM_CONFIG = "globalCustomConfig"
const val REMOTE_DNS = "remoteDns"
const val DIRECT_DNS = "directDns"
const val ENABLE_DNS_ROUTING = "enableDnsRouting"

View File

@ -108,6 +108,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)
var showGroupInNotification by configurationStore.boolean("showGroupInNotification")
var globalCustomConfig by configurationStore.string(Key.GLOBAL_CUSTOM_CONFIG) { "" }
var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://dns.google/dns-query" }
var directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" }
var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true }

View File

@ -35,6 +35,7 @@ import moe.matsuri.nb4a.proxy.config.ConfigBean
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean
import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean
import moe.matsuri.nb4a.utils.JavaUtil.gson
import moe.matsuri.nb4a.utils.Util
import moe.matsuri.nb4a.utils.listByLineOrComma
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -335,9 +336,7 @@ fun buildConfig(
// internal outbound
currentOutbound = when (bean) {
is ConfigBean -> SingBoxOption().apply {
_hack_custom_config = bean.config
}
is ConfigBean -> CustomSingBoxOption(bean.config)
is ShadowTLSBean -> // before StandardV2RayBean
buildSingBoxOutboundShadowTLSBean(bean)
@ -367,7 +366,7 @@ fun buildConfig(
buildSingBoxOutboundAnyTLSBean(bean)
else -> throw IllegalStateException("can't reach")
} as SingBoxOption
}
// internal mux
if (!muxApplied) {
@ -739,10 +738,12 @@ fun buildConfig(
}
}
_hack_custom_config = proxy.requireBean().customConfigJson
_hack_custom_config = DataStore.globalCustomConfig
}.let {
val configMap = it.asMap()
Util.mergeJSON(configMap, proxy.requireBean().customConfigJson)
ConfigBuildResult(
gson.toJson(it.asMap()),
gson.toJson(configMap),
externalIndexMap,
proxy.id,
trafficMap,

View File

@ -23,6 +23,7 @@ import io.nekohasekai.sagernet.databinding.LayoutBackupBinding
import io.nekohasekai.sagernet.databinding.LayoutImportBinding
import io.nekohasekai.sagernet.databinding.LayoutProgressBinding
import io.nekohasekai.sagernet.ktx.*
import kotlinx.coroutines.delay
import moe.matsuri.nb4a.utils.Util
import org.json.JSONArray
import org.json.JSONObject
@ -34,33 +35,53 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
override fun name0() = app.getString(R.string.backup)
var content = ""
private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
if (data != null) {
runOnDefaultDispatcher {
try {
requireActivity().contentResolver.openOutputStream(
data
)!!.bufferedWriter().use {
it.write(content)
}
onMainDispatcher {
snackbar(getString(R.string.action_export_msg)).show()
}
} catch (e: Exception) {
Logs.w(e)
onMainDispatcher {
snackbar(e.readableMessage).show()
private val exportSettings =
registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
if (data != null) {
runOnDefaultDispatcher {
try {
requireActivity().contentResolver.openOutputStream(
data
)!!.bufferedWriter().use {
it.write(content)
}
onMainDispatcher {
snackbar(getString(R.string.action_export_msg)).show()
}
} catch (e: Exception) {
Logs.w(e)
onMainDispatcher {
snackbar(e.readableMessage).show()
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = LayoutBackupBinding.bind(view)
binding.resetSettings.setOnClickListener {
MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm)
.setMessage(R.string.reset_settings)
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ ->
runOnDefaultDispatcher {
DataStore.configurationStore.reset()
delay(500)
runOnMainDispatcher {
ProcessPhoenix.triggerRebirth(
requireContext(),
Intent(requireContext(), MainActivity::class.java)
)
}
}
}
.show()
}
binding.actionExport.setOnClickListener {
runOnDefaultDispatcher {
content = doBackup(

View File

@ -1,28 +0,0 @@
package io.nekohasekai.sagernet.ui
import android.os.Bundle
import android.view.View
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutDebugBinding
import io.nekohasekai.sagernet.ktx.snackbar
class DebugFragment : NamedFragment(R.layout.layout_debug) {
override fun name0() = "Debug"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = LayoutDebugBinding.bind(view)
binding.debugCrash.setOnClickListener {
error("test crash")
}
binding.resetSettings.setOnClickListener {
DataStore.configurationStore.reset()
snackbar("Cleared").show()
}
}
}

View File

@ -22,6 +22,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private lateinit var isProxyApps: SwitchPreference
private lateinit var globalCustomConfig: EditConfigPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -77,6 +80,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
val logLevel = findPreference<LongClickListPreference>(Key.LOG_LEVEL)!!
val mtu = findPreference<MTUPreference>(Key.MTU)!!
globalCustomConfig = findPreference(Key.GLOBAL_CUSTOM_CONFIG)!!
globalCustomConfig.useConfigStore(Key.GLOBAL_CUSTOM_CONFIG)
logLevel.dialogLayoutResource = R.layout.layout_loglevel_help
logLevel.setOnPreferenceChangeListener { _, _ ->
@ -162,7 +167,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
resolveDestination.onPreferenceChangeListener = reloadListener
tunImplementation.onPreferenceChangeListener = reloadListener
acquireWakeLock.onPreferenceChangeListener = reloadListener
globalCustomConfig.onPreferenceChangeListener = reloadListener
}
override fun onResume() {
@ -171,6 +176,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
if (::isProxyApps.isInitialized) {
isProxyApps.isChecked = DataStore.proxyApps
}
if (::globalCustomConfig.isInitialized) {
globalCustomConfig.notifyChanged()
}
}
}

View File

@ -7,7 +7,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.databinding.LayoutToolsBinding
import io.nekohasekai.sagernet.ktx.isExpert
class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
@ -19,8 +18,6 @@ class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
tools.add(NetworkFragment())
tools.add(BackupFragment())
if (isExpert) tools.add(DebugFragment())
val binding = LayoutToolsBinding.bind(view)
binding.toolsPager.adapter = ToolsAdapter(tools)

View File

@ -33,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() {
var dirty = false
var key = Key.SERVER_CONFIG
var useConfigStore = false
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
@ -55,6 +56,7 @@ class ConfigEditActivity : ThemedActivity() {
intent?.extras?.apply {
getString("key")?.let { key = it }
getString("useConfigStore")?.let { useConfigStore = true }
}
binding = LayoutEditConfigBinding.inflate(layoutInflater)
@ -70,7 +72,11 @@ class ConfigEditActivity : ThemedActivity() {
binding.editor.apply {
language = JsonLanguage()
setHorizontallyScrolling(true)
setTextContent(DataStore.profileCacheStore.getString(key)!!)
if (useConfigStore) {
setTextContent(DataStore.configurationStore.getString(key) ?: "")
} else {
setTextContent(DataStore.profileCacheStore.getString(key) ?: "")
}
addTextChangedListener {
if (!dirty) {
dirty = true
@ -142,7 +148,11 @@ class ConfigEditActivity : ThemedActivity() {
fun saveAndExit() {
formatText()?.let {
DataStore.profileCacheStore.putString(key, it)
if (useConfigStore) {
DataStore.configurationStore.putString(key, it)
} else {
DataStore.profileCacheStore.putString(key, it)
}
finish()
}
}

View File

@ -47,6 +47,24 @@ public class SingBoxOptions {
}
public static final class CustomSingBoxOption extends SingBoxOption {
public transient String config;
public CustomSingBoxOption(String config) {
super();
this.config = config;
}
public Map<String, Object> getBasicMap() {
Map<String, Object> map = gsonSingbox.fromJson(config, Map.class);
if (map == null) {
map = new HashMap<>();
}
return map;
}
}
// 自定义序列化器
public static class SingBoxOptionSerializer implements JsonSerializer<SingBoxOption> {
@Override
@ -61,7 +79,12 @@ public class SingBoxOptions {
},
TypeToken.get(src.getClass())
);
Map<String, Object> map = gsonSingbox.fromJson(((TypeAdapter<SingBoxOption>) delegate).toJson(src), Map.class);
Map<String, Object> map;
if (src instanceof CustomSingBoxOption) {
map = ((CustomSingBoxOption) src).getBasicMap();
} else {
map = gsonSingbox.fromJson(((TypeAdapter<SingBoxOption>) delegate).toJson(src), Map.class);
}
if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) {
Util.INSTANCE.mergeMap(map, src._hack_config_map);
}

View File

@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import androidx.preference.Preference
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity
@ -26,9 +28,27 @@ class EditConfigPreference : Preference {
intent = Intent(context, ConfigEditActivity::class.java)
}
var configKey = Key.SERVER_CONFIG
var useConfigStore = false
fun useConfigStore(key: String) {
try {
this.configKey = key
useConfigStore = true
intent = intent!!.apply {
putExtra("useConfigStore", "1")
putExtra("key", key)
}
} catch (e: Exception) {
Logs.w(e)
}
}
override fun getSummary(): CharSequence {
val config = DataStore.serverConfig
return if (DataStore.serverConfig.isBlank()) {
val config =
(if (useConfigStore) DataStore.configurationStore.getString(configKey) else DataStore.serverConfig)
?: ""
return if (config.isBlank()) {
return app.resources.getString(androidx.preference.R.string.not_set)
} else {
app.resources.getString(R.string.lines, config.split('\n').size)

View File

@ -134,12 +134,12 @@ object Util {
} else if (v is List<*>) {
if (k.startsWith("+")) { // prepend
val dstKey = k.removePrefix("+")
var currentList = (dst[dstKey] as List<*>).toMutableList()
var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf()
currentList = (v + currentList).toMutableList()
dst[dstKey] = currentList
} else if (k.endsWith("+")) { // append
val dstKey = k.removeSuffix("+")
var currentList = (dst[dstKey] as List<*>).toMutableList()
var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf()
currentList = (currentList + v).toMutableList()
dst[dstKey] = currentList
} else {

View File

@ -5,6 +5,14 @@
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/reset_settings"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/reset_settings"
android:textColor="?whiteOrTextPrimary" />
<CheckBox
android:id="@+id/backup_configurations"
android:layout_width="wrap_content"

View File

@ -492,4 +492,5 @@
<string name="update_dialog_title">发现新版本</string>
<string name="update_dialog_message">当前版本:%1$s\n可升级版本%2$s\n是否前往下载</string>
<string name="check_update_no">检查成功,但没有更新。</string>
<string name="reset_settings">恢复默认设置</string>
</resources>

View File

@ -572,4 +572,5 @@
<string name="update_dialog_title">New version available</string>
<string name="update_dialog_message">Current version: %1$s\nAvailable version: %2$s\nDo you want to download it?</string>
<string name="check_update_no">Check successful, but no updates.</string>
<string name="reset_settings">Restore default settings</string>
</resources>

View File

@ -89,6 +89,11 @@
app:key="logLevel"
app:title="@string/log_level"
app:useSimpleSummaryProvider="true" />
<moe.matsuri.nb4a.ui.EditConfigPreference
app:icon="@drawable/ic_baseline_layers_24"
app:key="globalCustomConfig"
app:title="@string/custom_config"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/cag_route">