From 835fcca7b9c54223b9a50c364263031f2896ada6 Mon Sep 17 00:00:00 2001 From: armv9 <48624112+arm64v8a@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:16:29 +0900 Subject: [PATCH] dev global custom config --- .../java/io/nekohasekai/sagernet/Constants.kt | 2 + .../sagernet/database/DataStore.kt | 2 + .../nekohasekai/sagernet/fmt/ConfigBuilder.kt | 13 +++-- .../nekohasekai/sagernet/ui/BackupFragment.kt | 57 +++++++++++++------ .../nekohasekai/sagernet/ui/DebugFragment.kt | 28 --------- .../sagernet/ui/SettingsPreferenceFragment.kt | 10 +++- .../nekohasekai/sagernet/ui/ToolsFragment.kt | 3 - .../sagernet/ui/profile/ConfigEditActivity.kt | 14 ++++- .../java/moe/matsuri/nb4a/SingBoxOptions.java | 25 +++++++- .../matsuri/nb4a/ui/EditConfigPreference.kt | 24 +++++++- .../main/java/moe/matsuri/nb4a/utils/Util.kt | 4 +- app/src/main/res/layout/layout_backup.xml | 8 +++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/global_preferences.xml | 5 ++ 15 files changed, 134 insertions(+), 63 deletions(-) delete mode 100644 app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index d44a3b6..caf363f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -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" diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index df6f06a..86ff8bd 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -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 } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 9882e9e..397aab0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -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, diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt index 87aa2a0..4816f44 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt @@ -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( diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt deleted file mode 100644 index a5d33a3..0000000 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt +++ /dev/null @@ -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() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt index 5c91718..18d6454 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt @@ -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(Key.LOG_LEVEL)!! val mtu = findPreference(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() + } } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt index bf73ca1..32738ea 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt @@ -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) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt index a8f3ab2..ac8f046 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt @@ -33,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() { var dirty = false var key = Key.SERVER_CONFIG + var useConfigStore = false class UnsavedChangesDialogFragment : AlertDialogFragment() { 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() } } diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java index ebcc5eb..05f8927 100644 --- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java +++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java @@ -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 getBasicMap() { + Map map = gsonSingbox.fromJson(config, Map.class); + if (map == null) { + map = new HashMap<>(); + } + return map; + } + } + // 自定义序列化器 public static class SingBoxOptionSerializer implements JsonSerializer { @Override @@ -61,7 +79,12 @@ public class SingBoxOptions { }, TypeToken.get(src.getClass()) ); - Map map = gsonSingbox.fromJson(((TypeAdapter) delegate).toJson(src), Map.class); + Map map; + if (src instanceof CustomSingBoxOption) { + map = ((CustomSingBoxOption) src).getBasicMap(); + } else { + map = gsonSingbox.fromJson(((TypeAdapter) delegate).toJson(src), Map.class); + } if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) { Util.INSTANCE.mergeMap(map, src._hack_config_map); } diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt index 6b2ea81..1292ed5 100644 --- a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt +++ b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt @@ -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) diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt index dca08c0..c5ae09a 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt @@ -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 { diff --git a/app/src/main/res/layout/layout_backup.xml b/app/src/main/res/layout/layout_backup.xml index fb61aaf..81a7dec 100644 --- a/app/src/main/res/layout/layout_backup.xml +++ b/app/src/main/res/layout/layout_backup.xml @@ -5,6 +5,14 @@ android:orientation="vertical" android:padding="16dp"> +