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_VPN = "vpn"
const val MODE_PROXY = "proxy" const val MODE_PROXY = "proxy"
const val GLOBAL_CUSTOM_CONFIG = "globalCustomConfig"
const val REMOTE_DNS = "remoteDns" const val REMOTE_DNS = "remoteDns"
const val DIRECT_DNS = "directDns" const val DIRECT_DNS = "directDns"
const val ENABLE_DNS_ROUTING = "enableDnsRouting" const val ENABLE_DNS_ROUTING = "enableDnsRouting"

View File

@ -108,6 +108,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL) var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)
var showGroupInNotification by configurationStore.boolean("showGroupInNotification") 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 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 directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" }
var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true } 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.ShadowTLSBean
import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean
import moe.matsuri.nb4a.utils.JavaUtil.gson import moe.matsuri.nb4a.utils.JavaUtil.gson
import moe.matsuri.nb4a.utils.Util
import moe.matsuri.nb4a.utils.listByLineOrComma import moe.matsuri.nb4a.utils.listByLineOrComma
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -335,9 +336,7 @@ fun buildConfig(
// internal outbound // internal outbound
currentOutbound = when (bean) { currentOutbound = when (bean) {
is ConfigBean -> SingBoxOption().apply { is ConfigBean -> CustomSingBoxOption(bean.config)
_hack_custom_config = bean.config
}
is ShadowTLSBean -> // before StandardV2RayBean is ShadowTLSBean -> // before StandardV2RayBean
buildSingBoxOutboundShadowTLSBean(bean) buildSingBoxOutboundShadowTLSBean(bean)
@ -367,7 +366,7 @@ fun buildConfig(
buildSingBoxOutboundAnyTLSBean(bean) buildSingBoxOutboundAnyTLSBean(bean)
else -> throw IllegalStateException("can't reach") else -> throw IllegalStateException("can't reach")
} as SingBoxOption }
// internal mux // internal mux
if (!muxApplied) { if (!muxApplied) {
@ -739,10 +738,12 @@ fun buildConfig(
} }
} }
_hack_custom_config = proxy.requireBean().customConfigJson _hack_custom_config = DataStore.globalCustomConfig
}.let { }.let {
val configMap = it.asMap()
Util.mergeJSON(configMap, proxy.requireBean().customConfigJson)
ConfigBuildResult( ConfigBuildResult(
gson.toJson(it.asMap()), gson.toJson(configMap),
externalIndexMap, externalIndexMap,
proxy.id, proxy.id,
trafficMap, trafficMap,

View File

@ -23,6 +23,7 @@ import io.nekohasekai.sagernet.databinding.LayoutBackupBinding
import io.nekohasekai.sagernet.databinding.LayoutImportBinding import io.nekohasekai.sagernet.databinding.LayoutImportBinding
import io.nekohasekai.sagernet.databinding.LayoutProgressBinding import io.nekohasekai.sagernet.databinding.LayoutProgressBinding
import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.ktx.*
import kotlinx.coroutines.delay
import moe.matsuri.nb4a.utils.Util import moe.matsuri.nb4a.utils.Util
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
@ -34,33 +35,53 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
override fun name0() = app.getString(R.string.backup) override fun name0() = app.getString(R.string.backup)
var content = "" var content = ""
private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data -> private val exportSettings =
if (data != null) { registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
runOnDefaultDispatcher { if (data != null) {
try { runOnDefaultDispatcher {
requireActivity().contentResolver.openOutputStream( try {
data requireActivity().contentResolver.openOutputStream(
)!!.bufferedWriter().use { data
it.write(content) )!!.bufferedWriter().use {
} it.write(content)
onMainDispatcher { }
snackbar(getString(R.string.action_export_msg)).show() onMainDispatcher {
} snackbar(getString(R.string.action_export_msg)).show()
} catch (e: Exception) { }
Logs.w(e) } catch (e: Exception) {
onMainDispatcher { Logs.w(e)
snackbar(e.readableMessage).show() onMainDispatcher {
snackbar(e.readableMessage).show()
}
} }
} }
} }
} }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val binding = LayoutBackupBinding.bind(view) 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 { binding.actionExport.setOnClickListener {
runOnDefaultDispatcher { runOnDefaultDispatcher {
content = doBackup( 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 isProxyApps: SwitchPreference
private lateinit var globalCustomConfig: EditConfigPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -77,6 +80,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
val logLevel = findPreference<LongClickListPreference>(Key.LOG_LEVEL)!! val logLevel = findPreference<LongClickListPreference>(Key.LOG_LEVEL)!!
val mtu = findPreference<MTUPreference>(Key.MTU)!! 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.dialogLayoutResource = R.layout.layout_loglevel_help
logLevel.setOnPreferenceChangeListener { _, _ -> logLevel.setOnPreferenceChangeListener { _, _ ->
@ -162,7 +167,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
resolveDestination.onPreferenceChangeListener = reloadListener resolveDestination.onPreferenceChangeListener = reloadListener
tunImplementation.onPreferenceChangeListener = reloadListener tunImplementation.onPreferenceChangeListener = reloadListener
acquireWakeLock.onPreferenceChangeListener = reloadListener acquireWakeLock.onPreferenceChangeListener = reloadListener
globalCustomConfig.onPreferenceChangeListener = reloadListener
} }
override fun onResume() { override fun onResume() {
@ -171,6 +176,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
if (::isProxyApps.isInitialized) { if (::isProxyApps.isInitialized) {
isProxyApps.isChecked = DataStore.proxyApps 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 com.google.android.material.tabs.TabLayoutMediator
import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.databinding.LayoutToolsBinding import io.nekohasekai.sagernet.databinding.LayoutToolsBinding
import io.nekohasekai.sagernet.ktx.isExpert
class ToolsFragment : ToolbarFragment(R.layout.layout_tools) { class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
@ -19,8 +18,6 @@ class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
tools.add(NetworkFragment()) tools.add(NetworkFragment())
tools.add(BackupFragment()) tools.add(BackupFragment())
if (isExpert) tools.add(DebugFragment())
val binding = LayoutToolsBinding.bind(view) val binding = LayoutToolsBinding.bind(view)
binding.toolsPager.adapter = ToolsAdapter(tools) binding.toolsPager.adapter = ToolsAdapter(tools)

View File

@ -33,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() {
var dirty = false var dirty = false
var key = Key.SERVER_CONFIG var key = Key.SERVER_CONFIG
var useConfigStore = false
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() { class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
@ -55,6 +56,7 @@ class ConfigEditActivity : ThemedActivity() {
intent?.extras?.apply { intent?.extras?.apply {
getString("key")?.let { key = it } getString("key")?.let { key = it }
getString("useConfigStore")?.let { useConfigStore = true }
} }
binding = LayoutEditConfigBinding.inflate(layoutInflater) binding = LayoutEditConfigBinding.inflate(layoutInflater)
@ -70,7 +72,11 @@ class ConfigEditActivity : ThemedActivity() {
binding.editor.apply { binding.editor.apply {
language = JsonLanguage() language = JsonLanguage()
setHorizontallyScrolling(true) setHorizontallyScrolling(true)
setTextContent(DataStore.profileCacheStore.getString(key)!!) if (useConfigStore) {
setTextContent(DataStore.configurationStore.getString(key) ?: "")
} else {
setTextContent(DataStore.profileCacheStore.getString(key) ?: "")
}
addTextChangedListener { addTextChangedListener {
if (!dirty) { if (!dirty) {
dirty = true dirty = true
@ -142,7 +148,11 @@ class ConfigEditActivity : ThemedActivity() {
fun saveAndExit() { fun saveAndExit() {
formatText()?.let { formatText()?.let {
DataStore.profileCacheStore.putString(key, it) if (useConfigStore) {
DataStore.configurationStore.putString(key, it)
} else {
DataStore.profileCacheStore.putString(key, it)
}
finish() 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> { public static class SingBoxOptionSerializer implements JsonSerializer<SingBoxOption> {
@Override @Override
@ -61,7 +79,12 @@ public class SingBoxOptions {
}, },
TypeToken.get(src.getClass()) 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()) { if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) {
Util.INSTANCE.mergeMap(map, src._hack_config_map); Util.INSTANCE.mergeMap(map, src._hack_config_map);
} }

View File

@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.AttributeSet import android.util.AttributeSet
import androidx.preference.Preference import androidx.preference.Preference
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity
@ -26,9 +28,27 @@ class EditConfigPreference : Preference {
intent = Intent(context, ConfigEditActivity::class.java) 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 { override fun getSummary(): CharSequence {
val config = DataStore.serverConfig val config =
return if (DataStore.serverConfig.isBlank()) { (if (useConfigStore) DataStore.configurationStore.getString(configKey) else DataStore.serverConfig)
?: ""
return if (config.isBlank()) {
return app.resources.getString(androidx.preference.R.string.not_set) return app.resources.getString(androidx.preference.R.string.not_set)
} else { } else {
app.resources.getString(R.string.lines, config.split('\n').size) app.resources.getString(R.string.lines, config.split('\n').size)

View File

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

View File

@ -5,6 +5,14 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> 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 <CheckBox
android:id="@+id/backup_configurations" android:id="@+id/backup_configurations"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

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

View File

@ -572,4 +572,5 @@
<string name="update_dialog_title">New version available</string> <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="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="check_update_no">Check successful, but no updates.</string>
<string name="reset_settings">Restore default settings</string>
</resources> </resources>

View File

@ -89,6 +89,11 @@
app:key="logLevel" app:key="logLevel"
app:title="@string/log_level" app:title="@string/log_level"
app:useSimpleSummaryProvider="true" /> 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>
<PreferenceCategory app:title="@string/cag_route"> <PreferenceCategory app:title="@string/cag_route">