add import from url (1/3)

This commit is contained in:
Kr328 committed 2019-12-05 00:11:04 +08:00
1 parent f0aba3758d
commit c9973393ff
12 files changed
+346 -279

No files matched your search

+4
View File
@@ -40,6 +40,10 @@
android:label="@string/clash_import_from_file"
android:exported="false"
android:screenOrientation="portrait"/>
<activity android:name=".ImportUrlActivity"
android:label="@string/clash_import_from_url"
android:screenOrientation="portrait"
android:exported="false" />
<activity
android:name=".ClashStatusActivity"
android:exported="false"
@@ -63,13 +63,22 @@ class CreateProfileActivity : BaseActivity() {
adapter = Adapter(this@CreateProfileActivity)
setOnItemClickListener { _, _, index, _ ->
when (index) {
0 ->
0 -> {
startActivity(
Intent(
this@CreateProfileActivity,
ImportFileActivity::class.java
)
)
}
1 -> {
startActivity(
Intent(
this@CreateProfileActivity,
ImportUrlActivity::class.java
)
)
}
}
}
}
@@ -1,28 +1,30 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.webkit.MimeTypeMap
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import com.github.kr328.clash.adapter.FormAdapter
import com.github.kr328.clash.model.ClashProfile
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.utils.FileUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_import_file.*
import java.io.FileOutputStream
import kotlin.concurrent.thread
class ImportFileActivity : BaseActivity() {
companion object {
const val REQUEST_FILE_CODE = 185
}
private val elements: List<FormAdapter.Type> = listOf(
FormAdapter.TextType(R.drawable.ic_about,
R.string.clash_profile_name,
R.string.clash_profile_name_hint),
FormAdapter.FilePickerType(R.drawable.ic_new_profile_file,
R.string.clash_profile_file,
R.string.clash_profile_file_hint)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -30,93 +32,53 @@ class ImportFileActivity : BaseActivity() {
setSupportActionBar(activity_import_file_toolbar)
activity_import_file_form.also {
it.layoutManager = LinearLayoutManager(this)
it.adapter = FormAdapter(this, elements)
}
activity_import_file_save.setOnClickListener {
when {
activity_import_file_name_text.text.isEmpty() -> {
Snackbar.make(
activity_import_file_root,
R.string.clash_import_file_empty_name,
Snackbar.LENGTH_LONG
).show()
}
activity_import_file_path_text.tag == null -> {
Snackbar.make(
activity_import_file_root,
R.string.clash_import_file_empty_path,
Snackbar.LENGTH_LONG
).show()
}
else -> {
it.visibility = View.GONE
activity_import_file_saving.visibility = View.VISIBLE
activity_import_file_save.visibility = View.GONE
activity_import_file_saving.visibility = View.VISIBLE
thread {
checkAndInsert()
runOnUiThread {
it.visibility = View.VISIBLE
activity_import_file_saving.visibility = View.GONE
}
}
}
thread {
checkAndInsert()
}
}
val name = activity_import_file_name_text
activity_import_file_name.setOnClickListener {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.clash_profile_name)
.setView(R.layout.dialog_text_edit)
.setPositiveButton(R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
.create()
.apply {
setOnShowListener {
val data = findViewById<EditText>(R.id.dialog_text_edit).also {
it?.hint = name.hint
it?.setText(name.text)
}
getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
setTextColor(getColor(R.color.colorAccent))
setOnClickListener {
name.text = data?.text ?: ""
dismiss()
}
}
getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
setTextColor(getColor(R.color.colorAccent))
}
}
}
.show()
}
activity_import_file_path.setOnClickListener {
startActivityForResult(
Intent(Intent.ACTION_GET_CONTENT)
.setType(
MimeTypeMap.getSingleton()
.getMimeTypeFromExtension("yaml") ?: "*/*"
),
REQUEST_FILE_CODE
)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_FILE_CODE && resultCode == Activity.RESULT_OK) {
activity_import_file_path_text.tag = data?.data
activity_import_file_path_text.text = data?.data?.pathSegments?.last() ?: ""
} else {
super.onActivityResult(requestCode, resultCode, data)
}
if (data != null && (activity_import_file_form.adapter as FormAdapter).onActivityResult(requestCode, resultCode, data) )
return
super.onActivityResult(requestCode, resultCode, data)
}
private fun checkAndInsert() {
try {
val name = elements[0] as FormAdapter.TextType
val file = elements[1] as FormAdapter.FilePickerType
if ( name.content.isEmpty() ) {
runOnUiThread {
activity_import_file_save.visibility = View.VISIBLE
activity_import_file_saving.visibility = View.GONE
Snackbar.make(activity_import_file_root, R.string.clash_import_file_empty_name, Snackbar.LENGTH_LONG).show()
}
return
}
if ( file.content == null || file.content == Uri.EMPTY ) {
runOnUiThread {
activity_import_file_save.visibility = View.VISIBLE
activity_import_file_saving.visibility = View.GONE
Snackbar.make(activity_import_file_root, R.string.clash_import_file_empty_path, Snackbar.LENGTH_LONG).show()
}
return
}
val data =
contentResolver.openInputStream(activity_import_file_path_text.tag as Uri)?.use {
contentResolver.openInputStream(file.content!!)?.use {
it.readBytes().toString(Charsets.UTF_8)
} ?: throw NullPointerException("Unable to open config file")
@@ -134,8 +96,8 @@ class ImportFileActivity : BaseActivity() {
runClash {
it.profileService.addProfile(
ClashProfileEntity(
name = activity_import_file_name_text.text.toString(),
token = "file|" + activity_import_file_path_text.tag.toString(),
name = name.content,
token = "file|" + file.content!!.toString(),
cache = cache.absolutePath,
active = false,
proxies = parsed.proxies.size,
@@ -0,0 +1,41 @@
package com.github.kr328.clash
import android.content.Intent
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.kr328.clash.adapter.FormAdapter
import kotlinx.android.synthetic.main.activity_import_url.*
class ImportUrlActivity : BaseActivity() {
private val elements = listOf<FormAdapter.Type>(
FormAdapter.TextType(
R.drawable.ic_about,
R.string.clash_profile_name,
R.string.clash_profile_name_hint),
FormAdapter.TextType(
R.drawable.ic_link,
R.string.clash_profile_url,
R.string.clash_profile_url_hint
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_import_url)
setSupportActionBar(activity_import_url_toolbar)
activity_import_url_form.also {
it.layoutManager = LinearLayoutManager(this)
it.adapter = FormAdapter(this, elements)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && (activity_import_url_form.adapter as FormAdapter)
.onActivityResult(requestCode, resultCode, data))
return
super.onActivityResult(requestCode, resultCode, data)
}
}
@@ -1,30 +1,116 @@
package com.github.kr328.clash.adapter
import android.content.Context
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.fragment.FormFragment
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class FormAdapter(
private val context: Context,
private val elements: List<FormFragment.FormElement>
private val activity: Activity,
private val elements: List<Type>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val BASE_REQUEST_CODE = 14654
}
interface Type {
val content: Any?
}
data class TextType(
val icon: Int,
val title: Int,
val hint: Int,
override var content: String = ""
) : Type
data class FilePickerType(
val icon: Int,
val title: Int,
val hint: Int,
override var content: Uri? = Uri.EMPTY
) : Type
class TextHolder(view: View) : RecyclerView.ViewHolder(view) {
val icon: ImageView = view.findViewById(R.id.adapter_from_text_icon)
val edit: TextInputEditText = view.findViewById(R.id.adapter_from_text_text)
val icon: ImageView = view.findViewById(R.id.adapter_form_text_icon)
val title: TextView = view.findViewById(R.id.adapter_form_text_title)
val text: TextView = view.findViewById(R.id.adapter_form_text_text)
val clickable: View = view.findViewById(R.id.adapter_form_text_clickable)
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Boolean {
val position = requestCode - BASE_REQUEST_CODE
if (position in 0..elements.size) {
val element = elements[position]
if (element is FilePickerType) {
if (resultCode == RESULT_OK) {
element.content = data.data
notifyItemChanged(position)
}
return true
}
}
return false
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val current = elements[position]) {
is TextType -> {
val castedHolder = holder as TextHolder
castedHolder.icon.setImageResource(current.icon)
castedHolder.title.text = activity.getText(current.title)
castedHolder.text.hint = activity.getText(current.hint)
castedHolder.text.text = current.content
castedHolder.clickable.setOnClickListener {
showTextEditDialog(
castedHolder.text.text.toString(),
castedHolder.text.hint.toString()
) {
current.content = it
notifyItemChanged(position)
}
}
}
is FilePickerType -> {
val castedHolder = holder as TextHolder
castedHolder.icon.setImageResource(current.icon)
castedHolder.title.text = activity.getText(current.title)
castedHolder.text.hint = activity.getText(current.hint)
castedHolder.text.text = current.content?.pathSegments?.lastOrNull() ?: ""
castedHolder.clickable.setOnClickListener {
activity.startActivityForResult(
Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*"),
BASE_REQUEST_CODE + position
)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
FormFragment.FormType.STRING.ordinal, FormFragment.FormType.FILE.ordinal, FormFragment.FormType.URL.ordinal ->
TextType::class.java.hashCode(), FilePickerType::class.java.hashCode() ->
TextHolder(
LayoutInflater.from(context).inflate(
R.layout.adapter_from_text,
LayoutInflater.from(activity).inflate(
R.layout.adapter_form_text,
parent,
false
)
@@ -37,18 +123,35 @@ class FormAdapter(
return elements.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val current = elements[position]
when (holder) {
is TextHolder -> {
holder.icon.setImageResource(current.icon)
holder.edit.hint = context.getString(current.hint)
}
}
override fun getItemViewType(position: Int): Int {
return elements[position].javaClass.hashCode()
}
override fun getItemViewType(position: Int): Int {
return elements[position].type.ordinal
private fun showTextEditDialog(initial: String, hint: String, callback: (String) -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.clash_profile_name)
.setView(R.layout.dialog_text_edit)
.setPositiveButton(R.string.ok) { _, _ -> }
.setNegativeButton(R.string.cancel) { _, _ -> }
.create()
.apply {
setOnShowListener {
val data = findViewById<EditText>(R.id.dialog_text_edit).also {
it?.hint = hint
it?.setText(initial)
}
getButton(AlertDialog.BUTTON_POSITIVE)?.apply {
setTextColor(context.getColor(R.color.colorAccent))
setOnClickListener {
callback(data?.text?.toString() ?: "")
dismiss()
}
}
getButton(AlertDialog.BUTTON_NEGATIVE)?.apply {
setTextColor(context.getColor(R.color.colorAccent))
}
}
}
.show()
}
}
@@ -1,45 +0,0 @@
package com.github.kr328.clash.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.adapter.FormAdapter
class FormFragment(private val elements: List<FormElement>) : Fragment() {
enum class FormType {
STRING, URL, FILE
}
interface ResultDumper {
class DumpException(message: String) : Exception(message)
fun dump(element: FormElement, data: Any)
}
data class FormElement(
val type: FormType,
val icon: Int,
val title: Int,
val hint: Int,
val result: ResultDumper
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return RecyclerView(requireActivity()).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
layoutManager = LinearLayoutManager(requireActivity())
adapter = FormAdapter(requireActivity(), elements)
}
}
}
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>
@@ -43,105 +43,8 @@
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/activity_import_file_form"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:src="@drawable/ic_about"
android:contentDescription="@string/app_name"
android:layout_width="25dp"
android:layout_height="25dp" />
<LinearLayout
android:id="@+id/activity_import_file_name"
android:layout_marginStart="20dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<TextView
android:text="@string/clash_profile_name"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/activity_import_file_name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_profile_edit"
android:padding="5dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:hint="@string/clash_profile_name_hint" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorAccent" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_marginTop="30dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:src="@drawable/ic_new_profile_file"
android:contentDescription="@string/app_name"
android:layout_width="25dp"
android:layout_height="25dp" />
<LinearLayout
android:id="@+id/activity_import_file_path"
android:layout_marginStart="20dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<TextView
android:text="@string/clash_profile_file"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textColor="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/activity_import_file_path_text"
android:drawableEnd="@drawable/ic_profile_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:hint="@string/clash_profile_file_hint" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorAccent" />
</LinearLayout>
</LinearLayout>
android:layout_height="wrap_content" />
</LinearLayout>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_import_url_root"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/activity_import_url_toolbar"
android:layout_width="match_parent"
android:layout_height="60dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:layout_gravity="end">
<ImageView
android:id="@+id/activity_import_url_save"
android:padding="10dp"
android:src="@drawable/ic_save"
android:contentDescription="@string/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:focusable="true"
android:clickable="true"
android:foreground="?attr/selectableItemBackground" />
<ProgressBar
android:id="@+id/activity_import_url_saving"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="40dp"
android:layout_height="40dp" />
</FrameLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/activity_import_url_form"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/adapter_form_text_icon"
android:src="@drawable/ic_about"
android:contentDescription="@string/app_name"
android:layout_width="25dp"
android:layout_height="25dp" />
<LinearLayout
android:id="@+id/adapter_form_text_clickable"
android:layout_marginStart="20dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true">
<TextView
android:id="@+id/adapter_form_text_title"
android:text="@string/clash_profile_name"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/adapter_form_text_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_profile_edit"
android:padding="5dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:hint="@string/clash_profile_name_hint" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorAccent" />
</LinearLayout>
</LinearLayout>
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="15dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/adapter_from_text_icon"
android:src="@drawable/ic_about"
android:contentDescription="@string/app_name"
android:layout_width="30dp"
android:layout_height="30dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/adapter_from_text_text"
android:drawableEnd="@drawable/ic_about"
android:layout_marginStart="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:inputType="none" />
</LinearLayout>
+4
View File
@@ -28,6 +28,7 @@
<string name="clash_new_profile_url_summary">From Internet</string>
<string name="clash_import_from_file">Import From File</string>
<string name="clash_import_from_url">Import From URL</string>
<string name="clash_profile_name">Name</string>
<string name="clash_profile_name_hint">Unlabeled</string>
@@ -35,6 +36,9 @@
<string name="clash_profile_file">File</string>
<string name="clash_profile_file_hint">content://</string>
<string name="clash_profile_url">URL</string>
<string name="clash_profile_url_hint">https://</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>