ariane/app/src/main/java/oppen/tva/ui/TvaActivity.kt

267 lines
10 KiB
Kotlin

package oppen.tva.ui
import android.content.Context
import android.content.Intent
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.inputmethod.EditorInfo
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.set
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import oppen.hideKeyboard
import oppen.tva.R
import oppen.tva.Tva
import oppen.tva.databinding.ActivityTvaBinding
import oppen.tva.io.TvaState
import oppen.tva.io.gemini.Datasource
import oppen.tva.io.gemini.GeminiResponse
import oppen.tva.io.gemini.RuntimeCache
import oppen.tva.io.history.tabs.TabHistoryInterface
import oppen.tva.io.history.uris.HistoryInterface
import oppen.tva.ui.audio_player.AudioPlayer
import oppen.tva.ui.content_image.ImageDialog
import oppen.tva.ui.content_text.TextDialog
import oppen.tva.ui.modals_menus.about.AboutDialog
import oppen.tva.ui.modals_menus.history.HistoryDialog
import oppen.tva.ui.modals_menus.input.InputDialog
import oppen.tva.ui.modals_menus.overflow.OverflowPopup
import oppen.tva.ui.modals_menus.set_home.SetHomeDialog
import oppen.tva.ui.modals_menus.tabs.NewTabPopup
import oppen.tva.ui.modals_menus.tabs.TabsDialog
import oppen.visibleRetainingSpace
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URLEncoder
const val CREATE_IMAGE_FILE_REQ = 628
const val CREATE_AUDIO_FILE_REQ = 629
class TvaActivity : AppCompatActivity() {
private var inSearch = false
private val mediaPlayer = MediaPlayer()
private val model by viewModels<TvaViewModel>()
private lateinit var binding: ActivityTvaBinding
private lateinit var history: HistoryInterface
private val adapter = GemtextAdapter { uri, longTap, view ->
if(longTap){
NewTabPopup.show(view){ menuId ->
when (menuId) {
R.id.link_menu_open_in_new_tab -> {
model.newTab(uri)
}
}
}
}else{
//Reset input text hint after user has been searching
if(inSearch) {
binding.addressEdit.hint = getString(R.string.main_input_hint)
inSearch = false
}
model.request(uri)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_tva)
binding.viewmodel = model
binding.lifecycleOwner = this
binding.gemtextRecycler.layoutManager = LinearLayoutManager(this)
binding.gemtextRecycler.adapter = adapter
history = HistoryInterface.default(this)
model.initialise(TabHistoryInterface.default(this), Datasource.factory(this)){ state ->
when(state){
is TvaState.AppQuery -> runOnUiThread{ showAlert("App backdoor/query not implemented yet") }
is TvaState.ResponseInput -> runOnUiThread {
loadingView(false)
InputDialog.show(this, state){ queryAddress ->
model.request(queryAddress)
}
}
is TvaState.Requesting -> loadingView(true)
is TvaState.NotGeminiRequest -> externalProtocol(state)
is TvaState.ResponseError -> showAlert("${GeminiResponse.getCodeString(state.header.code)}: ${state.header.meta}")
is TvaState.ResponseGemtext -> renderGemtext(state)
is TvaState.ResponseText -> renderText(state)
is TvaState.ResponseImage -> renderImage(state)
is TvaState.ResponseAudio -> renderAudio(state)
is TvaState.TabChange -> binding.tabCount.text = "${state.count}"
is TvaState.Blank -> {
binding.addressEdit.setText("")
adapter.render(arrayListOf())
}
}
}
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_GO -> {
val input = binding.addressEdit.text.toString()
if(input.startsWith("gemini://")){
model.request(input)
}else{
model.request("${Tva.GEMINI_USER_SEARCH_BASE}${URLEncoder.encode(input, "UTF-8")}")
}
binding.addressEdit.hideKeyboard()
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
}
}
binding.more.setOnClickListener {
OverflowPopup.show(binding.more){menuId ->
when (menuId) {
R.id.overflow_menu_search -> {
binding.addressEdit.hint = getString(R.string.main_input_search_hint)
binding.addressEdit.text?.clear()
binding.addressEdit.requestFocus()
inSearch = true
}
R.id.overflow_menu_share -> {
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, binding.addressEdit.text.toString())
type = "text/plain"
startActivity(Intent.createChooser(this, null))
}
}
R.id.overflow_menu_reload -> {
val address = binding.addressEdit.text.toString()
RuntimeCache.remove(address)
model.request(address)
}
R.id.overflow_menu_history -> HistoryDialog.show(this){ historyAddress ->
model.request(historyAddress)
}
R.id.overflow_menu_about -> AboutDialog.show(this)
R.id.overflow_menu_set_home -> {
SetHomeDialog.show(this, binding.addressEdit.text.toString()){
showAlert("Home capsule updated")
}
}
}
}
}
binding.home.setOnClickListener {
val prefs = getSharedPreferences("oppen.tva.ui.dialogs.set_home", Context.MODE_PRIVATE)
val home = prefs.getString("home", Tva.DEFAULT_HOME_CAPSULE)
model.request(home!!)
}
binding.tabs.setOnClickListener {
TabsDialog().show(this, model)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data.toString().let{model.request(it)}
}
private fun showAlert(message: String) = runOnUiThread{
loadingView(false)
if(message.length > 40){
AlertDialog.Builder(this)
.setMessage(message)
.show()
}else {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
}
private fun externalProtocol(state: TvaState.NotGeminiRequest) = runOnUiThread {
loadingView(false)
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(state.uri.toString()))
startActivity(browserIntent)
}
private fun renderGemtext(state: TvaState.ResponseGemtext) = runOnUiThread {
loadingView(false)
val addressSpan = SpannableString(state.uri.toString())
addressSpan.set(0, 9, ForegroundColorSpan(resources.getColor(R.color.protocol_address)))
binding.addressEdit.setText(addressSpan)
adapter.render(state.lines)
history.add(state.uri.toString())
}
private fun renderText(state: TvaState.ResponseText) = runOnUiThread {
loadingView(false)
TextDialog.show(this, state)
}
var imageState: TvaState.ResponseImage? = null
private fun renderAudio(state: TvaState.ResponseAudio) = runOnUiThread {
loadingView(false)
AudioPlayer.play(this, binding, mediaPlayer, state)
}
private fun renderImage(state: TvaState.ResponseImage) = runOnUiThread{
loadingView(false)
ImageDialog.show(this, state){ state ->
imageState = state
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
startActivityForResult(intent, CREATE_IMAGE_FILE_REQ)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == RESULT_OK && requestCode == CREATE_IMAGE_FILE_REQ){
if(imageState == null) return
data?.data?.let{ uri ->
val cachedFile = File(imageState!!.cacheUri.path ?: "")
contentResolver.openFileDescriptor(uri, "w")?.use { fileDescriptor ->
FileOutputStream(fileDescriptor.fileDescriptor).use { destOutput ->
val sourceChannel = FileInputStream(cachedFile).channel
val destChannel = destOutput.channel
sourceChannel.transferTo(0, sourceChannel.size(), destChannel)
sourceChannel.close()
destChannel.close()
}
}
}
}
}
private fun loadingView(visible: Boolean) = runOnUiThread {
binding.progressBar.visibleRetainingSpace(visible)
if(visible) binding.appBar.setExpanded(true)
}
@ExperimentalStdlibApi
override fun onBackPressed() {
if(model.canGoBack()){
model.goBack()
}else{
super.onBackPressed()
}
}
override fun onDestroy() {
super.onDestroy()
model.persistTabState()
}
}