____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.ui.profile
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.blacksquircle.ui.editorkit.listener.OnChangeListener
import com.blacksquircle.ui.editorkit.model.ColorScheme
import com.blacksquircle.ui.language.base.model.SyntaxScheme
import com.blacksquircle.ui.language.json.JsonLanguage
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding
import io.nekohasekai.sagernet.ktx.getColorAttr
import io.nekohasekai.sagernet.ktx.onMainDispatcher
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ui.ThemedActivity
import io.nekohasekai.sagernet.utils.Theme
import androidx.core.graphics.toColorInt
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.ktx.parseJson
import io.nekohasekai.sagernet.ktx.readableMessage
class ConfigEditActivity : ThemedActivity() {
companion object {
private const val KEY_CONFIG = "config"
private const val KEY_DIRTY = "dirty"
}
private lateinit var binding: LayoutEditConfigBinding
var config = ""
var dirty = false
override val onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
if (ViewCompat.getRootWindowInsets(binding.editor)
?.isVisible(WindowInsetsCompat.Type.ime()) == true
/* this also works on Android Emulator Android 5.0 anyway */
) {
this@ConfigEditActivity.currentFocus?.windowToken?.let {
SagerNet.ime.hideSoftInputFromWindow(it, 0)
}
} else {
MaterialAlertDialogBuilder(this@ConfigEditActivity)
.setTitle(R.string.unsaved_changes_prompt)
.setPositiveButton(android.R.string.ok) { _, _ ->
saveAndExit()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
finish()
}
.show()
}
}
}
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutEditConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.layout_editor)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
v.updatePadding(
left = bars.left,
bottom = ime.bottom,
)
insets
}
ViewCompat.setOnApplyWindowInsetsListener(binding.editor) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
v.updatePadding(
right = bars.right,
bottom = if (ime.bottom > bars.bottom) 0 else bars.bottom - ime.bottom,
)
insets
}
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()
dirty = true
onBackPressedCallback.isEnabled = true
}
runOnDefaultDispatcher {
config = DataStore.serverConfig
savedInstanceState?.getString(KEY_CONFIG)?.takeIf { it.isNotEmpty() }?.let {
config = it
}
onMainDispatcher {
binding.editor.setTextContent(config)
}
}
savedInstanceState?.getBoolean(KEY_DIRTY)?.let {
dirty = it
onBackPressedCallback.isEnabled = it
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_CONFIG, config)
outState.putBoolean(KEY_DIRTY, dirty)
}
@SuppressLint("CheckResult")
fun saveAndExit() {
try {
parseJson(config, lenient = true).asJsonObject
} catch (e: Exception) {
MaterialAlertDialogBuilder(this).setTitle(R.string.error_title)
.setMessage(e.readableMessage).show()
return
}
DataStore.serverConfig = config
finish()
}
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)
}
fun mkTheme(): ColorScheme {
val colorPrimary = getColorAttr(androidx.appcompat.R.attr.colorPrimary)
val colorPrimaryDark = getColorAttr(androidx.appcompat.R.attr.colorPrimaryDark)
return ColorScheme(
textColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_Black -> Color.WHITE
R.style.Theme_SagerNet_LightBlack -> Color.BLACK
else -> colorPrimary
},
backgroundColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_Black -> Color.BLACK
R.style.Theme_SagerNet_LightBlack -> Color.WHITE
else -> if (Theme.usingNightMode()) Color.BLACK else Color.WHITE
},
gutterColor = colorPrimary,
gutterDividerColor = if (Theme.usingNightMode()) Color.BLACK else Color.WHITE,
gutterCurrentLineNumberColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_LightBlack -> Color.BLACK
else -> Color.WHITE
},
gutterTextColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_LightBlack -> Color.BLACK
else -> Color.WHITE
},
selectedLineColor = if (Theme.usingNightMode()) "#2C2C2C".toColorInt() else "#D3D3D3".toColorInt(),
selectionColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_Black -> "#4C4C4C".toColorInt()
R.style.Theme_SagerNet_LightBlack -> "#B3B3B3".toColorInt()
else -> colorPrimary
},
suggestionQueryColor = "#7CE0F3".toColorInt(),
findResultBackgroundColor = "#5F5E5A".toColorInt(),
delimiterBackgroundColor = "#5F5E5A".toColorInt(),
syntaxScheme = SyntaxScheme(
numberColor = "#BB8FF8".toColorInt(),
operatorColor = if (Theme.usingNightMode()) Color.WHITE else Color.BLACK,
keywordColor = "#EB347E".toColorInt(),
typeColor = "#7FD0E4".toColorInt(),
langConstColor = "#EB347E".toColorInt(),
preprocessorColor = "#EB347E".toColorInt(),
variableColor = "#7FD0E4".toColorInt(),
methodColor = "#B6E951".toColorInt(),
stringColor = when (Theme.getTheme()) {
R.style.Theme_SagerNet_Black -> Color.WHITE
R.style.Theme_SagerNet_LightBlack -> Color.BLACK
else -> colorPrimaryDark
},
commentColor = "#89826D".toColorInt(),
tagColor = "#F8F8F8".toColorInt(),
tagNameColor = "#EB347E".toColorInt(),
attrNameColor = "#B6E951".toColorInt(),
attrValueColor = "#EBE48C".toColorInt(),
entityRefColor = "#BB8FF8".toColorInt()
)
)
}
}