feat: editorkit json

This commit is contained in:
arm64v8a 2023-04-08 12:36:32 +09:00
parent 24a4e1617c
commit 0eb3c6710c
21 changed files with 542 additions and 86 deletions

View File

@ -30,11 +30,11 @@ dependencies {
implementation(fileTree("libs")) implementation(fileTree("libs"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.3.0") implementation("androidx.recyclerview:recyclerview:1.3.0")
implementation("androidx.activity:activity-ktx:1.6.1") implementation("androidx.activity:activity-ktx:1.7.0")
implementation("androidx.fragment:fragment-ktx:1.5.5") implementation("androidx.fragment:fragment-ktx:1.5.6")
implementation("androidx.browser:browser:1.5.0") implementation("androidx.browser:browser:1.5.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
@ -42,15 +42,16 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.5.3") implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.work:work-runtime-ktx:2.8.0") implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.work:work-multiprocess:2.8.0") implementation("androidx.work:work-multiprocess:2.8.1")
implementation("com.google.android.material:material:1.8.0") implementation("com.google.android.material:material:1.8.0")
implementation("com.google.code.gson:gson:2.8.9") implementation("com.google.code.gson:gson:2.9.0")
implementation("com.github.jenly1314:zxing-lite:2.1.1") implementation("com.github.jenly1314:zxing-lite:2.1.1")
implementation("com.afollestad.material-dialogs:core:3.3.0") implementation("com.blacksquircle.ui:editorkit:2.6.0")
implementation("com.afollestad.material-dialogs:input:3.3.0") implementation("com.blacksquircle.ui:language-base:2.6.0")
implementation("com.blacksquircle.ui:language-json:2.6.0")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3") implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
implementation("org.yaml:snakeyaml:1.30") implementation("org.yaml:snakeyaml:1.30")
@ -68,11 +69,11 @@ dependencies {
exclude(group = "com.google.guava", module = "guava") exclude(group = "com.google.guava", module = "guava")
} }
implementation("androidx.room:room-runtime:2.5.0") implementation("androidx.room:room-runtime:2.5.1")
kapt("androidx.room:room-compiler:2.5.0") kapt("androidx.room:room-compiler:2.5.1")
implementation("androidx.room:room-ktx:2.5.0") implementation("androidx.room:room-ktx:2.5.1")
implementation("com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4") implementation("com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4")
kapt("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4") kapt("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
} }

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" /> <uses-sdk tools:overrideLibrary="com.google.zxing.client.android, com.blacksquircle.ui.editorkit" />
<permission <permission
android:name="${applicationId}.SERVICE" android:name="${applicationId}.SERVICE"
@ -143,6 +143,9 @@
android:name="io.nekohasekai.sagernet.ui.VpnRequestActivity" android:name="io.nekohasekai.sagernet.ui.VpnRequestActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" /> android:taskAffinity="" />
<activity
android:name="io.nekohasekai.sagernet.ui.profile.ConfigEditActivity"
android:configChanges="uiMode" />
<activity <activity
android:name="io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity" android:name="io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity"
android:configChanges="uiMode" /> android:configChanges="uiMode" />
@ -152,9 +155,6 @@
<activity <activity
android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity" android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity"
android:configChanges="uiMode" /> android:configChanges="uiMode" />
<activity
android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksRSettingsActivity"
android:configChanges="uiMode" />
<activity <activity
android:name="io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity" android:name="io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity"
android:configChanges="uiMode" /> android:configChanges="uiMode" />

View File

@ -91,7 +91,10 @@ object Key {
const val SERVER_ENCRYPTION = "serverEncryption" const val SERVER_ENCRYPTION = "serverEncryption"
const val SERVER_ALPN = "serverALPN" const val SERVER_ALPN = "serverALPN"
const val SERVER_CERTIFICATES = "serverCertificates" const val SERVER_CERTIFICATES = "serverCertificates"
const val SERVER_CONFIG = "serverConfig" const val SERVER_CONFIG = "serverConfig"
const val SERVER_CUSTOM = "serverCustom"
const val SERVER_CUSTOM_OUTBOUND = "serverCustomOutbound"
const val SERVER_SECURITY_CATEGORY = "serverSecurityCategory" const val SERVER_SECURITY_CATEGORY = "serverSecurityCategory"
const val SERVER_TLS_CAMOUFLAGE_CATEGORY = "serverTlsCamouflageCategory" const val SERVER_TLS_CAMOUFLAGE_CATEGORY = "serverTlsCamouflageCategory"

View File

@ -232,6 +232,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var landingProxyTmp by profileCacheStore.stringToInt(Key.GROUP_LANDING_PROXY) var landingProxyTmp by profileCacheStore.stringToInt(Key.GROUP_LANDING_PROXY)
var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG) var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG)
var serverCustom by profileCacheStore.string(Key.SERVER_CUSTOM)
var serverCustomOutbound by profileCacheStore.string(Key.SERVER_CUSTOM_OUTBOUND)
var groupName by profileCacheStore.string(Key.GROUP_NAME) var groupName by profileCacheStore.string(Key.GROUP_NAME)
var groupType by profileCacheStore.stringToInt(Key.GROUP_TYPE) var groupType by profileCacheStore.stringToInt(Key.GROUP_TYPE)

View File

@ -101,7 +101,7 @@ fun buildConfig(
val globalOutbounds = HashMap<Long, String>() val globalOutbounds = HashMap<Long, String>()
val selectorNames = ArrayList<String>() val selectorNames = ArrayList<String>()
val group = SagerDatabase.groupDao.getById(proxy.groupId) val group = SagerDatabase.groupDao.getById(proxy.groupId)
var optionsToMerge = "" val optionsToMerge = proxy.requireBean().customConfigJson ?: ""
fun ProxyEntity.resolveChainInternal(): MutableList<ProxyEntity> { fun ProxyEntity.resolveChainInternal(): MutableList<ProxyEntity> {
val bean = requireBean() val bean = requireBean()
@ -424,9 +424,6 @@ fun buildConfig(
if (bean.customOutboundJson.isNotBlank()) { if (bean.customOutboundJson.isNotBlank()) {
mergeJSON(bean.customOutboundJson, currentOutbound) mergeJSON(bean.customOutboundJson, currentOutbound)
} }
if (index == 0 && bean.customConfigJson.isNotBlank()) {
optionsToMerge = bean.customConfigJson
}
} }
pastEntity?.requireBean()?.apply { pastEntity?.requireBean()?.apply {

View File

@ -6,9 +6,9 @@ import android.text.InputType
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.webkit.* import android.webkit.*
import android.widget.EditText
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.afollestad.materialdialogs.input.input
import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.DataStore
@ -55,17 +55,18 @@ class WebviewFragment : ToolbarFragment(R.layout.layout_webview), Toolbar.OnMenu
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_set_url -> { R.id.action_set_url -> {
MaterialDialog(requireContext()).show { val view = EditText(context).apply {
title(R.string.set_panel_url) inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
input( setText(DataStore.yacdURL)
prefill = DataStore.yacdURL, }
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.set_panel_url)
) { _, str -> .setView(view)
DataStore.yacdURL = str.toString() .setPositiveButton(android.R.string.ok) { _, _ ->
DataStore.yacdURL = view.text.toString()
mWebView.loadUrl(DataStore.yacdURL) mWebView.loadUrl(DataStore.yacdURL)
} }
positiveButton(R.string.save) .setNegativeButton(android.R.string.cancel, null)
} .show()
} }
R.id.close -> { R.id.close -> {
mWebView.onPause() mWebView.onPause()

View File

@ -0,0 +1,146 @@
package io.nekohasekai.sagernet.ui.profile
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.insert
import com.blacksquircle.ui.language.json.JsonLanguage
import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.ui.ThemedActivity
import moe.matsuri.nb4a.ui.ExtendedKeyboard
import org.json.JSONObject
class ConfigEditActivity : ThemedActivity() {
var dirty = false
var key = Key.SERVER_CONFIG
class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setTitle(R.string.unsaved_changes_prompt)
setPositiveButton(R.string.yes) { _, _ ->
(requireActivity() as ConfigEditActivity).saveAndExit()
}
setNegativeButton(R.string.no) { _, _ ->
requireActivity().finish()
}
setNeutralButton(android.R.string.cancel, null)
}
}
lateinit var binding: LayoutEditConfigBinding
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.extras?.apply {
getString("key")?.let { key = it }
}
binding = LayoutEditConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.apply {
setTitle(R.string.config_settings)
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_navigation_close)
}
// binding.editor.colorScheme = mkTheme()
binding.editor.language = JsonLanguage()
// binding.editor.onChangeListener = OnChangeListener {
// config = binding.editor.text.toString()
// if (!dirty) {
// dirty = true
// DataStore.dirty = true
// }
// }
binding.editor.setHorizontallyScrolling(true)
binding.actionTab.setOnClickListener {
binding.editor.insert(binding.editor.tab())
}
binding.actionUndo.setOnClickListener {
try {
binding.editor.undo()
} catch (_: Exception) {
}
}
binding.actionRedo.setOnClickListener {
try {
binding.editor.redo()
} catch (_: Exception) {
}
}
binding.actionFormat.setOnClickListener {
formatText()?.let {
binding.editor.setTextContent(it)
}
}
val extendedKeyboard = findViewById<ExtendedKeyboard>(R.id.extended_keyboard)
extendedKeyboard.setKeyListener { char -> binding.editor.insert(char) }
extendedKeyboard.setHasFixedSize(true)
extendedKeyboard.submitList("{},:_\"".map { it.toString() })
extendedKeyboard.setBackgroundColor(getColorAttr(R.attr.primaryOrTextPrimary))
binding.editor.setTextContent(DataStore.profileCacheStore.getString(key)!!)
}
fun formatText(): String? {
try {
val txt = binding.editor.text.toString()
if (txt.isBlank()) {
return ""
}
return JSONObject(txt).toStringPretty()
} catch (e: Exception) {
MaterialAlertDialogBuilder(this).setTitle(R.string.error_title)
.setMessage(e.readableMessage).show()
return null
}
}
fun saveAndExit() {
formatText()?.let {
DataStore.profileCacheStore.putString(key, it)
finish()
}
}
override fun onBackPressed() {
if (dirty) UnsavedChangesDialogFragment().apply { key() }
.show(supportFragmentManager, null) else super.onBackPressed()
}
override fun onSupportNavigateUp(): Boolean {
if (!super.onSupportNavigateUp()) finish()
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.profile_apply_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_apply -> {
saveAndExit()
return true
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@ -6,11 +6,13 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.InputType
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -22,8 +24,6 @@ import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import com.github.shadowsocks.plugin.Empty import com.github.shadowsocks.plugin.Empty
import com.github.shadowsocks.plugin.fragment.AlertDialogFragment import com.github.shadowsocks.plugin.fragment.AlertDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -217,10 +217,6 @@ abstract class ProfileSettingsActivity<T : AbstractBean>(
preferenceManager.preferenceDataStore = DataStore.profileCacheStore preferenceManager.preferenceDataStore = DataStore.profileCacheStore
activity.apply { activity.apply {
createPreferences(savedInstanceState, rootKey) createPreferences(savedInstanceState, rootKey)
if (isSubscription) {
// findPreference<Preference>(Key.PROFILE_NAME)?.isEnabled = false
}
} }
} }
@ -237,6 +233,21 @@ abstract class ProfileSettingsActivity<T : AbstractBean>(
DataStore.profileCacheStore.registerChangeListener(activity) DataStore.profileCacheStore.registerChangeListener(activity)
} }
var callbackCustom: ((String) -> Unit)? = null
var callbackCustomOutbound: ((String) -> Unit)? = null
val resultCallbackCustom = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { (_, _) ->
callbackCustom?.let { it(DataStore.serverCustom) }
}
val resultCallbackCustomOutbound = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { (_, _) ->
callbackCustomOutbound?.let { it(DataStore.serverCustomOutbound) }
}
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
@ -263,36 +274,30 @@ abstract class ProfileSettingsActivity<T : AbstractBean>(
R.id.action_custom_outbound_json -> { R.id.action_custom_outbound_json -> {
activity.proxyEntity?.apply { activity.proxyEntity?.apply {
val bean = requireBean() val bean = requireBean()
MaterialDialog(activity).show { DataStore.serverCustomOutbound = bean.customOutboundJson
title(R.string.custom_outbound_json) callbackCustomOutbound = { bean.customOutboundJson = it }
input( resultCallbackCustomOutbound.launch(
prefill = bean.customOutboundJson, Intent(
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE, requireContext(),
allowEmpty = true ConfigEditActivity::class.java
) { _, str -> ).apply {
bean.customOutboundJson = str.toString() putExtra("key", Key.SERVER_CUSTOM_OUTBOUND)
DataStore.dirty = true })
}
positiveButton(R.string.save)
}
} }
true true
} }
R.id.action_custom_config_json -> { R.id.action_custom_config_json -> {
activity.proxyEntity?.apply { activity.proxyEntity?.apply {
val bean = requireBean() val bean = requireBean()
MaterialDialog(activity).show { DataStore.serverCustom = bean.customConfigJson
title(R.string.custom_config_json) callbackCustom = { bean.customConfigJson = it }
input( resultCallbackCustom.launch(
prefill = bean.customConfigJson, Intent(
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE, requireContext(),
allowEmpty = true ConfigEditActivity::class.java
) { _, str -> ).apply {
bean.customConfigJson = str.toString() putExtra("key", Key.SERVER_CUSTOM)
DataStore.dirty = true })
}
positiveButton(R.string.save)
}
} }
true true
} }

View File

@ -1,7 +1,6 @@
package moe.matsuri.nb4a.proxy.config package moe.matsuri.nb4a.proxy.config
import android.os.Bundle import android.os.Bundle
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
@ -10,14 +9,13 @@ import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity import io.nekohasekai.sagernet.ui.profile.ProfileSettingsActivity
import moe.matsuri.nb4a.ui.EditConfigPreference
class ConfigSettingActivity : class ConfigSettingActivity :
ProfileSettingsActivity<ConfigBean>(), ProfileSettingsActivity<ConfigBean>(),
OnPreferenceDataStoreChangeListener { OnPreferenceDataStoreChangeListener {
var beanType: Int = 0 private var beanType: Int = 0
lateinit var configPreference: EditTextPreference
override fun createEntity() = ConfigBean() override fun createEntity() = ConfigBean()
@ -44,24 +42,30 @@ class ConfigSettingActivity :
if (key != Key.PROFILE_DIRTY) { if (key != Key.PROFILE_DIRTY) {
DataStore.dirty = true DataStore.dirty = true
} }
if (key == Key.SERVER_CONFIG) { if (key == "isOutboundOnly") {
if (::configPreference.isInitialized) {
configPreference.text = store.getString(key, "")
}
} else if (key == "isOutboundOnly") {
beanType = if (store.getBoolean(key, false)) 1 else 0 beanType = if (store.getBoolean(key, false)) 1 else 0
} }
} }
private lateinit var editConfigPreference: EditConfigPreference
override fun PreferenceFragmentCompat.createPreferences( override fun PreferenceFragmentCompat.createPreferences(
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
rootKey: String?, rootKey: String?,
) { ) {
addPreferencesFromResource(R.xml.config_preferences) addPreferencesFromResource(R.xml.config_preferences)
configPreference = findPreference(Key.SERVER_CONFIG)!! editConfigPreference = findPreference(Key.SERVER_CONFIG)!!
findPreference<SwitchPreference>("isOutboundOnly")!!.isChecked = beanType == 1 findPreference<SwitchPreference>("isOutboundOnly")!!.isChecked = beanType == 1
} }
override fun onResume() {
super.onResume()
if (::editConfigPreference.isInitialized) {
editConfigPreference.notifyChanged()
}
}
} }

View File

@ -0,0 +1,42 @@
package moe.matsuri.nb4a.ui
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import androidx.preference.Preference
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity
class EditConfigPreference : Preference {
constructor(
context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context, attrs, defStyleAttr
)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
init {
intent = Intent(context, ConfigEditActivity::class.java)
}
override fun getSummary(): CharSequence {
val config = DataStore.serverConfig
return if (DataStore.serverConfig.isBlank()) {
return app.resources.getString(androidx.preference.R.string.not_set)
} else {
app.resources.getString(R.string.lines, config.split('\n').size)
}
}
public override fun notifyChanged() {
super.notifyChanged()
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2021 Squircle IDE contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package moe.matsuri.nb4a.ui
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.sagernet.databinding.ItemKeyboardKeyBinding
class ExtendedKeyboard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
private lateinit var keyAdapter: KeyAdapter
fun setKeyListener(keyListener: OnKeyListener) {
keyAdapter = KeyAdapter(keyListener)
adapter = keyAdapter
}
fun submitList(keys: List<String>) {
keyAdapter.submitList(keys)
}
private class KeyAdapter(
private val keyListener: OnKeyListener
) : ListAdapter<String, KeyAdapter.KeyViewHolder>(diffCallback) {
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeyViewHolder {
return KeyViewHolder.create(parent, keyListener)
}
override fun onBindViewHolder(holder: KeyViewHolder, position: Int) {
holder.bind(getItem(position))
}
private class KeyViewHolder(
private val binding: ItemKeyboardKeyBinding,
private val keyListener: OnKeyListener
) : ViewHolder(binding.root) {
companion object {
fun create(parent: ViewGroup, keyListener: OnKeyListener): KeyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemKeyboardKeyBinding.inflate(inflater, parent, false)
return KeyViewHolder(binding, keyListener)
}
}
private lateinit var char: String
init {
itemView.setOnClickListener {
keyListener.onKey(char)
}
}
fun bind(item: String) {
char = item
binding.itemTitle.text = char
binding.itemTitle.setTextColor(Color.WHITE)
}
}
}
fun interface OnKeyListener {
fun onKey(char: String)
}
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.59,7.41L15.17,11H1v2h14.17l-3.59,3.59L13,18l6,-6 -6,-6 -1.41,1.41zM20,6v12h2V6h-2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
</vector>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2021 Squircle IDE contributors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_title"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?selectableItemBackground"
android:gravity="center"
android:paddingBottom="2dp"
android:textSize="16sp"
android:typeface="monospace"
tools:text="{" />

View File

@ -0,0 +1,101 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/layout_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@android:color/darker_gray" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.blacksquircle.ui.editorkit.widget.TextProcessor
android:id="@+id/editor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:completionThreshold="2"
android:importantForAutofill="no"
android:scrollbars="none" />
<com.blacksquircle.ui.editorkit.widget.TextScroller
android:id="@+id/scroller"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_gravity="end"
app:thumbTint="?colorPrimary" />
</FrameLayout>
<LinearLayout
android:id="@+id/keyboard_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/action_tab"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?primaryOrTextPrimary"
android:padding="8dp"
android:src="@drawable/baseline_keyboard_tab_24"
app:tint="?colorOnSurface" />
<moe.matsuri.nb4a.ui.ExtendedKeyboard
android:id="@+id/extended_keyboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?colorSecondaryVariant"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6"
tools:listitem="@layout/item_keyboard_key" />
<ImageView
android:id="@+id/action_undo"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?primaryOrTextPrimary"
android:padding="8dp"
android:src="@drawable/baseline_undo_24"
app:tint="?colorOnSurface" />
<ImageView
android:id="@+id/action_redo"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?primaryOrTextPrimary"
android:padding="8dp"
android:src="@drawable/baseline_redo_24"
app:tint="?colorOnSurface" />
<ImageView
android:id="@+id/action_format"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?primaryOrTextPrimary"
android:padding="8dp"
android:src="@drawable/ic_baseline_format_align_left_24"
app:tint="?colorOnSurface" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?primaryOrTextPrimary" />
</LinearLayout>
</LinearLayout>

View File

@ -11,7 +11,7 @@
app:key="isOutboundOnly" app:key="isOutboundOnly"
app:title="@string/is_outbound_only" /> app:title="@string/is_outbound_only" />
<EditTextPreference <moe.matsuri.nb4a.ui.EditConfigPreference
app:icon="@drawable/ic_baseline_layers_24" app:icon="@drawable/ic_baseline_layers_24"
app:key="serverConfig" app:key="serverConfig"
app:title="@string/custom_config" app:title="@string/custom_config"

View File

@ -8,13 +8,13 @@ bash buildScript/lib/assets.sh
exit exit
#### Download "external" from Internet #### Download "external" from Internet
rm -rf external #rm -rf external
mkdir -p external #mkdir -p external
cd external #cd external
#
echo "Downloading preferencex-android" #echo "Downloading preferencex-android"
wget -q -O tmp.zip https://github.com/SagerNet/preferencex-android/archive/8bdb0c6ae44f378b073c6a1c850d03d729b70ff8.zip #wget -q -O tmp.zip https://github.com/SagerNet/preferencex-android/archive/8bdb0c6ae44f378b073c6a1c850d03d729b70ff8.zip
unzip tmp.zip > /dev/null 2>&1 #unzip tmp.zip > /dev/null 2>&1
mv preferencex-android-* preferencex #mv preferencex-android-* preferencex
#
rm tmp.zip #rm tmp.zip

View File

@ -7,6 +7,6 @@ apply(from = "../repositories.gradle.kts")
dependencies { dependencies {
// Gradle Plugins // Gradle Plugins
implementation("com.android.tools.build:gradle:7.3.1") implementation("com.android.tools.build:gradle:7.4.2")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
} }

View File

@ -1,8 +1,11 @@
import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.AbstractAppExtension
import com.android.build.gradle.BaseExtension import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.gradle.api.JavaVersion
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.kotlin.dsl.getByName import org.gradle.kotlin.dsl.getByName
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -96,6 +99,13 @@ fun Project.setupCommon() {
isMinifyEnabled = true isMinifyEnabled = true
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
(android as ExtensionAware).extensions.getByName<KotlinJvmOptions>("kotlinOptions").apply {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
lintOptions { lintOptions {
isShowAll = true isShowAll = true
isCheckAllWarnings = true isCheckAllWarnings = true

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME