diff --git a/app/src/main/java/oppen/Extensions.kt b/app/src/main/java/oppen/Extensions.kt index 5dbd37c..24b5945 100644 --- a/app/src/main/java/oppen/Extensions.kt +++ b/app/src/main/java/oppen/Extensions.kt @@ -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) {} diff --git a/app/src/main/java/oppen/tva/io/Datasource.kt b/app/src/main/java/oppen/tva/io/Datasource.kt deleted file mode 100644 index e496e75..0000000 --- a/app/src/main/java/oppen/tva/io/Datasource.kt +++ /dev/null @@ -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() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/TvaState.kt b/app/src/main/java/oppen/tva/io/TvaState.kt index 626b06c..c52974e 100644 --- a/app/src/main/java/oppen/tva/io/TvaState.kt +++ b/app/src/main/java/oppen/tva/io/TvaState.kt @@ -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) : 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() diff --git a/app/src/main/java/oppen/tva/io/gemini/Datasource.kt b/app/src/main/java/oppen/tva/io/gemini/Datasource.kt new file mode 100644 index 0000000..723b687 --- /dev/null +++ b/app/src/main/java/oppen/tva/io/gemini/Datasource.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/DummyTrustManager.kt b/app/src/main/java/oppen/tva/io/gemini/DummyTrustManager.kt similarity index 96% rename from app/src/main/java/oppen/tva/io/DummyTrustManager.kt rename to app/src/main/java/oppen/tva/io/gemini/DummyTrustManager.kt index 0f33c30..03585ef 100644 --- a/app/src/main/java/oppen/tva/io/DummyTrustManager.kt +++ b/app/src/main/java/oppen/tva/io/gemini/DummyTrustManager.kt @@ -1,4 +1,4 @@ -package oppen.tva.io +package oppen.tva.io.gemini import java.security.cert.X509Certificate import javax.net.ssl.TrustManager diff --git a/app/src/main/java/oppen/tva/io/GeminiDatasource.kt b/app/src/main/java/oppen/tva/io/gemini/GeminiDatasource.kt similarity index 70% rename from app/src/main/java/oppen/tva/io/GeminiDatasource.kt rename to app/src/main/java/oppen/tva/io/gemini/GeminiDatasource.kt index f1d6f95..a2cd141 100644 --- a/app/src/main/java/oppen/tva/io/GeminiDatasource.kt +++ b/app/src/main/java/oppen/tva/io/gemini/GeminiDatasource.kt @@ -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())) + } + } } \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/GeminiResponse.kt b/app/src/main/java/oppen/tva/io/gemini/GeminiResponse.kt similarity index 60% rename from app/src/main/java/oppen/tva/io/GeminiResponse.kt rename to app/src/main/java/oppen/tva/io/gemini/GeminiResponse.kt index ed4b60c..06450cd 100644 --- a/app/src/main/java/oppen/tva/io/GeminiResponse.kt +++ b/app/src/main/java/oppen/tva/io/gemini/GeminiResponse.kt @@ -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 + ) } } diff --git a/app/src/main/java/oppen/tva/io/GemtextHelper.kt b/app/src/main/java/oppen/tva/io/gemini/GemtextHelper.kt similarity index 96% rename from app/src/main/java/oppen/tva/io/GemtextHelper.kt rename to app/src/main/java/oppen/tva/io/gemini/GemtextHelper.kt index 1cf9c18..280ddcd 100644 --- a/app/src/main/java/oppen/tva/io/GemtextHelper.kt +++ b/app/src/main/java/oppen/tva/io/gemini/GemtextHelper.kt @@ -1,4 +1,4 @@ -package oppen.tva.io +package oppen.tva.io.gemini import java.lang.StringBuilder diff --git a/app/src/main/java/oppen/tva/io/RuntimeCache.kt b/app/src/main/java/oppen/tva/io/gemini/RuntimeCache.kt similarity index 87% rename from app/src/main/java/oppen/tva/io/RuntimeCache.kt rename to app/src/main/java/oppen/tva/io/gemini/RuntimeCache.kt index 8489429..8121c65 100644 --- a/app/src/main/java/oppen/tva/io/RuntimeCache.kt +++ b/app/src/main/java/oppen/tva/io/gemini/RuntimeCache.kt @@ -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>>(CACHE_SIZE) + private val lruCache = LruCache>>( + CACHE_SIZE + ) fun put(uri: URI, header: GeminiResponse.Header, lines: List){ lruCache.put(uri.toString(), Pair(header, lines)) diff --git a/app/src/main/java/oppen/tva/ui/GemtextAdapter.kt b/app/src/main/java/oppen/tva/ui/GemtextAdapter.kt index a0ac8ff..0370365 100644 --- a/app/src/main/java/oppen/tva/ui/GemtextAdapter.kt +++ b/app/src/main/java/oppen/tva/ui/GemtextAdapter.kt @@ -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){ @@ -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() diff --git a/app/src/main/java/oppen/tva/ui/TvaActivity.kt b/app/src/main/java/oppen/tva/ui/TvaActivity.kt index ea753b2..ad9a8bf 100644 --- a/app/src/main/java/oppen/tva/ui/TvaActivity.kt +++ b/app/src/main/java/oppen/tva/ui/TvaActivity.kt @@ -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) diff --git a/app/src/main/java/oppen/tva/ui/TvaViewModel.kt b/app/src/main/java/oppen/tva/ui/TvaViewModel.kt index fdbcaa4..48e4d20 100644 --- a/app/src/main/java/oppen/tva/ui/TvaViewModel.kt +++ b/app/src/main/java/oppen/tva/ui/TvaViewModel.kt @@ -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() - 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) } diff --git a/app/src/main/java/oppen/tva/ui/content_image/ImageDialog.kt b/app/src/main/java/oppen/tva/ui/content_image/ImageDialog.kt new file mode 100644 index 0000000..2fec1fc --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/content_image/ImageDialog.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/content_image/TouchImageView.java b/app/src/main/java/oppen/tva/ui/content_image/TouchImageView.java new file mode 100644 index 0000000..56374b8 --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/content_image/TouchImageView.java @@ -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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/modals_menus/history/HistoryDialog.kt b/app/src/main/java/oppen/tva/ui/modals_menus/history/HistoryDialog.kt index dd5ab36..6054f07 100644 --- a/app/src/main/java/oppen/tva/ui/modals_menus/history/HistoryDialog.kt +++ b/app/src/main/java/oppen/tva/ui/modals_menus/history/HistoryDialog.kt @@ -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 { diff --git a/app/src/main/res/layout/dialog_content_image.xml b/app/src/main/res/layout/dialog_content_image.xml new file mode 100644 index 0000000..2e1fcfe --- /dev/null +++ b/app/src/main/res/layout/dialog_content_image.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_quote.xml b/app/src/main/res/layout/gemtext_quote.xml new file mode 100644 index 0000000..4de74e1 --- /dev/null +++ b/app/src/main/res/layout/gemtext_quote.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/app/src/test/java/oppen/tva/io/GemtextHelperUnitTest.kt b/app/src/test/java/oppen/tva/io/GemtextHelperUnitTest.kt index 9623ee0..ddac319 100644 --- a/app/src/test/java/oppen/tva/io/GemtextHelperUnitTest.kt +++ b/app/src/test/java/oppen/tva/io/GemtextHelperUnitTest.kt @@ -1,5 +1,6 @@ package oppen.tva.io +import oppen.tva.io.gemini.GemtextHelper import org.junit.Test import org.junit.Assert.*