commit 49956456e0fb4d9a83986b45e8aabaefbd57a022 Author: Öppen Date: Sat Aug 15 15:52:27 2020 +0100 ::: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..30976af --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Två \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..88ea3aa --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b9f8a5e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bfef59 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d7e2ead --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,59 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.1" + + defaultConfig { + applicationId "oppen.tva" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + buildFeatures{ + dataBinding true + } + + compileOptions{ + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions{ + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.1' + implementation 'com.google.android.material:material:1.3.0-alpha02' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.activity:activity-ktx:1.1.0' + implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' + + + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/oppen/tva/ExampleInstrumentedTest.kt b/app/src/androidTest/java/oppen/tva/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..7e3b500 --- /dev/null +++ b/app/src/androidTest/java/oppen/tva/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package oppen.tva + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("oppen.tva", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0d88970 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/Tva.kt b/app/src/main/java/oppen/tva/Tva.kt new file mode 100644 index 0000000..e31a5a4 --- /dev/null +++ b/app/src/main/java/oppen/tva/Tva.kt @@ -0,0 +1,6 @@ +package oppen.tva + +import android.app.Application + +class Tva: Application() { +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/Datasource.kt b/app/src/main/java/oppen/tva/io/Datasource.kt new file mode 100644 index 0000000..e496e75 --- /dev/null +++ b/app/src/main/java/oppen/tva/io/Datasource.kt @@ -0,0 +1,13 @@ +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/DummyTrustManager.kt b/app/src/main/java/oppen/tva/io/DummyTrustManager.kt new file mode 100644 index 0000000..0f33c30 --- /dev/null +++ b/app/src/main/java/oppen/tva/io/DummyTrustManager.kt @@ -0,0 +1,33 @@ +package oppen.tva.io + +import java.security.cert.X509Certificate +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager +import javax.security.cert.CertificateException + +object DummyTrustManager { + + fun get(): Array { + return arrayOf( + object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } + + @Throws(CertificateException::class) + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } + + override fun getAcceptedIssuers(): Array? { + return arrayOf() + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/GeminiDatasource.kt b/app/src/main/java/oppen/tva/io/GeminiDatasource.kt new file mode 100644 index 0000000..0e68d39 --- /dev/null +++ b/app/src/main/java/oppen/tva/io/GeminiDatasource.kt @@ -0,0 +1,91 @@ +package oppen.tva.io + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.io.* +import java.net.URI +import java.security.SecureRandom +import javax.net.ssl.* + +const val GEMINI_SCHEME = "gemini" + +class GeminiDatasource: Datasource{ + + override fun request(uri: URI, onUpdate: (state: TvaState) -> Unit) { + + //Any inputted uri starting with a colon is an app-specific command, eg. :prefs :settings + if(uri.toString().startsWith(":")){ + onUpdate(TvaState.AppQuery(uri)) + return + } + + when (uri.scheme) { + GEMINI_SCHEME -> { + //Indicate app should show progress indicator + onUpdate(TvaState.Requesting(uri)) + + GlobalScope.launch { + geminiRequest(uri, onUpdate) + } + + } + else -> onUpdate(TvaState.NotGeminiRequest(uri)) + } + } + + /** + * + * This was largely copied from https://framagit.org/waweic/gemini-client/-/blob/master/app/src/main/java/rocks/ism/decentral/geminiclient/GeminiConnection.kt + * + */ + private fun geminiRequest(uri: URI, onUpdate: (state: TvaState) -> Unit){ + val port = if(uri.port == -1) 1965 else uri.port + + val sslContext = SSLContext.getInstance("TLSv1.2") + sslContext.init(null, DummyTrustManager.get(), SecureRandom()) + + val factory: SSLSocketFactory = sslContext.socketFactory + val socket: SSLSocket = factory.createSocket(uri.host, port) as SSLSocket + socket.enabledProtocols = arrayOf("TLSv1.2") + + socket.startHandshake() + + // OUT >>>>>>>>>>>>>>>>>>>>>>>>>> + val outputStreamWriter = OutputStreamWriter(socket.outputStream) + val bufferedWriter = BufferedWriter(outputStreamWriter) + val outWriter = PrintWriter(bufferedWriter) + + outWriter.print(uri.toString() + "\r\n") + outWriter.flush() + + if (outWriter.checkError()) { + onUpdate(TvaState.GeminiPrintWriterError) + outWriter.close() + return + } + + // IN <<<<<<<<<<<<<<<<<<<<<<<<<<< + val lines = mutableListOf() + socket.inputStream.reader().use { inputStreamReader -> + BufferedReader(inputStreamReader).use { reader -> + lines.addAll(reader.readLines()) + } + } + + val header = lines.firstOrNull() ?: "" + lines.removeAt(0) + + println("Tva: header: $header") + + lines.forEach { + println("Tva: line: $it") + } + + println("Tva: ends -----------------") + + outputStreamWriter.close() + bufferedWriter.close() + outWriter.close() + onUpdate(TvaState.GeminiResponse(uri, header, lines)) + } +} \ 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 new file mode 100644 index 0000000..9c1ff9d --- /dev/null +++ b/app/src/main/java/oppen/tva/io/TvaState.kt @@ -0,0 +1,11 @@ +package oppen.tva.io + +import java.net.URI + +sealed class TvaState { + data class AppQuery(val uri: URI): TvaState() + data class Requesting(val uri: URI): TvaState() + data class NotGeminiRequest(val uri: URI) : TvaState() + data class GeminiResponse(val uri: URI, val header: String, val lines: List) : TvaState() + object GeminiPrintWriterError : TvaState() +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/history/CacheInterface.kt b/app/src/main/java/oppen/tva/io/history/CacheInterface.kt new file mode 100644 index 0000000..2d357ae --- /dev/null +++ b/app/src/main/java/oppen/tva/io/history/CacheInterface.kt @@ -0,0 +1,14 @@ +package oppen.tva.io.history + +import android.content.Context + +interface CacheInterface { + fun getTabs(onTabs: (tabs: MutableList, activeIndex: Int)-> Unit) + fun update(tabs: List, activeIndex: Int) + + companion object{ + fun default(context: Context): CacheInterface{ + return SimplePrefsCache(context) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/history/SimplePrefsCache.kt b/app/src/main/java/oppen/tva/io/history/SimplePrefsCache.kt new file mode 100644 index 0000000..06c5579 --- /dev/null +++ b/app/src/main/java/oppen/tva/io/history/SimplePrefsCache.kt @@ -0,0 +1,68 @@ +package oppen.tva.io.history + +import android.content.Context +import org.json.JSONArray +import org.json.JSONObject + +/** + * + * This is a simple SharedPreferences based store for now because I don't want to get bogged down setting up Room at this point + * + */ +class SimplePrefsCache(context: Context): CacheInterface { + + private val prefsKey = "oppen.tva.io.history.SimplePrefsCache.PREFS_KEY" + private val prefsCacheKey = "oppen.tva.io.history.SimplePrefsCache.PREFS_CACHE_KEY" + private val prefsActiveIndexKey = "oppen.tva.io.history.SimplePrefsCache.PREFS_ACTIVE_INDEX_KEY" + private val prefs = context.getSharedPreferences(prefsKey, Context.MODE_PRIVATE) + + override fun getTabs(onTabs: (tabs: MutableList, activeIndex: Int) -> Unit) { + val activeIndex = prefs.getInt(prefsActiveIndexKey, 0) + when (val rawJson = prefs.getString(prefsCacheKey, null)) { + null -> onTabs(arrayListOf(), activeIndex) + else -> { + val tabs = mutableListOf() + val tabsJsonArray = JSONArray(rawJson) + val tabCount = tabsJsonArray.length() + for(index in 0 until tabCount){ + val jsonTabObject = tabsJsonArray.getJSONObject(index) + val tabIndex = jsonTabObject.getInt("index") + val tab = Tab(tabIndex) + val jsonTabHistory = jsonTabObject.getJSONArray("history") + val historySize = jsonTabHistory.length() + for(historyIndex in 0 until historySize){ + tab.add(jsonTabHistory.getString(historyIndex)) + } + tabs.add(tab) + } + + onTabs(tabs, activeIndex) + } + } + } + + override fun update(tabs: List, activeIndex: Int) { + val edit = prefs.edit() + edit.putInt(prefsActiveIndexKey, activeIndex) + edit.putString(prefsCacheKey, tabsToJson(tabs)) + edit.apply() + } + + private fun tabsToJson(tabs: List): String { + val jsonArray = JSONArray() + tabs.forEach { tab -> + val jsonTab = JSONObject() + jsonTab.put("index", tab.index) + + val jsonHistoryArray = JSONArray() + tab.history.forEach { uri -> + jsonHistoryArray.put(uri.toString()) + } + jsonTab.put("history", jsonHistoryArray) + jsonArray.put(jsonTab) + } + val rawJsonArray = jsonArray.toString(5) + println("Raw tabs json to store:\n$rawJsonArray") + return rawJsonArray + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/io/history/Tab.kt b/app/src/main/java/oppen/tva/io/history/Tab.kt new file mode 100644 index 0000000..224facb --- /dev/null +++ b/app/src/main/java/oppen/tva/io/history/Tab.kt @@ -0,0 +1,23 @@ +package oppen.tva.io.history + +import java.net.URI + +class Tab(val index: Int) { + val history = mutableListOf() + + fun add(uri: URI){ + history.add(uri) + } + + fun add(address: String){ + history.add(URI.create(address)) + } + + companion object{ + fun new(index: Int, address: String): Tab { + val tab = Tab(index) + tab.add(address) + return tab + } + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/TvaActivity.kt b/app/src/main/java/oppen/tva/ui/TvaActivity.kt new file mode 100644 index 0000000..baa3bbe --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/TvaActivity.kt @@ -0,0 +1,57 @@ +package oppen.tva.ui + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.inputmethod.EditorInfo +import androidx.activity.viewModels +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.LinearLayoutManager +import oppen.tva.R +import oppen.tva.databinding.ActivityTvaBinding +import oppen.tva.io.TvaState +import oppen.tva.io.history.CacheInterface +import oppen.tva.ui.gemtext.GemtextAdapter + +class TvaActivity : AppCompatActivity() { + + private val model by viewModels() + private lateinit var binding: ActivityTvaBinding + private val adapter = GemtextAdapter{ uri -> + 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 + + model.initialise(CacheInterface.default(this)){ state -> + when(state){ + is TvaState.AppQuery -> TODO() + is TvaState.Requesting -> TODO() + is TvaState.NotGeminiRequest -> TODO() + is TvaState.GeminiResponse -> renderGemtext(state) + TvaState.GeminiPrintWriterError -> TODO() + } + } + + binding.addressEdit.setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_GO -> { + model.request(binding.addressEdit.text.toString()) + return@setOnEditorActionListener true + } + else -> return@setOnEditorActionListener false + } + } + } + + private fun renderGemtext(state: TvaState.GeminiResponse) = runOnUiThread { + binding.addressEdit.setText(state.uri.toString()) + adapter.render(state.lines) + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/TvaViewModel.kt b/app/src/main/java/oppen/tva/ui/TvaViewModel.kt new file mode 100644 index 0000000..8a3e0e3 --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/TvaViewModel.kt @@ -0,0 +1,62 @@ +package oppen.tva.ui + +import androidx.lifecycle.ViewModel +import oppen.tva.io.Datasource +import oppen.tva.io.TvaState +import oppen.tva.io.history.CacheInterface +import oppen.tva.io.history.Tab +import java.net.URI + +class TvaViewModel: ViewModel() { + + private val gemini = Datasource.factory() + private var onState: (state: TvaState) -> Unit = {} + private var activeTab = 0 + + private lateinit var cache: CacheInterface + private var tabs = mutableListOf() + + fun initialise(cache: CacheInterface, onState: (state: TvaState) -> Unit){ + this.cache = cache + this.onState = onState + + + cache.getTabs { tabs, activeIndex -> + this.tabs.addAll(tabs) + if(tabs.isEmpty()){ + this.tabs.add(Tab(0)) + activeTab = 0 + request(URI.create("gemini://gemini.circumlunar.space/")) + }else{ + activeTab = activeIndex + request(tabs[activeTab].history.last()) + } + } + } + + fun request(address: String) { + request(URI.create(address)) + } + + fun request(uri: URI){ + gemini.request(uri){ state -> + when(state){ + is TvaState.Requesting -> {} + is TvaState.AppQuery -> {} + is TvaState.NotGeminiRequest -> {} + is TvaState.GeminiResponse -> renderGemini(state) + TvaState.GeminiPrintWriterError -> {} + } + } + } + + private fun renderGemini(state: TvaState.GeminiResponse) { + if(tabs[activeTab].history.isEmpty() || tabs[activeTab].history.last() != state.uri){ + tabs[activeTab].add(state.uri) + cache.update(tabs, activeTab) + } + + onState(state) + + } +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/gemtext/Gemtext.kt b/app/src/main/java/oppen/tva/ui/gemtext/Gemtext.kt new file mode 100644 index 0000000..6965481 --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/gemtext/Gemtext.kt @@ -0,0 +1,8 @@ +package oppen.tva.ui.gemtext + +sealed class Gemtext { + data class Text(val text: String): Gemtext() + data class Header(val text: String): Gemtext() + data class HalfHeader(val text: String): Gemtext() + data class ThirdHeader(val text: String): Gemtext() +} \ No newline at end of file diff --git a/app/src/main/java/oppen/tva/ui/gemtext/GemtextAdapter.kt b/app/src/main/java/oppen/tva/ui/gemtext/GemtextAdapter.kt new file mode 100644 index 0000000..fc64a94 --- /dev/null +++ b/app/src/main/java/oppen/tva/ui/gemtext/GemtextAdapter.kt @@ -0,0 +1,94 @@ +package oppen.tva.ui.gemtext + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.gemtext_link.view.* +import kotlinx.android.synthetic.main.gemtext_text.view.gemtext_text_textview +import oppen.tva.R +import java.net.URI + +class GemtextAdapter(val onLink: (link: URI) -> Unit): RecyclerView.Adapter() { + + var lines = mutableListOf() + + val typeText = 0 + val typeH1 = 1 + val typeH2 = 2 + val typeH3 = 3 + val typeListItem = 4 + val typeLink = 5 + + sealed class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ + class Text(itemView: View): ViewHolder(itemView) + class H1(itemView: View): ViewHolder(itemView) + class H2(itemView: View): ViewHolder(itemView) + class H3(itemView: View): ViewHolder(itemView) + class ListItem(itemView: View): ViewHolder(itemView) + class Link(itemView: View): ViewHolder(itemView) + } + + fun render(lines: List){ + this.lines.clear() + this.lines.addAll(lines) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return when(viewType){ + typeText -> ViewHolder.Text( LayoutInflater.from(parent.context).inflate(R.layout.gemtext_text, parent, false) ) + typeH1 -> ViewHolder.H1( LayoutInflater.from(parent.context).inflate(R.layout.gemtext_h1, parent, false) ) + typeH2 -> ViewHolder.H1( LayoutInflater.from(parent.context).inflate(R.layout.gemtext_h2, parent, false) ) + typeH3 -> ViewHolder.H1( LayoutInflater.from(parent.context).inflate(R.layout.gemtext_h3, parent, false) ) + 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) ) + else -> ViewHolder.Text( LayoutInflater.from(parent.context).inflate(R.layout.gemtext_text, parent, false) ) + } + } + + override fun getItemViewType(position: Int): Int { + val line = lines[position] + return when { + line.startsWith("###") -> typeH3 + line.startsWith("##") -> typeH2 + line.startsWith("#") -> typeH1 + line.startsWith("*") -> typeListItem + line.startsWith("=>") -> typeLink + else -> typeText + } + } + + override fun getItemCount(): Int = lines.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val line = lines[position] + + when(holder){ + is ViewHolder.Text -> holder.itemView.gemtext_text_textview.text = line + is ViewHolder.H1 -> holder.itemView.gemtext_text_textview.text = line.drop(2).trim() + is ViewHolder.H2 -> holder.itemView.gemtext_text_textview.text = line.drop(3).trim() + is ViewHolder.H3 -> holder.itemView.gemtext_text_textview.text = line.drop(4).trim() + is ViewHolder.ListItem -> holder.itemView.gemtext_text_textview.text = "• ${line.drop(1)}".trim() + is ViewHolder.Link -> { + println("Tva: link: $line") + val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2) + var linkName = linkParts[0] + if(linkParts.size > 1) { + linkName = linkParts[1] + } + val displayText = linkName + holder.itemView.gemtext_text_textview.text = displayText + holder.itemView.gemtext_text_textview.paint.isUnderlineText = true + holder.itemView.gemtext_link.setOnClickListener { + onLink(getUri(lines[holder.adapterPosition])) + } + } + } + } + + private fun getUri(linkLine: String): URI{ + val linkParts = linkLine.substring(2).trim().split("\\s+".toRegex(), 2) + return URI.create(linkParts.first()) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/vector_app_icon.xml b/app/src/main/res/drawable/vector_app_icon.xml new file mode 100644 index 0000000..6cdd984 --- /dev/null +++ b/app/src/main/res/drawable/vector_app_icon.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/vector_home.xml b/app/src/main/res/drawable/vector_home.xml new file mode 100644 index 0000000..3a4c7da --- /dev/null +++ b/app/src/main/res/drawable/vector_home.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/vector_overflow.xml b/app/src/main/res/drawable/vector_overflow.xml new file mode 100644 index 0000000..34b93ec --- /dev/null +++ b/app/src/main/res/drawable/vector_overflow.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/vector_tab.xml b/app/src/main/res/drawable/vector_tab.xml new file mode 100644 index 0000000..d8a1ee7 --- /dev/null +++ b/app/src/main/res/drawable/vector_tab.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_tva.xml b/app/src/main/res/layout/activity_tva.xml new file mode 100644 index 0000000..adbc129 --- /dev/null +++ b/app/src/main/res/layout/activity_tva.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_h1.xml b/app/src/main/res/layout/gemtext_h1.xml new file mode 100644 index 0000000..7b6ac2d --- /dev/null +++ b/app/src/main/res/layout/gemtext_h1.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_h2.xml b/app/src/main/res/layout/gemtext_h2.xml new file mode 100644 index 0000000..8d7a62b --- /dev/null +++ b/app/src/main/res/layout/gemtext_h2.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_h3.xml b/app/src/main/res/layout/gemtext_h3.xml new file mode 100644 index 0000000..989f1ff --- /dev/null +++ b/app/src/main/res/layout/gemtext_h3.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_link.xml b/app/src/main/res/layout/gemtext_link.xml new file mode 100644 index 0000000..6855c60 --- /dev/null +++ b/app/src/main/res/layout/gemtext_link.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gemtext_text.xml b/app/src/main/res/layout/gemtext_text.xml new file mode 100644 index 0000000..dffb12e --- /dev/null +++ b/app/src/main/res/layout/gemtext_text.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..4faecfa --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #6200EE + #3700B3 + #03DAC5 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..053cc13 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + 8dp + 12dp + 16sp + 28sp + 22sp + 18sp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5993e9f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Två + gemini:// + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..6c2e593 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + +