mirror of
https://github.com/Corewala/Buran
synced 2024-12-21 15:22:45 +00:00
Initial commit
This commit is contained in:
commit
b80bc9948a
131 changed files with 6877 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.gradle
|
||||
.idea
|
||||
build
|
||||
release
|
305
LICENSE
Normal file
305
LICENSE
Normal file
|
@ -0,0 +1,305 @@
|
|||
European Union Public Licence v. 1.2
|
||||
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the 'EUPL') applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the copyright
|
||||
notice for the Work:
|
||||
|
||||
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
— 'The Licence': this Licence.
|
||||
|
||||
— 'The Original Work': the work or software distributed or communicated by
|
||||
the Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
— 'Derivative Works': the works or software that could be created by the Licensee,
|
||||
based upon the Original Work or modifications thereof. This Licence does not
|
||||
define the extent of modification or dependence on the Original Work required
|
||||
in order to classify a work as a Derivative Work; this extent is determined
|
||||
by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
— 'The Work': the Original Work or its Derivative Works.
|
||||
|
||||
— 'The Source Code': the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
— 'The Executable Code': any code which has generally been compiled and which
|
||||
is meant to be interpreted by a computer as a program.
|
||||
|
||||
— 'The Licensor': the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
— 'Contributor(s)': any natural or legal person who modifies the Work under
|
||||
the Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
— 'The Licensee' or 'You': any natural or legal person who makes any usage
|
||||
of the Work under the terms of the Licence.
|
||||
|
||||
— 'Distribution' or 'Communication': any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making available,
|
||||
online or offline, copies of the Work or providing access to its essential
|
||||
functionalities at the disposal of any other natural or legal person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable
|
||||
licence to do the following, for the duration of copyright vested in the Original
|
||||
Work:
|
||||
|
||||
— use the Work in any circumstance and for all usage,
|
||||
|
||||
— reproduce the Work,
|
||||
|
||||
— modify the Work, and make Derivative Works based upon the Work,
|
||||
|
||||
— communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case
|
||||
may be, the Work,
|
||||
|
||||
— distribute the Work or copies thereof,
|
||||
|
||||
— lend and rent the Work or copies thereof,
|
||||
|
||||
— sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether
|
||||
now known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights
|
||||
to any patents held by the Licensor, to the extent necessary to make use of
|
||||
the rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as Executable
|
||||
Code. If the Work is provided as Executable Code, the Licensor provides in
|
||||
addition a machine-readable copy of the Source Code of the Work along with
|
||||
each copy of the Work that the Licensor distributes or indicates, in a notice
|
||||
following the copyright notice attached to the Work, a repository where the
|
||||
Source Code is easily and freely accessible for as long as the Licensor continues
|
||||
to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits
|
||||
from any exception or limitation to the exclusive rights of the rights owners
|
||||
in the Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the disclaimer
|
||||
of warranties. The Licensee must include a copy of such notices and a copy
|
||||
of the Licence with every copy of the Work he/she distributes or communicates.
|
||||
The Licensee must cause any Derivative Work to carry prominent notices stating
|
||||
that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will
|
||||
be done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version
|
||||
of the Licence — for example by communicating 'EUPL v. 1.2 only'. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions
|
||||
on the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed
|
||||
under a Compatible Licence, this Distribution or Communication can be done
|
||||
under the terms of this Compatible Licence. For the sake of this clause, 'Compatible
|
||||
Licence' refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the
|
||||
Work, the Licensee will provide a machine-readable copy of the Source Code
|
||||
or indicate a repository where this Source will be easily and freely available
|
||||
for as long as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade
|
||||
names, trademarks, service marks, or names of the Licensor, except as required
|
||||
for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has
|
||||
the power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent Contributors
|
||||
grant You a licence to their contributions to the Work, under the terms of
|
||||
this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects
|
||||
or 'bugs' inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an 'as is'
|
||||
basis and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other
|
||||
than copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the
|
||||
use of the Work, including without limitation, damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws
|
||||
as far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor
|
||||
by the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon 'I agree'
|
||||
placed under the bottom of a window displaying the text of this Licence or
|
||||
by affirming consent in any other similar way, in accordance with the rules
|
||||
of applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and conditions
|
||||
by exercising any rights granted to You by Article 2 of this Licence, such
|
||||
as the use of the Work, the creation by You of a Derivative Work or the Distribution
|
||||
or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website)
|
||||
must at least provide to the public the information requested by the applicable
|
||||
law regarding the Licensor, the Licence and the way it may be accessible,
|
||||
concluded, stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically
|
||||
upon any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has received
|
||||
the Work from the Licensee under the Licence, provided such persons remain
|
||||
in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as
|
||||
a whole. Such provision will be construed or reformed so as necessary to make
|
||||
it valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions
|
||||
of this Licence or updated versions of the Appendix, so far this is required
|
||||
and reasonable, without reducing the scope of the rights granted by the Licence.
|
||||
New versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version
|
||||
of their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
— any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty
|
||||
on the Functioning of the European Union,
|
||||
|
||||
— any litigation arising between other parties and resulting from the interpretation
|
||||
of this License, will be subject to the exclusive jurisdiction of the competent
|
||||
court where the Licensor resides or conducts its primary business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
— this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
— this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
'Compatible Licences' according to Article 5 EUPL are:
|
||||
|
||||
— GNU General Public License (GPL) v. 2, v. 3
|
||||
|
||||
— GNU Affero General Public License (AGPL) v. 3
|
||||
|
||||
— Open Software License (OSL) v. 2.1, v. 3.0
|
||||
|
||||
— Eclipse Public License (EPL) v. 1.0
|
||||
|
||||
— CeCILL v. 2.0, v. 2.1
|
||||
|
||||
— Mozilla Public Licence (MPL) v. 2
|
||||
|
||||
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
|
||||
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
|
||||
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
|
||||
— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity
|
||||
(LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the
|
||||
above licences without producing a new version of the EUPL, as long as they
|
||||
provide the rights granted in Article 2 of this Licence and protect the covered
|
||||
Source Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of
|
||||
a new EUPL version.
|
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Buran
|
||||
|
||||
<img width="180" src="buran.svg" />
|
||||
|
||||
[![forthebadge](https://forthebadge.com/images/badges/built-for-android.svg)](https://github.com/Corewala/Buran#buran)
|
||||
[![forthebadge](https://forthebadge.com/images/badges/as-seen-on-tv.svg)](https://github.com/Corewala/Buran#buran)
|
||||
|
||||
[![shields](https://img.shields.io/badge/Download-Here-orange?style=for-the-badge&logo=github)](https://github.com/Corewala/Buran/releases/latest)
|
||||
|
||||
Buran is a simple Gemini protocol browser for Android.
|
||||
|
||||
It is currently a quite minimal fork of the Ariane browser, but I hope to implement more features in the future.
|
||||
|
||||
## Credits
|
||||
|
||||
Buran is based on the [Ariane source code](https://web.archive.org/web/20210920212507/https://codeberg.org/oppenlab/Ariane) (now [Seren](https://orllewin.uk/)), created by ÖLAB.
|
||||
|
||||
The font used in code blocks is [JetBrains Mono](https://www.jetbrains.com/lp/mono/), created by JetBrains.
|
||||
|
||||
The glyphs used throughout the project are from [Material Icons](https://fonts.google.com/icons), created by Google.
|
4
app/local.properties
Normal file
4
app/local.properties
Normal file
|
@ -0,0 +1,4 @@
|
|||
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
|
59
app/src/main/AndroidManifest.xml
Normal file
59
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="corewala.buran">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="corewala.buran.Buran"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/launcher"
|
||||
android:label="Buran"
|
||||
android:roundIcon="@drawable/launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name="corewala.buran.ui.ProcessTextActivity"
|
||||
android:label="Open in Buran"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="corewala.buran.ui.GemActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="gemini" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="corewala.buran.ui.settings.SettingsActivity"
|
||||
android:label="@string/settings"
|
||||
android:theme="@style/SettingsTheme"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
75
app/src/main/java/corewala/Extensions.kt
Normal file
75
app/src/main/java/corewala/Extensions.kt
Normal file
|
@ -0,0 +1,75 @@
|
|||
package corewala
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.CountDownTimer
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import java.net.URI
|
||||
|
||||
|
||||
fun View.visible(visible: Boolean) = when {
|
||||
visible -> this.visibility = View.VISIBLE
|
||||
else -> this.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun View.visibleRetainingSpace(visible: Boolean) = when {
|
||||
visible -> this.visibility = View.VISIBLE
|
||||
else -> this.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
fun View.hideKeyboard(){
|
||||
val imm: InputMethodManager? = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
|
||||
fun View.showKeyboard(){
|
||||
val imm: InputMethodManager? = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
imm?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
|
||||
}
|
||||
|
||||
fun String.toURI(): URI {
|
||||
return URI.create(this)
|
||||
}
|
||||
|
||||
fun URI.toUri(): Uri {
|
||||
return Uri.parse(this.toString())
|
||||
}
|
||||
|
||||
fun Uri.toURI(): URI {
|
||||
return URI.create(this.toString())
|
||||
}
|
||||
|
||||
fun Uri.isGemini(): Boolean{
|
||||
return this.toString().startsWith("gemini://")
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun String.endsWithImage(): Boolean{
|
||||
return this.toLowerCase().endsWith(".png") ||
|
||||
this.toLowerCase().endsWith(".jpg") ||
|
||||
this.toLowerCase().endsWith(".jpeg") ||
|
||||
this.toLowerCase().endsWith(".gif")
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun String.isWeb(): Boolean{
|
||||
return this.toLowerCase().startsWith("https://") ||
|
||||
this.toLowerCase().startsWith("http://")
|
||||
}
|
||||
|
||||
fun delay(ms: Long, action: () -> Unit){
|
||||
object : CountDownTimer(ms, ms/2) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
|
||||
override fun onFinish() {
|
||||
action.invoke()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun Int.toPx(): Float {
|
||||
return (this.toFloat() * Resources.getSystem().displayMetrics.density)
|
||||
}
|
33
app/src/main/java/corewala/buran/Buran.kt
Normal file
33
app/src/main/java/corewala/buran/Buran.kt
Normal file
|
@ -0,0 +1,33 @@
|
|||
package corewala.buran
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class Buran: Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
when {
|
||||
prefs.getBoolean("theme_Light", false) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
prefs.getBoolean("theme_Dark", false) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
prefs.getBoolean("theme_FollowSystem", true) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object{
|
||||
const val DEFAULT_HOME_CAPSULE = "gemini://rawtext.club/~sloum/spacewalk.gmi"
|
||||
|
||||
const val FEATURE_CLIENT_CERTS = true
|
||||
|
||||
const val PREF_KEY_CLIENT_CERT_URI = "client_cert_uri"
|
||||
const val PREF_KEY_CLIENT_CERT_HUMAN_READABLE = "client_cert_uri_human_readable"
|
||||
const val PREF_KEY_CLIENT_CERT_ACTIVE = "client_cert_active"
|
||||
const val PREF_KEY_CLIENT_CERT_PASSWORD = "client_cert_password"
|
||||
const val PREF_KEY_USE_CUSTOM_TAB = "use_custom_tabs"
|
||||
}
|
||||
}
|
100
app/src/main/java/corewala/buran/OmniTerm.kt
Normal file
100
app/src/main/java/corewala/buran/OmniTerm.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
package corewala.buran
|
||||
|
||||
import android.net.Uri
|
||||
import java.util.*
|
||||
|
||||
const val GEM_SCHEME = "gemini://"
|
||||
const val GUS_SEARCH_BASE = "gemini://geminispace.info/search?"
|
||||
|
||||
class OmniTerm(private val listener: Listener) {
|
||||
val history = ArrayList<OppenURI>()
|
||||
var uri = OppenURI()
|
||||
var penultimate = OppenURI()
|
||||
|
||||
/**
|
||||
* User input to the 'omni bar' - could be an address or a search term
|
||||
* @param term - User-inputted term
|
||||
*/
|
||||
fun input(term: String){
|
||||
when {
|
||||
term.startsWith(GEM_SCHEME) && term != GEM_SCHEME -> {
|
||||
listener.request(term)
|
||||
return
|
||||
}
|
||||
term.contains(".") -> {
|
||||
listener.request("gemini://${term}")
|
||||
}
|
||||
else -> {
|
||||
val encoded = Uri.encode(term)
|
||||
listener.request("$GUS_SEARCH_BASE$encoded")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(term: String){
|
||||
val encoded = Uri.encode(term)
|
||||
listener.request("$GUS_SEARCH_BASE$encoded")
|
||||
}
|
||||
|
||||
|
||||
fun navigation(link: String) {
|
||||
navigation(link, true)
|
||||
}
|
||||
|
||||
fun imageAddress(link: String){
|
||||
navigation(link, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* A clicked link, could be absolute or relative
|
||||
* @param link - a Gemtext link
|
||||
*/
|
||||
private fun navigation(link: String, invokeListener: Boolean) {
|
||||
when {
|
||||
link.startsWith("http") -> listener.openBrowser(link)
|
||||
link.startsWith(GEM_SCHEME) -> uri.set(link)
|
||||
link.startsWith("//") -> uri.set("gemini:$link")
|
||||
else -> uri.resolve(link)
|
||||
}
|
||||
|
||||
//todo - fix this, the double slash fix breaks the scheme, so this hack puts it back... uggh
|
||||
val address = uri.toString().replace("%2F", "/").replace("//", "/").replace("gemini:/", "gemini://")
|
||||
println("OmniTerm resolved address: $address")
|
||||
|
||||
if(invokeListener) listener.request(address)
|
||||
}
|
||||
|
||||
fun traverse(address: String): String {
|
||||
return OppenURI(address).traverse().toString()
|
||||
}
|
||||
|
||||
fun reset(){
|
||||
uri = penultimate.copy()
|
||||
}
|
||||
|
||||
fun set(address: String) {
|
||||
penultimate.set(address)
|
||||
uri.set(address)
|
||||
if (history.isEmpty() || history.last().toString() != address) {
|
||||
history.add(uri.copy())
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrent(): String {
|
||||
return history.last().toString()
|
||||
}
|
||||
|
||||
fun canGoBack(): Boolean {
|
||||
return history.size > 1
|
||||
}
|
||||
|
||||
fun goBack(): String {
|
||||
history.removeLast()
|
||||
return history.last().toString()
|
||||
}
|
||||
|
||||
interface Listener{
|
||||
fun request(address: String)
|
||||
fun openBrowser(address: String)
|
||||
}
|
||||
}
|
107
app/src/main/java/corewala/buran/OppenURI.kt
Normal file
107
app/src/main/java/corewala/buran/OppenURI.kt
Normal file
|
@ -0,0 +1,107 @@
|
|||
package corewala.buran
|
||||
|
||||
const val SCHEME = "gemini://"
|
||||
const val TRAVERSE = "../"
|
||||
const val SOLIDUS = "/"
|
||||
const val DIREND = "/"
|
||||
|
||||
/**
|
||||
*
|
||||
* Easy uri path handling for Gemini
|
||||
*
|
||||
*/
|
||||
class OppenURI constructor(private var ouri: String) {
|
||||
|
||||
constructor(): this("")
|
||||
|
||||
var host: String = ""
|
||||
|
||||
init {
|
||||
extractHost()
|
||||
}
|
||||
|
||||
fun set(ouri: String){
|
||||
this.ouri = ouri
|
||||
extractHost()
|
||||
}
|
||||
|
||||
fun resolve(reference: String) {
|
||||
if(ouri == "$SCHEME$host") ouri = "$ouri/"
|
||||
return when {
|
||||
reference.startsWith(SCHEME) -> set(reference)
|
||||
reference.startsWith(SOLIDUS) -> ouri = "$SCHEME$host$reference"
|
||||
reference.startsWith(TRAVERSE) -> {
|
||||
if(!ouri.endsWith(DIREND)) ouri = ouri.removeFile()
|
||||
val traversalCount = reference.split(TRAVERSE).size - 1
|
||||
ouri = traverse(traversalCount) + reference.replace(TRAVERSE, "")
|
||||
}
|
||||
else -> {
|
||||
ouri = when {
|
||||
ouri.endsWith(DIREND) -> "${ouri}$reference"
|
||||
else -> "${ouri.substring(0, ouri.lastIndexOf("/"))}/$reference"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private fun traverse(count: Int): String{
|
||||
val path = ouri.removePrefix("$SCHEME$host")
|
||||
val segments = path.split(SOLIDUS).filter { it.isNotEmpty() }
|
||||
val segmentCount = segments.size
|
||||
var nouri = "$SCHEME$host"
|
||||
|
||||
segments.forEachIndexed{ index, segment ->
|
||||
if(index < segmentCount - count){
|
||||
nouri += "/$segment"
|
||||
}
|
||||
}
|
||||
|
||||
return "$nouri/"
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private fun String.removeFile(): String{
|
||||
return this.substring(0, ouri.lastIndexOf("/") + 1)
|
||||
}
|
||||
}
|
24
app/src/main/java/corewala/buran/io/GemState.kt
Normal file
24
app/src/main/java/corewala/buran/io/GemState.kt
Normal file
|
@ -0,0 +1,24 @@
|
|||
package corewala.buran.io
|
||||
|
||||
import android.net.Uri
|
||||
import corewala.buran.io.gemini.GeminiResponse
|
||||
import java.net.URI
|
||||
|
||||
sealed class GemState {
|
||||
data class AppQuery(val uri: URI): GemState()
|
||||
data class Requesting(val uri: URI): 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()
|
||||
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()
|
||||
data class ResponseUnknownMime(val uri: URI, val header: GeminiResponse.Header) : GemState()
|
||||
data class ResponseError(val header: GeminiResponse.Header): GemState()
|
||||
data class ResponseUnknownHost(val uri: URI): GemState()
|
||||
|
||||
data class ClientCertError(val header: GeminiResponse.Header): GemState()
|
||||
|
||||
object Blank: GemState()
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package corewala.buran.io.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import corewala.buran.io.database.bookmarks.BookmarkEntity
|
||||
import corewala.buran.io.database.bookmarks.BookmarksDao
|
||||
import corewala.buran.io.database.history.HistoryDao
|
||||
import corewala.buran.io.database.history.HistoryEntity
|
||||
|
||||
@Database(entities = [BookmarkEntity::class, HistoryEntity::class], version = 3)
|
||||
abstract class BuranAbstractDatabase: RoomDatabase() {
|
||||
abstract fun bookmarks(): BookmarksDao
|
||||
abstract fun history(): HistoryDao
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package corewala.buran.io.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import corewala.buran.io.database.bookmarks.BuranBookmarks
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
|
||||
class BuranDatabase(context: Context) {
|
||||
|
||||
private val db: BuranAbstractDatabase = Room.databaseBuilder(context, BuranAbstractDatabase::class.java, "buran_database_v1").build()
|
||||
|
||||
fun bookmarks(): BuranBookmarks = BuranBookmarks(db)
|
||||
fun history(): BuranHistory = BuranHistory(db)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package corewala.buran.io.database.bookmarks
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "bookmarks")
|
||||
class BookmarkEntity(
|
||||
@ColumnInfo(name = "label") val label: String?,
|
||||
@ColumnInfo(name = "uri") val uri: String?,
|
||||
@ColumnInfo(name = "uiIndex") val uiIndex: Int?,
|
||||
@ColumnInfo(name = "folder") val folder: String?
|
||||
){
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var uid: Int = 0
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package corewala.buran.io.database.bookmarks
|
||||
|
||||
import java.net.URI
|
||||
|
||||
class BookmarkEntry(
|
||||
val uid: Int,
|
||||
val label: String,
|
||||
val uri: URI,
|
||||
val index: Int
|
||||
){
|
||||
var visible = true
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package corewala.buran.io.database.bookmarks
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
@Dao
|
||||
interface BookmarksDao {
|
||||
@Query("SELECT * FROM bookmarks ORDER BY uiIndex ASC")
|
||||
suspend fun getAll(): List<BookmarkEntity>
|
||||
|
||||
@Query("SELECT * from bookmarks WHERE uiIndex = :index LIMIT 1")
|
||||
suspend fun getBookmark(index: Int): BookmarkEntity
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(bookmarks: Array<BookmarkEntity>)
|
||||
|
||||
@Query("UPDATE bookmarks SET uiIndex=:index WHERE uid = :id")
|
||||
fun updateUIIndex(id: Int, index: Int)
|
||||
|
||||
@Query("UPDATE bookmarks SET label=:label, uri=:uri WHERE uid = :id")
|
||||
fun updateContent(id: Int, label: String, uri: String)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(bookmark: BookmarkEntity)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package corewala.buran.io.database.bookmarks
|
||||
|
||||
interface BookmarksDatasource {
|
||||
|
||||
fun get(onBookmarks: (List<BookmarkEntry>) -> Unit)
|
||||
fun add(bookmarkEntry: BookmarkEntry, onAdded: () -> Unit)
|
||||
fun add(bookmarkEntries: Array<BookmarkEntry>, onAdded: () -> Unit)
|
||||
fun delete(bookmarkEntry: BookmarkEntry, onDelete: () -> Unit)
|
||||
|
||||
fun moveUp(bookmarkEntry: BookmarkEntry, onMoved: () -> Unit)
|
||||
fun moveDown(bookmarkEntry: BookmarkEntry, onMoved: () -> Unit)
|
||||
fun update(bookmarkEntry: BookmarkEntry, label: String?, uri: String?, onUpdate: () -> Unit)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package corewala.buran.io.database.bookmarks
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import corewala.buran.io.database.BuranAbstractDatabase
|
||||
import java.net.URI
|
||||
|
||||
class BuranBookmarks(private val db: BuranAbstractDatabase): BookmarksDatasource {
|
||||
|
||||
override fun get(onBookmarks: (List<BookmarkEntry>) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val dbBookmarks = db.bookmarks().getAll()
|
||||
val bookmarks = mutableListOf<BookmarkEntry>()
|
||||
|
||||
dbBookmarks.forEach { bookmarkEntity ->
|
||||
bookmarks.add(
|
||||
BookmarkEntry(
|
||||
uid = bookmarkEntity.uid,
|
||||
label = bookmarkEntity.label ?: "Unknown",
|
||||
uri = URI.create(bookmarkEntity.uri),
|
||||
index = bookmarkEntity.uiIndex ?: 0)
|
||||
)
|
||||
}
|
||||
onBookmarks(bookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(bookmarkEntry: BookmarkEntry, onAdded: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val bookmarkEntity = BookmarkEntity(
|
||||
label = bookmarkEntry.label,
|
||||
uri = bookmarkEntry.uri.toString(),
|
||||
uiIndex = bookmarkEntry.index,
|
||||
folder = "~/")
|
||||
|
||||
db.bookmarks().insertAll(arrayOf(bookmarkEntity))
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(bookmarkEntries: Array<BookmarkEntry>, onAdded: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val entities = bookmarkEntries.map { entry ->
|
||||
BookmarkEntity(
|
||||
label = entry.label,
|
||||
uri = entry.uri.toString(),
|
||||
uiIndex = entry.index,
|
||||
folder = "~/")
|
||||
}
|
||||
db.bookmarks().insertAll(entities.toTypedArray())
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveUp(bookmarkEntry: BookmarkEntry, onMoved: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
|
||||
//todo - this method is broken,
|
||||
//is it?
|
||||
val prev = db.bookmarks().getBookmark(bookmarkEntry.index -1)
|
||||
val target = db.bookmarks().getBookmark(bookmarkEntry.index)
|
||||
|
||||
db.bookmarks().updateUIIndex(prev.uid, bookmarkEntry.index)
|
||||
db.bookmarks().updateUIIndex(target.uid, bookmarkEntry.index - 1)
|
||||
onMoved()
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveDown(bookmarkEntry: BookmarkEntry, onMoved: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val next = db.bookmarks().getBookmark(bookmarkEntry.index + 1)
|
||||
val target = db.bookmarks().getBookmark(bookmarkEntry.index)
|
||||
|
||||
db.bookmarks().updateUIIndex(next.uid, bookmarkEntry.index)
|
||||
db.bookmarks().updateUIIndex(target.uid, bookmarkEntry.index + 1)
|
||||
onMoved()
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(bookmarkEntry: BookmarkEntry, label: String?, uri: String?, onUpdate: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
db.bookmarks().updateContent(bookmarkEntry.uid, label ?: "", uri ?: "")
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(bookmarkEntry: BookmarkEntry, onDelete: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val entity = db.bookmarks().getBookmark(bookmarkEntry.index)
|
||||
db.bookmarks().delete(entity)
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package corewala.buran.io.database.history
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import corewala.buran.io.database.BuranAbstractDatabase
|
||||
|
||||
class BuranHistory(private val db: BuranAbstractDatabase): HistoryDatasource {
|
||||
|
||||
override fun get(onHistory: (List<HistoryEntry>) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val dbBookmarks = db.history().getAll()
|
||||
val history = mutableListOf<HistoryEntry>()
|
||||
|
||||
dbBookmarks.forEach { entity ->
|
||||
history.add(HistoryEntry(entity.uid, entity.timestamp ?: 0L, Uri.parse(entity.uri)))
|
||||
}
|
||||
onHistory(history)
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(entry: HistoryEntry, onAdded: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val lastAdded = db.history().getLastAdded()
|
||||
val entity = HistoryEntity(entry.uri.toString(), System.currentTimeMillis())
|
||||
|
||||
when (lastAdded) {
|
||||
null -> db.history().insert(entity)
|
||||
else -> {
|
||||
when {
|
||||
lastAdded.uri.toString() != entry.uri.toString() -> db.history().insert(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(uri: Uri, onAdded: () -> Unit) {
|
||||
if(!uri.toString().startsWith("gemini://")){
|
||||
onAdded
|
||||
return
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val lastAdded = db.history().getLastAdded()
|
||||
val entity = HistoryEntity(uri.toString(), System.currentTimeMillis())
|
||||
|
||||
when (lastAdded) {
|
||||
null -> db.history().insert(entity)
|
||||
else -> {
|
||||
when {
|
||||
lastAdded.uri.toString() != uri.toString() -> db.history().insert(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAdded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear(onClear: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
db.history().clear()
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(entry: HistoryEntry, onDelete: () -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.IO){
|
||||
val entity = db.history().getEntry(entry.uid)
|
||||
db.history().delete(entity)
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package corewala.buran.io.database.history
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
@Query("SELECT * FROM history ORDER BY timestamp DESC")
|
||||
suspend fun getAll(): List<HistoryEntity>
|
||||
|
||||
@Query("SELECT * FROM history WHERE uid = :uid LIMIT 1")
|
||||
fun getEntry(uid: Int): HistoryEntity
|
||||
|
||||
@Query("SELECT * FROM history ORDER BY timestamp DESC LIMIT 1")
|
||||
fun getLastAdded(): HistoryEntity?
|
||||
|
||||
@Insert
|
||||
fun insert(vararg history: HistoryEntity)
|
||||
|
||||
@Delete
|
||||
fun delete(history: HistoryEntity)
|
||||
|
||||
@Query("DELETE FROM history")
|
||||
fun clear()
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package corewala.buran.io.database.history
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
interface HistoryDatasource {
|
||||
|
||||
fun get(onHistory: (List<HistoryEntry>) -> Unit)
|
||||
fun add(entry: HistoryEntry, onAdded: () -> Unit)
|
||||
fun add(uri: Uri, onAdded: () -> Unit)
|
||||
fun clear(onClear: () -> Unit)
|
||||
fun delete(entry: HistoryEntry, onDelete: () -> Unit)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package corewala.buran.io.database.history
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "history")
|
||||
class HistoryEntity(
|
||||
@ColumnInfo(name = "uri") val uri: String?,
|
||||
@ColumnInfo(name = "timestamp") val timestamp: Long?
|
||||
){
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var uid: Int = 0
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package corewala.buran.io.database.history
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
class HistoryEntry(
|
||||
val uid: Int,
|
||||
val timestamp: Long,
|
||||
val uri: Uri
|
||||
)
|
19
app/src/main/java/corewala/buran/io/gemini/Datasource.kt
Normal file
19
app/src/main/java/corewala/buran/io/gemini/Datasource.kt
Normal file
|
@ -0,0 +1,19 @@
|
|||
package corewala.buran.io.gemini
|
||||
|
||||
import android.content.Context
|
||||
import corewala.buran.io.GemState
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
import java.net.URI
|
||||
|
||||
interface Datasource {
|
||||
fun request(address: String, onUpdate: (state: GemState) -> Unit)
|
||||
fun request(address: String, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit)
|
||||
fun canGoBack(): Boolean
|
||||
fun goBack(onUpdate: (state: GemState) -> Unit)
|
||||
|
||||
companion object{
|
||||
fun factory(context: Context, history: BuranHistory): Datasource {
|
||||
return GeminiDatasource(context, history)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package corewala.buran.io.gemini
|
||||
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import javax.security.cert.CertificateException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object DummyTrustManager {
|
||||
|
||||
fun get(): Array<TrustManager> {
|
||||
return arrayOf(
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) {
|
||||
println("checkServerTrusted()")
|
||||
println("checkServerTrusted() authType: $authType")
|
||||
chain?.forEach { cert ->
|
||||
println("checkServerTrusted() cert: ${cert.subjectDN}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return arrayOf()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
245
app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt
Normal file
245
app/src/main/java/corewala/buran/io/gemini/GeminiDatasource.kt
Normal file
|
@ -0,0 +1,245 @@
|
|||
package corewala.buran.io.gemini
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import corewala.buran.Buran
|
||||
import corewala.buran.io.GemState
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
import corewala.buran.io.keymanager.BuranKeyManager
|
||||
import corewala.toUri
|
||||
import java.io.*
|
||||
import java.lang.IllegalStateException
|
||||
import java.net.ConnectException
|
||||
import java.net.URI
|
||||
import java.net.UnknownHostException
|
||||
import javax.net.ssl.*
|
||||
|
||||
const val GEMINI_SCHEME = "gemini"
|
||||
|
||||
class GeminiDatasource(private val context: Context, val history: BuranHistory): Datasource {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val runtimeHistory = mutableListOf<URI>()
|
||||
private var forceDownload = false
|
||||
|
||||
private var onUpdate: (state: GemState) -> Unit = {_ ->}
|
||||
|
||||
private val buranKeyManager = BuranKeyManager(context){ keyError ->
|
||||
onUpdate(GemState.ClientCertError(GeminiResponse.Header(-3, keyError)))
|
||||
}
|
||||
|
||||
private var socketFactory: SSLSocketFactory? = null
|
||||
|
||||
override fun request(address: String, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit) {
|
||||
this.forceDownload = forceDownload
|
||||
request(address, onUpdate)
|
||||
}
|
||||
|
||||
override fun request(address: String, onUpdate: (state: GemState) -> Unit) {
|
||||
this.onUpdate = onUpdate
|
||||
|
||||
val uri = URI.create(address)
|
||||
|
||||
onUpdate(GemState.Requesting(uri))
|
||||
|
||||
GlobalScope.launch {
|
||||
geminiRequest(uri, onUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSSLFactory(protocol: String){
|
||||
val sslContext = when (protocol) {
|
||||
"TLS_ALL" -> SSLContext.getInstance("TLS")
|
||||
else -> SSLContext.getInstance(protocol)
|
||||
}
|
||||
|
||||
sslContext.init(buranKeyManager.getFactory()?.keyManagers, DummyTrustManager.get(), null)
|
||||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
|
||||
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit){
|
||||
val protocol = prefs.getString("tls_protocol", "TLS")
|
||||
val useClientCert = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false)
|
||||
|
||||
//Update factory if operating mode has changed
|
||||
when {
|
||||
socketFactory == null -> initSSLFactory(protocol!!)
|
||||
useClientCert && !buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!)
|
||||
!useClientCert && buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!)
|
||||
}
|
||||
|
||||
println("REQ_PROTOCOL: $protocol")
|
||||
|
||||
val socket: SSLSocket?
|
||||
try {
|
||||
socket = socketFactory?.createSocket(uri.host, 1965) as SSLSocket
|
||||
|
||||
when (protocol) {
|
||||
"TLS" -> {
|
||||
}//Use default enabled protocols
|
||||
"TLS_ALL" -> socket.enabledProtocols = socket.supportedProtocols
|
||||
else -> socket.enabledProtocols = arrayOf(protocol)
|
||||
}
|
||||
|
||||
println("Buran socket handshake with ${uri.host}")
|
||||
socket.startHandshake()
|
||||
}catch (uhe: UnknownHostException){
|
||||
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())))
|
||||
return
|
||||
}catch (she: SSLHandshakeException){
|
||||
println("Buran socket error, ssl handshake exception: $she")
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
|
||||
return
|
||||
}
|
||||
|
||||
// OUT >>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
val outputStreamWriter = OutputStreamWriter(socket.outputStream)
|
||||
val bufferedWriter = BufferedWriter(outputStreamWriter)
|
||||
val outWriter = PrintWriter(bufferedWriter)
|
||||
|
||||
val requestEntity = uri.toString() + "\r\n"
|
||||
println("Buran socket requesting $requestEntity")
|
||||
outWriter.print(requestEntity)
|
||||
outWriter.flush()
|
||||
|
||||
if (outWriter.checkError()) {
|
||||
onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, "Print Writer Error")))
|
||||
outWriter.close()
|
||||
return
|
||||
}
|
||||
|
||||
// IN <<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
val inputStream = socket.inputStream
|
||||
val headerInputReader = InputStreamReader(inputStream)
|
||||
val bufferedReader = BufferedReader(headerInputReader)
|
||||
val headerLine = bufferedReader.readLine()
|
||||
|
||||
println("Buran: response header: $headerLine")
|
||||
|
||||
if(headerLine == null){
|
||||
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 -> request(URI.create(header.meta).toString(), onUpdate)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Close input
|
||||
bufferedReader.close()
|
||||
headerInputReader.close()
|
||||
|
||||
//Close output:
|
||||
outputStreamWriter.close()
|
||||
bufferedWriter.close()
|
||||
outWriter.close()
|
||||
|
||||
socket.close()
|
||||
}
|
||||
|
||||
private fun getGemtext(reader: BufferedReader, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
|
||||
|
||||
val lines = mutableListOf<String>()
|
||||
|
||||
lines.addAll(reader.readLines())
|
||||
|
||||
val processed = GemtextHelper.findCodeBlocks(lines)
|
||||
|
||||
when {
|
||||
!uri.toString().startsWith("gemini://") -> throw IllegalStateException("Not a Gemini Uri")
|
||||
}
|
||||
|
||||
updateHistory(uri)
|
||||
onUpdate(GemState.ResponseGemtext(uri, header, processed))
|
||||
}
|
||||
|
||||
private fun updateHistory(uri: URI) {
|
||||
if (runtimeHistory.isEmpty() || runtimeHistory.last().toString() != uri.toString()) {
|
||||
runtimeHistory.add(uri)
|
||||
println("Buran added $uri to runtime history (size ${runtimeHistory.size})")
|
||||
}
|
||||
|
||||
history.add(uri.toUri()){}
|
||||
}
|
||||
|
||||
private fun getString(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
|
||||
val content = socket?.inputStream?.bufferedReader().use {
|
||||
reader -> reader?.readText()
|
||||
}
|
||||
socket?.close()
|
||||
onUpdate(GemState.ResponseText(uri, header, content ?: "Error fetching content"))
|
||||
}
|
||||
|
||||
private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
|
||||
|
||||
var filename: String? = null
|
||||
val fileSegmentIndex: Int = uri.path.lastIndexOf('/')
|
||||
|
||||
when {
|
||||
fileSegmentIndex != -1 -> filename = uri.path.substring(fileSegmentIndex + 1)
|
||||
}
|
||||
|
||||
val host = uri.host.replace(".", "_")
|
||||
val cacheName = "${host}_$filename"
|
||||
println("Caching file: $filename from uri: $uri, cacheName: $cacheName")
|
||||
|
||||
val cacheFile = File(context.cacheDir, cacheName)
|
||||
|
||||
when {
|
||||
cacheFile.exists() -> {
|
||||
when {
|
||||
header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
|
||||
else -> onUpdate(GemState.ResponseBinary(uri, header, cacheFile.toUri()))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
cacheFile.createNewFile()
|
||||
cacheFile.outputStream().use{ outputStream ->
|
||||
socket?.inputStream?.copyTo(outputStream)
|
||||
socket?.close()
|
||||
}
|
||||
|
||||
when {
|
||||
header.meta.startsWith("image/") -> onUpdate(GemState.ResponseImage(uri, header, cacheFile.toUri()))
|
||||
else -> onUpdate(GemState.ResponseBinary(uri, header, cacheFile.toUri()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun canGoBack(): Boolean = runtimeHistory.isEmpty() || runtimeHistory.size > 1
|
||||
|
||||
override fun goBack(onUpdate: (state: GemState) -> Unit) {
|
||||
runtimeHistory.removeLast()
|
||||
request(runtimeHistory.last().toString(), onUpdate)
|
||||
}
|
||||
|
||||
//This just forces the factory to rebuild before the next request
|
||||
fun invalidate() {
|
||||
socketFactory = null
|
||||
}
|
||||
}
|
81
app/src/main/java/corewala/buran/io/gemini/GeminiResponse.kt
Normal file
81
app/src/main/java/corewala/buran/io/gemini/GeminiResponse.kt
Normal file
|
@ -0,0 +1,81 @@
|
|||
package corewala.buran.io.gemini
|
||||
|
||||
object GeminiResponse {
|
||||
|
||||
const val INPUT = 1
|
||||
const val SUCCESS = 2
|
||||
const val REDIRECT = 3
|
||||
const val TEMPORARY_FAILURE = 4
|
||||
const val PERMANENT_FAILURE = 5
|
||||
const val CLIENT_CERTIFICATE_REQUIRED = 6
|
||||
const val UNKNOWN = -1
|
||||
|
||||
fun parseHeader(header: String): Header {
|
||||
val cleanHeader = header.replace("\\s+".toRegex(), " ")
|
||||
val meta: String
|
||||
when {
|
||||
header.startsWith("2") -> {
|
||||
val segments = cleanHeader.trim().split(" ")
|
||||
meta = when {
|
||||
segments.size > 1 -> segments[1]
|
||||
else -> "text/gemini; charset=utf-8"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
||||
meta = when {
|
||||
cleanHeader.contains(" ") -> cleanHeader.substring(cleanHeader.indexOf(" ") + 1)
|
||||
else -> cleanHeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
header.startsWith("1") -> Header(
|
||||
INPUT,
|
||||
meta
|
||||
)
|
||||
header.startsWith("2") -> Header(
|
||||
SUCCESS,
|
||||
meta
|
||||
)
|
||||
header.startsWith("3") -> Header(
|
||||
REDIRECT,
|
||||
meta
|
||||
)
|
||||
header.startsWith("4") -> Header(
|
||||
TEMPORARY_FAILURE,
|
||||
meta
|
||||
)
|
||||
header.startsWith("5") -> Header(
|
||||
PERMANENT_FAILURE,
|
||||
meta
|
||||
)
|
||||
header.startsWith("6") -> Header(
|
||||
CLIENT_CERTIFICATE_REQUIRED,
|
||||
meta
|
||||
)
|
||||
else -> Header(
|
||||
UNKNOWN,
|
||||
meta
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCodeString(code: Int): String{
|
||||
return when(code){
|
||||
1 -> "Input"
|
||||
2 -> "Success"
|
||||
3 -> "Redirect"
|
||||
4 -> "Temporary Failure"
|
||||
5 -> "Permanent Failure"
|
||||
6 -> "Client Certificate Required"
|
||||
-3 -> "Client Certificate Error"
|
||||
-2 -> "Bad response: Server Error"
|
||||
-1 -> "Connection Error"
|
||||
else -> "Unknown: $code"
|
||||
}
|
||||
}
|
||||
|
||||
class Header(val code: Int, val meta: String)
|
||||
}
|
44
app/src/main/java/corewala/buran/io/gemini/GemtextHelper.kt
Normal file
44
app/src/main/java/corewala/buran/io/gemini/GemtextHelper.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
package corewala.buran.io.gemini
|
||||
|
||||
import java.lang.StringBuilder
|
||||
|
||||
object GemtextHelper {
|
||||
|
||||
/**
|
||||
*
|
||||
* This is safe for most cases but fails when a line starts with ``` _within_ a code block
|
||||
*
|
||||
*/
|
||||
fun findCodeBlocks(source: List<String>): List<String>{
|
||||
val sb = StringBuilder()
|
||||
var inCodeBlock = false
|
||||
val parsed = mutableListOf<String>()
|
||||
source.forEach { line ->
|
||||
if (line.startsWith("```")) {
|
||||
if (!inCodeBlock) {
|
||||
//New code block starting
|
||||
sb.clear()
|
||||
sb.append("```")
|
||||
|
||||
if(line.length > 3){
|
||||
//Code block has alt text
|
||||
val alt = line.substring(3)
|
||||
sb.append("<|ALT|>$alt</|ALT>")
|
||||
}
|
||||
} else {
|
||||
//End of code block
|
||||
parsed.add(sb.toString())
|
||||
}
|
||||
inCodeBlock = !inCodeBlock
|
||||
} else {
|
||||
if (inCodeBlock) {
|
||||
sb.append("$line\n")
|
||||
} else {
|
||||
parsed.add(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package corewala.buran.io.history.uris
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
*
|
||||
* Another shared prefs implementation so I don't get slowed down by a Room implementation at this point
|
||||
*
|
||||
*/
|
||||
class BasicURIHistory(context: Context): HistoryInterface {
|
||||
|
||||
private val DELIM = "||"
|
||||
private val prefsKey = "history.BasicURIHistory.prefsKey"
|
||||
private val prefsHistoryKey = "history.BasicURIHistory.prefsHistoryKey"
|
||||
private val prefs = context.getSharedPreferences(prefsKey, Context.MODE_PRIVATE)
|
||||
|
||||
override fun add(address: String) {
|
||||
|
||||
val history = get()
|
||||
|
||||
when {
|
||||
history.size >= 50 -> history.removeAt(0)
|
||||
}
|
||||
|
||||
if(history.isNotEmpty() && history.size > 10){
|
||||
if(history.subList(history.size - 10, history.size).contains(address)) return
|
||||
}
|
||||
|
||||
history.add(address)
|
||||
val raw = history.joinToString(DELIM)
|
||||
prefs.edit().putString(prefsHistoryKey, raw).apply()
|
||||
}
|
||||
|
||||
override fun clear(){
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
|
||||
override fun get(): ArrayList<String> {
|
||||
return when (val raw = prefs.getString(prefsHistoryKey, null)) {
|
||||
null -> arrayListOf()
|
||||
else -> ArrayList(raw.split(DELIM))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package corewala.buran.io.history.uris
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface HistoryInterface {
|
||||
fun add(address: String)
|
||||
fun get(): List<String>
|
||||
fun clear()
|
||||
|
||||
companion object{
|
||||
fun default(context: Context): HistoryInterface {
|
||||
return BasicURIHistory(context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package corewala.buran.io.keymanager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import corewala.buran.Buran
|
||||
import corewala.buran.R
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.security.KeyStore
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
|
||||
|
||||
class BuranKeyManager(val context: Context, val onKeyError: (error: String) -> Unit) {
|
||||
|
||||
var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var lastCallUsedKey = false
|
||||
|
||||
//If the user has a key loaded load it here - or else return null
|
||||
fun getFactory(): KeyManagerFactory? {
|
||||
val isClientCertActive = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false)
|
||||
return when {
|
||||
isClientCertActive -> {
|
||||
lastCallUsedKey = true
|
||||
val keyStore: KeyStore = KeyStore.getInstance("pkcs12")
|
||||
|
||||
val uriStr = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_URI, "")
|
||||
val password = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_PASSWORD, "")
|
||||
val uri = Uri.parse(uriStr)
|
||||
try {
|
||||
context.contentResolver?.openInputStream(uri)?.use {
|
||||
try {
|
||||
keyStore.load(it, password?.toCharArray())
|
||||
val keyManagerFactory: KeyManagerFactory =
|
||||
KeyManagerFactory.getInstance("X509")
|
||||
keyManagerFactory.init(keyStore, password?.toCharArray())
|
||||
return@use keyManagerFactory
|
||||
} catch (ioe: IOException) {
|
||||
onKeyError("${ioe.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
} catch(fnf: FileNotFoundException){
|
||||
onKeyError("Please link your client certificate again in Settings; after an update Buran loses permissions to access external files, or the certificate has been moved/deleted\n\n${fnf.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
lastCallUsedKey = false
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Working example with cert packaged with app
|
||||
fun getFactoryDemo(context: Context): KeyManagerFactory? {
|
||||
val keyStore: KeyStore = KeyStore.getInstance("pkcs12")
|
||||
keyStore.load(context.resources.openRawResource(R.raw.cert), "PASSWORD".toCharArray())
|
||||
|
||||
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("X509")
|
||||
keyManagerFactory.init(keyStore, "PASSWORD".toCharArray())
|
||||
|
||||
return keyManagerFactory
|
||||
}
|
||||
}
|
595
app/src/main/java/corewala/buran/ui/GemActivity.kt
Normal file
595
app/src/main/java/corewala/buran/ui/GemActivity.kt
Normal file
|
@ -0,0 +1,595 @@
|
|||
package corewala.buran.ui
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import corewala.*
|
||||
import kotlinx.android.synthetic.main.activity_gem.*
|
||||
import corewala.buran.Buran
|
||||
import corewala.buran.OmniTerm
|
||||
import corewala.buran.R
|
||||
import corewala.buran.databinding.ActivityGemBinding
|
||||
import corewala.buran.io.GemState
|
||||
import corewala.buran.io.database.BuranDatabase
|
||||
import corewala.buran.io.database.bookmarks.BookmarksDatasource
|
||||
import corewala.buran.io.gemini.Datasource
|
||||
import corewala.buran.io.gemini.GeminiResponse
|
||||
import corewala.buran.ui.bookmarks.BookmarkDialog
|
||||
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_adapters.*
|
||||
import corewala.buran.ui.modals_menus.about.AboutDialog
|
||||
import corewala.buran.ui.modals_menus.history.HistoryDialog
|
||||
import corewala.buran.ui.modals_menus.input.InputDialog
|
||||
import corewala.buran.ui.modals_menus.overflow.OverflowPopup
|
||||
import corewala.buran.ui.settings.SettingsActivity
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URI
|
||||
|
||||
const val CREATE_IMAGE_FILE_REQ = 628
|
||||
const val CREATE_BINARY_FILE_REQ = 630
|
||||
const val CREATE_BOOKMARK_EXPORT_FILE_REQ = 631
|
||||
const val CREATE_BOOKMARK_IMPORT_FILE_REQ = 632
|
||||
|
||||
class GemActivity : AppCompatActivity() {
|
||||
|
||||
lateinit var prefs: SharedPreferences
|
||||
private var inSearch = false
|
||||
private lateinit var bookmarkDatasource: BookmarksDatasource
|
||||
private var bookmarksDialog: BookmarksDialog? = null
|
||||
|
||||
private val model by viewModels<GemViewModel>()
|
||||
private lateinit var binding: ActivityGemBinding
|
||||
|
||||
private val omniTerm = OmniTerm(object : OmniTerm.Listener {
|
||||
override fun request(address: String) {
|
||||
loadingView(true)
|
||||
model.request(address)
|
||||
}
|
||||
|
||||
override fun openBrowser(address: String) = openWebLink(address)
|
||||
})
|
||||
|
||||
lateinit var adapter: AbstractGemtextAdapter
|
||||
|
||||
private val onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit = { uri, longTap, position: Int ->
|
||||
if(longTap){
|
||||
loadingView(true)
|
||||
|
||||
omniTerm.imageAddress(uri.toString())
|
||||
omniTerm.uri.let{
|
||||
model.requestInlineImage(URI.create(it.toString())){ imageUri ->
|
||||
imageUri?.let{
|
||||
runOnUiThread {
|
||||
loadingView(false)
|
||||
loadImage(position, imageUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}else{
|
||||
//Reset input text hint after user has been searching
|
||||
if(inSearch) {
|
||||
binding.addressEdit.hint = getString(R.string.main_input_hint)
|
||||
inSearch = false
|
||||
}
|
||||
|
||||
omniTerm.navigation(uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImage(position: Int, uri: Uri) {
|
||||
adapter.loadImage(position, uri)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
|
||||
|
||||
binding.gemtextRecycler.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
adapter = when {
|
||||
prefs.getBoolean("use_large_gemtext_adapter", false) -> AbstractGemtextAdapter.getLargeGmi(onLink)
|
||||
else -> AbstractGemtextAdapter.getDefault(onLink)
|
||||
}
|
||||
|
||||
binding.gemtextRecycler.adapter = adapter
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO -> {
|
||||
omniTerm.input(binding.addressEdit.text.toString().trim())
|
||||
binding.addressEdit.hideKeyboard()
|
||||
binding.addressEdit.clearFocus()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
else -> return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
binding.addressEdit.setOnFocusChangeListener { v, hasFocus ->
|
||||
|
||||
var addressPaddingRight = resources.getDimensionPixelSize(R.dimen.def_address_right_margin)
|
||||
|
||||
if(hasFocus) {
|
||||
binding.addressEdit.showKeyboard()
|
||||
focusEnd()
|
||||
}else{
|
||||
binding.addressEdit.hideKeyboard()
|
||||
}
|
||||
|
||||
binding.addressEdit.setPadding(
|
||||
binding.addressEdit.paddingLeft,
|
||||
binding.addressEdit.paddingTop,
|
||||
addressPaddingRight,
|
||||
binding.addressEdit.paddingBottom,
|
||||
)
|
||||
}
|
||||
|
||||
binding.more.setOnClickListener {
|
||||
OverflowPopup.show(binding.more){ menuId ->
|
||||
when (menuId) {
|
||||
R.id.overflow_menu_search -> {
|
||||
binding.addressEdit.hint = getString(R.string.main_input_search_hint)
|
||||
binding.addressEdit.text?.clear()
|
||||
binding.addressEdit.requestFocus()
|
||||
inSearch = true
|
||||
}
|
||||
R.id.overflow_menu_bookmark -> {
|
||||
val name = adapter.inferTitle()
|
||||
BookmarkDialog(
|
||||
this,
|
||||
BookmarkDialog.mode_new,
|
||||
bookmarkDatasource,
|
||||
binding.addressEdit.text.toString(),
|
||||
name ?: ""
|
||||
) { _, _ ->
|
||||
}.show()
|
||||
}
|
||||
R.id.overflow_menu_bookmarks -> {
|
||||
bookmarksDialog = BookmarksDialog(this, bookmarkDatasource) { bookmark ->
|
||||
model.request(bookmark.uri.toString())
|
||||
}
|
||||
bookmarksDialog?.show()
|
||||
}
|
||||
R.id.overflow_menu_share -> {
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, binding.addressEdit.text.toString())
|
||||
type = "text/plain"
|
||||
startActivity(Intent.createChooser(this, null))
|
||||
}
|
||||
}
|
||||
R.id.overflow_menu_history -> HistoryDialog.show(
|
||||
this,
|
||||
db.history()
|
||||
) { historyAddress ->
|
||||
model.request(historyAddress)
|
||||
}
|
||||
R.id.overflow_menu_about -> AboutDialog.show(this)
|
||||
R.id.overflow_menu_settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.home.setOnClickListener {
|
||||
val home = PreferenceManager.getDefaultSharedPreferences(this).getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
)
|
||||
omniTerm.history.clear()
|
||||
model.request(home!!)
|
||||
}
|
||||
|
||||
binding.pullToRefresh.setOnRefreshListener {
|
||||
refresh()
|
||||
}
|
||||
|
||||
checkIntentExtras(intent)
|
||||
}
|
||||
|
||||
private fun refresh(){
|
||||
omniTerm.getCurrent().run{
|
||||
binding.addressEdit.setText(this)
|
||||
focusEnd()
|
||||
model.request(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
when {
|
||||
prefs.contains("background_colour") -> {
|
||||
when (val backgroundColor = prefs.getString("background_colour", "#XXXXXX")) {
|
||||
"#XXXXXX" -> binding.rootCoord.background = null
|
||||
else -> binding.rootCoord.background = ColorDrawable(Color.parseColor("$backgroundColor"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
prefs.getBoolean(
|
||||
Buran.PREF_KEY_CLIENT_CERT_ACTIVE,
|
||||
false
|
||||
) -> {
|
||||
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(
|
||||
R.drawable.vector_client_cert,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
binding.addressEdit.compoundDrawablePadding = 6.toPx().toInt()
|
||||
}
|
||||
else -> hideClientCertShield()
|
||||
}
|
||||
|
||||
val useLargeGmiAdapter = prefs.getBoolean("use_large_gemtext_adapter", false)
|
||||
when {
|
||||
useLargeGmiAdapter -> {
|
||||
if(adapter.typeId != GEMTEXT_ADAPTER_LARGE){
|
||||
gemtext_recycler.adapter = null
|
||||
adapter = AbstractGemtextAdapter.getLargeGmi(onLink)
|
||||
gemtext_recycler.adapter = adapter
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if(adapter.typeId != GEMTEXT_ADAPTER_DEFAULT) {
|
||||
gemtext_recycler.adapter = null
|
||||
adapter = AbstractGemtextAdapter.getDefault(onLink)
|
||||
gemtext_recycler.adapter = adapter
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hideCodeBlocks = prefs.getBoolean(
|
||||
"collapse_code_blocks",
|
||||
false
|
||||
)
|
||||
adapter.hideCodeBlocks(hideCodeBlocks)
|
||||
|
||||
val showInlineIcons = prefs.getBoolean(
|
||||
"show_inline_icons",
|
||||
true
|
||||
)
|
||||
adapter.inlineIcons(showInlineIcons)
|
||||
|
||||
|
||||
model.invalidateDatasource()
|
||||
}
|
||||
|
||||
private fun hideClientCertShield(){
|
||||
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
private fun handleState(state: GemState) {
|
||||
binding.pullToRefresh.isRefreshing = false
|
||||
|
||||
when (state) {
|
||||
is GemState.AppQuery -> runOnUiThread { showAlert("App backdoor/query not implemented yet") }
|
||||
is GemState.ResponseInput -> runOnUiThread {
|
||||
loadingView(false)
|
||||
InputDialog.show(this, state) { queryAddress ->
|
||||
model.request(queryAddress)
|
||||
}
|
||||
}
|
||||
is GemState.Requesting -> loadingView(true)
|
||||
is GemState.NotGeminiRequest -> externalProtocol(state)
|
||||
is GemState.ResponseError -> {
|
||||
omniTerm.reset()
|
||||
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
|
||||
}
|
||||
is GemState.ClientCertError -> {
|
||||
hideClientCertShield()
|
||||
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
|
||||
}
|
||||
is GemState.ResponseGemtext -> renderGemtext(state)
|
||||
is GemState.ResponseText -> renderText(state)
|
||||
is GemState.ResponseImage -> renderImage(state)
|
||||
is GemState.ResponseBinary -> renderBinary(state)
|
||||
is GemState.Blank -> {
|
||||
binding.addressEdit.setText("")
|
||||
adapter.render(arrayListOf())
|
||||
}
|
||||
is GemState.ResponseUnknownMime -> {
|
||||
runOnUiThread {
|
||||
loadingView(false)
|
||||
|
||||
val download = getString(R.string.download)
|
||||
|
||||
AlertDialog.Builder(this, R.style.AppDialogTheme)
|
||||
.setTitle("$download: ${state.header.meta}")
|
||||
.setMessage("${state.uri}")
|
||||
.setPositiveButton(getString(R.string.download)) { _, _ ->
|
||||
loadingView(true)
|
||||
model.requestBinaryDownload(state.uri)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
is GemState.ResponseUnknownHost -> {
|
||||
runOnUiThread {
|
||||
loadingView(false)
|
||||
AlertDialog.Builder(this, R.style.AppDialogTheme)
|
||||
.setTitle(R.string.unknown_host_dialog_title)
|
||||
.setMessage("Host not found: ${state.uri}\n\nSearch with GUS instead?")
|
||||
.setPositiveButton(getString(R.string.search)) { _, _ ->
|
||||
loadingView(true)
|
||||
omniTerm.search(state.uri.toString())
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
intent?.let{
|
||||
checkIntentExtras(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Checks intent to see if Activity was opened to handle selected text
|
||||
*
|
||||
*/
|
||||
private fun checkIntentExtras(intent: Intent) {
|
||||
|
||||
//Via ProcessTextActivity from selected text in another app
|
||||
if(intent.hasExtra("process_text")){
|
||||
val processText = intent.getStringExtra("process_text")
|
||||
binding.addressEdit.setText(processText)
|
||||
model.request(processText ?: "")
|
||||
return
|
||||
}
|
||||
|
||||
//From clicking a gemini:// address
|
||||
val uri = intent.data
|
||||
if(uri != null){
|
||||
binding.addressEdit.setText(uri.toString())
|
||||
model.request(uri.toString())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAlert(message: String) = runOnUiThread{
|
||||
loadingView(false)
|
||||
|
||||
if(message.length > 40){
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.error))
|
||||
.setMessage(message)
|
||||
.setPositiveButton("OK"){ _, _ ->
|
||||
|
||||
}
|
||||
.show()
|
||||
}else {
|
||||
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun externalProtocol(state: GemState.NotGeminiRequest) = runOnUiThread {
|
||||
loadingView(false)
|
||||
val uri = state.uri.toString()
|
||||
|
||||
when {
|
||||
(uri.startsWith("http://") || uri.startsWith("https://")) -> openWebLink(uri)
|
||||
else -> {
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW)
|
||||
viewIntent.data = Uri.parse(state.uri.toString())
|
||||
|
||||
try {
|
||||
startActivity(viewIntent)
|
||||
}catch (e: ActivityNotFoundException){
|
||||
showAlert(
|
||||
String.format(
|
||||
getString(R.string.not_app_installed_that_can_open),
|
||||
state.uri
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWebLink(address: String){
|
||||
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
|
||||
Buran.PREF_KEY_USE_CUSTOM_TAB,
|
||||
true
|
||||
)) {
|
||||
val builder = CustomTabsIntent.Builder()
|
||||
val intent = builder.build()
|
||||
intent.launchUrl(this, Uri.parse(address))
|
||||
}else{
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW)
|
||||
viewIntent.data = Uri.parse(address)
|
||||
startActivity(viewIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGemtext(state: GemState.ResponseGemtext) = runOnUiThread {
|
||||
loadingView(false)
|
||||
|
||||
omniTerm.set(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())
|
||||
|
||||
adapter.render(state.lines)
|
||||
|
||||
//Scroll to top
|
||||
binding.gemtextRecycler.post {
|
||||
binding.gemtextRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
focusEnd()
|
||||
}
|
||||
|
||||
private fun renderText(state: GemState.ResponseText) = runOnUiThread {
|
||||
loadingView(false)
|
||||
TextDialog.show(this, state)
|
||||
}
|
||||
|
||||
var imageState: GemState.ResponseImage? = null
|
||||
var binaryState: GemState.ResponseBinary? = null
|
||||
|
||||
private fun renderImage(state: GemState.ResponseImage) = runOnUiThread{
|
||||
loadingView(false)
|
||||
ImageDialog.show(this, state){ state ->
|
||||
imageState = state
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "image/*"
|
||||
intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
|
||||
startActivityForResult(intent, CREATE_IMAGE_FILE_REQ)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderBinary(state: GemState.ResponseBinary) = runOnUiThread{
|
||||
loadingView(false)
|
||||
binaryState = state
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = state.header.meta
|
||||
intent.putExtra(Intent.EXTRA_TITLE, File(state.uri.path).name)
|
||||
startActivityForResult(intent, CREATE_BINARY_FILE_REQ)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if(resultCode == RESULT_OK && (requestCode == CREATE_IMAGE_FILE_REQ || requestCode == CREATE_BINARY_FILE_REQ)){
|
||||
//todo - tidy this mess up... refactor - none of this should be here
|
||||
if(imageState == null && binaryState == null) return
|
||||
data?.data?.let{ uri ->
|
||||
val cachedFile = when {
|
||||
imageState != null -> File(imageState!!.cacheUri.path ?: "")
|
||||
binaryState != null -> File(binaryState!!.cacheUri.path ?: "")
|
||||
else -> {
|
||||
println("File download error - no state object exists")
|
||||
showAlert(getString(R.string.no_state_object_exists))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
cachedFile?.let{
|
||||
contentResolver.openFileDescriptor(uri, "w")?.use { fileDescriptor ->
|
||||
FileOutputStream(fileDescriptor.fileDescriptor).use { destOutput ->
|
||||
val sourceChannel = FileInputStream(cachedFile).channel
|
||||
val destChannel = destOutput.channel
|
||||
sourceChannel.transferTo(0, sourceChannel.size(), destChannel)
|
||||
sourceChannel.close()
|
||||
destChannel.close()
|
||||
|
||||
cachedFile.deleteOnExit()
|
||||
|
||||
if(binaryState != null){
|
||||
startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS))
|
||||
}else{
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.file_saved_to_device),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageState = null
|
||||
binaryState = null
|
||||
}else if(resultCode == RESULT_OK && requestCode == CREATE_BOOKMARK_EXPORT_FILE_REQ){
|
||||
data?.data?.let{ uri ->
|
||||
bookmarksDialog?.bookmarksExportFileReady(uri)
|
||||
}
|
||||
}else if(resultCode == RESULT_OK && requestCode == CREATE_BOOKMARK_IMPORT_FILE_REQ){
|
||||
data?.data?.let{ uri ->
|
||||
bookmarksDialog?.bookmarksImportFileReady(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadingView(visible: Boolean) = runOnUiThread {
|
||||
binding.progressBar.visibleRetainingSpace(visible)
|
||||
if(visible) binding.appBar.setExpanded(true)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (omniTerm.canGoBack()){
|
||||
model.request(omniTerm.goBack())
|
||||
}else{
|
||||
println("Buran history is empty - exiting")
|
||||
super.onBackPressed()
|
||||
cacheDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun focusEnd(){
|
||||
binding.addressEdit.setSelection(binding.addressEdit.text?.length ?: 0)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
val uri = binding.addressEdit.text.toString()
|
||||
outState.putString("uri", uri)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
savedInstanceState.getString("uri")?.run {
|
||||
omniTerm.set(this)
|
||||
binding.addressEdit.setText(this)
|
||||
model.request(this)
|
||||
}
|
||||
}
|
||||
}
|
53
app/src/main/java/corewala/buran/ui/GemViewModel.kt
Normal file
53
app/src/main/java/corewala/buran/ui/GemViewModel.kt
Normal file
|
@ -0,0 +1,53 @@
|
|||
package corewala.buran.ui
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import corewala.buran.io.gemini.Datasource
|
||||
import corewala.buran.io.GemState
|
||||
import corewala.buran.io.database.BuranDatabase
|
||||
import corewala.buran.io.gemini.GeminiDatasource
|
||||
import java.net.URI
|
||||
|
||||
class GemViewModel: ViewModel() {
|
||||
|
||||
private lateinit var gemini: Datasource
|
||||
private lateinit var db: BuranDatabase
|
||||
private var onState: (state: GemState) -> Unit = {}
|
||||
|
||||
fun initialise(home: String, gemini: Datasource, db: BuranDatabase, onState: (state: GemState) -> Unit){
|
||||
this.gemini = gemini
|
||||
this.db = db
|
||||
this.onState = onState
|
||||
|
||||
request(home)
|
||||
}
|
||||
|
||||
fun request(address: String) {
|
||||
gemini.request(address){ state ->
|
||||
onState(state)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestBinaryDownload(uri: URI) {
|
||||
gemini.request(uri.toString(), true){ state ->
|
||||
onState(state)
|
||||
}
|
||||
}
|
||||
|
||||
//todo - same action as above... refactor
|
||||
fun requestInlineImage(uri: URI, onImageReady: (cacheUri: Uri?) -> Unit){
|
||||
gemini.request(uri.toString()){ state ->
|
||||
when (state) {
|
||||
is GemState.ResponseImage -> onImageReady(state.cacheUri)
|
||||
else -> onState(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If user changes client cert prefs in Settings this awful hack causes it to refresh state on next request
|
||||
fun invalidateDatasource() {
|
||||
if(gemini is GeminiDatasource){
|
||||
(gemini as GeminiDatasource).invalidate()
|
||||
}
|
||||
}
|
||||
}
|
24
app/src/main/java/corewala/buran/ui/ProcessTextActivity.kt
Normal file
24
app/src/main/java/corewala/buran/ui/ProcessTextActivity.kt
Normal file
|
@ -0,0 +1,24 @@
|
|||
package corewala.buran.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class ProcessTextActivity: AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val processText = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && intent.hasExtra(Intent.EXTRA_PROCESS_TEXT) -> intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString()
|
||||
else -> null
|
||||
}
|
||||
|
||||
Intent(this, GemActivity::class.java).run {
|
||||
putExtra("process_text", processText)
|
||||
startActivity(this)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package corewala.buran.ui.bookmarks
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import kotlinx.android.synthetic.main.fragment_bookmark_dialog.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.bookmarks.BookmarkEntry
|
||||
import corewala.buran.io.database.bookmarks.BookmarksDatasource
|
||||
import java.net.URI
|
||||
|
||||
class BookmarkDialog(
|
||||
context: Context,
|
||||
private val mode: Int,
|
||||
private val bookmarkDatasource: BookmarksDatasource?,
|
||||
val uri: String,
|
||||
val name: String,
|
||||
onDismiss: (label: String?, uri: String?) -> Unit) : AppCompatDialog(context, R.style.FSDialog) {
|
||||
|
||||
companion object{
|
||||
const val mode_new = 0
|
||||
const val mode_edit = 1
|
||||
}
|
||||
|
||||
init {
|
||||
val view = View.inflate(context, R.layout.fragment_bookmark_dialog, null)
|
||||
|
||||
setContentView(view)
|
||||
|
||||
view.bookmark_toolbar.setNavigationIcon(R.drawable.vector_close)
|
||||
view.bookmark_toolbar.setNavigationOnClickListener {
|
||||
onDismiss(null, null)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
view.bookmark_name.setText(name)
|
||||
view.bookmark_uri.setText(uri)
|
||||
|
||||
view.bookmark_toolbar.inflateMenu(R.menu.add_bookmark)
|
||||
view.bookmark_toolbar.setOnMenuItemClickListener {menuItem ->
|
||||
if(menuItem.itemId == R.id.menu_action_save_bookmark){
|
||||
|
||||
if(mode == mode_new) {
|
||||
//Determine index:
|
||||
//todo - this is expensive, just get last item, limit1?
|
||||
bookmarkDatasource?.get { allBookmarks ->
|
||||
|
||||
val index = when {
|
||||
allBookmarks.isEmpty() -> 0
|
||||
else -> allBookmarks.last().index + 1
|
||||
}
|
||||
|
||||
bookmarkDatasource.add(
|
||||
|
||||
BookmarkEntry(
|
||||
uid = -1,
|
||||
label = view.bookmark_name.text.toString(),
|
||||
uri = URI.create(view.bookmark_uri.text.toString()),
|
||||
index = index
|
||||
)
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
onDismiss(null, null)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}else if(mode == mode_edit){
|
||||
onDismiss(
|
||||
view.bookmark_name.text.toString(),
|
||||
view.bookmark_uri.text.toString())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package corewala.buran.ui.bookmarks
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.bookmark.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.bookmarks.BookmarkEntry
|
||||
import corewala.visible
|
||||
|
||||
class BookmarksAdapter(val onBookmark: (bookmarkEntry: BookmarkEntry) -> Unit, val onOverflow: (view: View, bookmarkEntry: BookmarkEntry, isFirst: Boolean, isLast: Boolean) -> Unit): RecyclerView.Adapter<BookmarksAdapter.ViewHolder>() {
|
||||
|
||||
val bookmarks = mutableListOf<BookmarkEntry>()
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
fun update(bookmarkEntries: List<BookmarkEntry>){
|
||||
this.bookmarks.clear()
|
||||
this.bookmarks.addAll(bookmarkEntries)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.bookmark, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val bookmark = bookmarks[position]
|
||||
|
||||
if(bookmark.visible) {
|
||||
holder.itemView.visible(true)
|
||||
holder.itemView.bookmark_name.text = bookmark.label
|
||||
holder.itemView.bookmark_uri.text = bookmark.uri.toString()
|
||||
|
||||
holder.itemView.bookmark_layout.setOnClickListener {
|
||||
onBookmark(bookmarks[holder.adapterPosition])
|
||||
}
|
||||
|
||||
holder.itemView.bookmark_overflow.setOnClickListener { view ->
|
||||
val isFirst = (holder.adapterPosition == 0)
|
||||
val isLast = (holder.adapterPosition == bookmarks.size - 1)
|
||||
onOverflow(view, bookmarks[holder.adapterPosition], isFirst, isLast)
|
||||
}
|
||||
}else{
|
||||
holder.itemView.visible(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = bookmarks.size
|
||||
|
||||
fun hide(bookmarkEntry: BookmarkEntry) {
|
||||
bookmarkEntry.visible = false
|
||||
notifyItemChanged(bookmarks.indexOf(bookmarkEntry))
|
||||
}
|
||||
|
||||
fun show(bookmarkEntry: BookmarkEntry) {
|
||||
bookmarkEntry.visible = true
|
||||
notifyItemChanged(bookmarks.indexOf(bookmarkEntry))
|
||||
}
|
||||
|
||||
fun remove(bookmarkEntry: BookmarkEntry){
|
||||
val index = bookmarks.indexOf(bookmarkEntry)
|
||||
bookmarks.remove(bookmarkEntry)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
250
app/src/main/java/corewala/buran/ui/bookmarks/BookmarksDialog.kt
Normal file
250
app/src/main/java/corewala/buran/ui/bookmarks/BookmarksDialog.kt
Normal file
|
@ -0,0 +1,250 @@
|
|||
package corewala.buran.ui.bookmarks
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.forEach
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.dialog_bookmarks.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.bookmarks.BookmarkEntry
|
||||
import corewala.buran.io.database.bookmarks.BookmarksDatasource
|
||||
import corewala.buran.ui.CREATE_BOOKMARK_EXPORT_FILE_REQ
|
||||
import corewala.buran.ui.CREATE_BOOKMARK_IMPORT_FILE_REQ
|
||||
import corewala.visible
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.lang.StringBuilder
|
||||
import java.net.URI
|
||||
|
||||
|
||||
class BookmarksDialog(
|
||||
context: Activity,
|
||||
private val bookmarkDatasource: BookmarksDatasource,
|
||||
onBookmark: (bookmarkEntry: BookmarkEntry) -> Unit
|
||||
): AppCompatDialog(context, R.style.FSDialog) {
|
||||
|
||||
var bookmarksAdapter: BookmarksAdapter
|
||||
|
||||
var view: View = View.inflate(context, R.layout.dialog_bookmarks, null)
|
||||
|
||||
init {
|
||||
|
||||
setContentView(view)
|
||||
|
||||
view.bookmarks_toolbar.setNavigationIcon(R.drawable.vector_close)
|
||||
view.bookmarks_toolbar.setNavigationOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
view.bookmarks_toolbar.menu.forEach { menu ->
|
||||
menu.setOnMenuItemClickListener { item ->
|
||||
when(item.itemId){
|
||||
R.id.menu_action_import_bookmarks -> {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.type = "application/json"
|
||||
context.startActivityForResult(intent, CREATE_BOOKMARK_IMPORT_FILE_REQ)
|
||||
}
|
||||
R.id.menu_action_export_bookmarks -> {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "application/json"
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "buran_bookmarks.json")
|
||||
context.startActivityForResult(intent, CREATE_BOOKMARK_EXPORT_FILE_REQ)
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//None as yet
|
||||
view.bookmarks_toolbar.inflateMenu(R.menu.add_bookmarks)
|
||||
view.bookmarks_toolbar.setOnMenuItemClickListener { _ ->
|
||||
true
|
||||
}
|
||||
|
||||
view.bookmarks_recycler.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
bookmarksAdapter = BookmarksAdapter({ bookmark ->
|
||||
//onBookmark
|
||||
onBookmark(bookmark)
|
||||
dismiss()
|
||||
|
||||
}){ view, bookmark, isFirst, isLast ->
|
||||
//onOverflow
|
||||
val bookmarkOverflow = PopupMenu(context, view)
|
||||
|
||||
bookmarkOverflow.inflate(R.menu.menu_bookmark)
|
||||
|
||||
if(isFirst) bookmarkOverflow.menu.removeItem(R.id.menu_bookmark_move_up)
|
||||
if(isLast) bookmarkOverflow.menu.removeItem(R.id.menu_bookmark_move_down)
|
||||
|
||||
bookmarkOverflow.setOnMenuItemClickListener { menuItem ->
|
||||
when(menuItem.itemId){
|
||||
R.id.menu_bookmark_edit -> edit(bookmark)
|
||||
R.id.menu_bookmark_delete -> delete(bookmark)
|
||||
R.id.menu_bookmark_move_up -> moveUp(bookmark)
|
||||
R.id.menu_bookmark_move_down -> moveDown(bookmark)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
bookmarkOverflow.show()
|
||||
}
|
||||
|
||||
view.bookmarks_recycler.adapter = bookmarksAdapter
|
||||
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
when {
|
||||
bookmarks.isEmpty() -> view.bookmarks_empty_layout.visible(true)
|
||||
else -> bookmarksAdapter.update(bookmarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun edit(bookmarkEntry: BookmarkEntry){
|
||||
BookmarkDialog(
|
||||
context,
|
||||
BookmarkDialog.mode_edit,
|
||||
null,
|
||||
bookmarkEntry.uri.toString(),
|
||||
bookmarkEntry.label
|
||||
){ label, uri ->
|
||||
bookmarkDatasource.update(bookmarkEntry, label, uri){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Bookmark isn't actually deleted from the DB until the Snackbar disappears. Which is nice.
|
||||
*
|
||||
*/
|
||||
private fun delete(bookmarkEntry: BookmarkEntry){
|
||||
//OnDelete
|
||||
bookmarksAdapter.hide(bookmarkEntry)
|
||||
Snackbar.make(view, "Deleted ${bookmarkEntry.label}", Snackbar.LENGTH_SHORT).addCallback(
|
||||
object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) = when (event) {
|
||||
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_ACTION -> bookmarksAdapter.show(
|
||||
bookmarkEntry
|
||||
)
|
||||
else -> bookmarkDatasource.delete(bookmarkEntry) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.remove(bookmarkEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).setAction("Undo"){
|
||||
//Action listener unused
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun moveUp(bookmarkEntry: BookmarkEntry){
|
||||
bookmarkDatasource.moveUp(bookmarkEntry){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveDown(bookmarkEntry: BookmarkEntry){
|
||||
bookmarkDatasource.moveDown(bookmarkEntry){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarksExportFileReady(uri: Uri){
|
||||
val model = BookmarksViewModel()
|
||||
model.initialise(bookmarkDatasource){
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "Bookmarks Exported", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
model.exportBookmarks(context.contentResolver, uri)
|
||||
}
|
||||
|
||||
fun bookmarksImportFileReady(uri: Uri){
|
||||
context.contentResolver.openInputStream(uri).use{ inputStream ->
|
||||
InputStreamReader(inputStream).use { streamReader ->
|
||||
BufferedReader(streamReader).use { bufferedReader ->
|
||||
val sb = StringBuilder()
|
||||
var line: String?
|
||||
while (bufferedReader.readLine().also { line = it } != null) {
|
||||
sb.append(line).append('\n')
|
||||
}
|
||||
val bookmarksRawJson = sb.toString()
|
||||
val bookmarksJson = JSONObject(bookmarksRawJson)
|
||||
val bookmarks = bookmarksJson.getJSONArray("bookmarks")
|
||||
val bookmarkEntries = arrayListOf<BookmarkEntry>()
|
||||
|
||||
var skipped = 0
|
||||
var added = 0
|
||||
|
||||
repeat(bookmarks.length()){ index ->
|
||||
val bookmark = bookmarks.getJSONObject(index)
|
||||
val bookmarkLabel = bookmark.getString("label")
|
||||
val bookmarkUri = bookmark.getString("uri")
|
||||
println("Importing bookmark: $bookmarkLabel : $uri")
|
||||
val existing = bookmarksAdapter.bookmarks.filter { entry ->
|
||||
entry.uri.toString() == bookmarkUri
|
||||
}
|
||||
when {
|
||||
existing.isNotEmpty() -> skipped++
|
||||
else -> {
|
||||
added++
|
||||
bookmarkEntries.add(BookmarkEntry(-1, bookmarkLabel, URI.create(bookmarkUri), index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookmarkDatasource.add(bookmarkEntries.toTypedArray()){
|
||||
bookmarkDatasource.get { bookmarks ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view.bookmarks_empty_layout.visible(false)
|
||||
bookmarksAdapter.update(bookmarks)
|
||||
when {
|
||||
skipped > 0 -> {
|
||||
Toast.makeText(context, "$added bookmarks imported ($skipped duplicates)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> Toast.makeText(context, "$added bookmarks imported", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package corewala.buran.ui.bookmarks
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import corewala.buran.io.database.bookmarks.BookmarksDatasource
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.FileOutputStream
|
||||
import java.io.PrintStream
|
||||
|
||||
/**
|
||||
*
|
||||
* Pseudo viewmodel for now until I can find time to refactor the entire dialog - putting new functionality here
|
||||
*
|
||||
*/
|
||||
class BookmarksViewModel {
|
||||
|
||||
lateinit var datasource: BookmarksDatasource
|
||||
|
||||
var onExport: () -> Unit = {}
|
||||
|
||||
fun initialise(datasource: BookmarksDatasource, onExport: () -> Unit){
|
||||
this.datasource = datasource
|
||||
this.onExport = onExport
|
||||
}
|
||||
|
||||
|
||||
fun exportBookmarks(contentResolver: ContentResolver, uri: Uri){
|
||||
datasource.get { bookmarks ->
|
||||
val json = JSONObject()
|
||||
val bookmarksJson = JSONArray()
|
||||
|
||||
|
||||
bookmarks.forEach { entry ->
|
||||
val bookmarkJson = JSONObject()
|
||||
bookmarkJson.put("label", entry.label)
|
||||
bookmarkJson.put("uri", entry.uri)
|
||||
bookmarksJson.put(bookmarkJson)
|
||||
}
|
||||
|
||||
json.put("bookmarks", bookmarksJson)
|
||||
|
||||
val bookmarks = json.toString(2)
|
||||
println("Bookmarks json to export: $bookmarks")
|
||||
|
||||
contentResolver.openFileDescriptor(uri, "w")?.use { fileDescriptor ->
|
||||
FileOutputStream(fileDescriptor.fileDescriptor).use { os ->
|
||||
PrintStream(os).use{
|
||||
it.print(bookmarks)
|
||||
it.flush()
|
||||
it.close()
|
||||
os.close()
|
||||
onExport.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package corewala.buran.ui.content_image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import kotlinx.android.synthetic.main.dialog_content_image.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.GemState
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object ImageDialog {
|
||||
|
||||
fun show(context: Context, state: GemState.ResponseImage, onDownloadRequest: (state: GemState.ResponseImage) -> Unit){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_content_image, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.image_view.setImageURI(state.cacheUri)
|
||||
|
||||
view.close_image_content_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
view.image_overflow.setOnClickListener {
|
||||
val overflowMenu = PopupMenu(context, view.image_overflow)
|
||||
val inflater: MenuInflater = overflowMenu.menuInflater
|
||||
inflater.inflate(R.menu.image_overflow_menu, overflowMenu.menu)
|
||||
overflowMenu.setOnMenuItemClickListener { menuItem ->
|
||||
if(menuItem.itemId == R.id.image_overflow_save_image){
|
||||
onDownloadRequest(state)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
overflowMenu.show()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Save bitmap using Storage Access Framework Uri
|
||||
* @param bitmap
|
||||
* @param uri - must be a SAF Uri
|
||||
* @param onComplete
|
||||
*/
|
||||
fun publicExport(context: Context, bitmap: Bitmap?, uri: Uri, onComplete: (uri: Uri) -> Unit) {
|
||||
context.contentResolver.openFileDescriptor(uri, "w")?.use {
|
||||
FileOutputStream(it.fileDescriptor).use { outputStream ->
|
||||
bitmap?.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||
}
|
||||
bitmap?.recycle()
|
||||
onComplete(uri)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
package corewala.buran.ui.content_image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
*
|
||||
* From SO: https://stackoverflow.com/a/54474590/7641428
|
||||
*
|
||||
* todo - Rewrite in Kotlin at some point, possibly, maybe, never
|
||||
*
|
||||
*/
|
||||
public class TouchImageView extends androidx.appcompat.widget.AppCompatImageView implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
|
||||
|
||||
Matrix matrix;
|
||||
|
||||
// We can be in one of these 3 states
|
||||
static final int NONE = 0;
|
||||
static final int DRAG = 1;
|
||||
static final int ZOOM = 2;
|
||||
int mode = NONE;
|
||||
|
||||
// Remember some things for zooming
|
||||
PointF last = new PointF();
|
||||
PointF start = new PointF();
|
||||
float minScale = 0.9f;
|
||||
float maxScale = 3f;
|
||||
float[] m;
|
||||
|
||||
int viewWidth, viewHeight;
|
||||
static final int CLICK = 3;
|
||||
float saveScale = 1f;
|
||||
protected float origWidth, origHeight;
|
||||
int oldMeasuredWidth, oldMeasuredHeight;
|
||||
|
||||
ScaleGestureDetector mScaleDetector;
|
||||
|
||||
Context context;
|
||||
|
||||
public TouchImageView(Context context) {
|
||||
super(context);
|
||||
sharedConstructing(context);
|
||||
}
|
||||
|
||||
public TouchImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
sharedConstructing(context);
|
||||
}
|
||||
|
||||
GestureDetector mGestureDetector;
|
||||
|
||||
private void sharedConstructing(Context context) {
|
||||
super.setClickable(true);
|
||||
this.context = context;
|
||||
mGestureDetector = new GestureDetector(context, this);
|
||||
mGestureDetector.setOnDoubleTapListener(this);
|
||||
|
||||
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
|
||||
matrix = new Matrix();
|
||||
m = new float[9];
|
||||
setImageMatrix(matrix);
|
||||
setScaleType(ScaleType.MATRIX);
|
||||
|
||||
setOnTouchListener(new OnTouchListener() {
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
mScaleDetector.onTouchEvent(event);
|
||||
mGestureDetector.onTouchEvent(event);
|
||||
|
||||
PointF curr = new PointF(event.getX(), event.getY());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
last.set(curr);
|
||||
start.set(last);
|
||||
mode = DRAG;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mode == DRAG) {
|
||||
float deltaX = curr.x - last.x;
|
||||
float deltaY = curr.y - last.y;
|
||||
float fixTransX = getFixDragTrans(deltaX, viewWidth,
|
||||
origWidth * saveScale);
|
||||
float fixTransY = getFixDragTrans(deltaY, viewHeight,
|
||||
origHeight * saveScale);
|
||||
matrix.postTranslate(fixTransX, fixTransY);
|
||||
fixTrans();
|
||||
last.set(curr.x, curr.y);
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
mode = NONE;
|
||||
int xDiff = (int) Math.abs(curr.x - start.x);
|
||||
int yDiff = (int) Math.abs(curr.y - start.y);
|
||||
if (xDiff < CLICK && yDiff < CLICK)
|
||||
performClick();
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
mode = NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
setImageMatrix(matrix);
|
||||
invalidate();
|
||||
return true; // indicate event was handled
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setMaxZoom(float x) {
|
||||
maxScale = x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
saveScale = 1f;
|
||||
fixTrans();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
// Double tap is detected
|
||||
|
||||
float origScale = saveScale;
|
||||
float mScaleFactor;
|
||||
|
||||
if (saveScale == maxScale) {
|
||||
saveScale = minScale;
|
||||
mScaleFactor = minScale / origScale;
|
||||
} else {
|
||||
saveScale = maxScale;
|
||||
mScaleFactor = maxScale / origScale;
|
||||
}
|
||||
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);
|
||||
|
||||
fixTrans();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
mode = ZOOM;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
float mScaleFactor = detector.getScaleFactor();
|
||||
float origScale = saveScale;
|
||||
saveScale *= mScaleFactor;
|
||||
if (saveScale > maxScale) {
|
||||
saveScale = maxScale;
|
||||
mScaleFactor = maxScale / origScale;
|
||||
} else if (saveScale < minScale) {
|
||||
saveScale = minScale;
|
||||
mScaleFactor = minScale / origScale;
|
||||
}
|
||||
|
||||
if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) {
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);
|
||||
}else {
|
||||
matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
|
||||
}
|
||||
|
||||
fixTrans();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void fixTrans() {
|
||||
matrix.getValues(m);
|
||||
float transX = m[Matrix.MTRANS_X];
|
||||
float transY = m[Matrix.MTRANS_Y];
|
||||
|
||||
float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
|
||||
float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale);
|
||||
|
||||
if (fixTransX != 0 || fixTransY != 0) matrix.postTranslate(fixTransX, fixTransY);
|
||||
}
|
||||
|
||||
float getFixTrans(float trans, float viewSize, float contentSize) {
|
||||
float minTrans, maxTrans;
|
||||
|
||||
if (contentSize <= viewSize) {
|
||||
minTrans = 0;
|
||||
maxTrans = viewSize - contentSize;
|
||||
} else {
|
||||
minTrans = viewSize - contentSize;
|
||||
maxTrans = 0;
|
||||
}
|
||||
|
||||
if (trans < minTrans) return -trans + minTrans;
|
||||
if (trans > maxTrans) return -trans + maxTrans;
|
||||
return 0;
|
||||
}
|
||||
|
||||
float getFixDragTrans(float delta, float viewSize, float contentSize) {
|
||||
if (contentSize <= viewSize) {
|
||||
return 0;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
viewHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
//
|
||||
// Rescales image on rotation
|
||||
//
|
||||
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return;
|
||||
oldMeasuredHeight = viewHeight;
|
||||
oldMeasuredWidth = viewWidth;
|
||||
|
||||
if (saveScale == 1) {
|
||||
// Fit to screen.
|
||||
float scale;
|
||||
|
||||
Drawable drawable = getDrawable();
|
||||
if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) return;
|
||||
int bmWidth = drawable.getIntrinsicWidth();
|
||||
int bmHeight = drawable.getIntrinsicHeight();
|
||||
|
||||
Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);
|
||||
|
||||
float scaleX = (float) viewWidth / (float) bmWidth;
|
||||
float scaleY = (float) viewHeight / (float) bmHeight;
|
||||
scale = Math.min(scaleX, scaleY);
|
||||
matrix.setScale(scale, scale);
|
||||
|
||||
// Center the image
|
||||
float redundantYSpace = (float) viewHeight - (scale * (float) bmHeight);
|
||||
float redundantXSpace = (float) viewWidth - (scale * (float) bmWidth);
|
||||
redundantYSpace /= (float) 2;
|
||||
redundantXSpace /= (float) 2;
|
||||
|
||||
matrix.postTranslate(redundantXSpace, redundantYSpace);
|
||||
|
||||
origWidth = viewWidth - 2 * redundantXSpace;
|
||||
origHeight = viewHeight - 2 * redundantYSpace;
|
||||
setImageMatrix(matrix);
|
||||
}
|
||||
fixTrans();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package corewala.buran.ui.content_text
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import kotlinx.android.synthetic.main.dialog_content_text.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.GemState
|
||||
|
||||
object TextDialog {
|
||||
|
||||
fun show(context: Context, state: GemState.ResponseText){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_content_text, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.text_content.text = state.content
|
||||
|
||||
view.close_text_content_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package corewala.buran.ui.gemtext_adapters
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.net.URI
|
||||
|
||||
const val GEMTEXT_ADAPTER_DEFAULT = 0
|
||||
const val GEMTEXT_ADAPTER_LARGE = 1
|
||||
|
||||
abstract class AbstractGemtextAdapter(
|
||||
val typeId: Int,
|
||||
val onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit
|
||||
): RecyclerView.Adapter<GmiViewHolder>() {
|
||||
|
||||
var showInlineIcons: Boolean = false
|
||||
var hideCodeBlocks: Boolean = false
|
||||
|
||||
abstract fun render(lines: List<String>)
|
||||
abstract fun loadImage(position: Int, cacheUri: Uri)
|
||||
abstract fun inlineIcons(visible: Boolean)
|
||||
abstract fun hideCodeBlocks(hideCodeBlocks: Boolean)
|
||||
|
||||
abstract fun inferTitle(): String?
|
||||
|
||||
companion object{
|
||||
fun getDefault(onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit): AbstractGemtextAdapter {
|
||||
return DefaultGemtextAdapter(GEMTEXT_ADAPTER_DEFAULT, onLink)
|
||||
}
|
||||
|
||||
fun getLargeGmi(onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit): AbstractGemtextAdapter {
|
||||
return LargeGemtextAdapter(GEMTEXT_ADAPTER_LARGE, onLink)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package corewala.buran.ui.gemtext_adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.gemtext_code_block.view.*
|
||||
import kotlinx.android.synthetic.main.gemtext_image_link.view.*
|
||||
import kotlinx.android.synthetic.main.gemtext_link.view.gemtext_text_link
|
||||
import kotlinx.android.synthetic.main.gemtext_text.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.endsWithImage
|
||||
import corewala.visible
|
||||
import java.net.URI
|
||||
|
||||
class DefaultGemtextAdapter(
|
||||
typeId: Int,
|
||||
onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit)
|
||||
: AbstractGemtextAdapter(typeId, onLink) {
|
||||
|
||||
private var lines = mutableListOf<String>()
|
||||
private var inlineImages = HashMap<Int, Uri>()
|
||||
|
||||
private val typeText = 0
|
||||
private val typeH1 = 1
|
||||
private val typeH2 = 2
|
||||
private val typeH3 = 3
|
||||
private val typeListItem = 4
|
||||
private val typeImageLink = 5
|
||||
private val typeLink = 6
|
||||
private val typeCodeBlock = 7
|
||||
private val typeQuote = 8
|
||||
|
||||
override fun render(lines: List<String>){
|
||||
this.inlineImages.clear()
|
||||
this.lines.clear()
|
||||
this.lines.addAll(lines)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun inflate(parent: ViewGroup, layout: Int): View{
|
||||
return LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GmiViewHolder {
|
||||
return when(viewType){
|
||||
typeText -> GmiViewHolder.Text(inflate(parent, R.layout.gemtext_text))
|
||||
typeH1 -> GmiViewHolder.H1(inflate(parent, R.layout.gemtext_h1))
|
||||
typeH2 -> GmiViewHolder.H2(inflate(parent, R.layout.gemtext_h2))
|
||||
typeH3 -> GmiViewHolder.H3(inflate(parent, R.layout.gemtext_h3))
|
||||
typeListItem -> GmiViewHolder.ListItem(inflate(parent, R.layout.gemtext_text))
|
||||
typeImageLink -> GmiViewHolder.ImageLink(inflate(parent, R.layout.gemtext_image_link))
|
||||
typeLink -> GmiViewHolder.Link(inflate(parent, R.layout.gemtext_link))
|
||||
typeCodeBlock-> GmiViewHolder.Code(inflate(parent, R.layout.gemtext_code_block))
|
||||
typeQuote -> GmiViewHolder.Quote(inflate(parent, R.layout.gemtext_quote))
|
||||
else -> GmiViewHolder.Text(inflate(parent, R.layout.gemtext_text))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val line = lines[position]
|
||||
return when {
|
||||
line.startsWith("```") -> typeCodeBlock
|
||||
line.startsWith("###") -> typeH3
|
||||
line.startsWith("##") -> typeH2
|
||||
line.startsWith("#") -> typeH1
|
||||
line.startsWith("*") -> typeListItem
|
||||
line.startsWith("=>") && getLink(line).endsWithImage() -> typeImageLink
|
||||
line.startsWith("=>") -> typeLink
|
||||
line.startsWith(">") -> typeQuote
|
||||
else -> typeText
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = lines.size
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GmiViewHolder, position: Int) {
|
||||
val line = lines[position]
|
||||
|
||||
when(holder){
|
||||
is GmiViewHolder.Text -> holder.itemView.gemtext_text_textview.text = line
|
||||
is GmiViewHolder.Code -> {
|
||||
|
||||
var altText: String? = null
|
||||
|
||||
if(line.startsWith("```<|ALT|>")){
|
||||
//there's alt text: "```<|ALT|>$alt</|ALT>"
|
||||
altText = line.substring(10, line.indexOf("</|ALT>"))
|
||||
holder.itemView.gemtext_text_monospace_textview.text = line.substring(line.indexOf("</|ALT>") + 7)
|
||||
}else{
|
||||
holder.itemView.gemtext_text_monospace_textview.text = line.substring(3)
|
||||
}
|
||||
|
||||
if(hideCodeBlocks){
|
||||
holder.itemView.show_code_block.setText(R.string.show_code)//reset for recycling
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
holder.itemView.show_code_block.visible(true)
|
||||
holder.itemView.show_code_block.paint.isUnderlineText = true
|
||||
holder.itemView.show_code_block.setOnClickListener {
|
||||
setupCodeBlockToggle(holder, altText)
|
||||
}
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(false)
|
||||
|
||||
when {
|
||||
showInlineIcons -> holder.itemView.show_code_block.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.vector_code, 0)
|
||||
else -> holder.itemView.show_code_block.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}else{
|
||||
holder.itemView.show_code_block.visible(false)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(true)
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.Quote -> holder.itemView.gemtext_text_monospace_textview.text = line.substring(1).trim()
|
||||
is GmiViewHolder.H1 -> {
|
||||
when {
|
||||
line.length > 2 -> holder.itemView.gemtext_text_textview.text = line.substring(2).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.H2 -> {
|
||||
when {
|
||||
line.length > 3 -> holder.itemView.gemtext_text_textview.text = line.substring(3).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.H3 -> {
|
||||
when {
|
||||
line.length > 4 -> holder.itemView.gemtext_text_textview.text = line.substring(4).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.ListItem -> holder.itemView.gemtext_text_textview.text = "• ${line.substring(1)}".trim()
|
||||
is GmiViewHolder.Link -> {
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
var linkName = linkParts[0]
|
||||
|
||||
if(linkParts.size > 1) linkName = linkParts[1]
|
||||
|
||||
val displayText = linkName
|
||||
holder.itemView.gemtext_text_link.text = displayText
|
||||
holder.itemView.gemtext_text_link.paint.isUnderlineText = true
|
||||
|
||||
when {
|
||||
showInlineIcons && linkParts.first().startsWith("http") -> holder.itemView.gemtext_text_link.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.vector_open_browser, 0)
|
||||
else -> holder.itemView.gemtext_text_link.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
holder.itemView.gemtext_text_link.setOnClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User clicked link: $uri")
|
||||
onLink(uri, false, holder.adapterPosition)
|
||||
|
||||
}
|
||||
holder.itemView.gemtext_text_link.setOnLongClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User long-clicked link: $uri")
|
||||
onLink(uri, true, holder.adapterPosition)
|
||||
true
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.ImageLink -> {
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
var linkName = linkParts[0]
|
||||
|
||||
if(linkParts.size > 1) linkName = linkParts[1]
|
||||
|
||||
val displayText = linkName
|
||||
holder.itemView.gemtext_text_link.text = displayText
|
||||
holder.itemView.gemtext_text_link.paint.isUnderlineText = true
|
||||
holder.itemView.gemtext_text_link.setOnClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User clicked link: $uri")
|
||||
onLink(uri, false, holder.adapterPosition)
|
||||
|
||||
}
|
||||
holder.itemView.gemtext_text_link.setOnLongClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User long-clicked link: $uri")
|
||||
onLink(uri, true, holder.adapterPosition)
|
||||
true
|
||||
}
|
||||
|
||||
when {
|
||||
inlineImages.containsKey(position) -> {
|
||||
holder.itemView.gemtext_inline_image.visible(true)
|
||||
holder.itemView.gemtext_inline_image.setImageURI(inlineImages[position])
|
||||
}
|
||||
else -> holder.itemView.gemtext_inline_image.visible(false)
|
||||
}
|
||||
|
||||
when {
|
||||
showInlineIcons -> holder.itemView.gemtext_text_link.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.vector_photo, 0)
|
||||
else -> holder.itemView.gemtext_text_link.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCodeBlockToggle(holder: GmiViewHolder.Code, altText: String?) {
|
||||
//val adapterPosition = holder.adapterPosition
|
||||
when {
|
||||
holder.itemView.gemtext_text_monospace_textview.isVisible -> {
|
||||
holder.itemView.show_code_block.setText(R.string.show_code)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(false)
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
holder.itemView.show_code_block.setText(R.string.hide_code)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(true)
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLink(line: String): String{
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
return linkParts[0]
|
||||
}
|
||||
|
||||
private fun getUri(linkLine: String): URI{
|
||||
val linkParts = linkLine.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
return URI.create(linkParts.first())
|
||||
}
|
||||
|
||||
override fun inferTitle(): String? {
|
||||
lines.forEach { line ->
|
||||
if(line.startsWith("#")) return line.replace("#", "").trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun loadImage(position: Int, cacheUri: Uri){
|
||||
inlineImages[position] = cacheUri
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
override fun inlineIcons(visible: Boolean){
|
||||
this.showInlineIcons = visible
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun hideCodeBlocks(hideCodeBlocks: Boolean) {
|
||||
this.hideCodeBlocks = hideCodeBlocks
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package corewala.buran.ui.gemtext_adapters
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
sealed class GmiViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
|
||||
class Text(itemView: View): GmiViewHolder(itemView)
|
||||
class H1(itemView: View): GmiViewHolder(itemView)
|
||||
class H2(itemView: View): GmiViewHolder(itemView)
|
||||
class H3(itemView: View): GmiViewHolder(itemView)
|
||||
class ListItem(itemView: View): GmiViewHolder(itemView)
|
||||
class ImageLink(itemView: View): GmiViewHolder(itemView)
|
||||
class Link(itemView: View): GmiViewHolder(itemView)
|
||||
class Code(itemView: View): GmiViewHolder(itemView)
|
||||
class Quote(itemView: View): GmiViewHolder(itemView)
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
package corewala.buran.ui.gemtext_adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.gemtext_large_code_block.view.*
|
||||
import kotlinx.android.synthetic.main.gemtext_large_image_link.view.*
|
||||
import kotlinx.android.synthetic.main.gemtext_large_link.view.gemtext_text_link
|
||||
import kotlinx.android.synthetic.main.gemtext_large_text.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.endsWithImage
|
||||
import corewala.visible
|
||||
import java.net.URI
|
||||
|
||||
class LargeGemtextAdapter(
|
||||
typeId: Int,
|
||||
onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit)
|
||||
: AbstractGemtextAdapter(typeId, onLink) {
|
||||
|
||||
private var lines = mutableListOf<String>()
|
||||
private var inlineImages = HashMap<Int, Uri>()
|
||||
|
||||
private val typeText = 0
|
||||
private val typeH1 = 1
|
||||
private val typeH2 = 2
|
||||
private val typeH3 = 3
|
||||
private val typeListItem = 4
|
||||
private val typeImageLink = 5
|
||||
private val typeLink = 6
|
||||
private val typeCodeBlock = 7
|
||||
private val typeQuote = 8
|
||||
|
||||
override fun render(lines: List<String>){
|
||||
this.inlineImages.clear()
|
||||
this.lines.clear()
|
||||
this.lines.addAll(lines)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun inflate(parent: ViewGroup, layout: Int): View{
|
||||
return LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GmiViewHolder {
|
||||
return when(viewType){
|
||||
typeText -> GmiViewHolder.Text(inflate(parent, R.layout.gemtext_large_text))
|
||||
typeH1 -> GmiViewHolder.H1(inflate(parent, R.layout.gemtext_large_h1))
|
||||
typeH2 -> GmiViewHolder.H2(inflate(parent, R.layout.gemtext_large_h2))
|
||||
typeH3 -> GmiViewHolder.H3(inflate(parent, R.layout.gemtext_large_h3))
|
||||
typeListItem -> GmiViewHolder.ListItem(inflate(parent, R.layout.gemtext_large_text))
|
||||
typeImageLink -> GmiViewHolder.ImageLink(inflate(parent, R.layout.gemtext_large_image_link))
|
||||
typeLink -> GmiViewHolder.Link(inflate(parent, R.layout.gemtext_large_link))
|
||||
typeCodeBlock-> GmiViewHolder.Code(inflate(parent, R.layout.gemtext_large_code_block))
|
||||
typeQuote -> GmiViewHolder.Quote(inflate(parent, R.layout.gemtext_large_quote))
|
||||
else -> GmiViewHolder.Text(inflate(parent, R.layout.gemtext_large_text))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val line = lines[position]
|
||||
return when {
|
||||
line.startsWith("```") -> typeCodeBlock
|
||||
line.startsWith("###") -> typeH3
|
||||
line.startsWith("##") -> typeH2
|
||||
line.startsWith("#") -> typeH1
|
||||
line.startsWith("*") -> typeListItem
|
||||
line.startsWith("=>") && getLink(line).endsWithImage() -> typeImageLink
|
||||
line.startsWith("=>") -> typeLink
|
||||
line.startsWith(">") -> typeQuote
|
||||
else -> typeText
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = lines.size
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GmiViewHolder, position: Int) {
|
||||
val line = lines[position]
|
||||
|
||||
when(holder){
|
||||
is GmiViewHolder.Text -> holder.itemView.gemtext_text_textview.text = line
|
||||
is GmiViewHolder.Code -> {
|
||||
|
||||
var altText: String? = null
|
||||
|
||||
if(line.startsWith("```<|ALT|>")){
|
||||
//there's alt text: "```<|ALT|>$alt</|ALT>"
|
||||
altText = line.substring(10, line.indexOf("</|ALT>"))
|
||||
holder.itemView.gemtext_text_monospace_textview.text = line.substring(line.indexOf("</|ALT>") + 7)
|
||||
}else{
|
||||
holder.itemView.gemtext_text_monospace_textview.text = line.substring(3)
|
||||
}
|
||||
|
||||
if(hideCodeBlocks){
|
||||
holder.itemView.show_code_block.setText(R.string.show_code)//reset for recycling
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
holder.itemView.show_code_block.visible(true)
|
||||
holder.itemView.show_code_block.setOnClickListener {
|
||||
setupCodeBlockToggle(holder, altText)
|
||||
}
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(false)
|
||||
|
||||
when {
|
||||
showInlineIcons -> holder.itemView.show_code_block.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.vector_code, 0)
|
||||
else -> holder.itemView.show_code_block.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}else{
|
||||
holder.itemView.show_code_block.visible(false)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(true)
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.Quote -> holder.itemView.gemtext_text_monospace_textview.text = line.substring(1).trim()
|
||||
is GmiViewHolder.H1 -> {
|
||||
when {
|
||||
line.length > 2 -> holder.itemView.gemtext_text_textview.text = line.substring(2).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.H2 -> {
|
||||
when {
|
||||
line.length > 3 -> holder.itemView.gemtext_text_textview.text = line.substring(3).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.H3 -> {
|
||||
when {
|
||||
line.length > 4 -> holder.itemView.gemtext_text_textview.text = line.substring(4).trim()
|
||||
else -> holder.itemView.gemtext_text_textview.text = ""
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.ListItem -> holder.itemView.gemtext_text_textview.text = "• ${line.substring(1)}".trim()
|
||||
is GmiViewHolder.Link -> {
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
var linkName = linkParts[0]
|
||||
|
||||
if(linkParts.size > 1) linkName = linkParts[1]
|
||||
|
||||
val displayText = linkName
|
||||
holder.itemView.gemtext_text_link.text = displayText
|
||||
|
||||
when {
|
||||
showInlineIcons && linkParts.first().startsWith("http") -> holder.itemView.gemtext_text_link.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.vector_open_browser, 0)
|
||||
else -> holder.itemView.gemtext_text_link.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
holder.itemView.gemtext_text_link.setOnClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User clicked link: $uri")
|
||||
onLink(uri, false, holder.adapterPosition)
|
||||
|
||||
}
|
||||
holder.itemView.gemtext_text_link.setOnLongClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User long-clicked link: $uri")
|
||||
onLink(uri, true, holder.adapterPosition)
|
||||
true
|
||||
}
|
||||
}
|
||||
is GmiViewHolder.ImageLink -> {
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
var linkName = linkParts[0]
|
||||
|
||||
if(linkParts.size > 1) linkName = linkParts[1]
|
||||
|
||||
val displayText = linkName
|
||||
holder.itemView.gemtext_text_link.text = displayText
|
||||
holder.itemView.gemtext_text_link.setOnClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User clicked link: $uri")
|
||||
onLink(uri, false, holder.adapterPosition)
|
||||
|
||||
}
|
||||
holder.itemView.gemtext_text_link.setOnLongClickListener {
|
||||
val uri = getUri(lines[holder.adapterPosition])
|
||||
println("User long-clicked link: $uri")
|
||||
onLink(uri, true, holder.adapterPosition)
|
||||
true
|
||||
}
|
||||
|
||||
when {
|
||||
inlineImages.containsKey(position) -> {
|
||||
holder.itemView.gemtext_inline_image.visible(true)
|
||||
holder.itemView.gemtext_inline_image.setImageURI(inlineImages[position])
|
||||
}
|
||||
else -> holder.itemView.gemtext_inline_image.visible(false)
|
||||
}
|
||||
|
||||
when {
|
||||
showInlineIcons -> holder.itemView.gemtext_text_link.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.vector_photo, 0)
|
||||
else -> holder.itemView.gemtext_text_link.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCodeBlockToggle(holder: GmiViewHolder.Code, altText: String?) {
|
||||
//val adapterPosition = holder.adapterPosition
|
||||
when {
|
||||
holder.itemView.gemtext_text_monospace_textview.isVisible -> {
|
||||
holder.itemView.show_code_block.setText(R.string.show_code)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(false)
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
holder.itemView.show_code_block.setText(R.string.hide_code)
|
||||
holder.itemView.gemtext_text_monospace_textview.visible(true)
|
||||
altText?.let{
|
||||
holder.itemView.show_code_block.append(": $altText")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLink(line: String): String{
|
||||
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
return linkParts[0]
|
||||
}
|
||||
|
||||
private fun getUri(linkLine: String): URI{
|
||||
val linkParts = linkLine.substring(2).trim().split("\\s+".toRegex(), 2)
|
||||
return URI.create(linkParts.first())
|
||||
}
|
||||
|
||||
override fun inferTitle(): String? {
|
||||
lines.forEach { line ->
|
||||
if(line.startsWith("#")) return line.replace("#", "").trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun loadImage(position: Int, cacheUri: Uri){
|
||||
inlineImages[position] = cacheUri
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
override fun inlineIcons(visible: Boolean){
|
||||
this.showInlineIcons = visible
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun hideCodeBlocks(hideCodeBlocks: Boolean) {
|
||||
this.hideCodeBlocks = hideCodeBlocks
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package corewala.buran.ui.modals_menus
|
||||
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import corewala.buran.R
|
||||
import corewala.endsWithImage
|
||||
import corewala.isWeb
|
||||
import java.net.URI
|
||||
|
||||
object LinkPopup {
|
||||
|
||||
fun show(view: View?, uri: URI, onMenuOption: (menuId: Int) -> Unit){
|
||||
if(view != null) {
|
||||
|
||||
val popup = PopupMenu(view.context, view)
|
||||
val inflater: MenuInflater = popup.menuInflater
|
||||
|
||||
val uriStr = uri.toString()
|
||||
|
||||
when {
|
||||
uriStr.endsWithImage() && !uriStr.isWeb() -> inflater.inflate(R.menu.image_link_menu, popup.menu)
|
||||
else -> inflater.inflate(R.menu.link_menu, popup.menu)
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onMenuOption(menuItem.itemId)
|
||||
true
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
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 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.close_tab_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
view.source_button.setOnClickListener {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("https://github.com/Corewala/Buran")
|
||||
})
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package corewala.buran.ui.modals_menus.history
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.row_history.view.*
|
||||
import corewala.delay
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.history.HistoryEntry
|
||||
|
||||
class HistoryAdapter(val history: List<HistoryEntry>, val onClick:(entry: HistoryEntry) -> Unit): RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View): RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.row_history, parent, false))
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = history.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.history_address.text = history[position].uri.toString()
|
||||
holder.itemView.history_row.setOnClickListener {
|
||||
delay(500){
|
||||
onClick(history[holder.adapterPosition])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package corewala.buran.ui.modals_menus.history
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.MenuCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.dialog_about.view.close_tab_dialog
|
||||
import kotlinx.android.synthetic.main.dialog_history.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.database.history.BuranHistory
|
||||
|
||||
object HistoryDialog {
|
||||
fun show(context: Context, history: BuranHistory, onHistoryItem: (address: String) -> Unit){
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_history, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.close_tab_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
view.history_overflow.setOnClickListener {
|
||||
val popup = PopupMenu(view.context, view.history_overflow)
|
||||
val inflater: MenuInflater = popup.menuInflater
|
||||
inflater.inflate(R.menu.history_overflow_menu, popup.menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
if(menuItem.itemId == R.id.history_overflow_clear_history){
|
||||
history.clear {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
dialog.dismiss()
|
||||
Toast.makeText(context, context.getString(R.string.history_cleared), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}else if(menuItem.itemId == R.id.history_overflow_clear_runtime_cache){
|
||||
dialog.dismiss()
|
||||
Toast.makeText(context, context.getString(R.string.runtime_cahce_cleared), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
view.history_recycler.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
history.get { history ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
view.history_recycler.adapter = HistoryAdapter(history) { entry ->
|
||||
onHistoryItem(entry.uri.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package corewala.buran.ui.modals_menus.input
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import kotlinx.android.synthetic.main.dialog_input_query.view.*
|
||||
import corewala.buran.R
|
||||
import corewala.buran.io.GemState
|
||||
import java.net.URLEncoder
|
||||
|
||||
object InputDialog {
|
||||
|
||||
fun show(context: Context, state: GemState.ResponseInput, onQuery: (queryAddress: String) -> Unit) {
|
||||
val dialog = AppCompatDialog(context, R.style.AppTheme)
|
||||
|
||||
val view = View.inflate(context, R.layout.dialog_input_query, null)
|
||||
dialog.setContentView(view)
|
||||
|
||||
view.close_input_query_dialog.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
view.query_text.text = state.header.meta
|
||||
|
||||
view.query_submit_button.setOnClickListener {
|
||||
val encoded = URLEncoder.encode(view.query_input.text.toString(), "UTF-8")
|
||||
onQuery("${state.uri}?$encoded")
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package corewala.buran.ui.modals_menus.overflow
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.ImageSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.MenuCompat
|
||||
import corewala.buran.R
|
||||
|
||||
|
||||
object OverflowPopup {
|
||||
|
||||
fun show(view: View?, onMenuOption: (menuId: Int) -> Unit){
|
||||
if(view != null) {
|
||||
val popup = PopupMenu(view.context, view)
|
||||
val inflater: MenuInflater = popup.menuInflater
|
||||
inflater.inflate(R.menu.overflow_menu, popup.menu)
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
onMenuOption(menuItem.itemId)
|
||||
true
|
||||
}
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
//insertMenuItemIcons(view.context, popup)
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMenuItemIcons(context: Context, popupMenu: PopupMenu) {
|
||||
val menu: Menu = popupMenu.menu
|
||||
if (hasIcon(menu)) {
|
||||
for (i in 0 until menu.size()) {
|
||||
insertMenuItemIcon(context, menu.getItem(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasIcon(menu: Menu): Boolean {
|
||||
for (i in 0 until menu.size()) {
|
||||
if (menu.getItem(i).icon != null) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given MenuItem's title into a Spannable containing both its icon and title.
|
||||
*/
|
||||
private fun insertMenuItemIcon(context: Context, menuItem: MenuItem) {
|
||||
val icon: Drawable = menuItem.icon
|
||||
val iconSize = context.resources.getDimensionPixelSize(R.dimen.menu_item_icon_size)
|
||||
icon.setBounds(0, 0, iconSize, iconSize)
|
||||
icon.setTint(Color.WHITE)
|
||||
val imageSpan = ImageSpan(icon)
|
||||
val ssb = SpannableStringBuilder(" " + menuItem.title)
|
||||
ssb.setSpan(imageSpan, 1, 2, 0)
|
||||
menuItem.title = ssb
|
||||
menuItem.icon = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package corewala.buran.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import corewala.buran.R
|
||||
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
setSupportActionBar(findViewById(R.id.settings_toolbar))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.vector_close)
|
||||
|
||||
supportFragmentManager.beginTransaction().replace(R.id.settings_container, SettingsFragment()).commit()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
finish()
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
455
app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt
Normal file
455
app/src/main/java/corewala/buran/ui/settings/SettingsFragment.kt
Normal file
|
@ -0,0 +1,455 @@
|
|||
package corewala.buran.ui.settings
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.*
|
||||
import corewala.buran.Buran
|
||||
import corewala.buran.R
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
|
||||
|
||||
const val PREFS_SET_CLIENT_CERT_REQ = 20
|
||||
|
||||
class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
|
||||
|
||||
lateinit var prefs: SharedPreferences
|
||||
lateinit var protocols: Array<String>
|
||||
|
||||
private lateinit var clientCertPref: Preference
|
||||
private lateinit var useClientCertPreference: SwitchPreferenceCompat
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
||||
prefs = preferenceManager.sharedPreferences
|
||||
|
||||
val context = preferenceManager.context
|
||||
val screen = preferenceManager.createPreferenceScreen(context)
|
||||
|
||||
/**
|
||||
* Buran App Settings
|
||||
*/
|
||||
val appCategory = PreferenceCategory(context)
|
||||
appCategory.key = "app_category"
|
||||
appCategory.title = getString(R.string.configure_buran)
|
||||
screen.addPreference(appCategory)
|
||||
|
||||
//Home ---------------------------------------------
|
||||
val homePreference = EditTextPreference(context)
|
||||
homePreference.title = getString(R.string.home_capsule)
|
||||
homePreference.key = "home_capsule"
|
||||
homePreference.dialogTitle = getString(R.string.home_capsule)
|
||||
|
||||
val homecapsule = preferenceManager.sharedPreferences.getString(
|
||||
"home_capsule",
|
||||
Buran.DEFAULT_HOME_CAPSULE
|
||||
)
|
||||
|
||||
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()
|
||||
true
|
||||
}
|
||||
homePreference.setOnBindEditTextListener{ editText ->
|
||||
editText.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
editText.setSelection(editText.text.toString().length)//Set caret position to end
|
||||
}
|
||||
appCategory.addPreference(homePreference)
|
||||
|
||||
//Home - Certificates
|
||||
buildClientCertificateSection(context, appCategory)
|
||||
|
||||
//Theme --------------------------------------------
|
||||
buildThemeSection(context, appCategory)
|
||||
|
||||
//Accessibility ------------------------------------
|
||||
buildsAccessibility(context, screen)
|
||||
|
||||
//Web ----------------------------------------------
|
||||
buildWebSection(context, screen)
|
||||
|
||||
//TLS ----------------------------------------------
|
||||
buildTLSSection(context, screen)
|
||||
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
private fun buildWebSection(context: Context?, screen: PreferenceScreen){
|
||||
val webCategory = PreferenceCategory(context)
|
||||
webCategory.key = "web_category"
|
||||
webCategory.title = getString(R.string.web_content)
|
||||
screen.addPreference(webCategory)
|
||||
|
||||
val customTabInfo = Preference(context)
|
||||
customTabInfo.summary = getString(R.string.web_content_label)
|
||||
webCategory.addPreference(customTabInfo)
|
||||
|
||||
val useCustomTabsPreference = SwitchPreferenceCompat(context)
|
||||
useCustomTabsPreference.setDefaultValue(true)
|
||||
useCustomTabsPreference.key = Buran.PREF_KEY_USE_CUSTOM_TAB
|
||||
useCustomTabsPreference.title = getString(R.string.web_content_switch_label)
|
||||
webCategory.addPreference(useCustomTabsPreference)
|
||||
|
||||
}
|
||||
|
||||
private fun buildThemeSection(context: Context?, appCategory: PreferenceCategory) {
|
||||
val themeCategory = PreferenceCategory(context)
|
||||
themeCategory.key = "theme_category"
|
||||
themeCategory.title = getString(R.string.theme)
|
||||
appCategory.addPreference(themeCategory)
|
||||
|
||||
val themeFollowSystemPreference = SwitchPreferenceCompat(context)
|
||||
themeFollowSystemPreference.key = "theme_FollowSystem"
|
||||
themeFollowSystemPreference.title = getString(R.string.system_default)
|
||||
themeFollowSystemPreference.onPreferenceChangeListener = this
|
||||
themeCategory.addPreference(themeFollowSystemPreference)
|
||||
|
||||
val themeLightPreference = SwitchPreferenceCompat(context)
|
||||
themeLightPreference.key = "theme_Light"
|
||||
themeLightPreference.title = getString(R.string.light)
|
||||
themeLightPreference.onPreferenceChangeListener = this
|
||||
themeCategory.addPreference(themeLightPreference)
|
||||
|
||||
val themeDarkPreference = SwitchPreferenceCompat(context)
|
||||
themeDarkPreference.key = "theme_Dark"
|
||||
themeDarkPreference.title = getString(R.string.dark)
|
||||
themeDarkPreference.onPreferenceChangeListener = this
|
||||
themeCategory.addPreference(themeDarkPreference)
|
||||
|
||||
|
||||
val isThemePrefSet =
|
||||
prefs.getBoolean("theme_FollowSystem", false) ||
|
||||
prefs.getBoolean("theme_Light", false) ||
|
||||
prefs.getBoolean("theme_Dark", false)
|
||||
if (!isThemePrefSet) themeFollowSystemPreference.isChecked = true
|
||||
|
||||
val coloursCSV = resources.openRawResource(R.raw.colours).bufferedReader().use { it.readLines() }
|
||||
|
||||
val labels = mutableListOf<String>()
|
||||
val values = mutableListOf<String>()
|
||||
|
||||
coloursCSV.forEach{ line ->
|
||||
val colour = line.split(",")
|
||||
labels.add(colour[0])
|
||||
values.add(colour[1])
|
||||
}
|
||||
|
||||
val backgroundColourPreference = ListPreference(context)
|
||||
backgroundColourPreference.key = "background_colour";
|
||||
backgroundColourPreference.setDialogTitle(R.string.prefs_override_page_background_dialog_title)
|
||||
backgroundColourPreference.setTitle(R.string.prefs_override_page_background_title)
|
||||
backgroundColourPreference.setSummary(R.string.prefs_override_page_background)
|
||||
backgroundColourPreference.setDefaultValue("#XXXXXX")
|
||||
backgroundColourPreference.entries = labels.toTypedArray()
|
||||
backgroundColourPreference.entryValues = values.toTypedArray()
|
||||
|
||||
backgroundColourPreference.setOnPreferenceChangeListener { _, colour ->
|
||||
when (colour) {
|
||||
"#XXXXXX" -> this.view?.background = null
|
||||
else -> this.view?.background = ColorDrawable(Color.parseColor("$colour"))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
themeCategory.addPreference(backgroundColourPreference)
|
||||
}
|
||||
|
||||
private fun buildsAccessibility(context: Context?, screen: PreferenceScreen){
|
||||
val accessibilityCategory = PreferenceCategory(context)
|
||||
accessibilityCategory.key = "accessibility_category"
|
||||
accessibilityCategory.title = getString(R.string.accessibility)
|
||||
screen.addPreference(accessibilityCategory)
|
||||
|
||||
//Accessibility - code blocks
|
||||
val aboutCodeBlocksPref = Preference(context)
|
||||
aboutCodeBlocksPref.key = "unused_accessibility_pref"
|
||||
aboutCodeBlocksPref.summary = getString(R.string.collapse_code_blocks_about)
|
||||
aboutCodeBlocksPref.isPersistent = false
|
||||
aboutCodeBlocksPref.isSelectable = false
|
||||
accessibilityCategory.addPreference(aboutCodeBlocksPref)
|
||||
|
||||
val collapseCodeBlocksPreference = SwitchPreferenceCompat(context)
|
||||
collapseCodeBlocksPreference.key = "collapse_code_blocks"
|
||||
collapseCodeBlocksPreference.title = getString(R.string.collapse_code_blocks)
|
||||
accessibilityCategory.addPreference(collapseCodeBlocksPreference)
|
||||
|
||||
//Accessibility - large text and buttons
|
||||
val largeGemtextPreference = SwitchPreferenceCompat(context)
|
||||
largeGemtextPreference.key = "use_large_gemtext_adapter"
|
||||
largeGemtextPreference.title = getString(R.string.large_gemtext_and_button)
|
||||
accessibilityCategory.addPreference(largeGemtextPreference)
|
||||
|
||||
//Accessibility - inline icons
|
||||
val showInlineIconsPreference = SwitchPreferenceCompat(context)
|
||||
showInlineIconsPreference.key = "show_inline_icons"
|
||||
showInlineIconsPreference.title = getString(R.string.show_inline_icons)
|
||||
accessibilityCategory.addPreference(showInlineIconsPreference)
|
||||
}
|
||||
|
||||
private fun buildTLSSection(context: Context?, screen: PreferenceScreen) {
|
||||
val tlsCategory = PreferenceCategory(context)
|
||||
tlsCategory.key = "tls_category"
|
||||
tlsCategory.title = getString(R.string.tls_config)
|
||||
screen.addPreference(tlsCategory)
|
||||
|
||||
val tlsDefaultPreference = SwitchPreferenceCompat(context)
|
||||
tlsDefaultPreference.key = "tls_Default"
|
||||
tlsDefaultPreference.title = getString(R.string.tls_default)
|
||||
tlsDefaultPreference.onPreferenceChangeListener = this
|
||||
tlsCategory.addPreference(tlsDefaultPreference)
|
||||
|
||||
//This feel inelegant:
|
||||
var tlsPrefSet = false
|
||||
prefs.all.forEach { pref ->
|
||||
if (pref.key.startsWith("tls_")) tlsPrefSet = true
|
||||
}
|
||||
|
||||
if (!tlsPrefSet) {
|
||||
tlsDefaultPreference.isChecked = true
|
||||
}
|
||||
|
||||
val tlsAllSupportedPreference = SwitchPreferenceCompat(context)
|
||||
tlsAllSupportedPreference.key = "tls_All_Supported"
|
||||
tlsAllSupportedPreference.title = getString(R.string.tls_enable_all_supported)
|
||||
tlsAllSupportedPreference.onPreferenceChangeListener = this
|
||||
tlsCategory.addPreference(tlsAllSupportedPreference)
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, null, SecureRandom())
|
||||
val factory: SSLSocketFactory = sslContext.socketFactory
|
||||
val socket = factory.createSocket() as SSLSocket
|
||||
protocols = socket.supportedProtocols
|
||||
protocols.forEach { protocol ->
|
||||
val tlsPreference = SwitchPreferenceCompat(context)
|
||||
tlsPreference.key = "tls_${protocol.toLowerCase(Locale.getDefault())}"
|
||||
tlsPreference.title = protocol
|
||||
tlsPreference.onPreferenceChangeListener = this
|
||||
tlsCategory.addPreference(tlsPreference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildClientCertificateSection(context: Context?, appCategory: PreferenceCategory) {
|
||||
if (Buran.FEATURE_CLIENT_CERTS) {
|
||||
|
||||
val aboutPref = Preference(context)
|
||||
aboutPref.key = "unused_pref"
|
||||
aboutPref.summary = getString(R.string.pkcs_notice)
|
||||
aboutPref.isPersistent = false
|
||||
aboutPref.isSelectable = false
|
||||
appCategory.addPreference(aboutPref)
|
||||
|
||||
clientCertPref = Preference(context)
|
||||
clientCertPref.title = getString(R.string.client_certificate)
|
||||
clientCertPref.key = Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE
|
||||
|
||||
val clientCertUriHumanReadable = preferenceManager.sharedPreferences.getString(
|
||||
Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE,
|
||||
null
|
||||
)
|
||||
|
||||
val hasCert = clientCertUriHumanReadable != null
|
||||
if (!hasCert) {
|
||||
clientCertPref.summary = getString(R.string.tap_to_select_client_certificate)
|
||||
} else {
|
||||
clientCertPref.summary = clientCertUriHumanReadable
|
||||
}
|
||||
|
||||
clientCertPref.setOnPreferenceClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
type = "*/*"
|
||||
}
|
||||
startActivityForResult(intent, PREFS_SET_CLIENT_CERT_REQ)
|
||||
true
|
||||
}
|
||||
|
||||
appCategory.addPreference(clientCertPref)
|
||||
|
||||
|
||||
val clientCertPassword = EditTextPreference(context)
|
||||
clientCertPassword.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD
|
||||
clientCertPassword.title = getString(R.string.client_certificate_password)
|
||||
|
||||
val certPasword = preferenceManager.sharedPreferences.getString(
|
||||
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
|
||||
null
|
||||
)
|
||||
if (certPasword != null && certPasword.isNotEmpty()) {
|
||||
clientCertPassword.summary = getDots(certPasword)
|
||||
} else {
|
||||
clientCertPassword.summary = getString(R.string.no_password)
|
||||
}
|
||||
clientCertPassword.dialogTitle = getString(R.string.client_certificate_password)
|
||||
clientCertPassword.setOnPreferenceChangeListener { _, newValue ->
|
||||
val passphrase = "$newValue"
|
||||
if (passphrase.isEmpty()) {
|
||||
clientCertPassword.summary = getString(R.string.no_password)
|
||||
} else {
|
||||
clientCertPassword.summary = getDots(passphrase)
|
||||
}
|
||||
|
||||
true//update the value
|
||||
}
|
||||
|
||||
appCategory.addPreference(clientCertPassword)
|
||||
|
||||
useClientCertPreference = SwitchPreferenceCompat(context)
|
||||
useClientCertPreference.key = Buran.PREF_KEY_CLIENT_CERT_ACTIVE
|
||||
useClientCertPreference.title = getString(R.string.use_client_certificate)
|
||||
appCategory.addPreference(useClientCertPreference)
|
||||
|
||||
if (!hasCert) {
|
||||
useClientCertPreference.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDots(value: String): String {
|
||||
val sb = StringBuilder()
|
||||
repeat(value.length){
|
||||
sb.append("•")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean {
|
||||
if(preference == null) return false
|
||||
|
||||
if(preference.key.startsWith("tls")){
|
||||
tlsChangeListener(preference, newValue)
|
||||
return true
|
||||
}
|
||||
|
||||
if(preference.key.startsWith("theme")){
|
||||
when(preference.key){
|
||||
"theme_FollowSystem" -> {
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_Light")?.isChecked =
|
||||
false
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_Dark")?.isChecked =
|
||||
false
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
"theme_Light" -> {
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_FollowSystem")?.isChecked =
|
||||
false
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_Dark")?.isChecked =
|
||||
false
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
"theme_Dark" -> {
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_FollowSystem")?.isChecked =
|
||||
false
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>("theme_Light")?.isChecked =
|
||||
false
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun tlsChangeListener(
|
||||
preference: Preference?, newValue: Any?
|
||||
) {
|
||||
if (preference is SwitchPreferenceCompat && newValue is Boolean && newValue == true) {
|
||||
preference.key?.let { key ->
|
||||
when {
|
||||
key.startsWith("tls_") -> {
|
||||
if (key != "tls_Default") {
|
||||
val default = preferenceScreen.findPreference<SwitchPreferenceCompat>("tls_Default")
|
||||
default?.isChecked = false
|
||||
}
|
||||
if (key != "tls_All_Supported") {
|
||||
val all = preferenceScreen.findPreference<SwitchPreferenceCompat>("tls_All_Supported")
|
||||
all?.isChecked = false
|
||||
}
|
||||
protocols.forEach { protocol ->
|
||||
val tlsSwitchKey = "tls_${protocol.toLowerCase(Locale.getDefault())}"
|
||||
if (tlsSwitchKey != key) {
|
||||
val otherTLSSwitch =
|
||||
preferenceScreen.findPreference<SwitchPreferenceCompat>(
|
||||
tlsSwitchKey
|
||||
)
|
||||
otherTLSSwitch?.isChecked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (preference.key) {
|
||||
"tls_Default" -> setTLSProtocol("TLS")
|
||||
"tls_All_Supported" -> setTLSProtocol("TLS_ALL")
|
||||
else -> {
|
||||
val prefTitle = preference.title.toString()
|
||||
setTLSProtocol(prefTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTLSProtocol(protocol: String) = preferenceManager.sharedPreferences.edit().putString(
|
||||
"tls_protocol",
|
||||
protocol
|
||||
).apply()
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if(requestCode == PREFS_SET_CLIENT_CERT_REQ && resultCode == RESULT_OK){
|
||||
data?.data?.also { uri ->
|
||||
preferenceManager.sharedPreferences.edit().putString(
|
||||
Buran.PREF_KEY_CLIENT_CERT_URI,
|
||||
uri.toString()
|
||||
).apply()
|
||||
persistPermissions(uri)
|
||||
findFilename(uri)
|
||||
}
|
||||
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun persistPermissions(uri: Uri) {
|
||||
val contentResolver = requireContext().contentResolver
|
||||
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
|
||||
private fun findFilename(uri: Uri) {
|
||||
|
||||
var readableReference = uri.toString()
|
||||
if (uri.scheme == "content") {
|
||||
requireContext().contentResolver.query(uri, null, null, null, null).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
readableReference = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preferenceManager.sharedPreferences.edit().putString(
|
||||
Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE,
|
||||
readableReference
|
||||
).apply()
|
||||
clientCertPref.summary = readableReference
|
||||
useClientCertPreference.isChecked = true
|
||||
}
|
||||
}
|
6
app/src/main/res/anim/fade_in.xml
Normal file
6
app/src/main/res/anim/fade_in.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"
|
||||
android:duration="1000"
|
||||
/>
|
4
app/src/main/res/drawable-anydpi-v26/laucher_round.xml
Normal file
4
app/src/main/res/drawable-anydpi-v26/laucher_round.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/vector_app_icon"/>
|
||||
</layer-list>
|
4
app/src/main/res/drawable-anydpi-v26/launcher.xml
Normal file
4
app/src/main/res/drawable-anydpi-v26/launcher.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/vector_app_icon"/>
|
||||
</layer-list>
|
6
app/src/main/res/drawable/block_background.xml
Normal file
6
app/src/main/res/drawable/block_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:width="1.0dip" android:color="#ffcccccc" />
|
||||
<corners android:radius="12.0dip" />
|
||||
</shape>
|
11
app/src/main/res/drawable/drawable_filled_rounded_rect.xml
Normal file
11
app/src/main/res/drawable/drawable_filled_rounded_rect.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetBottom="0dp"
|
||||
android:insetLeft="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetTop="0dp">
|
||||
<shape
|
||||
android:shape="rectangle" android:tint="@color/address_background">
|
||||
<stroke android:color="?attr/colorControlNormal" android:width="1dp" />
|
||||
<corners android:radius="22dp" />
|
||||
</shape>
|
||||
</inset>
|
11
app/src/main/res/drawable/drawable_stroke_rounded_rect.xml
Normal file
11
app/src/main/res/drawable/drawable_stroke_rounded_rect.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetBottom="3dp"
|
||||
android:insetLeft="3dp"
|
||||
android:insetRight="3dp"
|
||||
android:insetTop="3dp">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<stroke android:color="?attr/colorControlNormal" android:width="1dp" />
|
||||
<corners android:radius="5dp" />
|
||||
</shape>
|
||||
</inset>
|
4
app/src/main/res/drawable/launcher.xml
Normal file
4
app/src/main/res/drawable/launcher.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/vector_app_icon"/>
|
||||
</layer-list>
|
4
app/src/main/res/drawable/launcher_round.xml
Normal file
4
app/src/main/res/drawable/launcher_round.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/vector_app_icon"/>
|
||||
</layer-list>
|
161
app/src/main/res/drawable/vector_app_icon.xml
Normal file
161
app/src/main/res/drawable/vector_app_icon.xml
Normal file
|
@ -0,0 +1,161 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64"
|
||||
android:width="64dp"
|
||||
android:height="64dp">
|
||||
<group
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:pathData="M-3.999487 31.99922A27.9995 27.9995 0 0 1 -59.99848 31.99922A27.9995 27.9995 0 0 1 -3.999487 31.99922Z"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="round">
|
||||
<aapt:attr
|
||||
name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="-59.99848"
|
||||
android:startY="31.99922"
|
||||
android:endX="-3.999483"
|
||||
android:endY="31.99922"
|
||||
android:tileMode="clamp">
|
||||
<item
|
||||
android:color="#54C0CD"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#40DD9D"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.2446361"
|
||||
android:scaleY="0.2446361"
|
||||
android:translateX="-90.46356"
|
||||
android:translateY="-32.59816">
|
||||
<path
|
||||
android:pathData="M516.15807 333.26205l-4.23644 9.99629h-11.32999 -11.32999l-4.23644 -9.99629"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.264583" />
|
||||
<group
|
||||
android:translateX="359.0252"
|
||||
android:translateY="115.3219">
|
||||
<path
|
||||
android:pathData="M128.74043 197.67004v22.41127l-23.74528 -5.01339v-17.39788"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M104.99515 200.83206v14.23586L82.837366 210.5837v-9.75164"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
</group>
|
||||
<group
|
||||
android:rotation="-0"
|
||||
android:scaleX="-1"
|
||||
android:translateX="642.1581"
|
||||
android:translateY="115.3218">
|
||||
<path
|
||||
android:pathData="M128.74043 197.67004v22.41127l-23.74528 -5.01339v-17.39788"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M104.99515 200.83206v14.23586L82.837366 210.5837v-9.75164"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M513.31018 318.17512h46.03014c0 0 0.0809 -5.1157 0 -5.7368 -0.50023 -3.84272 -1.20912 -4.15349 -2.404 -6.2027 -1.19488 -2.0492 -31.50157 -32.08606 -33.31329 -35.01628 -1.75432 -2.83739 -11.62819 -52.60827 -11.62819 -52.60827h-11.4032 -11.4032c0 0 -9.87387 49.77088 -11.62819 52.60827 -1.81172 2.93022 -32.11841 32.96708 -33.31329 35.01628 -1.19488 2.04921 -1.90377 2.35998 -2.404 6.2027 -0.0808 0.6211 0 5.7368 0 5.7368h46.03014"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="2.64583" />
|
||||
<path
|
||||
android:pathData="M500.59164 333.40007h-2.78055v1.19954h-7.39536l-2.65015 -21.6077v-95.81955c0.0563 -7.71636 7.72843 -34.71998 12.82606 -34.87227 5.09763 0.15229 12.76976 27.15591 12.82606 34.87227v95.81955l-2.65015 21.6077h-7.39536v-1.19954h-2.78055"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 217.17236H513.4177"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 241.12725H513.4177"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 265.08214H513.4177"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 289.03702H513.4177"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 312.99191H513.4177"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M487.76558 312.99191v32.8164h6.6234v-11.20867h-3.97324z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<path
|
||||
android:pathData="M513.41771 312.99188v32.8164h-6.6234v-11.20867h3.97324z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292" />
|
||||
<group
|
||||
android:translateX="360.5539"
|
||||
android:translateY="115.3219">
|
||||
<path
|
||||
android:pathData="M130.2622 222.7477A0.4003793 0.4003793 0 0 1 129.4615 222.7477A0.4003793 0.4003793 0 0 1 130.2622 222.7477Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
<path
|
||||
android:pathData="M130.2622 224.8851A0.4003793 0.4003793 0 0 1 129.4615 224.8851A0.4003793 0.4003793 0 0 1 130.2622 224.8851Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
<path
|
||||
android:pathData="M130.2622 227.0326A0.4003793 0.4003793 0 0 1 129.4615 227.0326A0.4003793 0.4003793 0 0 1 130.2622 227.0326Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="380.9057"
|
||||
android:translateY="115.3219">
|
||||
<path
|
||||
android:pathData="M130.2622 222.7477A0.4003793 0.4003793 0 0 1 129.4615 222.7477A0.4003793 0.4003793 0 0 1 130.2622 222.7477Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
<path
|
||||
android:pathData="M130.2622 224.8851A0.4003793 0.4003793 0 0 1 129.4615 224.8851A0.4003793 0.4003793 0 0 1 130.2622 224.8851Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
<path
|
||||
android:pathData="M130.2622 227.0326A0.4003793 0.4003793 0 0 1 129.4615 227.0326A0.4003793 0.4003793 0 0 1 130.2622 227.0326Z"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="0.0264583"
|
||||
android:strokeLineCap="square"
|
||||
android:strokeLineJoin="bevel" />
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_cancel.xml
Normal file
10
app/src/main/res/drawable/vector_cancel.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15.59,7L12,10.59 8.41,7 7,8.41 10.59,12 7,15.59 8.41,17 12,13.41 15.59,17 17,15.59 13.41,12 17,8.41z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_client_cert.xml
Normal file
10
app/src/main/res/drawable/vector_client_cert.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39s-4.66,1.97 -4.66,4.39c0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94s3.08,1.32 3.08,2.94c0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_close.xml
Normal file
10
app/src/main/res/drawable/vector_close.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_code.xml
Normal file
10
app/src/main/res/drawable/vector_code.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,4H4C2.89,4 2,4.9 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.11,4 20,4zM20,18H4V8h16V18zM18,17h-6v-2h6V17zM7.5,17l-1.41,-1.41L8.67,13l-2.59,-2.59L7.5,9l4,4L7.5,17z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_confirm.xml
Normal file
10
app/src/main/res/drawable/vector_confirm.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41L9,16.17z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_home.xml
Normal file
10
app/src/main/res/drawable/vector_home.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/colorAccent">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_link.xml
Normal file
10
app/src/main/res/drawable/vector_link.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17,7h-4v2h4c1.65,0 3,1.35 3,3s-1.35,3 -3,3h-4v2h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM11,15L7,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-2zM8,11h8v2L8,13z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_open_browser.xml
Normal file
10
app/src/main/res/drawable/vector_open_browser.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_overflow.xml
Normal file
10
app/src/main/res/drawable/vector_overflow.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_photo.xml
Normal file
10
app/src/main/res/drawable/vector_photo.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/vector_refresh.xml
Normal file
10
app/src/main/res/drawable/vector_refresh.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
13
app/src/main/res/drawable/vector_save.xml
Normal file
13
app/src/main/res/drawable/vector_save.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,9l-1.06,-1.06l-2.19,2.19l0,-7.13l-1.5,0l0,7.13l-2.19,-2.19l-1.06,1.06l4,4z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14.5,13v1.5h-9V13H4v1.5C4,15.33 4.67,16 5.5,16h9c0.83,0 1.5,-0.67 1.5,-1.5V13H14.5z"/>
|
||||
</vector>
|
7
app/src/main/res/font/code_font.xml
Normal file
7
app/src/main/res/font/code_font.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<font
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="400"
|
||||
android:font="@font/jet_brains_mono"/>
|
||||
</font-family>
|
BIN
app/src/main/res/font/jet_brains_mono.ttf
Normal file
BIN
app/src/main/res/font/jet_brains_mono.ttf
Normal file
Binary file not shown.
125
app/src/main/res/layout/activity_gem.xml
Normal file
125
app/src/main/res/layout/activity_gem.xml
Normal file
|
@ -0,0 +1,125 @@
|
|||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<data>
|
||||
<variable name="viewmodel" type="corewala.buran.ui.GemViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_coord"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.GemActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/header_background"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:contentScrim="?attr/colorPrimaryDark"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/default_margin">
|
||||
|
||||
<RelativeLayout android:id="@+id/address_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/home"
|
||||
android:layout_width="@dimen/button_size"
|
||||
android:layout_height="@dimen/button_size"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_home" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@+id/home"
|
||||
android:layout_toStartOf="@+id/more">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/address_edit"
|
||||
android:background="@drawable/drawable_filled_rounded_rect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:inputType="textNoSuggestions|textUri"
|
||||
android:singleLine="true"
|
||||
android:textSize="12sp"
|
||||
android:imeOptions="actionGo"
|
||||
android:hint="@string/main_input_hint" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/more"
|
||||
android:layout_width="@dimen/button_size"
|
||||
android:layout_height="@dimen/button_size"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:indeterminateTint="@color/stroke"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_below="@+id/address_bar"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/pull_to_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/gemtext_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:scrollbars="vertical"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:paddingTop="@dimen/screen_margin"
|
||||
android:paddingBottom="@dimen/screen_margin"
|
||||
/>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
20
app/src/main/res/layout/activity_settings.xml
Normal file
20
app/src/main/res/layout/activity_settings.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/settings_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/settings"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/settings_container"
|
||||
android:layout_below="@+id/settings_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
39
app/src/main/res/layout/bookmark.xml
Normal file
39
app/src/main/res/layout/bookmark.xml
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/bookmark_layout"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/default_margin_big"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/bookmark_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="My Homepage"
|
||||
android:layout_toLeftOf="@+id/bookmark_overflow"
|
||||
android:textColor="@color/vibrant_stroke"
|
||||
android:textSize="@dimen/text_large" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/bookmark_uri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="gemini://myhomepage.gmi"
|
||||
android:textSize="@dimen/text_small"
|
||||
android:layout_toLeftOf="@+id/bookmark_overflow"
|
||||
android:layout_below="@+id/bookmark_name"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/bookmark_overflow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
115
app/src/main/res/layout/dialog_about.xml
Normal file
115
app/src/main/res/layout/dialog_about.xml
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?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">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_tab_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
</RelativeLayout>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/header">
|
||||
<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>
|
54
app/src/main/res/layout/dialog_bookmarks.xml
Normal file
54
app/src/main/res/layout/dialog_bookmarks.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?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:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/bookmarks_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_height="@dimen/bar_height"
|
||||
app:menu="@menu/bookmark_import_export"
|
||||
app:title="@string/bookmarks"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/bookmarks_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/bookmarks_toolbar"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/bookmarks_empty_layout"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerInParent="true"
|
||||
android:paddingBottom="60dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/bookmarks_empty"
|
||||
android:paddingTop="@dimen/default_margin_big"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</RelativeLayout>
|
43
app/src/main/res/layout/dialog_content_image.xml
Normal file
43
app/src/main/res/layout/dialog_content_image.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?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">
|
||||
|
||||
<corewala.buran.ui.content_image.TouchImageView
|
||||
android:id="@+id/image_view"
|
||||
android:scaleType="centerInside"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerHorizontal="true"
|
||||
/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_image_content_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/image_overflow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
50
app/src/main/res/layout/dialog_content_text.xml
Normal file
50
app/src/main/res/layout/dialog_content_text.xml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?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">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_text_content_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/history_overflow"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/default_margin"
|
||||
android:layout_below="@+id/header">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/text_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:textIsSelectable="true"/>
|
||||
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</RelativeLayout>
|
40
app/src/main/res/layout/dialog_history.xml
Normal file
40
app/src/main/res/layout/dialog_history.xml
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?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">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_tab_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/history_overflow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/history_recycler"
|
||||
android:layout_below="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</RelativeLayout>
|
62
app/src/main/res/layout/dialog_input_query.xml
Normal file
62
app/src/main/res/layout/dialog_input_query.xml
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?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">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_input_query_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
</RelativeLayout>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/header">
|
||||
<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">
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/query_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
tools:text="Type your query"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/query_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/default_margin"/>
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/query_submit_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/submit"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</RelativeLayout>
|
30
app/src/main/res/layout/dialog_set_home.xml
Normal file
30
app/src/main/res/layout/dialog_set_home.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?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="wrap_content"
|
||||
android:padding="@dimen/default_margin">
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/home_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/set_home_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:layout_alignParentRight="true"
|
||||
android:text="Update"
|
||||
android:layout_below="@+id/home_edit_text"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/use_current_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toLeftOf="@+id/set_home_button"
|
||||
android:text="Use current"
|
||||
android:layout_below="@+id/home_edit_text"/>
|
||||
</RelativeLayout>
|
43
app/src/main/res/layout/dialog_tabs.xml
Normal file
43
app/src/main/res/layout/dialog_tabs.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?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">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/default_margin"
|
||||
android:paddingBottom="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/close_tab_dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_close" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/tab_dialog_overflow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:visibility="gone"
|
||||
android:layout_margin="@dimen/button_margin"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/vector_overflow" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tab_dialog_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/header" />
|
||||
|
||||
</RelativeLayout>
|
50
app/src/main/res/layout/fragment_bookmark_dialog.xml
Normal file
50
app/src/main/res/layout/fragment_bookmark_dialog.xml
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?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:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/bookmark_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_height="@dimen/bar_height"
|
||||
app:title="@string/add_bookmark"/>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/bookmark_toolbar"
|
||||
android:padding="@dimen/default_margin_big"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/name" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/bookmark_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:lines="1"
|
||||
android:maxLines="1"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/gemini_uri" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/bookmark_uri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:lines="1"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
|
||||
</RelativeLayout>
|
56
app/src/main/res/layout/gemtext_code_block.xml
Normal file
56
app/src/main/res/layout/gemtext_code_block.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:id="@id/show_code_block"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:focusable="true" android:clickable="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/default_margin_small"
|
||||
android:layout_marginBottom="@dimen/default_margin_small"
|
||||
android:text="@string/show_code"
|
||||
android:drawablePadding="4.0dip"
|
||||
/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:background="@drawable/block_background"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/def_quarter"
|
||||
android:layout_marginBottom="@dimen/def_quarter"
|
||||
android:layout_below="@id/show_code_block">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
android:textSize="@dimen/code_text_size"
|
||||
android:typeface="monospace"
|
||||
android:textColor="@color/stroke"
|
||||
android:id="@id/gemtext_text_monospace_textview"
|
||||
android:paddingLeft="@dimen/default_margin_big"
|
||||
android:paddingTop="@dimen/default_margin_big"
|
||||
android:paddingRight="@dimen/default_margin_big"
|
||||
android:paddingBottom="0.0dip"
|
||||
android:scrollbars="horizontal|vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollHorizontally="true"
|
||||
android:isScrollContainer="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:textIsSelectable="true"
|
||||
android:fontFamily="@font/code_font"
|
||||
app:fontFamily="@font/code_font"
|
||||
app:lineHeight="@dimen/code_text_size"/>
|
||||
|
||||
</HorizontalScrollView>
|
||||
</RelativeLayout>
|
14
app/src/main/res/layout/gemtext_h1.xml
Normal file
14
app/src/main/res/layout/gemtext_h1.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/h1_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
14
app/src/main/res/layout/gemtext_h2.xml
Normal file
14
app/src/main/res/layout/gemtext_h2.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/h2_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
14
app/src/main/res/layout/gemtext_h3.xml
Normal file
14
app/src/main/res/layout/gemtext_h3.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/h3_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
34
app/src/main/res/layout/gemtext_image_link.xml
Normal file
34
app/src/main/res/layout/gemtext_image_link.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/gemtext_text_link"
|
||||
android:textColor="@color/stroke"
|
||||
android:textSize="@dimen/default_text_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:drawableEnd="@drawable/vector_photo"
|
||||
android:drawablePadding="4dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
tools:text="an image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/gemtext_inline_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
android:background="#ffffff"
|
||||
android:visibility="gone"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_below="@+id/gemtext_text_link"/>
|
||||
</RelativeLayout>
|
58
app/src/main/res/layout/gemtext_large_code_block.xml
Normal file
58
app/src/main/res/layout/gemtext_large_code_block.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/show_code_block"
|
||||
android:textColor="@color/stroke"
|
||||
android:layout_marginTop="@dimen/default_margin_small"
|
||||
android:layout_marginBottom="@dimen/default_margin_small"
|
||||
android:textSize="@dimen/large_text_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/accessibility_button_padding"
|
||||
android:textAllCaps="false"
|
||||
android:backgroundTint="@color/accessibility_button_background"
|
||||
android:textAlignment="viewStart"
|
||||
android:drawablePadding="4dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/show_code"
|
||||
tools:text="Show code: Ascii art banner: Spacewalk - Updates from around geminispace"
|
||||
/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/show_code_block">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/gemtext_text_monospace_textview"
|
||||
android:textSize="@dimen/code_text_size"
|
||||
app:lineHeight="@dimen/code_text_size"
|
||||
app:fontFamily="@font/code_font"
|
||||
android:fontFamily="@font/code_font"
|
||||
android:typeface="monospace"
|
||||
android:textColor="@color/stroke"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="@dimen/default_margin"
|
||||
android:paddingRight="@dimen/default_margin"
|
||||
android:paddingTop="@dimen/default_margin"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:isScrollContainer="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:scrollHorizontally="true"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
android:scrollbars="vertical|horizontal"/>
|
||||
</HorizontalScrollView>
|
||||
</RelativeLayout>
|
14
app/src/main/res/layout/gemtext_large_h1.xml
Normal file
14
app/src/main/res/layout/gemtext_large_h1.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/large_h1_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
14
app/src/main/res/layout/gemtext_large_h2.xml
Normal file
14
app/src/main/res/layout/gemtext_large_h2.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/large_h2_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
14
app/src/main/res/layout/gemtext_large_h3.xml
Normal file
14
app/src/main/res/layout/gemtext_large_h3.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/large_h3_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin_tiny" />
|
37
app/src/main/res/layout/gemtext_large_image_link.xml
Normal file
37
app/src/main/res/layout/gemtext_large_image_link.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/gemtext_text_link"
|
||||
android:textColor="@color/stroke"
|
||||
android:textSize="@dimen/large_text_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:drawableEnd="@drawable/vector_photo"
|
||||
android:drawablePadding="4dp"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:padding="@dimen/accessibility_button_padding"
|
||||
android:textAllCaps="false"
|
||||
tools:text="an image"
|
||||
android:backgroundTint="@color/accessibility_button_background"
|
||||
android:textAlignment="viewStart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/gemtext_inline_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin"
|
||||
android:layout_marginBottom="@dimen/default_margin"
|
||||
android:background="#ffffff"
|
||||
android:visibility="gone"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_below="@+id/gemtext_text_link"/>
|
||||
</RelativeLayout>
|
26
app/src/main/res/layout/gemtext_large_link.xml
Normal file
26
app/src/main/res/layout/gemtext_large_link.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?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="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/gemtext_text_link"
|
||||
android:textColor="@color/stroke"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:layout_marginTop="@dimen/default_margin_small"
|
||||
android:layout_marginBottom="@dimen/default_margin_small"
|
||||
android:textSize="@dimen/large_text_size"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:textAllCaps="false"
|
||||
android:textAlignment="viewStart"
|
||||
android:padding="@dimen/accessibility_button_padding"
|
||||
android:backgroundTint="@color/accessibility_button_background"
|
||||
android:drawablePadding="4dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</RelativeLayout>
|
18
app/src/main/res/layout/gemtext_large_quote.xml
Normal file
18
app/src/main/res/layout/gemtext_large_quote.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_monospace_textview"
|
||||
android:textSize="@dimen/large_text_size"
|
||||
android:textColor="@color/stroke"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:paddingLeft="@dimen/default_margin"
|
||||
android:paddingRight="@dimen/default_margin"
|
||||
android:paddingTop="@dimen/default_margin"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
android:textStyle="italic"
|
||||
android:background="@color/code_background"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineHeight="@dimen/large_line_height"/>
|
12
app/src/main/res/layout/gemtext_large_text.xml
Normal file
12
app/src/main/res/layout/gemtext_large_text.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/gemtext_text_textview"
|
||||
android:textSize="@dimen/large_text_size"
|
||||
android:layout_marginLeft="@dimen/screen_margin"
|
||||
android:layout_marginRight="@dimen/screen_margin"
|
||||
android:textColor="@color/stroke"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineHeight="@dimen/large_line_height"/>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue