Compare commits
77 Commits
Author | SHA1 | Date |
---|---|---|
Corewala | 326c20f5ff | |
Corewala | 867688a075 | |
Corewala | bedbc9fb98 | |
Corewala | df6c159c2c | |
Corewala | 93ef22a4f8 | |
Corewala | 0a47f391e5 | |
Corewala | 73afc7c684 | |
Corewala | 0fdffee966 | |
Corewala | da4375b832 | |
Corewala | 9f4d1c8027 | |
Corewala | 0df1501fd4 | |
Ariel Costas | 2bd546d7ed | |
Corewala | 2c3c1db96b | |
strooonger | 9038f49d40 | |
Corewala | eac1adb0cb | |
Corewala | eee109bb28 | |
Corewala | f89f41ae14 | |
Corewala | 26093144dd | |
Corewala | 47dd2722e1 | |
Corewala | b85de17c88 | |
Corewala | 53c1980fa6 | |
Corewala | 0ae42a214d | |
Corewala | 319b0b4d14 | |
Corewala | 13f21bc09b | |
Corewala | 79a3564569 | |
Corewala | 4a451ef5ca | |
Corewala | 467e3fc0b7 | |
Corewala | affa99e8f2 | |
Corewala | 4c94bd97e4 | |
Corewala | 679c3bd0be | |
Corewala | aa6dcdad91 | |
Corewala | 9d4386939a | |
Corewala | 001e7d3ffe | |
Corewala | 882617df29 | |
Corewala | ba901e49ba | |
Corewala | cf8efbc625 | |
Corewala | 09fc2a480a | |
Corewala | a47d003f59 | |
Corewala | f79d0bf5ff | |
Licaon_Kter | 011eb7487c | |
Corewala | bae12d17a8 | |
Corewala | f98756bebb | |
Corewala | e4cc13bb03 | |
Corewala | 3dee099dcf | |
Corewala | 132749016a | |
Corewala | a791521565 | |
Corewala | 8258c48bb0 | |
Corewala | c12702ef2f | |
Corewala | 28fa72f59e | |
Corewala | 13af2089d8 | |
Corewala | 2c24fa0c77 | |
Corewala | 0a59ac08cb | |
Corewala | c23b3efb70 | |
Corewala | 6d4dbbda94 | |
Corewala | 95ed0b815b | |
Corewala | 125c34c6d0 | |
Corewala | 1c3905b197 | |
Corewala | ee8ff30210 | |
Corewala | 45b50afd47 | |
Corewala | dd95af5e5b | |
Corewala | fbdb04bf31 | |
Corewala | 98d9af2871 | |
Corewala | 79a0d32bf1 | |
Corewala | 5ccdaeb816 | |
Corewala | f2d633e7e2 | |
Corewala | 05e2f64ebd | |
Corewala | 2db5aa74ca | |
Corewala | 7828f3ea7f | |
Corewala | bd8bbbc903 | |
Corewala | 8f8bb15455 | |
Corewala | 918deb4cdf | |
Corewala | cdcbdcdb2c | |
Corewala | 35352fa6f3 | |
Corewala | 0505e5d5bf | |
Corewala | 764cee3042 | |
Corewala | c0be77ef41 | |
Corewala | 60673c2fb3 |
|
@ -2,3 +2,5 @@
|
|||
.idea
|
||||
build
|
||||
release
|
||||
local.properties
|
||||
app/local.properties
|
14
README.md
|
@ -8,6 +8,12 @@
|
|||
[![shields](https://img.shields.io/badge/Download-Here-orange?style=for-the-badge)](https://github.com/Corewala/Buran/releases/latest)
|
||||
[![shields](https://img.shields.io/badge/license-GPL-blue?style=for-the-badge)](https://github.com/Corewala/Buran/blob/master/LICENSE)
|
||||
|
||||
<a href="https://f-droid.org/packages/corewala.gemini.buran">
|
||||
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">
|
||||
</a>
|
||||
|
||||
Buran is a simple Gemini protocol browser for Android.
|
||||
|
||||
### Todo list
|
||||
|
@ -21,11 +27,9 @@ Buran is a simple Gemini protocol browser for Android.
|
|||
- [ ] Page navigation feature
|
||||
- [X] Update notifier
|
||||
- [X] Attention guide mode
|
||||
|
||||
|
||||
## Statement about Ariane
|
||||
|
||||
> I've seen some pretty cringe discussion in geminispace complaining about the decision by Öppen to close-source the Ariane browser, sometimes referencing this project as a more "stable" or "principled" project. Just to be 100% clear, I think that the license that Öppen used for Ariane 4 was actually much better than the one that Ariane 3 used, and I would have made the same decision if the EUPL was compatible with copyfarleft. Although I wish Ariane could continue to grow as an open-source project, I respect his decision to make Seren closed source. I only made this fork out of a legitimate desire to keep some fragment of this project public and an interest in learning Kotlin. Even though this fork exists, you should still consider buying a copy of Seren. He's definitely a better developer than me, and I don't want this project to be used as a rhetorical tool against him.
|
||||
- [ ] Simple A/B page switching system
|
||||
- [ ] Pass [Egsam test](https://github.com/pitr/egsam)
|
||||
- [X] Option to define an HTTPS gateway
|
||||
|
||||
## Credits
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ android {
|
|||
applicationId "corewala.gemini.buran"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 10
|
||||
versionName "v1.9"
|
||||
versionCode 13
|
||||
versionName "v1.12"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
sdk.dir=/home/vagrant/android-sdk
|
||||
sdk-location=/home/vagrant/android-sdk
|
||||
ndk.dir=/home/vagrant/android-ndk/r12b
|
||||
ndk-location=/home/vagrant/android-ndk/r12b
|
|
@ -5,7 +5,7 @@ import android.app.Application
|
|||
class Buran: Application() {
|
||||
|
||||
companion object{
|
||||
const val DEFAULT_HOME_CAPSULE = "gemini://tlgs.one"
|
||||
const val DEFAULT_HOME_CAPSULE = ""
|
||||
const val DEFAULT_SEARCH_BASE = "gemini://tlgs.one/search?"
|
||||
|
||||
const val PREF_KEY_CLIENT_CERT_URI = "client_cert_uri"
|
||||
|
|
|
@ -16,6 +16,10 @@ class OmniTerm(private val listener: Listener) {
|
|||
*/
|
||||
fun input(term: String, searchbase: String?){
|
||||
when {
|
||||
term.contains(" ") -> {
|
||||
val encoded = Uri.encode(term)
|
||||
listener.request("$searchbase$encoded")
|
||||
}
|
||||
term.startsWith(GEM_SCHEME) && term != GEM_SCHEME -> {
|
||||
listener.request(term)
|
||||
return
|
||||
|
@ -52,14 +56,25 @@ class OmniTerm(private val listener: Listener) {
|
|||
when {
|
||||
link.startsWith(GEM_SCHEME) -> uri.set(link)
|
||||
link.startsWith("//") -> uri.set("gemini:$link")
|
||||
link.startsWith("http://") or link.startsWith("https://") -> {
|
||||
uri.set(link)
|
||||
}
|
||||
link.contains(":") -> listener.openExternal(link)
|
||||
else -> uri.resolve(link)
|
||||
}
|
||||
|
||||
val address = uri.toString().replace("//", "/").replace("gemini:/", "gemini://")
|
||||
println("OmniTerm resolved address: $address")
|
||||
val address = uri.toString().replace("//", "/").replace(":/", "://")
|
||||
|
||||
if(invokeListener) listener.request(address)
|
||||
println("OmniTerm resolved address: $address")
|
||||
}
|
||||
|
||||
fun getGlobalUri(reference: String): String {
|
||||
return when {
|
||||
reference.contains(":") -> reference
|
||||
reference.startsWith("//") -> "gemini:$reference"
|
||||
else -> uri.resolve(reference, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun reset(){
|
||||
|
@ -75,7 +90,11 @@ class OmniTerm(private val listener: Listener) {
|
|||
}
|
||||
|
||||
fun getCurrent(): String {
|
||||
return history.last().toString()
|
||||
return if(history.size > 0){
|
||||
history.last().toString()
|
||||
}else{
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun canGoBack(): Boolean {
|
||||
|
@ -87,6 +106,10 @@ class OmniTerm(private val listener: Listener) {
|
|||
return history.last().toString()
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
interface Listener{
|
||||
fun request(address: String)
|
||||
fun openExternal(address: String)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package corewala.buran
|
||||
|
||||
const val SCHEME = "gemini://"
|
||||
import corewala.toURI
|
||||
|
||||
const val GEMSCHEME = "gemini://"
|
||||
const val TRAVERSE = "../"
|
||||
const val SOLIDUS = "/"
|
||||
const val DIREND = "/"
|
||||
const val QUERY = "?"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -15,69 +18,65 @@ class OppenURI constructor(private var ouri: String) {
|
|||
constructor(): this("")
|
||||
|
||||
var host: String = ""
|
||||
var scheme: String = ""
|
||||
|
||||
init {
|
||||
extractHost()
|
||||
if(ouri.isNotEmpty()){
|
||||
host = ouri.toURI().host
|
||||
scheme = ouri.toURI().scheme
|
||||
}
|
||||
}
|
||||
|
||||
fun set(ouri: String){
|
||||
this.ouri = ouri
|
||||
extractHost()
|
||||
if(ouri.isNotEmpty()){
|
||||
host = ouri.toURI().host
|
||||
scheme = ouri.toURI().scheme
|
||||
}
|
||||
}
|
||||
|
||||
fun resolve(reference: String): String{
|
||||
if(ouri == "$SCHEME$host") ouri = "$ouri/"
|
||||
fun resolve(reference: String, persistent: Boolean): String{
|
||||
if(ouri == "$GEMSCHEME$host") ouri = "$ouri/"
|
||||
var resolvedUri = ""
|
||||
when {
|
||||
reference.startsWith(SCHEME) -> set(reference)
|
||||
reference.startsWith(SOLIDUS) -> ouri = "$SCHEME$host$reference"
|
||||
reference.startsWith(GEMSCHEME) -> set(reference)
|
||||
reference.startsWith(SOLIDUS) -> resolvedUri = "$scheme://$host$reference"
|
||||
reference.startsWith(TRAVERSE) -> {
|
||||
if(!ouri.endsWith(DIREND)) ouri = ouri.removeFile()
|
||||
if(!ouri.endsWith(DIREND)) resolvedUri = ouri.removeFile()
|
||||
val traversalCount = reference.split(TRAVERSE).size - 1
|
||||
ouri = traverse(traversalCount) + reference.replace(TRAVERSE, "")
|
||||
resolvedUri = traverse(traversalCount) + reference.replace(TRAVERSE, "")
|
||||
}
|
||||
reference.startsWith(QUERY) -> {
|
||||
resolvedUri = if(reference.contains(QUERY)){
|
||||
ouri.substringBefore(QUERY) + reference
|
||||
}else{
|
||||
ouri + reference
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
ouri = when {
|
||||
ouri.endsWith(DIREND) -> "${ouri}$reference"
|
||||
resolvedUri = when {
|
||||
ouri.endsWith(DIREND) -> {
|
||||
"${ouri}$reference"
|
||||
}
|
||||
else -> "${ouri.substring(0, ouri.lastIndexOf("/"))}/$reference"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ouri
|
||||
if(persistent){
|
||||
ouri = resolvedUri
|
||||
}
|
||||
return resolvedUri
|
||||
}
|
||||
|
||||
fun traverse(): OppenURI{
|
||||
val path = ouri.removePrefix("$SCHEME$host")
|
||||
val segments = path.split(SOLIDUS).filter { it.isNotEmpty() }
|
||||
|
||||
var nouri = "$SCHEME$host"
|
||||
|
||||
when (ouri) {
|
||||
"" -> {
|
||||
}
|
||||
SCHEME -> ouri = ""
|
||||
"$nouri/" -> ouri = SCHEME
|
||||
else -> {
|
||||
when {
|
||||
segments.isNotEmpty() -> {
|
||||
val remaining = segments.dropLast(1)
|
||||
remaining.forEach { segment ->
|
||||
nouri += "/$segment"
|
||||
}
|
||||
ouri = "$nouri/"
|
||||
}
|
||||
else -> ouri = "$nouri/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
fun resolve(reference: String): String{
|
||||
return resolve(reference, true)
|
||||
}
|
||||
|
||||
private fun traverse(count: Int): String{
|
||||
val path = ouri.removePrefix("$SCHEME$host")
|
||||
val path = ouri.removePrefix("$GEMSCHEME$host")
|
||||
val segments = path.split(SOLIDUS).filter { it.isNotEmpty() }
|
||||
val segmentCount = segments.size
|
||||
var nouri = "$SCHEME$host"
|
||||
var nouri = "$GEMSCHEME$host"
|
||||
|
||||
segments.forEachIndexed{ index, segment ->
|
||||
if(index < segmentCount - count){
|
||||
|
@ -89,15 +88,6 @@ class OppenURI constructor(private var ouri: String) {
|
|||
|
||||
}
|
||||
|
||||
private fun extractHost(){
|
||||
if(ouri.isEmpty()) return
|
||||
val urn = ouri.removePrefix(SCHEME)
|
||||
host = when {
|
||||
urn.contains(SOLIDUS) -> urn.substring(0, urn.indexOf(SOLIDUS))
|
||||
else -> urn
|
||||
}
|
||||
}
|
||||
|
||||
fun copy(): OppenURI = OppenURI(ouri)
|
||||
|
||||
override fun toString(): String = ouri
|
||||
|
|
|
@ -10,7 +10,7 @@ sealed class GemState {
|
|||
data class NotGeminiRequest(val uri: URI) : GemState()
|
||||
data class ResponseGemtext(val uri: URI, val header: GeminiResponse.Header, val lines: List<String>) : GemState()
|
||||
data class ResponseInput(val uri: URI, val header: GeminiResponse.Header) : GemState()
|
||||
class Redirect(val uri: String) : GemState()
|
||||
data class Redirect(val uri: URI, val header: GeminiResponse.Header) : GemState()
|
||||
data class ResponseText(val uri: URI, val header: GeminiResponse.Header, val content: String) : GemState()
|
||||
data class ResponseImage(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
|
||||
data class ResponseBinary(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
|
||||
|
|
|
@ -6,9 +6,10 @@ import corewala.buran.io.database.history.BuranHistory
|
|||
import java.net.URI
|
||||
|
||||
interface Datasource {
|
||||
fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, onUpdate: (state: GemState) -> Unit)
|
||||
fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, alternativeRequest: String?, onUpdate: (state: GemState) -> Unit)
|
||||
fun isRequesting(): Boolean
|
||||
fun cancel()
|
||||
fun canGoBack(): Boolean
|
||||
|
||||
companion object{
|
||||
fun factory(context: Context, history: BuranHistory): Datasource {
|
||||
return GeminiDatasource(context, history)
|
||||
|
|
|
@ -8,9 +8,9 @@ import corewala.buran.OppenURI
|
|||
import corewala.buran.io.GemState
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
import corewala.buran.io.keymanager.BuranKeyManager
|
||||
import corewala.toURI
|
||||
import corewala.toUri
|
||||
import java.io.*
|
||||
import java.lang.IllegalStateException
|
||||
import java.net.ConnectException
|
||||
import java.net.URI
|
||||
import java.net.UnknownHostException
|
||||
|
@ -30,7 +30,9 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
|
||||
private var socketFactory: SSLSocketFactory? = null
|
||||
|
||||
override fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, onUpdate: (state: GemState) -> Unit) {
|
||||
private var currentRequestAddress: String? = null
|
||||
|
||||
override fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, alternativeRequest: String?, onUpdate: (state: GemState) -> Unit){
|
||||
this.forceDownload = forceDownload
|
||||
|
||||
this.onUpdate = onUpdate
|
||||
|
@ -39,9 +41,21 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
|
||||
onUpdate(GemState.Requesting(uri))
|
||||
|
||||
GlobalScope.launch {
|
||||
geminiRequest(uri, onUpdate, clientCertPassword)
|
||||
if(address.startsWith("gemini://")){
|
||||
currentRequestAddress = address
|
||||
}
|
||||
|
||||
GlobalScope.launch {
|
||||
geminiRequest(uri, onUpdate, clientCertPassword, alternativeRequest)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isRequesting(): Boolean{
|
||||
return !currentRequestAddress.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun cancel(){
|
||||
currentRequestAddress = null
|
||||
}
|
||||
|
||||
private fun initSSLFactory(protocol: String, clientCertPassword: String?){
|
||||
|
@ -54,28 +68,40 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
|
||||
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit, clientCertPassword: String?){
|
||||
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit, clientCertPassword: String?, alternativeRequest: String?){
|
||||
val protocol = "TLS"
|
||||
|
||||
initSSLFactory(protocol, clientCertPassword)
|
||||
|
||||
val port = if(uri.port != -1){
|
||||
uri.port
|
||||
}else{
|
||||
1965
|
||||
}
|
||||
|
||||
val socket: SSLSocket?
|
||||
try {
|
||||
socket = socketFactory?.createSocket(uri.host, 1965) as SSLSocket
|
||||
socket = socketFactory?.createSocket(uri.host, port) as SSLSocket
|
||||
|
||||
println("Buran socket handshake with ${uri.host}")
|
||||
socket.startHandshake()
|
||||
}catch (uhe: UnknownHostException){
|
||||
if(currentRequestAddress == uri.toString()) {
|
||||
println("Buran socket error, unknown host: $uhe")
|
||||
onUpdate(GemState.ResponseUnknownHost(uri))
|
||||
}
|
||||
return
|
||||
}catch (ce: ConnectException){
|
||||
if(currentRequestAddress == uri.toString()) {
|
||||
println("Buran socket error, connect exception: $ce")
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
|
||||
}
|
||||
return
|
||||
}catch (she: SSLHandshakeException){
|
||||
if(currentRequestAddress == uri.toString()) {
|
||||
println("Buran socket error, ssl handshake exception: $she")
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -84,7 +110,12 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
val bufferedWriter = BufferedWriter(outputStreamWriter)
|
||||
val outWriter = PrintWriter(bufferedWriter)
|
||||
|
||||
val requestEntity = uri.toString() + "\r\n"
|
||||
val requestEntity = if(alternativeRequest.isNullOrEmpty()){
|
||||
uri.toString()
|
||||
}else{
|
||||
alternativeRequest
|
||||
} + "\r\n"
|
||||
|
||||
println("Buran socket requesting $requestEntity")
|
||||
outWriter.print(requestEntity)
|
||||
outWriter.flush()
|
||||
|
@ -102,21 +133,25 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
val bufferedReader = BufferedReader(headerInputReader)
|
||||
val headerLine = bufferedReader.readLine()
|
||||
|
||||
println("Buran: response header: $headerLine")
|
||||
println("Buran response header: $headerLine")
|
||||
|
||||
if(headerLine == null){
|
||||
if(currentRequestAddress == uri.toString()){
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, "Server did not respond with a Gemini header: $uri")))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val header = GeminiResponse.parseHeader(headerLine)
|
||||
|
||||
if(currentRequestAddress == uri.toString()){
|
||||
currentRequestAddress = null
|
||||
when {
|
||||
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
|
||||
header.code == GeminiResponse.REDIRECT -> onUpdate(GemState.Redirect(resolve(uri.host, header.meta)))
|
||||
header.code == GeminiResponse.REDIRECT -> onUpdate(GemState.Redirect(uri, header))
|
||||
header.code == GeminiResponse.CLIENT_CERTIFICATE_REQUIRED -> onUpdate(GemState.ClientCertRequired(uri, header))
|
||||
header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header))
|
||||
header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, uri, header, onUpdate)
|
||||
header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, requestEntity.trim().toURI(), header, onUpdate)
|
||||
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
|
||||
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
|
||||
else -> {
|
||||
|
@ -128,6 +163,9 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
println("Buran dropped response from $uri: request cancelled or superseded")
|
||||
}
|
||||
|
||||
//Close input
|
||||
bufferedReader.close()
|
||||
|
@ -149,10 +187,6 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
|
||||
val processed = GemtextHelper.findCodeBlocks(lines)
|
||||
|
||||
when {
|
||||
!uri.toString().startsWith("gemini://") -> throw IllegalStateException("Not a Gemini Uri")
|
||||
}
|
||||
|
||||
updateHistory(uri)
|
||||
onUpdate(GemState.ResponseGemtext(uri, header, processed))
|
||||
}
|
||||
|
@ -209,12 +243,6 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
|
|||
}
|
||||
}
|
||||
|
||||
private fun resolve(host: String, address: String): String{
|
||||
val ouri = OppenURI()
|
||||
ouri.set(host)
|
||||
return ouri.resolve(address)
|
||||
}
|
||||
|
||||
override fun canGoBack(): Boolean = runtimeHistory.isEmpty() || runtimeHistory.size > 1
|
||||
|
||||
//This just forces the factory to rebuild before the next request
|
||||
|
|
|
@ -7,7 +7,9 @@ import android.content.SharedPreferences
|
|||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
|
@ -44,7 +46,6 @@ import corewala.buran.ui.bookmarks.BookmarksDialog
|
|||
import corewala.buran.ui.content_image.ImageDialog
|
||||
import corewala.buran.ui.content_text.TextDialog
|
||||
import corewala.buran.ui.gemtext_adapter.AbstractGemtextAdapter
|
||||
import corewala.buran.ui.modals_menus.about.AboutDialog
|
||||
import corewala.buran.ui.modals_menus.history.HistoryDialog
|
||||
import corewala.buran.ui.modals_menus.overflow.OverflowPopup
|
||||
import corewala.buran.ui.settings.SettingsActivity
|
||||
|
@ -64,6 +65,7 @@ class GemActivity : AppCompatActivity() {
|
|||
lateinit var prefs: SharedPreferences
|
||||
private var inSearch = false
|
||||
private lateinit var bookmarkDatasource: BookmarksDatasource
|
||||
private lateinit var db: BuranDatabase
|
||||
private var bookmarksDialog: BookmarksDialog? = null
|
||||
|
||||
private val model by viewModels<GemViewModel>()
|
||||
|
@ -79,19 +81,23 @@ class GemActivity : AppCompatActivity() {
|
|||
|
||||
private var certPassword: String? = null
|
||||
|
||||
private var internetStatus: Boolean = false
|
||||
private var proxiedAddress: String? = null
|
||||
|
||||
private var previousPosition: Int = 0
|
||||
|
||||
private var initialised: Boolean = false
|
||||
|
||||
lateinit var adapter: AbstractGemtextAdapter
|
||||
private var goingBack: Boolean = false
|
||||
|
||||
private lateinit var adapter: AbstractGemtextAdapter
|
||||
|
||||
private lateinit var home: String
|
||||
|
||||
private lateinit var searchBase: String
|
||||
|
||||
private val onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit = { uri, longTap, _: Int ->
|
||||
if(longTap){
|
||||
val globalURI = if(!uri.toString().contains("//") and !uri.toString().contains(":")){
|
||||
(omniTerm.getCurrent() + uri.toString()).replace("//", "/").replace("gemini:/", "gemini://")
|
||||
} else {
|
||||
uri.toString()
|
||||
}
|
||||
val globalURI = omniTerm.getGlobalUri(uri.toString())
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, globalURI)
|
||||
|
@ -104,6 +110,7 @@ class GemActivity : AppCompatActivity() {
|
|||
binding.addressEdit.hint = getString(R.string.main_input_hint)
|
||||
inSearch = false
|
||||
}
|
||||
|
||||
omniTerm.navigation(uri.toString())
|
||||
}
|
||||
}
|
||||
|
@ -136,9 +143,6 @@ class GemActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val db = BuranDatabase(applicationContext)
|
||||
bookmarkDatasource = db.bookmarks()
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_gem)
|
||||
binding.viewmodel = model
|
||||
binding.lifecycleOwner = this
|
||||
|
@ -159,47 +163,35 @@ class GemActivity : AppCompatActivity() {
|
|||
|
||||
binding.gemtextRecycler.adapter = adapter
|
||||
|
||||
internetStatus = getInternetStatus()
|
||||
|
||||
if(internetStatus){
|
||||
if(intent.data == null){
|
||||
model.initialise(
|
||||
home = prefs.getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
) ?: Buran.DEFAULT_HOME_CAPSULE,
|
||||
gemini = Datasource.factory(this, db.history()),
|
||||
db = db,
|
||||
onState = this::handleState
|
||||
)
|
||||
}else{
|
||||
model.initialise(
|
||||
home = intent.data.toString(),
|
||||
gemini = Datasource.factory(this, db.history()),
|
||||
db = db,
|
||||
onState = this::handleState
|
||||
)
|
||||
) ?: Buran.DEFAULT_HOME_CAPSULE
|
||||
|
||||
if(
|
||||
!home.startsWith("gemini://")
|
||||
or home.contains(" ")
|
||||
or !home.contains(".")
|
||||
){
|
||||
home = ""
|
||||
}
|
||||
|
||||
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
|
||||
"check_for_updates",
|
||||
false
|
||||
)){
|
||||
val updates = BuranUpdates()
|
||||
val latestVersion = updates.getLatestVersion()
|
||||
searchBase = prefs.getString(
|
||||
"search_base",
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
) ?: Buran.DEFAULT_SEARCH_BASE
|
||||
|
||||
if (latestVersion == BuildConfig.VERSION_NAME){
|
||||
println("No new version available")
|
||||
} else {
|
||||
println("New version available")
|
||||
|
||||
Snackbar.make(binding.root, getString(R.string.new_version_available), Snackbar.LENGTH_LONG).setAction(getString(R.string.update)) {
|
||||
updates.installUpdate(this, latestVersion)
|
||||
}.show()
|
||||
}
|
||||
if(
|
||||
!searchBase.startsWith("gemini://")
|
||||
or searchBase.contains(" ")
|
||||
or !searchBase.contains(".")
|
||||
or !searchBase.endsWith("?")
|
||||
){
|
||||
searchBase = Buran.DEFAULT_SEARCH_BASE
|
||||
}
|
||||
|
||||
initialised = true
|
||||
if(getInternetStatus()){
|
||||
initialise()
|
||||
}else{
|
||||
loadingView(false)
|
||||
val home = PreferenceManager.getDefaultSharedPreferences(this).getString(
|
||||
|
@ -207,20 +199,16 @@ class GemActivity : AppCompatActivity() {
|
|||
Buran.DEFAULT_HOME_CAPSULE
|
||||
)
|
||||
val title = "# ${this.getString(R.string.no_internet)}"
|
||||
val link = "=> $home ${this.getString(R.string.retry)}"
|
||||
val text = this.getString(R.string.retry)
|
||||
omniTerm.set(home!!)
|
||||
adapter.render(listOf(title, link))
|
||||
adapter.render(listOf(title, text))
|
||||
binding.addressEdit.inputType = InputType.TYPE_NULL
|
||||
}
|
||||
|
||||
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO -> {
|
||||
val searchbase = prefs.getString(
|
||||
"search_base",
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
)
|
||||
omniTerm.input(binding.addressEdit.text.toString().trim(), searchbase)
|
||||
omniTerm.input(binding.addressEdit.text.toString().trim(), searchBase)
|
||||
binding.addressEdit.clearFocus()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
|
@ -303,11 +291,12 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
R.id.overflow_menu_history -> HistoryDialog.show(
|
||||
this,
|
||||
db.history()
|
||||
db.history(),
|
||||
omniTerm
|
||||
) { historyAddress ->
|
||||
gemRequest(historyAddress)
|
||||
}
|
||||
R.id.overflow_menu_about -> AboutDialog.show(this)
|
||||
R.id.overflow_menu_about -> gemRequest("")
|
||||
R.id.overflow_menu_settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
@ -326,12 +315,8 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
binding.home.setOnClickListener {
|
||||
val home = PreferenceManager.getDefaultSharedPreferences(this).getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
)
|
||||
omniTerm.history.clear()
|
||||
gemRequest(home!!, false)
|
||||
gemRequest(home, false)
|
||||
}
|
||||
|
||||
binding.pullToRefresh.setOnRefreshListener {
|
||||
|
@ -357,6 +342,33 @@ class GemActivity : AppCompatActivity() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
home = prefs.getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
) ?: Buran.DEFAULT_HOME_CAPSULE
|
||||
|
||||
if(
|
||||
!home.startsWith("gemini://")
|
||||
or home.contains(" ")
|
||||
or !home.contains(".")
|
||||
){
|
||||
home = ""
|
||||
}
|
||||
|
||||
searchBase = prefs.getString(
|
||||
"search_base",
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
) ?: Buran.DEFAULT_SEARCH_BASE
|
||||
|
||||
if(
|
||||
!searchBase.startsWith("gemini://")
|
||||
or searchBase.contains(" ")
|
||||
or !searchBase.contains(".")
|
||||
or !searchBase.endsWith("?")
|
||||
){
|
||||
searchBase = Buran.DEFAULT_SEARCH_BASE
|
||||
}
|
||||
|
||||
when {
|
||||
prefs.contains("background_colour") -> {
|
||||
when (val backgroundColor = prefs.getString("background_colour", "#XXXXXX")) {
|
||||
|
@ -391,7 +403,7 @@ class GemActivity : AppCompatActivity() {
|
|||
)
|
||||
adapter.inlineImages(showInlineImages)
|
||||
|
||||
if(internetStatus){
|
||||
if(getInternetStatus()){
|
||||
model.invalidateDatasource()
|
||||
}
|
||||
}
|
||||
|
@ -446,7 +458,10 @@ class GemActivity : AppCompatActivity() {
|
|||
.show()
|
||||
}
|
||||
|
||||
is GemState.Redirect -> gemRequest(state.uri)
|
||||
is GemState.Redirect -> {
|
||||
omniTerm.set(state.uri.toString())
|
||||
gemRequest(omniTerm.getGlobalUri(state.header.meta))
|
||||
}
|
||||
|
||||
is GemState.ClientCertRequired -> runOnUiThread {
|
||||
loadingView(false)
|
||||
|
@ -480,8 +495,12 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
is GemState.Requesting -> loadingView(true)
|
||||
is GemState.NotGeminiRequest -> externalProtocol(state)
|
||||
is GemState.Requesting -> {
|
||||
loadingView(true)
|
||||
}
|
||||
is GemState.NotGeminiRequest -> {
|
||||
externalProtocol(state.uri)
|
||||
}
|
||||
is GemState.ResponseError -> {
|
||||
omniTerm.reset()
|
||||
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
|
||||
|
@ -491,7 +510,14 @@ class GemActivity : AppCompatActivity() {
|
|||
updateClientCertIcon()
|
||||
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
|
||||
}
|
||||
is GemState.ResponseGemtext -> renderGemtext(state)
|
||||
is GemState.ResponseGemtext -> {
|
||||
if(state.uri.scheme != "gemini"){
|
||||
Snackbar.make(binding.root, getString(R.string.proxied_content), Snackbar.LENGTH_LONG).setAction(getString(R.string.open_original)) {
|
||||
externalProtocol(state.uri)
|
||||
}.show()
|
||||
}
|
||||
renderGemtext(state)
|
||||
}
|
||||
is GemState.ResponseText -> renderText(state)
|
||||
is GemState.ResponseImage -> renderImage(state)
|
||||
is GemState.ResponseBinary -> renderBinary(state)
|
||||
|
@ -516,7 +542,7 @@ class GemActivity : AppCompatActivity() {
|
|||
.setMessage("${state.uri}")
|
||||
.setPositiveButton(getString(R.string.download).toUpperCase()) { _, _ ->
|
||||
loadingView(true)
|
||||
model.requestBinaryDownload(state.uri, clientCertPassword)
|
||||
model.requestBinaryDownload(state.uri, clientCertPassword, null)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> }
|
||||
.show()
|
||||
|
@ -526,18 +552,15 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
is GemState.ResponseUnknownHost -> {
|
||||
omniTerm.reset()
|
||||
runOnUiThread {
|
||||
val searchbase = prefs.getString(
|
||||
"search_base",
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
)
|
||||
loadingView(false)
|
||||
AlertDialog.Builder(this, R.style.AppDialogTheme)
|
||||
.setTitle(getString(R.string.unknown_host))
|
||||
.setMessage("${getString(R.string.unknown_host)}: ${state.uri}\n\n${getString(R.string.search_instead)}")
|
||||
.setPositiveButton(getString(R.string.search).toUpperCase()) { _, _ ->
|
||||
loadingView(true)
|
||||
omniTerm.search(state.uri.toString(), searchbase)
|
||||
omniTerm.search(state.uri.toString(), searchBase)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> }
|
||||
.show()
|
||||
|
@ -626,15 +649,15 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun externalProtocol(state: GemState.NotGeminiRequest) = runOnUiThread {
|
||||
private fun externalProtocol(uri: URI) = runOnUiThread {
|
||||
loadingView(false)
|
||||
val uri = state.uri.toString()
|
||||
val uri = uri.toString()
|
||||
|
||||
when {
|
||||
(uri.startsWith("http://") || uri.startsWith("https://")) -> openExternalLink(uri)
|
||||
else -> {
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW)
|
||||
viewIntent.data = Uri.parse(state.uri.toString())
|
||||
viewIntent.data = Uri.parse(uri.toString())
|
||||
|
||||
try {
|
||||
startActivity(viewIntent)
|
||||
|
@ -642,7 +665,7 @@ class GemActivity : AppCompatActivity() {
|
|||
showAlert(
|
||||
String.format(
|
||||
getString(R.string.no_app_installed_that_can_open),
|
||||
state.uri
|
||||
uri
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -671,7 +694,6 @@ class GemActivity : AppCompatActivity() {
|
|||
}else{
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW)
|
||||
viewIntent.data = Uri.parse(address)
|
||||
|
||||
startActivity(viewIntent)
|
||||
}
|
||||
}
|
||||
|
@ -679,19 +701,30 @@ class GemActivity : AppCompatActivity() {
|
|||
private fun renderGemtext(state: GemState.ResponseGemtext) = runOnUiThread {
|
||||
loadingView(false)
|
||||
|
||||
omniTerm.set(state.uri.toString())
|
||||
omniTerm.set(proxiedAddress ?: state.uri.toString())
|
||||
|
||||
//todo - colours didn't change when switching themes, so disabled for now
|
||||
//val addressSpan = SpannableString(state.uri.toString())
|
||||
//addressSpan.set(0, 9, ForegroundColorSpan(resources.getColor(R.color.protocol_address)))
|
||||
binding.addressEdit.setText(state.uri.toString())
|
||||
|
||||
if(!goingBack){
|
||||
previousPosition = (binding.gemtextRecycler.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
}
|
||||
|
||||
adapter.render(state.lines)
|
||||
|
||||
//Scroll to top
|
||||
//Scroll to correct position
|
||||
if(goingBack){
|
||||
println("Returning to previous position: $previousPosition")
|
||||
binding.gemtextRecycler.scrollToPosition(previousPosition)
|
||||
previousPosition = 0
|
||||
goingBack = false
|
||||
}else{
|
||||
binding.gemtextRecycler.post {
|
||||
binding.gemtextRecycler.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
focusEnd()
|
||||
}
|
||||
|
@ -787,15 +820,91 @@ class GemActivity : AppCompatActivity() {
|
|||
|
||||
private fun getInternetStatus(): Boolean {
|
||||
val connectivityManager = this.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return if(capabilities != null){
|
||||
println("Internet access found")
|
||||
true
|
||||
}else{
|
||||
println("No internet access found")
|
||||
false
|
||||
if(capabilities != null){
|
||||
when {
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { return true }
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { return true }
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { return true }
|
||||
}
|
||||
}
|
||||
}else{
|
||||
val activeNetworkInfo = connectivityManager.activeNetworkInfo
|
||||
if(activeNetworkInfo != null && activeNetworkInfo.isConnected){
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun initialise(){
|
||||
db = BuranDatabase(applicationContext)
|
||||
bookmarkDatasource = db.bookmarks()
|
||||
|
||||
if(intent.data == null){
|
||||
model.initialise(
|
||||
home = home,
|
||||
gemini = Datasource.factory(this, db.history()),
|
||||
db = db,
|
||||
onState = this::handleState
|
||||
)
|
||||
if(home.isEmpty()){
|
||||
loadLocalHome()
|
||||
}
|
||||
}else{
|
||||
model.initialise(
|
||||
home = intent.data.toString(),
|
||||
gemini = Datasource.factory(this, db.history()),
|
||||
db = db,
|
||||
onState = this::handleState
|
||||
)
|
||||
}
|
||||
|
||||
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
|
||||
"check_for_updates",
|
||||
false
|
||||
)){
|
||||
val updates = BuranUpdates()
|
||||
val latestVersion = updates.getLatestVersion()
|
||||
|
||||
if (latestVersion == BuildConfig.VERSION_NAME){
|
||||
println("No new version available")
|
||||
} else {
|
||||
println("New version available")
|
||||
|
||||
Snackbar.make(binding.root, getString(R.string.new_version_available), Snackbar.LENGTH_LONG).setAction(getString(R.string.update)) {
|
||||
updates.installUpdate(this, latestVersion)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
initialised = true
|
||||
}
|
||||
|
||||
private fun loadLocalHome(){
|
||||
loadingView(false)
|
||||
binding.pullToRefresh.isRefreshing = false
|
||||
binding.addressEdit.text?.clear()
|
||||
|
||||
val title = "# ${getString(R.string.app_name)}"
|
||||
val sourceLink = "=> https://github.com/Corewala/Buran ${getString(R.string.source)}"
|
||||
adapter.render(listOf(
|
||||
title,
|
||||
"",
|
||||
getString(R.string.about_body),
|
||||
"",
|
||||
getString(R.string.about_ariane_source),
|
||||
"",
|
||||
getString(R.string.about_font),
|
||||
"",
|
||||
getString(R.string.about_glyphs),
|
||||
"",
|
||||
sourceLink,
|
||||
getString(R.string.copyright)
|
||||
))
|
||||
omniTerm.set("")
|
||||
}
|
||||
|
||||
private fun isHostSigned(uri: URI): Boolean{
|
||||
if((uri.host != omniTerm.getCurrent().toURI().host) && !certPassword.isNullOrEmpty()) {
|
||||
|
@ -812,15 +921,30 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
updateClientCertIcon()
|
||||
|
||||
if(address.startsWith("http://") or address.startsWith("https://")){
|
||||
val httpProxy = prefs.getString("http_proxy", null) ?: ""
|
||||
|
||||
if(
|
||||
httpProxy.isNullOrEmpty()
|
||||
or !httpProxy.startsWith("gemini://")
|
||||
or httpProxy.contains(" ")
|
||||
or !httpProxy.contains(".")
|
||||
){
|
||||
openExternalLink(address)
|
||||
}else{
|
||||
model.request(httpProxy, certPassword, address)
|
||||
}
|
||||
}
|
||||
|
||||
if(getInternetStatus()){
|
||||
if(initialised){
|
||||
loadingView(true)
|
||||
model.request(address, certPassword)
|
||||
if(address.isEmpty()){
|
||||
loadLocalHome()
|
||||
}else{
|
||||
val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
|
||||
intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
finish()
|
||||
startActivity(intent)
|
||||
model.request(address, certPassword, null)
|
||||
}
|
||||
}else{
|
||||
initialise()
|
||||
}
|
||||
}else{
|
||||
Snackbar.make(binding.root, getString(R.string.no_internet), Snackbar.LENGTH_LONG).show()
|
||||
|
@ -833,7 +957,11 @@ class GemActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (omniTerm.canGoBack()){
|
||||
if(model.isRequesting()){
|
||||
model.cancel()
|
||||
loadingView(false)
|
||||
}else if(omniTerm.canGoBack()){
|
||||
goingBack = true
|
||||
gemRequest(omniTerm.goBack())
|
||||
}else{
|
||||
println("Buran history is empty - exiting")
|
||||
|
|
|
@ -19,24 +19,34 @@ class GemViewModel: ViewModel() {
|
|||
this.db = db
|
||||
this.onState = onState
|
||||
|
||||
request(home, null)
|
||||
if(home.startsWith("gemini://") and !home.contains(" ")){
|
||||
request(home, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun request(address: String, clientCertPassword: String?) {
|
||||
gemini.request(address, false, clientCertPassword){ state ->
|
||||
fun request(address: String, clientCertPassword: String?, alternativeRequest: String?) {
|
||||
gemini.request(address, false, clientCertPassword, alternativeRequest){ state ->
|
||||
onState(state)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestBinaryDownload(uri: URI, clientCertPassword: String?) {
|
||||
gemini.request(uri.toString(), true, clientCertPassword){ state ->
|
||||
fun isRequesting(): Boolean{
|
||||
return gemini.isRequesting()
|
||||
}
|
||||
|
||||
fun cancel(){
|
||||
gemini.cancel()
|
||||
}
|
||||
|
||||
fun requestBinaryDownload(uri: URI, clientCertPassword: String?, alternativeRequest: String?) {
|
||||
gemini.request(uri.toString(), true, clientCertPassword, alternativeRequest){ state ->
|
||||
onState(state)
|
||||
}
|
||||
}
|
||||
|
||||
//todo - same action as above... refactor
|
||||
fun requestInlineImage(uri: URI, clientCertPassword: String?, onImageReady: (cacheUri: Uri?) -> Unit){
|
||||
gemini.request(uri.toString(), false, clientCertPassword){ state ->
|
||||
gemini.request(uri.toString(), false, clientCertPassword, null){ state ->
|
||||
when (state) {
|
||||
is GemState.ResponseImage -> onImageReady(state.cacheUri)
|
||||
else -> onState(state)
|
||||
|
|
|
@ -303,7 +303,9 @@ class GemtextAdapter(
|
|||
.append("${component.substring(index)}$joiner")
|
||||
} else {
|
||||
var offset = 1
|
||||
while (!component.substring(offset).first().isLetterOrDigit()) {
|
||||
|
||||
if (component.length - offset > 1) {
|
||||
while ((component.length - offset > 1) and !component.substring(offset).first().isLetterOrDigit()) {
|
||||
offset += 1
|
||||
}
|
||||
val index = (component.length - offset) / 2
|
||||
|
@ -311,6 +313,9 @@ class GemtextAdapter(
|
|||
.append(component.substring(0, offset))
|
||||
.bold { append(component.substring(offset, index + offset)) }
|
||||
.append("${component.substring(index + offset)}$joiner")
|
||||
}else{
|
||||
attentionGuideText.append("$component$joiner")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attentionGuideText.append("$component$joiner")
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
package corewala.buran.ui.modals_menus.about
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import kotlinx.android.synthetic.main.dialog_about.view.*
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import corewala.buran.R
|
||||
import kotlinx.android.synthetic.main.dialog_content_text.view.*
|
||||
import java.lang.StringBuilder
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
object AboutDialog {
|
||||
|
||||
fun show(context: Context){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_about, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.about_toolbar.setNavigationIcon(R.drawable.vector_close)
|
||||
view.about_toolbar.setNavigationOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
view.source_button.setOnClickListener {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("https://github.com/Corewala/Buran")
|
||||
})
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
}
|
|
@ -8,13 +8,13 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.core.view.forEach
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import corewala.buran.OmniTerm
|
||||
import kotlinx.android.synthetic.main.dialog_history.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
import kotlinx.android.synthetic.main.dialog_bookmarks.view.*
|
||||
|
||||
object HistoryDialog {
|
||||
fun show(context: Context, history: BuranHistory, onHistoryItem: (address: String) -> Unit){
|
||||
fun show(context: Context, history: BuranHistory, omniTerm: OmniTerm, onHistoryItem: (address: String) -> Unit){
|
||||
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
|
@ -37,6 +37,7 @@ object HistoryDialog {
|
|||
}
|
||||
}
|
||||
R.id.menu_action_clear_runtime_cache -> {
|
||||
omniTerm.clearCache()
|
||||
Toast.makeText(context, context.getString(R.string.runtime_cache_cleared), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Activity.RESULT_OK
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
|
@ -50,14 +51,35 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
val homecapsule = preferenceManager.sharedPreferences.getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
)
|
||||
)?.trim()
|
||||
|
||||
homePreference.summary = if(homecapsule.isNullOrEmpty()){
|
||||
context.getString(R.string.no_home_capsule_set)
|
||||
}else if(
|
||||
!homecapsule.startsWith("gemini://")
|
||||
or homecapsule.contains(" ")
|
||||
or !homecapsule.contains(".")
|
||||
){
|
||||
context.getString(R.string.not_valid_address)
|
||||
}else{
|
||||
homecapsule
|
||||
}
|
||||
|
||||
homePreference.summary = homecapsule
|
||||
homePreference.positiveButtonText = getString(R.string.update)
|
||||
homePreference.negativeButtonText = getString(R.string.cancel)
|
||||
homePreference.title = getString(R.string.home_capsule)
|
||||
homePreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
homePreference.summary = newValue.toString()
|
||||
val newHomecapsule = newValue.toString().trim()
|
||||
homePreference.summary = if(newHomecapsule.isNullOrEmpty()){
|
||||
context.getString(R.string.no_home_capsule_set)
|
||||
}else if(
|
||||
!newHomecapsule.startsWith("gemini://")
|
||||
or newHomecapsule.contains(" ")
|
||||
or !newHomecapsule.contains(".")
|
||||
){
|
||||
context.getString(R.string.not_valid_address)
|
||||
}else{
|
||||
newHomecapsule
|
||||
}
|
||||
true
|
||||
}
|
||||
homePreference.setOnBindEditTextListener{ editText ->
|
||||
|
@ -75,14 +97,39 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
val searchengine = preferenceManager.sharedPreferences.getString(
|
||||
"search_base",
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
)
|
||||
)?.trim()
|
||||
|
||||
searchPreference.summary = if(searchengine.isNullOrEmpty()){
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
}else if(
|
||||
!searchengine.startsWith("gemini://")
|
||||
or searchengine.contains(" ")
|
||||
or !searchengine.contains(".")
|
||||
){
|
||||
context.getString(R.string.not_valid_address)
|
||||
}else if(!searchengine.endsWith("?")){
|
||||
context.getString(R.string.not_valid_search_string)
|
||||
}else{
|
||||
searchengine
|
||||
}
|
||||
|
||||
searchPreference.summary = searchengine
|
||||
searchPreference.positiveButtonText = getString(R.string.update)
|
||||
searchPreference.negativeButtonText = getString(R.string.cancel)
|
||||
searchPreference.title = getString(R.string.search_engine)
|
||||
searchPreference.setOnPreferenceChangeListener { _, newValue ->
|
||||
searchPreference.summary = newValue.toString()
|
||||
val newSearchBase = newValue.toString().trim()
|
||||
searchPreference.summary = if(newSearchBase.isNullOrEmpty()){
|
||||
Buran.DEFAULT_SEARCH_BASE
|
||||
}else if(
|
||||
!newSearchBase.startsWith("gemini://")
|
||||
or newSearchBase.contains(" ")
|
||||
or !newSearchBase.contains(".")
|
||||
){
|
||||
context.getString(R.string.not_valid_address)
|
||||
}else if(!newSearchBase.endsWith("?")){
|
||||
context.getString(R.string.not_valid_search_string)
|
||||
}else{
|
||||
newSearchBase
|
||||
}
|
||||
true
|
||||
}
|
||||
searchPreference.setOnBindEditTextListener{ editText ->
|
||||
|
@ -92,16 +139,17 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
appCategory.addPreference(searchPreference)
|
||||
|
||||
//Updates ---------------------------------------------
|
||||
val aboutUpdater = Preference(context)
|
||||
aboutUpdater.summary = getString(R.string.self_update_summary)
|
||||
aboutUpdater.isPersistent = false
|
||||
aboutUpdater.isSelectable = false
|
||||
appCategory.addPreference(aboutUpdater)
|
||||
val sideloadedHashCode = -899861527
|
||||
val isSideloaded = context.packageManager.getPackageInfo(
|
||||
context.packageName,
|
||||
PackageManager.GET_SIGNATURES
|
||||
).signatures[0].hashCode() == sideloadedHashCode
|
||||
|
||||
val checkForUpdates = SwitchPreferenceCompat(context)
|
||||
checkForUpdates.setDefaultValue(true)
|
||||
checkForUpdates.setDefaultValue(false)
|
||||
checkForUpdates.key = "check_for_updates"
|
||||
checkForUpdates.title = getString(R.string.check_for_updates)
|
||||
checkForUpdates.isVisible = isSideloaded
|
||||
appCategory.addPreference(checkForUpdates)
|
||||
|
||||
//Certificates
|
||||
|
@ -143,6 +191,50 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
showInlineImages.title = getString(R.string.show_inline_images)
|
||||
webCategory.addPreference(showInlineImages)
|
||||
|
||||
val httpGeminiProxy = EditTextPreference(context)
|
||||
httpGeminiProxy.title = getString(R.string.http_proxy)
|
||||
httpGeminiProxy.key = "http_proxy"
|
||||
httpGeminiProxy.dialogTitle = getString(R.string.http_proxy)
|
||||
|
||||
val httpProxy = preferenceManager.sharedPreferences.getString(
|
||||
"http_proxy",
|
||||
null
|
||||
)?.trim()
|
||||
|
||||
httpGeminiProxy.summary = if(httpProxy.isNullOrEmpty()){
|
||||
getString(R.string.no_http_proxy_set)
|
||||
}else if(
|
||||
!httpProxy.startsWith("gemini://")
|
||||
or httpProxy.contains(" ")
|
||||
or !httpProxy.contains(".")
|
||||
){
|
||||
getString(R.string.not_valid_address)
|
||||
}else{
|
||||
httpProxy
|
||||
}
|
||||
|
||||
httpGeminiProxy.positiveButtonText = getString(R.string.update)
|
||||
httpGeminiProxy.negativeButtonText = getString(R.string.cancel)
|
||||
httpGeminiProxy.setOnPreferenceChangeListener { _, newValue ->
|
||||
val newHomecapsule = newValue.toString().trim()
|
||||
httpGeminiProxy.summary = if(newHomecapsule.isNullOrEmpty()){
|
||||
getString(R.string.no_http_proxy_set)
|
||||
}else if(
|
||||
!newHomecapsule.startsWith("gemini://")
|
||||
or newHomecapsule.contains(" ")
|
||||
or !newHomecapsule.contains(".")
|
||||
){
|
||||
getString(R.string.not_valid_address)
|
||||
}else{
|
||||
newHomecapsule
|
||||
}
|
||||
true
|
||||
}
|
||||
httpGeminiProxy.setOnBindEditTextListener{ editText ->
|
||||
editText.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
editText.setSelection(editText.text.toString().length)//Set caret position to end
|
||||
}
|
||||
webCategory.addPreference(httpGeminiProxy)
|
||||
}
|
||||
|
||||
private fun buildAppearanceSection(context: Context?, appCategory: PreferenceCategory) {
|
||||
|
@ -273,7 +365,7 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
type = "*/*"
|
||||
type = "application/x-pkcs12"
|
||||
}
|
||||
startActivityForResult(intent, PREFS_SET_CLIENT_CERT_REQ)
|
||||
true
|
||||
|
@ -281,7 +373,6 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
|
||||
certificateCategory.addPreference(clientCertPref)
|
||||
|
||||
|
||||
val clientCertPassword = EditTextPreference(context)
|
||||
clientCertPassword.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD
|
||||
clientCertPassword.title = getString(R.string.client_certificate_password)
|
||||
|
@ -307,7 +398,6 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
useBiometrics.isVisible = false
|
||||
certificateCategory.addPreference(useBiometrics)
|
||||
|
||||
|
||||
val passwordCiphertext = EditTextPreference(context)
|
||||
passwordCiphertext.key = "password_ciphertext"
|
||||
passwordCiphertext.isVisible = false
|
||||
|
@ -353,6 +443,7 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
clientCertPassword.summary = getDots(clientCertPassword.text)
|
||||
}
|
||||
clientCertPassword.isVisible = !(newValue as Boolean)
|
||||
clientCertPref.isEnabled = !(newValue as Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +496,6 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
|
|||
persistPermissions(uri)
|
||||
findFilename(uri)
|
||||
}
|
||||
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
android:layout_width="@dimen/button_size"
|
||||
android:layout_height="@dimen/button_size"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_home"/>
|
||||
|
@ -80,7 +81,7 @@
|
|||
android:id="@+id/more"
|
||||
android:layout_width="@dimen/button_size"
|
||||
android:layout_height="@dimen/button_size"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
<?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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/about_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_height="@dimen/bar_height"
|
||||
app:title="@string/about"/>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/about_toolbar">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/default_margin_big"
|
||||
android:paddingRight="@dimen/default_margin_big"
|
||||
android:paddingBottom="@dimen/default_margin_big"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Description -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin_big"
|
||||
android:textColor="@color/stroke"
|
||||
android:text="@string/about_body"/>
|
||||
|
||||
<!-- Version -->
|
||||
<TextView
|
||||
android:id="@+id/version_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/stroke"
|
||||
tools:text="1.0.0 alpha delta"/>
|
||||
|
||||
<!-- Copyright -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textColor="@color/stroke"
|
||||
android:text="@string/copyright"/>
|
||||
|
||||
<!-- Source button -->
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/source_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Source"/>
|
||||
|
||||
<!-- DIV -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:alpha="0.5"
|
||||
android:background="?attr/colorOnSurface" />
|
||||
|
||||
<!-- Ariane source attribution-->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textColor="@color/stroke"
|
||||
android:text="@string/about_ariane_source"/>
|
||||
|
||||
<!-- Font Attribution -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textColor="@color/stroke"
|
||||
android:text="@string/about_font"/>
|
||||
|
||||
<!-- Glyph Attribution -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textColor="@color/stroke"
|
||||
android:text="@string/about_glyphs"/>
|
||||
|
||||
<!-- DIV -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:alpha="0.5"
|
||||
android:background="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/gemtext_quote_textview"
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
|
@ -15,4 +15,4 @@
|
|||
android:textIsSelectable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineHeight="@dimen/default_line_height"/>
|
||||
app:lineHeight="@dimen/default_line_height" />
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
|
@ -9,4 +9,4 @@
|
|||
android:textIsSelectable="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineHeight="@dimen/default_line_height"/>
|
||||
app:lineHeight="@dimen/default_line_height" />
|
|
@ -4,18 +4,18 @@
|
|||
<string name="main_input_hint">Entrez l\'adresse gemini://</string>
|
||||
<string name="main_input_search_hint">Entrez un terme de recherche</string>
|
||||
<string name="copy_address">Partager l\'adresse</string>
|
||||
<string name="load_image">Afficher en ligne</string>
|
||||
<string name="about">À propos</string>
|
||||
<string name="address_copied_to_clipboard">Adresse copiée dans le presse-papiers</string>
|
||||
<string name="gemini_address">Adresse Gemini</string>
|
||||
<string name="share">Partager</string>
|
||||
<string name="set_home">Choisir comme Accueil</string>
|
||||
<string name="settings">Paramètres</string>
|
||||
<string name="about_body">Bourane: Un client pour le protocole Gemini par Corewala</string>
|
||||
<string name="about_body">Un navigateur minimaliste pour le protocole Gemini par Corewala</string>
|
||||
<string name="copyright">Copyright © 2022 Corewala</string>
|
||||
<string name="about_ariane_source">Buran est basé sur le navigateur Ariane d\'ÖLAB sous la Licence Publique de l\'Union Européenne</string>
|
||||
<string name="about_ariane_source">Bourane est basé sur le navigateur Ariane d\'ÖLAB sous la Licence Publique de l\'Union Européenne</string>
|
||||
<string name="about_font">Les blocs de code sont rendus avec JetBrains Mono de JetBrains</string>
|
||||
<string name="about_glyphs">Les glyphes utilisés proviennent de Material Icons par Google</string>
|
||||
<string name="source">Source</string>
|
||||
<string name="clear_cache">Vider le cache d\'exécution</string>
|
||||
<string name="history">Historique</string>
|
||||
<string name="clear_history">Vider l\'historique</string>
|
||||
|
@ -49,6 +49,9 @@
|
|||
<string name="file_saved_to_device">Fichier sauvegardé dans l\'appareil</string>
|
||||
<string name="configure_buran">Configurer Buran</string>
|
||||
<string name="home_capsule">Capsule d\'accueil</string>
|
||||
<string name="not_valid_search_string">Ceci n\'est pas une base de recherche valide</string>
|
||||
<string name="not_valid_address">Ceci n\'est pas une adresse valide</string>
|
||||
<string name="no_home_capsule_set">Pas de capsule d\'accueil</string>
|
||||
<string name="search_engine">Moteur de recherche</string>
|
||||
<string name="search_base">Base de recherche</string>
|
||||
<string name="update">Mettre à jour</string>
|
||||
|
@ -62,6 +65,10 @@
|
|||
<string name="web_content">Contenu Web</string>
|
||||
<string name="web_content_label">Ouvrir les sites web en interne en utilisant des \'Onglets Personnalisés\', plutôt que d\'utiliser le navigateur par défaut. Cela pourrait vous aider à rester dans le Geminispace plutôt que d\'être distrait·e par le vaste web. Cela requiert un navigateur par défaut compatible.</string>
|
||||
<string name="web_content_switch_label">Ouvrir en interne</string>
|
||||
<string name="http_proxy">Mandataire HTTP</string>
|
||||
<string name="no_http_proxy_set">Pas de mandataire HTTP</string>
|
||||
<string name="proxied_content">Ce contenu est visualisé via un mandataire</string>
|
||||
<string name="open_original">Ouvrir l\'original</string>
|
||||
<string name="show_inline_images">Images locales en ligne</string>
|
||||
<string name="pkcs_notice">Seuls les magasins de clés client PKCS12 sont actuellement supportés.</string>
|
||||
<string name="client_certificate">Certificat Client</string>
|
||||
|
@ -79,10 +86,9 @@
|
|||
<string name="cert_unloaded">Certificat dechargé</string>
|
||||
<string name="set_home_capsule">Choisir comme capsule d\'accueil</string>
|
||||
<string name="check_for_updates">Vérifier pour des mises à jour</string>
|
||||
<string name="self_update_summary">L\'instalation automatique des mises à jour ne fonctionne que si Bourane est installée manuellement.</string>
|
||||
<string name="new_version_available">Nouvelle version disponible</string>
|
||||
<string name="no_internet">Aucun accès internet</string>
|
||||
<string name="retry">Réessayer</string>
|
||||
<string name="retry">Rafraichissez cette page pour réessayer</string>
|
||||
<string name="history_cleared">Historique vidé</string>
|
||||
<string name="runtime_cache_cleared">Cache d\'exécution vidé</string>
|
||||
<string name="show_inline_icons">Icônes de lien en ligne</string>
|
||||
|
|
|
@ -4,18 +4,18 @@
|
|||
<string name="main_input_hint">Enter gemini:// address</string>
|
||||
<string name="main_input_search_hint">Enter search term</string>
|
||||
<string name="copy_address">Share address</string>
|
||||
<string name="load_image">Display inline</string>
|
||||
<string name="about">About</string>
|
||||
<string name="address_copied_to_clipboard">Address copied to clipboard</string>
|
||||
<string name="gemini_address">Gemini address</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="set_home">Set Home</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="about_body">Buran: A Gemini protocol browser from Corewala</string>
|
||||
<string name="about_body">A simple Gemini protocol browser from Corewala</string>
|
||||
<string name="copyright">Copyright © 2022 Corewala</string>
|
||||
<string name="about_ariane_source">Buran is based on the Ariane browser by ÖLAB under the European Union Public Licence</string>
|
||||
<string name="about_font">Code blocks are rendered using JetBrains Mono by JetBrains</string>
|
||||
<string name="about_glyphs">Glyphs used are from Material Icons by Google</string>
|
||||
<string name="source">Source</string>
|
||||
<string name="clear_cache">Clear runtime cache</string>
|
||||
<string name="history">History</string>
|
||||
<string name="clear_history">Clear history</string>
|
||||
|
@ -49,6 +49,9 @@
|
|||
<string name="file_saved_to_device">File saved to device</string>
|
||||
<string name="configure_buran">Configure Buran</string>
|
||||
<string name="home_capsule">Home Capsule</string>
|
||||
<string name="not_valid_search_string">This is not a valid search base</string>
|
||||
<string name="not_valid_address">This is not a valid address</string>
|
||||
<string name="no_home_capsule_set">No home capsule set</string>
|
||||
<string name="search_engine">Search Engine</string>
|
||||
<string name="search_base">Search Base</string>
|
||||
<string name="update">Update</string>
|
||||
|
@ -62,6 +65,10 @@
|
|||
<string name="web_content">Web Content</string>
|
||||
<string name="web_content_label">Open websites internally using \'Custom Tabs\', instead of using the default browser. This might help you stay in Geminispace instead of being distracted by the wider web. Requires compatible default browser.</string>
|
||||
<string name="web_content_switch_label">Open internally</string>
|
||||
<string name="http_proxy">HTTP proxy</string>
|
||||
<string name="no_http_proxy_set">No HTTP proxy set</string>
|
||||
<string name="proxied_content">This content is rendered through a proxy</string>
|
||||
<string name="open_original">Open original</string>
|
||||
<string name="show_inline_images">Inline local images</string>
|
||||
<string name="pkcs_notice">Only PKCS12 client keystores are currently supported.</string>
|
||||
<string name="client_certificate">Client Certificate</string>
|
||||
|
@ -79,10 +86,9 @@
|
|||
<string name="cert_unloaded">Certificate unloaded</string>
|
||||
<string name="set_home_capsule">Set home capsule</string>
|
||||
<string name="check_for_updates">Check for updates</string>
|
||||
<string name="self_update_summary">Automatic update installation will only work if Buran is sideloaded.</string>
|
||||
<string name="new_version_available">New version available</string>
|
||||
<string name="no_internet">No internet access</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="retry">Refresh this page to try again</string>
|
||||
<string name="history_cleared">History cleared</string>
|
||||
<string name="runtime_cache_cleared">Runtime cache cleared</string>
|
||||
<string name="show_inline_icons">Inline link icons</string>
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Mon Nov 29 22:38:09 EST 2021
|
||||
sdk.dir=/home/corewala/.android_sdk
|
||||
sdk-location=/home/vagrant/android-sdk
|
||||
ndk.dir=/home/vagrant/android-ndk/r12b
|
||||
ndk-location=/home/vagrant/android-ndk/r12b
|
|
@ -0,0 +1,9 @@
|
|||
Changelog:
|
||||
|
||||
- Default homepage is stored locally
|
||||
|
||||
- Only the latest request is rendered
|
||||
|
||||
- Back button cancels current request
|
||||
|
||||
- Lots of bugfixes
|
|
@ -0,0 +1,9 @@
|
|||
Changelog:
|
||||
|
||||
- HTTP proxy support
|
||||
|
||||
- Previous scroll position is stored
|
||||
|
||||
- Local links and redirects are fixed
|
||||
|
||||
- Minor tweaks and bugfixes
|
|
@ -0,0 +1,9 @@
|
|||
Changelog:
|
||||
|
||||
- Fixed broken unproxied HTTP links
|
||||
|
||||
- Un-bugged the back button
|
||||
|
||||
- This is a really tiny release
|
||||
|
||||
- Actual features coming at some point in the nebulous future
|
|
@ -0,0 +1,5 @@
|
|||
Buran is a simple Gemini protocol browser for Android which allows users to explore geminispace in style.
|
||||
|
||||
This application has no external dependencies and does not require any nonfree services, using only default system libraries. It has been fully localized in English and French, and supports an array of accessibility and quality-of-life features.
|
||||
|
||||
Buran is a fork of Ariane by ÖLAB allowed under the terms of the European Union Public Licence.
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 692 KiB |
After Width: | Height: | Size: 1.4 MiB |
|
@ -0,0 +1 @@
|
|||
Simple Gemini browser for Android
|
|
@ -0,0 +1 @@
|
|||
Buran
|
|
@ -0,0 +1,5 @@
|
|||
Bourane est un navigateur minimaliste du protocole Gemini sur Android pour explorer l'espace Gemini avec style.
|
||||
|
||||
Cette application n'a aucune dépendance externe et ne nécessite aucun service non gratuit, en utilisant uniquement les bibliothèques système.
|
||||
|
||||
Bourane est basé sur le navigateur Ariane d'ÖLAB autorisé conformément aux termes de la Licence Publique de l'Union Européenne.
|
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 165 KiB |
After Width: | Height: | Size: 1.1 MiB |
|
@ -0,0 +1 @@
|
|||
Navigateur minimaliste pour le protocole Gemini
|
|
@ -0,0 +1 @@
|
|||
Bourane
|