images now viewable in-app

This commit is contained in:
Öppen 2020-08-21 16:12:00 +01:00
parent d2011e0d0c
commit eb0f12aa8c
18 changed files with 536 additions and 52 deletions

View File

@ -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) {}

View File

@ -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()
}
}
}

View File

@ -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()

View File

@ -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)
}
}
}

View File

@ -1,4 +1,4 @@
package oppen.tva.io
package oppen.tva.io.gemini
import java.security.cert.X509Certificate
import javax.net.ssl.TrustManager

View File

@ -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()))
}
}
}

View File

@ -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
)
}
}

View File

@ -1,4 +1,4 @@
package oppen.tva.io
package oppen.tva.io.gemini
import java.lang.StringBuilder

View File

@ -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))

View File

@ -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()

View File

@ -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)

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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" />

View File

@ -1,5 +1,6 @@
package oppen.tva.io
import oppen.tva.io.gemini.GemtextHelper
import org.junit.Test
import org.junit.Assert.*