mirror of https://git.sr.ht/~oppen/ariane
images now viewable in-app
This commit is contained in:
parent
d2011e0d0c
commit
eb0f12aa8c
|
@ -22,12 +22,6 @@ fun View.hideKeyboard(){
|
|||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.showKeyboard(){
|
||||
delay(200){
|
||||
this.callOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun delay(ms: Long, action: () -> Unit){
|
||||
object : CountDownTimer(ms, ms/2) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package oppen.tva.io
|
||||
|
||||
import java.net.URI
|
||||
|
||||
interface Datasource {
|
||||
fun request(uri: URI, onUpdate: (state: TvaState) -> Unit)
|
||||
|
||||
companion object{
|
||||
fun factory(): Datasource{
|
||||
return GeminiDatasource()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package oppen.tva.io
|
||||
|
||||
import android.net.Uri
|
||||
import oppen.tva.io.gemini.GeminiResponse
|
||||
import java.net.URI
|
||||
|
||||
sealed class TvaState {
|
||||
|
@ -10,6 +12,7 @@ sealed class TvaState {
|
|||
data class ResponseGemtext(val uri: URI, val header: GeminiResponse.Header, val lines: List<String>) : TvaState()
|
||||
data class ResponseInput(val uri: URI, val header: GeminiResponse.Header) : TvaState()
|
||||
data class ResponseText(val uri: URI, val header: GeminiResponse.Header, val content: String) : TvaState()
|
||||
data class ResponseImage(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : TvaState()
|
||||
data class ResponseError(val header: GeminiResponse.Header): TvaState()
|
||||
|
||||
data class TabChange(val count: Int) : TvaState()
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package oppen.tva.io.gemini
|
||||
|
||||
import android.content.Context
|
||||
import oppen.tva.io.TvaState
|
||||
import java.net.URI
|
||||
|
||||
interface Datasource {
|
||||
fun request(uri: URI, onUpdate: (state: TvaState) -> Unit)
|
||||
|
||||
companion object{
|
||||
fun factory(context: Context): Datasource {
|
||||
return GeminiDatasource(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.tva.io
|
||||
package oppen.tva.io.gemini
|
||||
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.TrustManager
|
|
@ -1,16 +1,22 @@
|
|||
package oppen.tva.io
|
||||
package oppen.tva.io.gemini
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import oppen.tva.io.TvaState
|
||||
import java.io.*
|
||||
import java.net.ConnectException
|
||||
import java.net.URI
|
||||
import java.security.SecureRandom
|
||||
import javax.net.ssl.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
const val GEMINI_SCHEME = "gemini"
|
||||
|
||||
class GeminiDatasource: Datasource{
|
||||
class GeminiDatasource(val context: Context): Datasource {
|
||||
|
||||
private var last: URI? = null
|
||||
override fun request(uri: URI, onUpdate: (state: TvaState) -> Unit) {
|
||||
|
@ -27,7 +33,13 @@ class GeminiDatasource: Datasource{
|
|||
val cached = RuntimeCache.get(uri)
|
||||
if(cached != null){
|
||||
last = uri
|
||||
onUpdate(TvaState.ResponseGemtext(uri, cached.first, cached.second))
|
||||
onUpdate(
|
||||
TvaState.ResponseGemtext(
|
||||
uri,
|
||||
cached.first,
|
||||
cached.second
|
||||
)
|
||||
)
|
||||
return
|
||||
}else{
|
||||
onUpdate(TvaState.Requesting(uri))
|
||||
|
@ -64,7 +76,13 @@ class GeminiDatasource: Datasource{
|
|||
val cached = RuntimeCache.get(parsedUri)
|
||||
if(cached != null){
|
||||
last = parsedUri
|
||||
onUpdate(TvaState.ResponseGemtext(parsedUri, cached.first, cached.second))
|
||||
onUpdate(
|
||||
TvaState.ResponseGemtext(
|
||||
parsedUri,
|
||||
cached.first,
|
||||
cached.second
|
||||
)
|
||||
)
|
||||
}else{
|
||||
request(parsedUri, onUpdate)
|
||||
}
|
||||
|
@ -92,10 +110,24 @@ class GeminiDatasource: Datasource{
|
|||
socket.enabledProtocols = arrayOf("TLSv1.2")
|
||||
socket.startHandshake()
|
||||
}catch(ce: ConnectException){
|
||||
onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
|
||||
onUpdate(
|
||||
TvaState.ResponseError(
|
||||
GeminiResponse.Header(
|
||||
-1,
|
||||
ce.message ?: ce.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
}catch(she: SSLHandshakeException){
|
||||
onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, she.message ?: she.toString())))
|
||||
onUpdate(
|
||||
TvaState.ResponseError(
|
||||
GeminiResponse.Header(
|
||||
-1,
|
||||
she.message ?: she.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -109,7 +141,14 @@ class GeminiDatasource: Datasource{
|
|||
outWriter.flush()
|
||||
|
||||
if (outWriter.checkError()) {
|
||||
onUpdate(TvaState.ResponseError(GeminiResponse.Header(-1, "Print Writer Error")))
|
||||
onUpdate(
|
||||
TvaState.ResponseError(
|
||||
GeminiResponse.Header(
|
||||
-1,
|
||||
"Print Writer Error"
|
||||
)
|
||||
)
|
||||
)
|
||||
outWriter.close()
|
||||
return
|
||||
}
|
||||
|
@ -137,6 +176,7 @@ class GeminiDatasource: Datasource{
|
|||
header.code != GeminiResponse.SUCCESS -> onUpdate(TvaState.ResponseError(header))
|
||||
header.meta.startsWith("text/gemini") -> getGemtext(socket, uri, header, onUpdate)
|
||||
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
|
||||
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
|
||||
else -> onUpdate(TvaState.ResponseError(header))
|
||||
}
|
||||
}
|
||||
|
@ -161,9 +201,25 @@ class GeminiDatasource: Datasource{
|
|||
|
||||
private fun getString(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
|
||||
val content = socket?.inputStream?.bufferedReader().use { reader -> reader?.readText() }
|
||||
|
||||
socket?.close()
|
||||
|
||||
onUpdate(TvaState.ResponseText(uri, header, content ?: "Error fetching content"))
|
||||
}
|
||||
|
||||
private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: TvaState) -> Unit){
|
||||
|
||||
val filenameRegex = Regex("[^A-Za-z0-9]")
|
||||
val cacheFile = File(context.cacheDir, filenameRegex.replace(uri.path, "_"))
|
||||
|
||||
if(cacheFile.exists()){
|
||||
onUpdate(TvaState.ResponseImage(uri, header, cacheFile.toUri()))
|
||||
}else{
|
||||
cacheFile.createNewFile()
|
||||
cacheFile.outputStream().use{ outputStream ->
|
||||
socket?.inputStream?.copyTo(outputStream)
|
||||
socket?.close()
|
||||
}
|
||||
|
||||
onUpdate(TvaState.ResponseImage(uri, header, cacheFile.toUri()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.tva.io
|
||||
package oppen.tva.io.gemini
|
||||
|
||||
object GeminiResponse {
|
||||
|
||||
|
@ -10,7 +10,7 @@ object GeminiResponse {
|
|||
const val CLIENT_CERTIFICATE_REQUIRED = 6
|
||||
const val UNKNOWN = -1
|
||||
|
||||
fun parseHeader(header: String): Header{
|
||||
fun parseHeader(header: String): Header {
|
||||
var cleanHeader = header.replace("\\s+".toRegex(), " ")
|
||||
var meta = ""
|
||||
when {
|
||||
|
@ -31,13 +31,34 @@ object GeminiResponse {
|
|||
}
|
||||
|
||||
return when {
|
||||
header.startsWith("1") -> Header(INPUT, meta)
|
||||
header.startsWith("2") -> Header(SUCCESS, meta)
|
||||
header.startsWith("3") -> Header(REDIRECT, meta)
|
||||
header.startsWith("4") -> Header(TEMPORARY_FAILURE, meta)
|
||||
header.startsWith("5") -> Header(PERMANENT_FAILURE, meta)
|
||||
header.startsWith("6") -> Header(CLIENT_CERTIFICATE_REQUIRED, meta)
|
||||
else -> Header(UNKNOWN, meta)
|
||||
header.startsWith("1") -> Header(
|
||||
INPUT,
|
||||
meta
|
||||
)
|
||||
header.startsWith("2") -> Header(
|
||||
SUCCESS,
|
||||
meta
|
||||
)
|
||||
header.startsWith("3") -> Header(
|
||||
REDIRECT,
|
||||
meta
|
||||
)
|
||||
header.startsWith("4") -> Header(
|
||||
TEMPORARY_FAILURE,
|
||||
meta
|
||||
)
|
||||
header.startsWith("5") -> Header(
|
||||
PERMANENT_FAILURE,
|
||||
meta
|
||||
)
|
||||
header.startsWith("6") -> Header(
|
||||
CLIENT_CERTIFICATE_REQUIRED,
|
||||
meta
|
||||
)
|
||||
else -> Header(
|
||||
UNKNOWN,
|
||||
meta
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.tva.io
|
||||
package oppen.tva.io.gemini
|
||||
|
||||
import java.lang.StringBuilder
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package oppen.tva.io
|
||||
package oppen.tva.io.gemini
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import java.net.URI
|
||||
|
@ -6,7 +6,9 @@ import java.net.URI
|
|||
object RuntimeCache {
|
||||
|
||||
private const val CACHE_SIZE = 4 * 1024 * 1024
|
||||
private val lruCache = LruCache<String, Pair<GeminiResponse.Header, List<String>>>(CACHE_SIZE)
|
||||
private val lruCache = LruCache<String, Pair<GeminiResponse.Header, List<String>>>(
|
||||
CACHE_SIZE
|
||||
)
|
||||
|
||||
fun put(uri: URI, header: GeminiResponse.Header, lines: List<String>){
|
||||
lruCache.put(uri.toString(), Pair(header, lines))
|
|
@ -21,6 +21,7 @@ class GemtextAdapter(val onLink: (link: URI, longTap: Boolean, view: View?) -> U
|
|||
val typeListItem = 4
|
||||
val typeLink = 5
|
||||
val typeCodeBlock = 6
|
||||
val typeQuote = 7
|
||||
|
||||
sealed class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
|
||||
class Text(itemView: View): ViewHolder(itemView)
|
||||
|
@ -30,6 +31,7 @@ class GemtextAdapter(val onLink: (link: URI, longTap: Boolean, view: View?) -> U
|
|||
class ListItem(itemView: View): ViewHolder(itemView)
|
||||
class Link(itemView: View): ViewHolder(itemView)
|
||||
class Code(itemView: View): ViewHolder(itemView)
|
||||
class Quote(itemView: View): ViewHolder(itemView)
|
||||
}
|
||||
|
||||
fun render(lines: List<String>){
|
||||
|
@ -47,6 +49,7 @@ class GemtextAdapter(val onLink: (link: URI, longTap: Boolean, view: View?) -> U
|
|||
typeListItem -> ViewHolder.ListItem(LayoutInflater.from(parent.context).inflate(R.layout.gemtext_text, parent, false))
|
||||
typeLink -> ViewHolder.Link(LayoutInflater.from(parent.context).inflate(R.layout.gemtext_link, parent, false))
|
||||
typeCodeBlock-> ViewHolder.Code(LayoutInflater.from(parent.context).inflate(R.layout.gemtext_code_block, parent, false))
|
||||
typeQuote -> ViewHolder.Quote(LayoutInflater.from(parent.context).inflate(R.layout.gemtext_quote, parent, false))
|
||||
else -> ViewHolder.Text(LayoutInflater.from(parent.context).inflate(R.layout.gemtext_text, parent, false))
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +63,7 @@ class GemtextAdapter(val onLink: (link: URI, longTap: Boolean, view: View?) -> U
|
|||
line.startsWith("#") -> typeH1
|
||||
line.startsWith("*") -> typeListItem
|
||||
line.startsWith("=>") -> typeLink
|
||||
line.startsWith(">") -> typeQuote
|
||||
else -> typeText
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +76,7 @@ class GemtextAdapter(val onLink: (link: URI, longTap: Boolean, view: View?) -> U
|
|||
when(holder){
|
||||
is ViewHolder.Text -> holder.itemView.gemtext_text_textview.text = line
|
||||
is ViewHolder.Code-> holder.itemView.gemtext_text_monospace_textview.text = line.substring(3)
|
||||
is ViewHolder.Quote-> holder.itemView.gemtext_text_monospace_textview.text = line.substring(1).trim()
|
||||
is ViewHolder.H1 -> holder.itemView.gemtext_text_textview.text = line.substring(2).trim()
|
||||
is ViewHolder.H2 -> holder.itemView.gemtext_text_textview.text = line.substring(3).trim()
|
||||
is ViewHolder.H3 -> holder.itemView.gemtext_text_textview.text = line.substring(4).trim()
|
||||
|
|
|
@ -14,15 +14,16 @@ import androidx.databinding.DataBindingUtil
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import oppen.hideKeyboard
|
||||
import oppen.showKeyboard
|
||||
import oppen.tva.R
|
||||
import oppen.tva.Tva
|
||||
import oppen.tva.databinding.ActivityTvaBinding
|
||||
import oppen.tva.io.GeminiResponse
|
||||
import oppen.tva.io.RuntimeCache
|
||||
import oppen.tva.io.gemini.GeminiResponse
|
||||
import oppen.tva.io.gemini.RuntimeCache
|
||||
import oppen.tva.io.TvaState
|
||||
import oppen.tva.io.gemini.Datasource
|
||||
import oppen.tva.io.history.tabs.TabHistoryInterface
|
||||
import oppen.tva.io.history.uris.HistoryInterface
|
||||
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
|
||||
|
@ -72,7 +73,7 @@ class TvaActivity : AppCompatActivity() {
|
|||
|
||||
history = HistoryInterface.default(this)
|
||||
|
||||
model.initialise(TabHistoryInterface.default(this)){ state ->
|
||||
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 {
|
||||
|
@ -86,6 +87,7 @@ class TvaActivity : AppCompatActivity() {
|
|||
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.TabChange -> binding.tabCount.text = "${state.count}"
|
||||
is TvaState.Blank -> {
|
||||
binding.addressEdit.setText("")
|
||||
|
@ -120,7 +122,6 @@ class TvaActivity : AppCompatActivity() {
|
|||
binding.addressEdit.hint = getString(R.string.main_input_search_hint)
|
||||
binding.addressEdit.text?.clear()
|
||||
binding.addressEdit.requestFocus()
|
||||
binding.addressEdit.showKeyboard()
|
||||
inSearch = true
|
||||
}
|
||||
R.id.overflow_menu_share -> {
|
||||
|
@ -207,6 +208,11 @@ class TvaActivity : AppCompatActivity() {
|
|||
TextDialog.show(this, state)
|
||||
}
|
||||
|
||||
private fun renderImage(state: TvaState.ResponseImage) = runOnUiThread{
|
||||
loadingView(false)
|
||||
ImageDialog.show(this, state)
|
||||
}
|
||||
|
||||
private fun loadingView(visible: Boolean) = runOnUiThread {
|
||||
binding.progressBar.visibleRetainingSpace(visible)
|
||||
if(visible) binding.appBar.setExpanded(true)
|
||||
|
|
|
@ -2,7 +2,7 @@ package oppen.tva.ui
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import oppen.tva.Tva
|
||||
import oppen.tva.io.Datasource
|
||||
import oppen.tva.io.gemini.Datasource
|
||||
import oppen.tva.io.TvaState
|
||||
import oppen.tva.io.history.tabs.TabHistoryInterface
|
||||
import oppen.tva.io.history.tabs.Tab
|
||||
|
@ -10,15 +10,16 @@ import java.net.URI
|
|||
|
||||
class TvaViewModel: ViewModel() {
|
||||
|
||||
private val gemini = Datasource.factory()
|
||||
private lateinit var gemini: Datasource
|
||||
private var onState: (state: TvaState) -> Unit = {}
|
||||
private var activeTab = 0
|
||||
|
||||
private lateinit var cache: TabHistoryInterface
|
||||
var tabs = mutableListOf<Tab>()
|
||||
|
||||
fun initialise(cache: TabHistoryInterface, onState: (state: TvaState) -> Unit){
|
||||
fun initialise(cache: TabHistoryInterface, gemini: Datasource, onState: (state: TvaState) -> Unit){
|
||||
this.cache = cache
|
||||
this.gemini = gemini
|
||||
this.onState = onState
|
||||
|
||||
cache.getTabs { tabs, activeIndex ->
|
||||
|
@ -54,6 +55,7 @@ class TvaViewModel: ViewModel() {
|
|||
is TvaState.AppQuery -> {}
|
||||
is TvaState.ResponseInput -> onState(state)
|
||||
is TvaState.ResponseGemtext -> renderGemini(state)
|
||||
is TvaState.ResponseImage -> onState(state)
|
||||
is TvaState.Requesting -> onState(state)
|
||||
is TvaState.ResponseError -> onState(state)
|
||||
is TvaState.NotGeminiRequest -> onState(state)
|
||||
|
@ -62,7 +64,7 @@ class TvaViewModel: ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun renderGemini(state: TvaState.ResponseGemtext) {
|
||||
private fun renderGemini(state: TvaState.ResponseGemtext) {
|
||||
if(tabs[activeTab].history.isEmpty() || tabs[activeTab].history.last() != state.uri){
|
||||
tabs[activeTab].add(state.uri)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package oppen.tva.ui.content_image
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import kotlinx.android.synthetic.main.dialog_content_image.view.*
|
||||
import oppen.tva.R
|
||||
import oppen.tva.io.TvaState
|
||||
|
||||
object ImageDialog {
|
||||
|
||||
fun show(context: Context, state: TvaState.ResponseImage){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_content_image, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.image_view.setImageURI(state.cacheUri)
|
||||
|
||||
view.close_image_content_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
package oppen.tva.ui.content_image;
|
||||
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
*
|
||||
* From SO: https://stackoverflow.com/a/54474590/7641428
|
||||
*
|
||||
* todo - Rewrite in Kotlin at some point, possibly, maybe, never
|
||||
*
|
||||
*/
|
||||
public class TouchImageView extends androidx.appcompat.widget.AppCompatImageView implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
|
||||
|
||||
Matrix matrix;
|
||||
|
||||
// We can be in one of these 3 states
|
||||
static final int NONE = 0;
|
||||
static final int DRAG = 1;
|
||||
static final int ZOOM = 2;
|
||||
int mode = NONE;
|
||||
|
||||
// Remember some things for zooming
|
||||
PointF last = new PointF();
|
||||
PointF start = new PointF();
|
||||
float minScale = 0.9f;
|
||||
float maxScale = 3f;
|
||||
float[] m;
|
||||
|
||||
int viewWidth, viewHeight;
|
||||
static final int CLICK = 3;
|
||||
float saveScale = 1f;
|
||||
protected float origWidth, origHeight;
|
||||
int oldMeasuredWidth, oldMeasuredHeight;
|
||||
|
||||
ScaleGestureDetector mScaleDetector;
|
||||
|
||||
Context context;
|
||||
|
||||
public TouchImageView(Context context) {
|
||||
super(context);
|
||||
sharedConstructing(context);
|
||||
}
|
||||
|
||||
public TouchImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
sharedConstructing(context);
|
||||
}
|
||||
|
||||
GestureDetector mGestureDetector;
|
||||
|
||||
private void sharedConstructing(Context context) {
|
||||
super.setClickable(true);
|
||||
this.context = context;
|
||||
mGestureDetector = new GestureDetector(context, this);
|
||||
mGestureDetector.setOnDoubleTapListener(this);
|
||||
|
||||
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
|
||||
matrix = new Matrix();
|
||||
m = new float[9];
|
||||
setImageMatrix(matrix);
|
||||
setScaleType(ScaleType.MATRIX);
|
||||
|
||||
setOnTouchListener(new OnTouchListener() {
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
mScaleDetector.onTouchEvent(event);
|
||||
mGestureDetector.onTouchEvent(event);
|
||||
|
||||
PointF curr = new PointF(event.getX(), event.getY());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
last.set(curr);
|
||||
start.set(last);
|
||||
mode = DRAG;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mode == DRAG) {
|
||||
float deltaX = curr.x - last.x;
|
||||
float deltaY = curr.y - last.y;
|
||||
float fixTransX = getFixDragTrans(deltaX, viewWidth,
|
||||
origWidth * saveScale);
|
||||
float fixTransY = getFixDragTrans(deltaY, viewHeight,
|
||||
origHeight * saveScale);
|
||||
matrix.postTranslate(fixTransX, fixTransY);
|
||||
fixTrans();
|
||||
last.set(curr.x, curr.y);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
mode = NONE;
|
||||
int xDiff = (int) Math.abs(curr.x - start.x);
|
||||
int yDiff = (int) Math.abs(curr.y - start.y);
|
||||
if (xDiff < CLICK && yDiff < CLICK)
|
||||
performClick();
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
mode = NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
setImageMatrix(matrix);
|
||||
invalidate();
|
||||
return true; // indicate event was handled
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setMaxZoom(float x) {
|
||||
maxScale = x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
saveScale = 1f;
|
||||
fixTrans();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
// Double tap is detected
|
||||
|
||||
float origScale = saveScale;
|
||||
float mScaleFactor;
|
||||
|
||||
if (saveScale == maxScale) {
|
||||
saveScale = minScale;
|
||||
mScaleFactor = minScale / origScale;
|
||||
} else {
|
||||
saveScale = maxScale;
|
||||
mScaleFactor = maxScale / origScale;
|
||||
}
|
||||
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);
|
||||
|
||||
fixTrans();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
mode = ZOOM;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
float mScaleFactor = detector.getScaleFactor();
|
||||
float origScale = saveScale;
|
||||
saveScale *= mScaleFactor;
|
||||
if (saveScale > maxScale) {
|
||||
saveScale = maxScale;
|
||||
mScaleFactor = maxScale / origScale;
|
||||
} else if (saveScale < minScale) {
|
||||
saveScale = minScale;
|
||||
mScaleFactor = minScale / origScale;
|
||||
}
|
||||
|
||||
if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) {
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);
|
||||
}else {
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
|
||||
}
|
||||
|
||||
fixTrans();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void fixTrans() {
|
||||
matrix.getValues(m);
|
||||
float transX = m[Matrix.MTRANS_X];
|
||||
float transY = m[Matrix.MTRANS_Y];
|
||||
|
||||
float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
|
||||
float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale);
|
||||
|
||||
if (fixTransX != 0 || fixTransY != 0) matrix.postTranslate(fixTransX, fixTransY);
|
||||
}
|
||||
|
||||
float getFixTrans(float trans, float viewSize, float contentSize) {
|
||||
float minTrans, maxTrans;
|
||||
|
||||
if (contentSize <= viewSize) {
|
||||
minTrans = 0;
|
||||
maxTrans = viewSize - contentSize;
|
||||
} else {
|
||||
minTrans = viewSize - contentSize;
|
||||
maxTrans = 0;
|
||||
}
|
||||
|
||||
if (trans < minTrans) return -trans + minTrans;
|
||||
if (trans > maxTrans) return -trans + maxTrans;
|
||||
return 0;
|
||||
}
|
||||
|
||||
float getFixDragTrans(float delta, float viewSize, float contentSize) {
|
||||
if (contentSize <= viewSize) {
|
||||
return 0;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
//
|
||||
// Rescales image on rotation
|
||||
//
|
||||
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return;
|
||||
oldMeasuredHeight = viewHeight;
|
||||
oldMeasuredWidth = viewWidth;
|
||||
|
||||
if (saveScale == 1) {
|
||||
// Fit to screen.
|
||||
float scale;
|
||||
|
||||
Drawable drawable = getDrawable();
|
||||
if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) return;
|
||||
int bmWidth = drawable.getIntrinsicWidth();
|
||||
int bmHeight = drawable.getIntrinsicHeight();
|
||||
|
||||
Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);
|
||||
|
||||
float scaleX = (float) viewWidth / (float) bmWidth;
|
||||
float scaleY = (float) viewHeight / (float) bmHeight;
|
||||
scale = Math.min(scaleX, scaleY);
|
||||
matrix.setScale(scale, scale);
|
||||
|
||||
// Center the image
|
||||
float redundantYSpace = (float) viewHeight - (scale * (float) bmHeight);
|
||||
float redundantXSpace = (float) viewWidth - (scale * (float) bmWidth);
|
||||
redundantYSpace /= (float) 2;
|
||||
redundantXSpace /= (float) 2;
|
||||
|
||||
matrix.postTranslate(redundantXSpace, redundantYSpace);
|
||||
|
||||
origWidth = viewWidth - 2 * redundantXSpace;
|
||||
origHeight = viewHeight - 2 * redundantYSpace;
|
||||
setImageMatrix(matrix);
|
||||
}
|
||||
fixTrans();
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import kotlinx.android.synthetic.main.dialog_about.view.close_tab_dialog
|
||||
import kotlinx.android.synthetic.main.dialog_history.view.*
|
||||
import oppen.tva.R
|
||||
import oppen.tva.io.RuntimeCache
|
||||
import oppen.tva.io.gemini.RuntimeCache
|
||||
import oppen.tva.io.history.uris.HistoryInterface
|
||||
|
||||
object HistoryDialog {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<oppen.tva.ui.content_image.TouchImageView
|
||||
android:id="@+id/image_view"
|
||||
android:scaleType="centerInside"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_image_content_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/history_overflow"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_monospace_textview"
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:layout_marginLeft="@dimen/default_margin"
|
||||
android:layout_marginRight="@dimen/default_margin"
|
||||
android:paddingLeft="@dimen/default_margin"
|
||||
android:paddingRight="@dimen/default_margin"
|
||||
android:paddingTop="@dimen/default_margin"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textStyle="italic"
|
||||
android:background="@color/code_background"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
|
@ -1,5 +1,6 @@
|
|||
package oppen.tva.io
|
||||
|
||||
import oppen.tva.io.gemini.GemtextHelper
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
|
Loading…
Reference in New Issue