Compare commits

...

77 Commits
v1.9 ... master

Author SHA1 Message Date
Corewala 326c20f5ff Removed "fix" for images because it breaks things. 2023-02-20 16:54:27 -05:00
Corewala 867688a075 Fixed bug where current uri would be changed by resolving long-taps and image dialogs 2023-02-20 16:46:04 -05:00
Corewala bedbc9fb98 Made preferences less vulnerable to accidentally changing certificates 2023-02-20 16:14:17 -05:00
Corewala df6c159c2c Removed unused string 2023-02-20 15:31:37 -05:00
Corewala 93ef22a4f8 Removed unnecessary reference to proxy in redirect fix 2023-02-20 15:28:57 -05:00
Corewala 0a47f391e5 Fixed issue with local redirects from links on another host 2023-02-04 20:38:57 -05:00
Corewala 73afc7c684 Fixed redirect drop bug 2023-02-04 19:46:26 -05:00
Corewala 0fdffee966 v1.12 2022-11-09 23:21:34 -05:00
Corewala da4375b832 Merge remote-tracking branch 'origin/master' 2022-11-09 23:02:32 -05:00
Corewala 9f4d1c8027 Fixed bugged back button
Been a while eh?
2022-11-09 23:02:15 -05:00
Corewala 0df1501fd4
Merge pull request #30 from arielcostas/master
Delete local.properties,which shouldn't be on VCS

Thanks @arielcostas :)
2022-09-02 14:44:00 -04:00
Ariel Costas 2bd546d7ed Delete local.properties,which shouldn't be on VCS
Signed-off-by: Ariel Costas <arielcostas@mailbox.org>
2022-09-01 15:02:19 +02:00
Corewala 2c3c1db96b
Merge pull request #29 from rudmannn/master
fix android:lineHeight
2022-08-11 09:39:53 -04:00
strooonger 9038f49d40 fix android:lineHeight 2022-08-06 22:42:08 +08:00
Corewala eac1adb0cb Removed unused transverse function 2022-08-04 12:18:47 -04:00
Corewala eee109bb28 Made relative query strings work in accordance with the URL spec 2022-08-04 12:17:20 -04:00
Corewala f89f41ae14 Removed broken null check on http proxy 2022-08-01 18:38:09 -04:00
Corewala 26093144dd Fixed apostrophes in French fastlane description 2022-07-31 14:08:46 -04:00
Corewala 47dd2722e1 v1.11 2022-07-29 19:11:45 -04:00
Corewala b85de17c88 v1.10 changelog fix 2022-07-29 18:55:55 -04:00
Corewala 53c1980fa6 Updated todo 2022-07-29 16:23:24 -04:00
Corewala 0ae42a214d Removed redundant ouri and fixed local redirects (again) 2022-07-29 16:21:00 -04:00
Corewala 319b0b4d14 Fixed sharing of relative links 2022-07-29 15:47:33 -04:00
Corewala 13f21bc09b Fixed relative link handling with proxy 2022-07-29 15:09:26 -04:00
Corewala 79a3564569 Fixed snackbar on proxied content 2022-07-26 12:31:04 -04:00
Corewala 4a451ef5ca Fixed attention guide crash with long non-alphanumeric words 2022-07-26 12:09:00 -04:00
Corewala 467e3fc0b7 Added HTTP proxy
It's a beautiful half-broken mess
2022-07-25 23:07:55 -04:00
Corewala affa99e8f2 Added HTTP proxy to settings
It doesn't do anything yet. Great feature I know.
2022-07-09 18:38:31 -04:00
Corewala 4c94bd97e4 Fixed previousPosition when not through onLink 2022-07-09 13:27:27 -04:00
Corewala 679c3bd0be Replaced screenshots 2022-07-08 13:11:49 -04:00
Corewala aa6dcdad91 Previous scroll position is cached to reduce unnecessary re-scrolling 2022-07-08 12:28:17 -04:00
Corewala 9d4386939a Added HTTPS gateway support to todo 2022-07-08 11:08:12 -04:00
Corewala 001e7d3ffe Fixed type in screenshots folder name
That's a bit embarrassing
2022-07-02 11:09:06 -04:00
Corewala 882617df29 Added correct link to f-droid button 2022-07-02 11:02:34 -04:00
Corewala ba901e49ba Added f-droid button 2022-07-02 10:58:51 -04:00
Corewala cf8efbc625 Addressbar is cleared when loading about page 2022-07-01 15:53:06 -04:00
Corewala 09fc2a480a Improved handling of broken addresses in settings 2022-06-30 18:54:06 -04:00
Corewala a47d003f59 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	metadata/android/en-US/changelogs/11.txt
2022-06-23 13:55:40 -04:00
Corewala f79d0bf5ff Changelog v1.10 2022-06-23 13:33:14 -04:00
Licaon_Kter 011eb7487c
Add changelog 2022-06-22 10:51:00 +00:00
Corewala bae12d17a8 v1.10 2022-06-21 17:28:48 -04:00
Corewala f98756bebb Made local home into about page 2022-06-21 15:06:19 -04:00
Corewala e4cc13bb03 Made local page default 2022-06-21 14:37:04 -04:00
Corewala 3dee099dcf Added search to default homepage 2022-06-21 14:35:30 -04:00
Corewala 132749016a Made home capsule setting look better when empty/invalid 2022-06-21 14:23:44 -04:00
Corewala a791521565 Fixed infinite refresh on default homepage 2022-06-20 18:03:35 -04:00
Corewala 8258c48bb0 Updated strings on no internet page 2022-06-20 18:02:22 -04:00
Corewala c12702ef2f Fixed broken check for valid URI 2022-06-20 17:53:26 -04:00
Corewala 28fa72f59e Made refresh stop on empty homepage 2022-06-20 17:44:09 -04:00
Corewala 13af2089d8 Added fastlane icon 2022-06-20 14:22:35 -04:00
Corewala 2c24fa0c77 Does not load a URI on first launch
And fixed bugs
2022-06-20 12:45:20 -04:00
Corewala 0a59ac08cb Fixed crash if whitespace present in addressbar 2022-06-20 12:07:40 -04:00
Corewala c23b3efb70 Only show self-update option if sideloaded 2022-06-20 11:37:33 -04:00
Corewala 6d4dbbda94 Made self-updating false by default 2022-06-18 12:45:00 -04:00
Corewala 95ed0b815b Removed statement about Ariane source
Things have stayed pretty quiet for a while now, so it feels a bit unnecessary to have such a big statement anymore.
2022-06-16 15:28:07 -04:00
Corewala 125c34c6d0 Replace Egsam gemini link with repo
Apparently Github doesn't like non-http links in the README
2022-06-16 15:03:09 -04:00
Corewala 1c3905b197 Added goal of passing the Egsam client test 2022-06-16 15:00:51 -04:00
Corewala ee8ff30210 Changed images 2022-06-09 15:00:45 -04:00
Corewala 45b50afd47 Remove unnecessary plural French 2022-06-09 14:25:54 -04:00
Corewala dd95af5e5b Made French spelling consistent 2022-06-09 14:24:06 -04:00
Corewala fbdb04bf31 Fixed punctuation 2022-06-09 13:22:17 -04:00
Corewala 98d9af2871 Fixed plural accordance issue 2022-06-09 13:21:36 -04:00
Corewala 79a0d32bf1 Made capitalisation consistent 2022-06-09 13:21:01 -04:00
Corewala 5ccdaeb816 Added screenshots 2022-06-09 13:19:09 -04:00
Corewala f2d633e7e2 Added fastlane metadata 2022-06-09 13:18:54 -04:00
Corewala 05e2f64ebd Updated about body text 2022-06-09 12:41:15 -04:00
Corewala 2db5aa74ca Removed canGoBack check on request cancellation 2022-06-09 11:37:27 -04:00
Corewala 7828f3ea7f Deleted unused variable 2022-06-09 11:22:15 -04:00
Corewala bd8bbbc903 Made clear runtime cache button actually do something 2022-06-09 11:21:41 -04:00
Corewala 8f8bb15455 Added page switching to todo 2022-06-09 10:52:45 -04:00
Corewala 918deb4cdf Forgot to disable loading when request is canceled 2022-06-07 16:38:36 -04:00
Corewala cdcbdcdb2c Only latest gemini request is handled
Also back button cancels current requests if used while loading
2022-06-07 15:36:16 -04:00
Corewala 35352fa6f3 Only latest gemini request is handled 2022-06-07 15:06:54 -04:00
Corewala 0505e5d5bf Fixed redirects without leading slash 2022-06-03 10:42:42 -04:00
Corewala 764cee3042 Fixed addressbar for right-to-left languages 2022-05-31 16:21:45 -04:00
Corewala c0be77ef41 Improved internet connection handling 2022-05-30 19:53:44 -04:00
Corewala 60673c2fb3 Fixed attention guide crash on non-alphanumeric words 2022-05-30 11:38:45 -04:00
40 changed files with 583 additions and 410 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.gradle
.idea
build
release
release
local.properties
app/local.properties

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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){
println("Buran socket error, unknown host: $uhe")
onUpdate(GemState.ResponseUnknownHost(uri))
if(currentRequestAddress == uri.toString()) {
println("Buran socket error, unknown host: $uhe")
onUpdate(GemState.ResponseUnknownHost(uri))
}
return
}catch (ce: ConnectException){
println("Buran socket error, connect exception: $ce")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
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){
println("Buran socket error, ssl handshake exception: $she")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
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,31 +133,38 @@ 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){
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, "Server did not respond with a Gemini header: $uri")))
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)
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.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/") -> getString(socket, uri, header, onUpdate)
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
else -> {
//File served over Gemini but not handled in-app, eg .pdf
if(forceDownload){
getBinary(socket, uri, header, onUpdate)
}else{
onUpdate(GemState.ResponseUnknownMime(uri, header))
if(currentRequestAddress == uri.toString()){
currentRequestAddress = null
when {
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
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, requestEntity.trim().toURI(), header, onUpdate)
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
else -> {
//File served over Gemini but not handled in-app, eg .pdf
if(forceDownload){
getBinary(socket, uri, header, onUpdate)
}else{
onUpdate(GemState.ResponseUnknownMime(uri, header))
}
}
}
}else{
println("Buran dropped response from $uri: request cancelled or superseded")
}
//Close input
@ -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

View File

@ -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()
home = prefs.getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
) ?: Buran.DEFAULT_HOME_CAPSULE
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
)
}
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")
if(
!searchBase.startsWith("gemini://")
or searchBase.contains(" ")
or !searchBase.contains(".")
or !searchBase.endsWith("?")
){
searchBase = Buran.DEFAULT_SEARCH_BASE
}
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
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,18 +701,29 @@ 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
binding.gemtextRecycler.post {
binding.gemtextRecycler.scrollToPosition(0)
//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,14 +820,90 @@ class GemActivity : AppCompatActivity() {
private fun getInternetStatus(): Boolean {
val connectivityManager = this.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return if(capabilities != null){
println("Internet access found")
true
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
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{
println("No internet access found")
false
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{
@ -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{
model.request(address, certPassword, null)
}
}else{
val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
finish()
startActivity(intent)
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")

View File

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

View File

@ -303,14 +303,19 @@ class GemtextAdapter(
.append("${component.substring(index)}$joiner")
} else {
var offset = 1
while (!component.substring(offset).first().isLetterOrDigit()) {
offset += 1
if (component.length - offset > 1) {
while ((component.length - offset > 1) and !component.substring(offset).first().isLetterOrDigit()) {
offset += 1
}
val index = (component.length - offset) / 2
attentionGuideText
.append(component.substring(0, offset))
.bold { append(component.substring(offset, index + offset)) }
.append("${component.substring(index + offset)}$joiner")
}else{
attentionGuideText.append("$component$joiner")
}
val index = (component.length - offset) / 2
attentionGuideText
.append(component.substring(0, offset))
.bold { append(component.substring(offset, index + offset)) }
.append("${component.substring(index + offset)}$joiner")
}
} else {
attentionGuideText.append("$component$joiner")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
Changelog:
- Default homepage is stored locally
- Only the latest request is rendered
- Back button cancels current request
- Lots of bugfixes

View File

@ -0,0 +1,9 @@
Changelog:
- HTTP proxy support
- Previous scroll position is stored
- Local links and redirects are fixed
- Minor tweaks and bugfixes

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1 @@
Simple Gemini browser for Android

View File

@ -0,0 +1 @@
Buran

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1 @@
Navigateur minimaliste pour le protocole Gemini

View File

@ -0,0 +1 @@
Bourane